diff --git a/oan-etl/pom.xml b/oan-etl/pom.xml index 3406d09..ae6d99b 100644 --- a/oan-etl/pom.xml +++ b/oan-etl/pom.xml @@ -7,7 +7,7 @@ org.jax.oan ontology-annotation-network - 1.0.3-SNAPSHOT + 1.0.4 oan-etl diff --git a/oan-model/pom.xml b/oan-model/pom.xml index 09a5e0e..af921a8 100644 --- a/oan-model/pom.xml +++ b/oan-model/pom.xml @@ -4,7 +4,7 @@ org.jax.oan ontology-annotation-network - 1.0.3-SNAPSHOT + 1.0.4 oan-model diff --git a/oan-model/src/main/java/org/jax/oan/core/SupportedEntity.java b/oan-model/src/main/java/org/jax/oan/core/SupportedEntity.java index 9b611f2..ba208d3 100644 --- a/oan-model/src/main/java/org/jax/oan/core/SupportedEntity.java +++ b/oan-model/src/main/java/org/jax/oan/core/SupportedEntity.java @@ -31,7 +31,7 @@ public static SupportedEntity from(TermId termId) { return SupportedEntity.UNKNOWN; } - public static boolean isSupportedDownload(SupportedEntity entity, SupportedEntity type){ + public static boolean isLinkedType(SupportedEntity entity, SupportedEntity type){ return switch (entity) { case PHENOTYPE -> (type.equals(DISEASE) || type.equals(GENE)); case DISEASE -> (type.equals(PHENOTYPE) || type.equals(GENE)); diff --git a/oan-model/src/test/java/org/jax/oan/core/SupportedEntityTest.java b/oan-model/src/test/java/org/jax/oan/core/SupportedEntityTest.java index b1181d9..1747c37 100644 --- a/oan-model/src/test/java/org/jax/oan/core/SupportedEntityTest.java +++ b/oan-model/src/test/java/org/jax/oan/core/SupportedEntityTest.java @@ -44,12 +44,12 @@ private static Stream from() { @ParameterizedTest @MethodSource - void isSupportedDownload(SupportedEntity entity, SupportedEntity association) { - assertTrue(SupportedEntity.isSupportedDownload(entity, association)); - assertFalse(SupportedEntity.isSupportedDownload(entity, entity)); + void isLinkedType(SupportedEntity entity, SupportedEntity association) { + assertTrue(SupportedEntity.isLinkedType(entity, association)); + assertFalse(SupportedEntity.isLinkedType(entity, entity)); } - private static Stream isSupportedDownload() { + private static Stream isLinkedType() { return Stream.of( Arguments.of(SupportedEntity.GENE, SupportedEntity.PHENOTYPE), Arguments.of(SupportedEntity.PHENOTYPE, SupportedEntity.DISEASE), diff --git a/oan-rest/pom.xml b/oan-rest/pom.xml index b8e5962..d270937 100644 --- a/oan-rest/pom.xml +++ b/oan-rest/pom.xml @@ -4,7 +4,7 @@ org.jax.oan ontology-annotation-network - 1.0.3-SNAPSHOT + 1.0.4 oan-rest @@ -78,11 +78,6 @@ javax.ws.rs-api test - - io.micronaut - micronaut-http-client - test - io.micronaut.test micronaut-test-junit5 diff --git a/oan-rest/src/main/java/org/jax/oan/Application.java b/oan-rest/src/main/java/org/jax/oan/Application.java index f5d2b25..fd0d5cc 100644 --- a/oan-rest/src/main/java/org/jax/oan/Application.java +++ b/oan-rest/src/main/java/org/jax/oan/Application.java @@ -12,11 +12,11 @@ @OpenAPIDefinition( info = @Info( title = "ontology-annotation-network", - version = "1.0.3-SNAPSHOT", + version = "1.0.4", description = "A restful service for access to the ontology annotation network.", contact = @Contact(name = "Michael Gargano", email = "Michael.Gargano@jax.org") ), servers = {@Server(url = "https://ontology.jax.org/api/network", description = "Production Server URL") - // @Server(url = "http://localhost:8080/api/network", description = "Development Server URL") +// @Server(url = "http://localhost:8080/api/network", description = "Development Server URL") } ) public class Application { diff --git a/oan-rest/src/main/java/org/jax/oan/controller/AnnotationController.java b/oan-rest/src/main/java/org/jax/oan/controller/AnnotationController.java index df6d3da..87e18f9 100644 --- a/oan-rest/src/main/java/org/jax/oan/controller/AnnotationController.java +++ b/oan-rest/src/main/java/org/jax/oan/controller/AnnotationController.java @@ -8,7 +8,6 @@ import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.http.server.types.files.SystemFile; import io.micronaut.serde.annotation.SerdeImport; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import org.jax.oan.core.*; import org.jax.oan.exception.OntologyAnnotationNetworkException; @@ -84,7 +83,7 @@ public SystemFile download( TermId termId = TermId.of(id); SupportedEntity entity = SupportedEntity.from(termId); SupportedEntity downloadType = SupportedEntity.valueOf(type.toUpperCase()); - if (SupportedEntity.isSupportedDownload(entity, downloadType)){ + if (SupportedEntity.isLinkedType(entity, downloadType)){ return this.downloadService.associations(termId, entity, downloadType); } else { throw new HttpStatusException(HttpStatus.BAD_REQUEST, String.format("Downloading %s associations for %s is not supported.", entity, termId.getValue())); diff --git a/oan-rest/src/main/java/org/jax/oan/controller/SearchController.java b/oan-rest/src/main/java/org/jax/oan/controller/SearchController.java index 486f5c9..ab26bce 100644 --- a/oan-rest/src/main/java/org/jax/oan/controller/SearchController.java +++ b/oan-rest/src/main/java/org/jax/oan/controller/SearchController.java @@ -1,22 +1,38 @@ package org.jax.oan.controller; import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.PathVariable; import io.micronaut.http.annotation.QueryValue; +import io.micronaut.http.exceptions.HttpStatusException; +import io.micronaut.http.server.types.files.SystemFile; import io.micronaut.serde.annotation.SerdeImport; import io.swagger.v3.oas.annotations.media.Schema; +import org.jax.oan.core.Disease; import org.jax.oan.core.SearchDto; +import org.jax.oan.core.SupportedEntity; +import org.jax.oan.exception.OntologyAnnotationNetworkRuntimeException; +import org.jax.oan.service.DiseaseService; import org.jax.oan.service.SearchService; +import org.monarchinitiative.phenol.base.PhenolRuntimeException; +import org.monarchinitiative.phenol.ontology.data.TermId; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; @Controller("/search") @SerdeImport(SearchDto.class) +@SerdeImport(Disease.class) public class SearchController { - private SearchService searchService; + private final SearchService searchService; + private final DiseaseService diseaseService; - public SearchController(SearchService searchService) { + public SearchController(SearchService searchService, DiseaseService diseaseService) { this.searchService = searchService; + this.diseaseService = diseaseService; } @Get(uri="/{entity}", produces="application/json") @@ -34,7 +50,34 @@ public HttpResponse searchEntity(@Schema(minLength = 1, maxLength = 20, type } else if (entity.equalsIgnoreCase("DISEASE")){ return HttpResponse.ok(this.searchService.searchDisease(q.toUpperCase(), page, limit)); } else { - return HttpResponse.noContent(); + return HttpResponse.badRequest(); + } + } + + + /** + * This is our base controller for annotations that deals with different ontology term types + * and returns a defined annotation schema. + * @param entity the entity you care about with your list of phenotypes + * @param p the list of comma-seperated phenotype(hp) term ids + * @return an http response with the specific annotation schema based on the type + * @throws OntologyAnnotationNetworkRuntimeException which will be a 500 + */ + @Get(uri="/{entity}/intersect", produces="application/json") + public HttpResponse intersect( + @Schema(minLength = 1, maxLength = 20, type = "string", pattern = ".*", format = "string") @PathVariable String entity, + @Schema(minLength = 1, maxLength = 20000, type = "string", pattern = ".*", format = "string") @QueryValue String p) { + try { + Collection terms = Arrays.stream(p.split(",")).map(TermId::of).toList(); + SupportedEntity target = SupportedEntity.valueOf(entity.toUpperCase()); + if (SupportedEntity.isLinkedType(SupportedEntity.PHENOTYPE, target)){ + return HttpResponse.ok(this.diseaseService.findIntersectingByPhenotypes(terms)); + } else { + throw new HttpStatusException(HttpStatus.BAD_REQUEST, String.format("Intersecting %s associations for your phenotypes is not supported.", entity)); + } + } catch(PhenolRuntimeException | OntologyAnnotationNetworkRuntimeException e){ + throw new HttpStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } } + } diff --git a/oan-rest/src/main/java/org/jax/oan/service/DiseaseService.java b/oan-rest/src/main/java/org/jax/oan/service/DiseaseService.java index 986e39b..00a3206 100644 --- a/oan-rest/src/main/java/org/jax/oan/service/DiseaseService.java +++ b/oan-rest/src/main/java/org/jax/oan/service/DiseaseService.java @@ -1,22 +1,26 @@ package org.jax.oan.service; +import io.micronaut.serde.annotation.SerdeImport; import jakarta.inject.Singleton; import org.jax.oan.core.*; import org.jax.oan.exception.OntologyAnnotationNetworkException; import org.jax.oan.repository.DiseaseRepository; +import org.jax.oan.repository.PhenotypeRepository; import org.monarchinitiative.phenol.ontology.data.TermId; -import java.util.Collection; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Singleton +@SerdeImport(Disease.class) public class DiseaseService { private final DiseaseRepository diseaseRepository; + private final PhenotypeRepository phenotypeRepository; - public DiseaseService(DiseaseRepository diseaseRepository) { + public DiseaseService(DiseaseRepository diseaseRepository, PhenotypeRepository phenotypeRepository) { this.diseaseRepository = diseaseRepository; + this.phenotypeRepository = phenotypeRepository; } public DiseaseAnnotationDto findAll(TermId termId) throws OntologyAnnotationNetworkException { @@ -33,4 +37,24 @@ public DiseaseAnnotationDto findAll(TermId termId) throws OntologyAnnotationNetw } throw new OntologyAnnotationNetworkException(String.format("Could not find disease with id %s", termId.getValue())); } + + public Collection findIntersectingByPhenotypes(Collection termIds){ + List intersecting = new ArrayList<>(); + for (TermId id: termIds){ + try { + if (intersecting.isEmpty()){ + intersecting.addAll(phenotypeRepository.findDiseasesByTerm(id)); + } else { + Collection diseases = phenotypeRepository.findDiseasesByTerm(id); + intersecting = intersecting.stream().distinct() + .filter(diseases::contains) + .collect(Collectors.toList()); + } + + } catch (Exception ex) { + return Collections.emptyList(); + } + } + return intersecting.stream().distinct().toList(); + } } diff --git a/oan-rest/src/test/java/org/jax/oan/controller/SearchControllerTest.java b/oan-rest/src/test/java/org/jax/oan/controller/SearchControllerTest.java new file mode 100644 index 0000000..4c7dc80 --- /dev/null +++ b/oan-rest/src/test/java/org/jax/oan/controller/SearchControllerTest.java @@ -0,0 +1,92 @@ +package org.jax.oan.controller; + +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.test.annotation.MockBean; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.restassured.specification.RequestSpecification; +import jakarta.inject.Inject; +import org.jax.oan.core.Disease; +import org.jax.oan.core.Gene; +import org.jax.oan.core.SearchDto; +import org.jax.oan.service.DiseaseService; +import org.jax.oan.service.SearchService; +import org.junit.jupiter.api.Test; +import org.monarchinitiative.phenol.ontology.data.TermId; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@MicronautTest +public class SearchControllerTest { + @Inject + EmbeddedApplication application; + + @Inject + DiseaseService diseaseService; + + @Inject + SearchService searchService; + + @Test + void positive_search_by_gene(RequestSpecification spec){ + when(searchService.searchGene("TP53", 0,10)) + .thenReturn(new SearchDto(List.of(new Gene(TermId.of("NCBIGene:093232"), "tp53"), + new Gene(TermId.of("NCBIGene:093234"), "tp53b")), 2)); + spec.when().get("/api/network/search/gene?q=tp53").then() + .statusCode(200) + .body("results.id", hasItems("NCBIGene:093234", "NCBIGene:093232")) + .body("totalCount", equalTo(2)); + } + + @Test + void positive_search_by_disease(RequestSpecification spec){ + when(searchService.searchDisease("MARFAN", 0,10)) + .thenReturn(new SearchDto(List.of( + new Disease(TermId.of("OMIM:333333"), "Bad disease 1", "MONDO:000001", "no description"), + new Disease(TermId.of("OMIM:444444"), "Bad disease 2", "MONDO:000002", "funky description"), + new Disease(TermId.of("OMIM:555555"), "Bad disease 3", "MONDO:000003", "description")), 3)); + spec.when().get("/api/network/search/disease?q=marfan").then() + .statusCode(200) + .body("results.id", hasItems("OMIM:333333", "OMIM:444444")) + .body("totalCount", equalTo(3)); + } + + @Test + void negative_search_by_bad_entity(RequestSpecification spec){ + spec.when().get("/api/network/search/variant?q=chr1:60023-2300").then().statusCode(400); + spec.when().get("/api/network/search/phenotype?q=bighead").then().statusCode(400); + } + + + @Test + void postive_intersecting(RequestSpecification spec){ + when(diseaseService.findIntersectingByPhenotypes(List.of(TermId.of("HP:333333"), TermId.of("HP:44444")))) + .thenReturn(List.of( + new Disease(TermId.of("OMIM:333333"), "Bad disease 1", "MONDO:000001", "no description"), + new Disease(TermId.of("OMIM:444444"), "Bad disease 2", "MONDO:000002", "funky description"), + new Disease(TermId.of("OMIM:555555"), "Bad disease 3", "MONDO:000003", "description"))); + spec.when().get("/api/network/search/disease/intersect?p=HP:333333,HP:44444").then() + .statusCode(200) + .body("id", hasItems("OMIM:333333", "OMIM:444444")); + } + + @Test + void negative_intersecting(RequestSpecification spec){ + spec.when().get("/api/network/search/disease/intersect?q=chr1:60023-2300").then().statusCode(400); + spec.when().get("/api/network/search/phenotype?q=bighead").then().statusCode(400); + } + + @MockBean(DiseaseService.class) + DiseaseService diseaseService() { + return mock(DiseaseService.class); + } + + @MockBean(SearchService.class) + SearchService searchService() { + return mock(SearchService.class); + } +} diff --git a/oan-rest/src/test/java/org/jax/oan/repository/DiseaseRepositoryTest.java b/oan-rest/src/test/java/org/jax/oan/repository/DiseaseRepositoryTest.java index 9b461a7..a86d54f 100644 --- a/oan-rest/src/test/java/org/jax/oan/repository/DiseaseRepositoryTest.java +++ b/oan-rest/src/test/java/org/jax/oan/repository/DiseaseRepositoryTest.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; @@ -70,7 +71,7 @@ void findPhenotypesByDisease() { } @Test - void findDisease(){ + void findDiseases(){ Collection diseases = diseaseRepository.findDiseases("bad"); List expected = List.of( new Disease(TermId.of("OMIM:555555"), "Bad disease", "MONDO:0000001", "Rare disease"), @@ -79,4 +80,13 @@ void findDisease(){ assertTrue(diseases.containsAll(expected)); } + + @Test + void findDiseaseById(){ + Optional disease = diseaseRepository.findDiseaseById(TermId.of("OMIM:555555")); + Disease expected = new Disease(TermId.of("OMIM:555555"), "Bad disease", "MONDO:0000001", "Rare disease"); + + assertTrue(disease.isPresent()); + assertEquals(disease.get(), expected); + } } diff --git a/oan-rest/src/test/java/org/jax/oan/service/DiseaseServiceTest.java b/oan-rest/src/test/java/org/jax/oan/service/DiseaseServiceTest.java index f284b37..06bb958 100644 --- a/oan-rest/src/test/java/org/jax/oan/service/DiseaseServiceTest.java +++ b/oan-rest/src/test/java/org/jax/oan/service/DiseaseServiceTest.java @@ -6,10 +6,10 @@ import org.jax.oan.core.*; import org.jax.oan.exception.OntologyAnnotationNetworkException; import org.jax.oan.repository.DiseaseRepository; +import org.jax.oan.repository.PhenotypeRepository; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.stubbing.Answer; import org.monarchinitiative.phenol.ontology.data.TermId; import java.util.Collection; @@ -32,6 +32,9 @@ class DiseaseServiceTest { @Inject DiseaseService diseaseService; + @Inject + PhenotypeRepository phenotypeRepository; + @ParameterizedTest @MethodSource void test_find_all(TermId id, List genes, List phenotypes) throws OntologyAnnotationNetworkException { @@ -46,6 +49,17 @@ void test_find_all(TermId id, List genes, List phenotypes) thro .collect(Collectors.toList())); } + @ParameterizedTest + @MethodSource + void test_intersect_disease_phenotypes(List ids, List diseaseGroup1, + List diseaseGroup2, List expected){ + + when(phenotypeRepository.findDiseasesByTerm(ids.get(0))).thenReturn(diseaseGroup1); + when(phenotypeRepository.findDiseasesByTerm(ids.get(1))).thenReturn(diseaseGroup2); + Collection intersecting = diseaseService.findIntersectingByPhenotypes(ids); + assertEquals(expected, intersecting); + } + private static Stream test_find_all(){ return Stream.of( Arguments.of(TermId.of("OMIM:0392932"), List.of( @@ -62,9 +76,31 @@ private static Stream test_find_all(){ ); } + private static Stream test_intersect_disease_phenotypes(){ + return Stream.of( + Arguments.of(List.of(TermId.of("HP:0001252"), TermId.of("HP:0001249")), List.of( + new Disease(TermId.of("OMIM:333333"), "Bad disease 1", "MONDO:000001", "no description"), + new Disease(TermId.of("OMIM:444444"), "Bad disease 2", "MONDO:000002", "funky description"), + new Disease(TermId.of("OMIM:555555"), "Bad disease 3", "MONDO:000003", "description") + + ), List.of( + new Disease(TermId.of("OMIM:777777"), "Bad disease 5", "MONDO:000005", ""), + new Disease(TermId.of("OMIM:666666"), "Bad disease 4", "MONDO:000004", ""), + new Disease(TermId.of("OMIM:555555"), "Bad disease 3", "MONDO:000003", "description") + ),List.of( + new Disease(TermId.of("OMIM:555555"), "Bad disease 3", "MONDO:000003", "description") + )) + ); + } + @MockBean(DiseaseRepository.class) DiseaseRepository diseaseRepository() { return mock(DiseaseRepository.class); } + @MockBean(PhenotypeRepository.class) + PhenotypeRepository phenotypeRepository() { + return mock(PhenotypeRepository.class); + } + } diff --git a/pom.xml b/pom.xml index c47f7b8..aa07139 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.jax.oan ontology-annotation-network pom - 1.0.3-SNAPSHOT + 1.0.4 ontology-annotation-network https://github.com/TheJacksonLaboratory/ontology-annotation-network