Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x1198 Include sample in graphql Roi type so client can retrieve info #407

Merged
merged 1 commit into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions src/main/java/uk/ac/sanger/sccp/stan/request/LabwareRoi.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package uk.ac.sanger.sccp.stan.request;

import uk.ac.sanger.sccp.stan.model.Address;
import uk.ac.sanger.sccp.stan.model.Sample;

import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -72,28 +73,28 @@ public int hashCode() {
public static class RoiResult {
private Integer slotId;
private Address address;
private Integer sampleId;
private Sample sample;
private Integer operationId;
private String roi;

// Deserialisation constructor
public RoiResult() {}

public RoiResult(Integer slotId, Address address, Integer sampleId, Integer operationId, String roi) {
this.sampleId = sampleId;
public RoiResult(Integer slotId, Address address, Sample sample, Integer operationId, String roi) {
this.slotId = slotId;
this.address = address;
this.sample = sample;
this.operationId = operationId;
this.roi = roi;
}

/** The id of the sample. */
public Integer getSampleId() {
return this.sampleId;
/** The sample in the ROI */
public Sample getSample() {
return this.sample;
}

public void setSampleId(Integer sampleId) {
this.sampleId = sampleId;
public void setSample(Sample sample) {
this.sample = sample;
}

/** The id of the slot. */
Expand Down Expand Up @@ -135,7 +136,7 @@ public void setRoi(String roi) {
@Override
public String toString() {
return describe(this)
.add("sampleId", sampleId)
.add("sample", sample==null ? "null": "["+sample.getId()+"]")
.add("slotId", slotId)
.add("address", address)
.add("operationId", operationId)
Expand All @@ -148,7 +149,7 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || o.getClass() != this.getClass()) return false;
RoiResult that = (RoiResult) o;
return (Objects.equals(this.sampleId, that.sampleId)
return (Objects.equals(this.sample, that.sample)
&& Objects.equals(this.slotId, that.slotId)
&& Objects.equals(this.address, that.address)
&& Objects.equals(this.operationId, that.operationId)
Expand All @@ -158,7 +159,7 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
return Objects.hash(sampleId, slotId, address, operationId, roi);
return Objects.hash(sample, slotId, address, operationId, roi);
}
}
}
47 changes: 38 additions & 9 deletions src/main/java/uk/ac/sanger/sccp/stan/service/RoiServiceImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import uk.ac.sanger.sccp.stan.model.*;
import uk.ac.sanger.sccp.stan.repo.LabwareRepo;
import uk.ac.sanger.sccp.stan.repo.RoiRepo;
import uk.ac.sanger.sccp.stan.repo.*;
import uk.ac.sanger.sccp.stan.request.LabwareRoi;
import uk.ac.sanger.sccp.stan.request.LabwareRoi.RoiResult;

import java.util.*;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toSet;
import static uk.ac.sanger.sccp.utils.BasicUtils.inMap;

/**
Expand All @@ -21,11 +21,13 @@
public class RoiServiceImp implements RoiService {
private final LabwareRepo lwRepo;
private final RoiRepo roiRepo;
private final SampleRepo sampleRepo;

@Autowired
public RoiServiceImp(LabwareRepo lwRepo, RoiRepo roiRepo) {
public RoiServiceImp(LabwareRepo lwRepo, RoiRepo roiRepo, SampleRepo sampleRepo) {
this.lwRepo = lwRepo;
this.roiRepo = roiRepo;
this.sampleRepo = sampleRepo;
}

@Override
Expand All @@ -35,26 +37,53 @@ public List<LabwareRoi> labwareRois(Collection<String> barcodes) {
if (labware.isEmpty()) {
return List.of();
}
Map<Integer, Slot> slotIdMap = labware.stream()
final Map<Integer, Slot> slotIdMap = labware.stream()
.flatMap(lw -> lw.getSlots().stream())
.collect(inMap(Slot::getId));
List<Roi> rois = roiRepo.findAllBySlotIdIn(slotIdMap.keySet());
Map<Integer, List<RoiResult>> lwIdRoiResults = rois.stream()
.map(roi -> toRoiResult(roi, slotIdMap))
final List<Roi> rois = roiRepo.findAllBySlotIdIn(slotIdMap.keySet());
final Map<Integer, Sample> sampleIdMap = loadSamples(labware, rois);
final Map<Integer, List<RoiResult>> lwIdRoiResults = rois.stream()
.map(roi -> toRoiResult(roi, slotIdMap, sampleIdMap))
.collect(groupingBy(rr -> slotIdMap.get(rr.getSlotId()).getLabwareId()));
return labware.stream()
.map(lw -> new LabwareRoi(lw.getBarcode(), lwIdRoiResults.get(lw.getId())))
.toList();
}

/**
* Gets a map of samples referenced in the rois.
* All samples inside the given labware are added to the map,
* and any others mentioned in the rois are loaded from the SampleRepo.
* @param labware the labware referenced in the ROIs
* @param rois the ROIs we are loading information about
* @return a map of samples from their IDs
*/
public Map<Integer, Sample> loadSamples(Collection<Labware> labware, Collection<Roi> rois) {
final Map<Integer, Sample> sampleMap = new HashMap<>();
labware.stream()
.flatMap(lw -> lw.getSlots().stream().flatMap(slot -> slot.getSamples().stream()))
.forEach(sample -> sampleMap.put(sample.getId(), sample));
Set<Integer> missingSampleIds = rois.stream()
.map(Roi::getSampleId)
.filter(id -> sampleMap.get(id)==null)
.collect(toSet());
if (!missingSampleIds.isEmpty()) {
for (Sample sample : sampleRepo.findAllByIdIn(missingSampleIds)) {
sampleMap.put(sample.getId(), sample);
}
}
return sampleMap;
}

/**
* Converts a roi to a roiresult (for passing back through graphql)
* @param roi the roi to convert
* @param slotIdMap map to look up slot by id
* @param sampleIdMap map to look up sample by id
* @return a roiresult object suitable for passing via graphql
*/
RoiResult toRoiResult(Roi roi, Map<Integer, Slot> slotIdMap) {
RoiResult toRoiResult(Roi roi, Map<Integer, Slot> slotIdMap, Map<Integer, Sample> sampleIdMap) {
Slot slot = slotIdMap.get(roi.getSlotId());
return new RoiResult(roi.getSlotId(), slot.getAddress(), roi.getSampleId(), roi.getOperationId(), roi.getRoi());
return new RoiResult(roi.getSlotId(), slot.getAddress(), sampleIdMap.get(roi.getSampleId()), roi.getOperationId(), roi.getRoi());
}
}
4 changes: 2 additions & 2 deletions src/main/resources/schema.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -1796,8 +1796,8 @@ type Roi {
slotId: Int!
"""The address of the slot."""
address: Address!
"""The id of the sample."""
sampleId: Int!
"""The sample in the ROI."""
sample: Sample!
"""The id of the operation in which the ROI was recorded."""
operationId: Int!
"""The description of the region of interest."""
Expand Down
69 changes: 59 additions & 10 deletions src/test/java/uk/ac/sanger/sccp/stan/service/TestRoiService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import org.mockito.*;
import uk.ac.sanger.sccp.stan.EntityFactory;
import uk.ac.sanger.sccp.stan.model.*;
import uk.ac.sanger.sccp.stan.repo.LabwareRepo;
import uk.ac.sanger.sccp.stan.repo.RoiRepo;
import uk.ac.sanger.sccp.stan.repo.*;
import uk.ac.sanger.sccp.stan.request.LabwareRoi;
import uk.ac.sanger.sccp.stan.request.LabwareRoi.RoiResult;
import uk.ac.sanger.sccp.utils.UCMap;
Expand All @@ -15,6 +14,7 @@
import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static uk.ac.sanger.sccp.utils.BasicUtils.inMap;
Expand All @@ -25,6 +25,8 @@ class TestRoiService {
private LabwareRepo mockLwRepo;
@Mock
private RoiRepo mockRoiRepo;
@Mock
private SampleRepo mockSampleRepo;

@InjectMocks
private RoiServiceImp service;
Expand All @@ -34,6 +36,7 @@ class TestRoiService {
@BeforeEach
void setup() {
mocking = MockitoAnnotations.openMocks(this);
service = spy(service);
}

@AfterEach
Expand All @@ -54,13 +57,16 @@ void testLabwareRois_none() {
void testLabwareRois() {
Sample[] samples = EntityFactory.makeSamples(2);
int[] sampleIds = Arrays.stream(samples).mapToInt(Sample::getId).toArray();
Map<Integer, Sample> sampleMap = Arrays.stream(samples)
.collect(inMap(Sample::getId));
LabwareType lt = EntityFactory.makeLabwareType(1,2);
Labware[] lws = {
EntityFactory.makeLabware(lt, samples),
EntityFactory.makeEmptyLabware(lt),
EntityFactory.makeEmptyLabware(lt)
};
when(mockLwRepo.findByBarcodeIn(any())).thenReturn(Arrays.asList(lws));
final List<Labware> lwList = Arrays.asList(lws);
when(mockLwRepo.findByBarcodeIn(any())).thenReturn(lwList);
for (int i = 1; i < lws.length; ++i) {
Slot slot = lws[i].getFirstSlot();
slot.addSample(samples[0]);
Expand All @@ -78,34 +84,77 @@ void testLabwareRois() {
new Roi(slotIds[2], sampleIds[1], 11, "Delta")
);
when(mockRoiRepo.findAllBySlotIdIn(any())).thenReturn(rois);
doReturn(sampleMap).when(service).loadSamples(any(), any());
List<String> barcodes = List.of("STAN-1", "STAN-2");
List<LabwareRoi> lwRois = service.labwareRois(barcodes);

verify(mockLwRepo).findByBarcodeIn(barcodes);
verify(service).loadSamples(lwList, rois);
verify(mockRoiRepo).findAllBySlotIdIn(Arrays.stream(slotIds).boxed().collect(toSet()));

assertThat(lwRois).hasSameSizeAs(lws);
UCMap<LabwareRoi> lwRoiMap = UCMap.from(lwRois, LabwareRoi::getBarcode);
final Address A1 = new Address(1,1), A2 = new Address(1,2);
assertThat(lwRoiMap.get(lws[0].getBarcode()).getRois()).containsExactlyInAnyOrder(
new RoiResult(slotIds[0], A1, sampleIds[0], 10, "Alpha"),
new RoiResult(slotIds[1], A2, sampleIds[1], 10, "Beta")
new RoiResult(slotIds[0], A1, samples[0], 10, "Alpha"),
new RoiResult(slotIds[1], A2, samples[1], 10, "Beta")
);
assertThat(lwRoiMap.get(lws[1].getBarcode()).getRois()).containsExactlyInAnyOrder(
new RoiResult(slotIds[2], A1, sampleIds[0], 11, "Gamma"),
new RoiResult(slotIds[2], A1, sampleIds[1], 11, "Delta")
new RoiResult(slotIds[2], A1, samples[0], 11, "Gamma"),
new RoiResult(slotIds[2], A1, samples[1], 11, "Delta")
);
assertThat(lwRoiMap.get(lws[2].getBarcode()).getRois()).isEmpty();
}

@Test
public void testLoadSamples_noneMissing() {
Sample[] samples = EntityFactory.makeSamples(2);
LabwareType lt = EntityFactory.getTubeType();
Labware[] labware = {EntityFactory.makeEmptyLabware(lt), EntityFactory.makeEmptyLabware(lt)};
labware[0].getFirstSlot().addSample(samples[0]);
labware[1].getFirstSlot().addSample(samples[0]);
labware[1].getFirstSlot().addSample(samples[1]);
List<Roi> rois = List.of(
new Roi(100, samples[0].getId(), 200, "roi1"),
new Roi(101, samples[1].getId(), 201, "roi2")
);
Map<Integer, Sample> sampleMap = service.loadSamples(Arrays.asList(labware), rois);
verifyNoInteractions(mockSampleRepo);
assertThat(sampleMap).hasSize(samples.length);
Arrays.stream(samples).forEach(sam -> assertSame(sampleMap.get(sam.getId()), sam));
}

@Test
public void testLoadSamples_someMissing() {
Sample[] samples = EntityFactory.makeSamples(4);
LabwareType lt = EntityFactory.getTubeType();
Labware[] labware = {EntityFactory.makeEmptyLabware(lt), EntityFactory.makeEmptyLabware(lt)};
labware[0].getFirstSlot().addSample(samples[0]);
labware[0].getFirstSlot().addSample(samples[1]);
labware[1].getFirstSlot().addSample(samples[1]);
List<Roi> rois = List.of(
new Roi(100, samples[0].getId(), 200, "roi1"),
new Roi(101, samples[2].getId(), 201, "roi2"),
new Roi(102, samples[2].getId(), 202, "roi3"),
new Roi(103, samples[3].getId(), 203, "roi4")
);
when(mockSampleRepo.findAllByIdIn(any())).thenReturn(Arrays.asList(samples).subList(2,4));
Map<Integer, Sample> sampleMap = service.loadSamples(Arrays.asList(labware), rois);
verify(mockSampleRepo).findAllByIdIn(Set.of(samples[2].getId(), samples[3].getId()));
assertThat(sampleMap).hasSize(samples.length);
Arrays.stream(samples).forEach(sam -> assertSame(sampleMap.get(sam.getId()), sam));
}

@Test
void testToRoiResult() {
Labware lw = EntityFactory.makeEmptyLabware(EntityFactory.makeLabwareType(1,2));
Sample sample = EntityFactory.getSample();
Map<Integer, Sample> sampleIdMap = Map.of(sample.getId(), sample);
Map<Integer, Slot> slotIdMap = lw.getSlots().stream().collect(inMap(Slot::getId));
final Address A2 = new Address(1,2);
Slot slot = lw.getSlot(A2);
Roi roi = new Roi(slot.getId(), 1000, 2000, "Alpha");
RoiResult rr = service.toRoiResult(roi, slotIdMap);
assertEquals(new RoiResult(slot.getId(), A2, 1000, 2000, "Alpha"), rr);
Roi roi = new Roi(slot.getId(), sample.getId(), 2000, "Alpha");
RoiResult rr = service.toRoiResult(roi, slotIdMap, sampleIdMap);
assertEquals(new RoiResult(slot.getId(), A2, sample, 2000, "Alpha"), rr);
}
}
Loading