diff --git a/exomiser-rest-prioritiser/README.md b/exomiser-rest-prioritiser/README.md new file mode 100644 index 000000000..194bfa0ff --- /dev/null +++ b/exomiser-rest-prioritiser/README.md @@ -0,0 +1,63 @@ +Exomiser Prioritiser REST API +=== + +Requirements +-- +The jar file built from this maven module, or pre-built versions can be found on GitHub e.g. +https://github.com/exomiser/Exomiser/releases/download/14.1.0/exomiser-rest-prioritiser-14.1.0.jar + +And a current version of the phenotype data. The data is updated a few times a year and the release announcements are +also on GitHub: https://github.com/exomiser/Exomiser/discussions/categories/data-release + +In this example we're using the 2410_phenotype data release which can be found here: +https://g-879a9f.f5dc97.75bc.dn.glob.us/data/2410_phenotype.zip + + +Setup +-- +This is a Spring Boot application, which means it can probably be configured to run the way you need for your setup. It +will require configuration either using a properties file, which you can find in +`src/main/resources/application.properties`. Alternatively, these can be provided as command-line arguments or environment +variables when launching the application. More info on configuration of Spring Boot applications can be found in their +[docs](https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.files) + +To set up on your local machine: +- Create a new directory called `exomiser` and download the jar, application.properties and zip files into this. Extract + the zip file so that you have should now have a `2410_phenotype` subfolder. + +- Edit the application.properties so that it looks like this: + ``` + exomiser.data-directory=full/path/to/your/new/exomiser/dir + exomiser.phenotype.data-version=2410 + ``` + You might need to delete the keys starting `info` - they will be present in the app, and you shouldn't need to change them. + +- In a terminal, launch the app using `java -jar exomiser-rest-prioritiser-14.1.0.jar` from the `exomiser` folder. You + should see a bunch of logging output which stops after a few seconds with these lines: + ``` + 2024-12-18T10:23:33.827Z INFO 452968 --- [exomiser-prioritiser-service] [ main] o.m.e.r.p.api.PrioritiserController : Started PrioritiserController with GeneIdentifier cache of 19762 entries + 2024-12-18T10:23:34.249Z INFO 452968 --- [exomiser-prioritiser-service] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator' + 2024-12-18T10:23:34.305Z INFO 452968 --- [exomiser-prioritiser-service] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8085 (http) with context path '/exomiser-prioritiser' + 2024-12-18T10:23:34.322Z INFO 452968 --- [exomiser-prioritiser-service] [ main] o.m.e.r.p.ExomiserPrioritiserServer : Started ExomiserPrioritiserServer in 6.056 seconds (process running for 6.676) + ``` + +It is now ready to use. Where you keep the jar and the data files is up to you. You just need to tell the application +where the data can be found using the full path to the parent directory where the data has been unpacked in the +application.properties. For example, if you unpacked the data to `/data/2410_phenotype` then`exomiser.data-directory=/data` +and `exomiser.phenotype.data-version=2410`. + +Note that if you have an existing Exomiser CLI installation, you can add this jar file to that directory and the REST +service will use the properties from the existing `application.properties`. Alternatively the `exomiser.data-directory` +and `exomiser.phenotype.data-version` can be supplied as command-line arguments when starting the jar without the need +for an `application.properties` file. + + +Running +--- +There is an OpenAPI 3 page which should be accessible here if everything went successfully: + +```shell +http://localhost:8085/exomiser-prioritiser/swagger-ui/index.html +``` + +This contains examples of the input parameters and the expected output. \ No newline at end of file diff --git a/exomiser-rest-prioritiser/pom.xml b/exomiser-rest-prioritiser/pom.xml index 83a45eb25..5dabb2ad1 100644 --- a/exomiser-rest-prioritiser/pom.xml +++ b/exomiser-rest-prioritiser/pom.xml @@ -52,6 +52,12 @@ org.springframework.boot spring-boot-starter-actuator + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + org.springframework.restdocs spring-restdocs-mockmvc diff --git a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/ExomiserPrioritiserServer.java b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/ExomiserPrioritiserServer.java index 1aeeec800..b20e65743 100644 --- a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/ExomiserPrioritiserServer.java +++ b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/ExomiserPrioritiserServer.java @@ -20,6 +20,8 @@ package org.monarchinitiative.exomiser.rest.prioritiser; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; import org.monarchinitiative.exomiser.autoconfigure.ExomiserAutoConfiguration; import org.monarchinitiative.exomiser.autoconfigure.genome.GenomeAnalysisServiceAutoConfiguration; import org.springframework.boot.SpringApplication; @@ -34,6 +36,13 @@ ExomiserAutoConfiguration.class, GenomeAnalysisServiceAutoConfiguration.class }) +@OpenAPIDefinition( + info = @Info( + title = "Exomiser Prioritiser API", + version = "1.0.0", + description = "API for prioritising genes based on phenotype semantic similarity") + +) public class ExomiserPrioritiserServer { public static void main(String[] args) { diff --git a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserController.java b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserController.java index 8a76a571c..e1f557ecf 100644 --- a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserController.java +++ b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserController.java @@ -20,63 +20,99 @@ package org.monarchinitiative.exomiser.rest.prioritiser.api; -import org.monarchinitiative.exomiser.core.model.Gene; -import org.monarchinitiative.exomiser.core.model.GeneIdentifier; -import org.monarchinitiative.exomiser.core.prioritisers.HiPhiveOptions; -import org.monarchinitiative.exomiser.core.prioritisers.Prioritiser; -import org.monarchinitiative.exomiser.core.prioritisers.PriorityFactory; -import org.monarchinitiative.exomiser.core.prioritisers.PriorityResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.monarchinitiative.exomiser.rest.prioritiser.service.PrioritiserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; -import java.io.IOException; -import java.time.Duration; -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static com.google.common.collect.ImmutableList.toImmutableList; +import java.util.Set; /** * @author Jules Jacobsen */ @RestController +@RequestMapping("api/v1/prioritise") +@Tag(name = "Prioritiser", description = "API endpoints for phenotype-based gene prioritisation") public class PrioritiserController { private static final Logger logger = LoggerFactory.getLogger(PrioritiserController.class); - private final Map geneIdentifiers; - private final PriorityFactory priorityFactory; + private final PrioritiserService prioritiserService; @Autowired - public PrioritiserController(Map geneIdentifiers, PriorityFactory priorityFactory) { - this.geneIdentifiers = geneIdentifiers; - this.priorityFactory = priorityFactory; - logger.info("Started PrioritiserController with GeneIdentifier cache of {} entries", geneIdentifiers.size()); - } - - @GetMapping(value = "/about") - public String about() { - byte[] bytes = new byte[0]; - try { - bytes = new ClassPathResource("about.html").getInputStream().readAllBytes(); - } catch (IOException e) { - logger.error("", e); - } - return new String(bytes); + public PrioritiserController(PrioritiserService prioritiserService) { + this.prioritiserService = prioritiserService; } - @GetMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE) - public PrioritiserResultSet prioritise(@RequestParam(value = "phenotypes") Set phenotypes, - @RequestParam(value = "genes", required = false, defaultValue = "") Set genesIds, - @RequestParam(value = "prioritiser") String prioritiserName, - @RequestParam(value = "prioritiser-params", required = false, defaultValue = "") String prioritiserParams, - @RequestParam(value = "limit", required = false, defaultValue = "0") Integer limit + @Operation( + summary = "Prioritise genes by phenotype", + description = "Prioritises genes based on provided phenotypes and other parameters" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Successfully prioritised genes", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PrioritiserResultSet.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid input parameters" + ) + }) + @GetMapping(value = "gene", produces = MediaType.APPLICATION_JSON_VALUE) + public PrioritiserResultSet prioritiseGenes( + @Parameter( + description = "Set of HPO phenotype identifiers", + example = "[\"HP:0001156\", \"HP:0001363\", \"HP:0011304\", \"HP:0010055\"]", + required = true + ) + @RequestParam(value = "phenotypes") Set phenotypes, + + @Parameter( + description = "Set of NCBI gene IDs to consider in prioritisation", + example = "[2263, 2264]", + required = false + ) + @RequestParam(value = "genes", required = false, defaultValue = "") Set genesIds, + + @Parameter( + description = "Name of the prioritiser algorithm to use. One of ['hiphive', 'phenix', 'phive']. " + + "Defaults to 'hiphive' which allows for cross-species and PPI hits. 'phenix' is a" + + " legacy prioritiser which will only prioritise human disease-gene associations. It is" + + " the equivalent of 'hiphive' with prioritiser-params='human'. 'phive' is just the" + + " mouse subset of hiphive, equivalent to 'hiphive' with prioritiser-params='mouse'.", + example = "hiphive", + required = false + ) + @RequestParam(value = "prioritiser", defaultValue = "hiphive") String prioritiserName, + + @Parameter( + description = "Additional parameters for the prioritiser. This is optional for the 'hiphive' prioritiser." + + " values can be at least one of 'human,mouse,fish,ppi'. Will default to all, however" + + " just 'human' will restrict matches to known human disease-gene associations.", + example = "human", + required = false + ) + @RequestParam(value = "prioritiser-params", required = false, defaultValue = "") String prioritiserParams, + + @Parameter( + description = "Maximum number of results to return (0 for unlimited)", + required = false, + example = "20" + ) + @RequestParam(value = "limit", required = false, defaultValue = "0") Integer limit ) { PrioritiserRequest prioritiserRequest = PrioritiserRequest.builder() .prioritiser(prioritiserName) @@ -86,79 +122,40 @@ public PrioritiserResultSet prioritise(@RequestParam(value = "phenotypes") Set prioritiser = parsePrioritiser(prioritiserRequest.getPrioritiser(), prioritiserRequest - .getPrioritiserParams()); - List genes = makeGenesFromIdentifiers(prioritiserRequest.getGenes()); - - List results = runLimitAndCollectResults(prioritiser, prioritiserRequest.getPhenotypes(), genes, prioritiserRequest - .getLimit()); - - Instant end = Instant.now(); - Duration duration = Duration.between(start, end); - - return new PrioritiserResultSet(prioritiserRequest, duration.toMillis(), results); - } - - private Prioritiser parsePrioritiser(String prioritiserName, String prioritiserParams) { - switch (prioritiserName) { - case "phenix": - return priorityFactory.makePhenixPrioritiser(); - case "phive": - return priorityFactory.makePhivePrioritiser(); - case "hiphive": - default: - HiPhiveOptions hiPhiveOptions = HiPhiveOptions.builder() - .runParams(prioritiserParams) - .build(); - return priorityFactory.makeHiPhivePrioritiser(hiPhiveOptions); - } - } - - private List makeGenesFromIdentifiers(Collection genesIds) { - if (genesIds.isEmpty()) { - logger.info("Gene identifiers not specified - will compare against all known genes."); - //If not specified, we'll assume they want to use the whole genome. Should save people a lot of typing. - //n.b. Gene is mutable so these can't be cached and returned. - return allGenes(); - } - // This is a hack - really the Prioritiser should only work on GeneIds, but currently this isn't possible as - // OmimPrioritiser uses some properties of Gene - return genesIds.stream() - .map(id -> new Gene(geneIdentifiers.getOrDefault(id, unrecognisedGeneIdentifier(id)))) - .collect(toImmutableList()); - } - - private List allGenes() { - return geneIdentifiers.values().parallelStream() - .map(Gene::new) - .collect(toImmutableList()); - } - - private GeneIdentifier unrecognisedGeneIdentifier(Integer id) { - return GeneIdentifier.builder().geneSymbol("GENE:" + id).build(); - } - - private List runLimitAndCollectResults(Prioritiser prioritiser, List phenotypes, List genes, int limit) { - Set wantedGeneIds = genes.stream().map(Gene::getEntrezGeneID).collect(Collectors.toSet()); - - Stream resultsStream = prioritiser.prioritise(phenotypes, genes) - .filter(result -> wantedGeneIds.contains(result.getGeneId())) - .sorted(Comparator.naturalOrder()); - - logger.info("Finished {}", prioritiser.getPriorityType()); - if (limit == 0) { - return resultsStream.collect(toImmutableList()); - } - return resultsStream.limit(limit).collect(toImmutableList()); + @Operation( + summary = "Prioritise genes using POST request", + description = "Prioritises genes based on provided request body containing phenotypes and configuration" + ) + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Successfully prioritised genes", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PrioritiserResultSet.class) + ) + ), + @ApiResponse( + responseCode = "400", + description = "Invalid request body" + ) + }) + @PostMapping( + value = "gene", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + public PrioritiserResultSet prioritiseGenes( + @Parameter( + description = "Prioritisation request parameters", + required = true + ) + @RequestBody PrioritiserRequest prioritiserRequest + ) { + return prioritiserService.prioritiseGenes(prioritiserRequest); } } diff --git a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserRequest.java b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserRequest.java index bb569582b..61e63f94e 100644 --- a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserRequest.java +++ b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserRequest.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import io.swagger.v3.oas.annotations.media.Schema; import java.util.ArrayList; import java.util.Collection; @@ -33,57 +34,59 @@ * @since 12.1.0 */ @JsonDeserialize(builder = PrioritiserRequest.Builder.class) -public class PrioritiserRequest { - - private final List phenotypes; - private final List genes; - private final String prioritiser; - private final String prioritiserParams; - private final int limit; - - private PrioritiserRequest(Builder builder) { - this.phenotypes = builder.phenotypes.stream().distinct().toList(); - this.genes = builder.genes.stream().distinct().toList(); - this.prioritiser = builder.prioritiser; - this.prioritiserParams = builder.prioritiserParams; - this.limit = builder.limit; - } - - public List getPhenotypes() { - return phenotypes; - } - - public List getGenes() { - return genes; - } - - public String getPrioritiser() { - return prioritiser; - } - - public String getPrioritiserParams() { - return prioritiserParams; - } - - public int getLimit() { - return limit; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof PrioritiserRequest)) return false; - PrioritiserRequest that = (PrioritiserRequest) o; - return limit == that.limit && - phenotypes.equals(that.phenotypes) && - genes.equals(that.genes) && - prioritiser.equals(that.prioritiser) && - prioritiserParams.equals(that.prioritiserParams); - } - - @Override - public int hashCode() { - return Objects.hash(phenotypes, genes, prioritiser, prioritiserParams, limit); +@Schema(description = "Request parameters for gene prioritisation") +public record PrioritiserRequest( + @Schema( + description = "Set of HPO phenotype identifiers", + example = "[\"HP:0001156\", \"HP:0001363\", \"HP:0011304\", \"HP:0010055\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + List phenotypes, + + @Schema( + description = "Set of NCBI gene IDs to consider in prioritisation", + example = "[2263, 2264]", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + List genes, + + @Schema( + description = "Name of the prioritiser algorithm to use. One of ['hiphive', 'phenix', 'phive']. " + + "Defaults to 'hiphive' which allows for cross-species and PPI hits. 'phenix' is a" + + " legacy prioritiser which will only prioritise human disease-gene associations. It is" + + " the equivalent of 'hiphive' with prioritiser-params='human'. 'phive' is just the" + + " mouse subset of hiphive, equivalent to 'hiphive' with prioritiser-params='mouse'.", + example = "hiphive", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + String prioritiser, + + @Schema( + description = "Additional parameters for the prioritiser. This is optional for the 'hiphive' prioritiser." + + " values can be at least one of 'human,mouse,fish,ppi'. Will default to all, however" + + " just 'human' will restrict matches to known human disease-gene associations.", + example = "human", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + String prioritiserParams, + + @Schema( + description = "Maximum number of results to return (0 for unlimited)", + example = "20", + defaultValue = "0", + requiredMode = Schema.RequiredMode.NOT_REQUIRED + ) + int limit +) { + + static PrioritiserRequest from(Builder builder) { + Objects.requireNonNull(builder); + return new PrioritiserRequest( + builder.phenotypes.stream().distinct().toList(), + builder.genes.stream().distinct().toList(), + builder.prioritiser, + builder.prioritiserParams, + builder.limit); } @Override @@ -105,7 +108,7 @@ public static Builder builder() { public static class Builder { private Collection phenotypes = new ArrayList<>(); private Collection genes = new ArrayList<>(); - private String prioritiser = ""; + private String prioritiser = "hiphive"; private String prioritiserParams = ""; private int limit; @@ -135,7 +138,7 @@ public Builder limit(int limit) { } public PrioritiserRequest build() { - return new PrioritiserRequest(this); + return PrioritiserRequest.from(this); } } } diff --git a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserResultSet.java b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserResultSet.java index 54e30e230..5ec88d08a 100644 --- a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserResultSet.java +++ b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserResultSet.java @@ -27,28 +27,4 @@ /** * @author Jules Jacobsen */ -public class PrioritiserResultSet { - - final PrioritiserRequest params; - final long queryTime; - final List results; - - public PrioritiserResultSet(PrioritiserRequest params, long queryTime, List results) { - this.params = params; - this.queryTime = queryTime; - this.results = results; - } - - public PrioritiserRequest getParams() { - return params; - } - - public long getQueryTime() { - return queryTime; - } - - public List getResults() { - return results; - } - -} +public record PrioritiserResultSet(PrioritiserRequest params, long queryTime, List results) {} diff --git a/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/service/PrioritiserService.java b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/service/PrioritiserService.java new file mode 100644 index 000000000..954ae9067 --- /dev/null +++ b/exomiser-rest-prioritiser/src/main/java/org/monarchinitiative/exomiser/rest/prioritiser/service/PrioritiserService.java @@ -0,0 +1,96 @@ +package org.monarchinitiative.exomiser.rest.prioritiser.service; + +import org.monarchinitiative.exomiser.core.model.Gene; +import org.monarchinitiative.exomiser.core.model.GeneIdentifier; +import org.monarchinitiative.exomiser.core.prioritisers.HiPhiveOptions; +import org.monarchinitiative.exomiser.core.prioritisers.Prioritiser; +import org.monarchinitiative.exomiser.core.prioritisers.PriorityFactory; +import org.monarchinitiative.exomiser.core.prioritisers.PriorityResult; +import org.monarchinitiative.exomiser.rest.prioritiser.api.PrioritiserRequest; +import org.monarchinitiative.exomiser.rest.prioritiser.api.PrioritiserResultSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Component +public record PrioritiserService(Map geneIdentifiers, PriorityFactory priorityFactory) { + + private static final Logger logger = LoggerFactory.getLogger(PrioritiserService.class); + + public PrioritiserService { + logger.info("Started PrioritiserService with GeneIdentifier cache of {} entries", geneIdentifiers.size()); + } + + public PrioritiserResultSet prioritiseGenes(PrioritiserRequest prioritiserRequest){ + logger.info("{}", prioritiserRequest); + + Instant start = Instant.now(); + + Prioritiser prioritiser = parsePrioritiser(prioritiserRequest.prioritiser(), prioritiserRequest + .prioritiserParams()); + List genes = makeGenesFromIdentifiers(prioritiserRequest.genes()); + + List results = runLimitAndCollectResults(prioritiser, prioritiserRequest.phenotypes(), genes, prioritiserRequest + .limit()); + + Instant end = Instant.now(); + Duration duration = Duration.between(start, end); + + return new PrioritiserResultSet(prioritiserRequest, duration.toMillis(), results); + } + + private Prioritiser parsePrioritiser(String prioritiserName, String prioritiserParams) { + return switch (prioritiserName) { + case "phenix" -> priorityFactory.makePhenixPrioritiser(); + case "phive" -> priorityFactory.makePhivePrioritiser(); + default -> { + HiPhiveOptions hiPhiveOptions = HiPhiveOptions.builder() + .runParams(prioritiserParams) + .build(); + yield priorityFactory.makeHiPhivePrioritiser(hiPhiveOptions); + } + }; + } + + private List makeGenesFromIdentifiers(Collection genesIds) { + if (genesIds.isEmpty()) { + logger.info("Gene identifiers not specified - will compare against all known genes."); + //If not specified, we'll assume they want to use the whole genome. Should save people a lot of typing. + //n.b. Gene is mutable so these can't be cached and returned. + return allGenes(); + } + // This is a hack - really the Prioritiser should only work on GeneIds, but currently this isn't possible as + // OmimPrioritiser uses some properties of Gene + return genesIds.stream() + .map(id -> new Gene(geneIdentifiers.getOrDefault(id, unrecognisedGeneIdentifier(id)))) + .toList(); + } + + private List allGenes() { + return geneIdentifiers.values().parallelStream() + .map(Gene::new) + .toList(); + } + + private GeneIdentifier unrecognisedGeneIdentifier(Integer id) { + return GeneIdentifier.builder().geneSymbol("GENE:" + id).build(); + } + + @SuppressWarnings("unchecked") + private List runLimitAndCollectResults(Prioritiser prioritiser, List phenotypes, List genes, int limit) { + Set wantedGeneIds = genes.stream().map(Gene::getEntrezGeneID).collect(Collectors.toSet()); + + Stream resultsStream = prioritiser.prioritise(phenotypes, genes) + .filter(result -> wantedGeneIds.contains(result.getGeneId())) + .sorted(Comparator.naturalOrder()); + + return limit == 0 ? (List) resultsStream.toList() : (List) resultsStream.limit(limit).toList(); + } + +} diff --git a/exomiser-rest-prioritiser/src/main/resources/about.html b/exomiser-rest-prioritiser/src/main/resources/about.html index 7b6c7c293..a766b6549 100644 --- a/exomiser-rest-prioritiser/src/main/resources/about.html +++ b/exomiser-rest-prioritiser/src/main/resources/about.html @@ -40,8 +40,8 @@

It supports either GET:

- - http://localhost:8085/exomiser/api/prioritise/?phenotypes=HP:0001156,HP:0001363,HP:0011304,HP:0010055&prioritiser=hiphive&limit=10&prioritiser-params=human + + http://localhost:8085/exomiser-prioritiser/api/v1/prioritise/?phenotypes=HP:0001156,HP:0001363,HP:0011304,HP:0010055&prioritiser=hiphive&limit=10&prioritiser-params=human

or POST: diff --git a/exomiser-rest-prioritiser/src/main/resources/application.properties b/exomiser-rest-prioritiser/src/main/resources/application.properties index ec319ab77..788f69e53 100644 --- a/exomiser-rest-prioritiser/src/main/resources/application.properties +++ b/exomiser-rest-prioritiser/src/main/resources/application.properties @@ -19,7 +19,7 @@ # spring.application.name=exomiser-prioritiser-service -server.servlet.context-path=/exomiser/api/prioritise +server.servlet.context-path=/exomiser-prioritiser server.port=8085 server.servlet.application-display-name=Exomiser Prioritiser Server @@ -33,4 +33,21 @@ info.name=${server.display-name} info.build.version=${project.version} info.build.timestamp=${build.timestamp} +# Customize the OpenAPI path +# these will be present at: +# host:{server.port}/{server.servlet.context-path}/{springdoc.api-docs.path} +# host:{server.port}/{server.servlet.context-path}/{springdoc.swagger-ui.path} +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui +# Enable or disable API docs +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true +# Show actuator endpoints in OpenApi-UI +springdoc.show-actuator=false +# This property enables the openapi and swagger-ui endpoints to be exposed beneath the actuator base path. +management.endpoints.web.exposure.include=openapi, swagger-ui + +# Sort by HTTP method +springdoc.swagger-ui.operationsSorter=method + logging.level.com.zaxxer.hikari=ERROR \ No newline at end of file diff --git a/exomiser-rest-prioritiser/src/test/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserControllerTest.java b/exomiser-rest-prioritiser/src/test/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserControllerTest.java new file mode 100644 index 000000000..d966fd87e --- /dev/null +++ b/exomiser-rest-prioritiser/src/test/java/org/monarchinitiative/exomiser/rest/prioritiser/api/PrioritiserControllerTest.java @@ -0,0 +1,239 @@ +package org.monarchinitiative.exomiser.rest.prioritiser.api; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.*; +import org.monarchinitiative.exomiser.core.prioritisers.PriorityResult; +import org.monarchinitiative.exomiser.core.prioritisers.PriorityType; +import org.monarchinitiative.exomiser.rest.prioritiser.service.PrioritiserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(PrioritiserController.class) +class PrioritiserControllerTest { + + private static final String API_V_1_PRIORITISE_GENE = "/api/v1/prioritise/gene"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private PrioritiserService prioritiserService; + + private PrioritiserResultSet sampleResultSet; + private PrioritiserRequest sampleRequest; + + @BeforeEach + void setUp() { + // Create sample request data + sampleRequest = new PrioritiserRequest( + List.of("HP:0001250", "HP:0001251"), + List.of(1234, 5678), + "hiphive", + "human,mouse,ppi", + 10 + ); + + // Create sample response data + List results = List.of( + new MockPriorityResult(PriorityType.HIPHIVE_PRIORITY, 1234, "", 0.95), + new MockPriorityResult(PriorityType.HIPHIVE_PRIORITY, 5678, "", 0.85) + ); + + sampleResultSet = new PrioritiserResultSet( + sampleRequest, + 100L, // queryTime + results + ); + } + + private record MockPriorityResult(PriorityType priorityType, int geneId, String geneSymbol, + double score) implements PriorityResult { + @Override + public int getGeneId() { + return geneId; + } + + @Override + public String getGeneSymbol() { + return geneSymbol; + } + + @Override + public double getScore() { + return score; + } + + @Override + public PriorityType getPriorityType() { + return priorityType; + } + } + + @Nested + @DisplayName("GET prioritisation endpoint tests") + class GetPrioritisationTests { + + @Test + @DisplayName("Should return prioritised results with valid parameters") + void shouldReturnPrioritisedResults() throws Exception { + when(prioritiserService.prioritiseGenes(any())) + .thenReturn(sampleResultSet); + + mockMvc.perform(get(API_V_1_PRIORITISE_GENE) + .param("phenotypes", "HP:0001250,HP:0001251") + .param("genes", "1234,5678") + .param("prioritiser", "phenix") + .param("prioritiser-params", "{\"key\":\"value\"}") + .param("limit", "10") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results", hasSize(2))) + .andExpect(jsonPath("$.results[0].geneId", is(1234))) + .andExpect(jsonPath("$.results[0].score", is(0.95))) + .andExpect(jsonPath("$.queryTime", is(100))); + } + + @Test + @DisplayName("Should handle missing optional parameters") + void shouldHandleMissingOptionalParams() throws Exception { + when(prioritiserService.prioritiseGenes(any())) + .thenReturn(sampleResultSet); + + mockMvc.perform(get(API_V_1_PRIORITISE_GENE) + .param("phenotypes", "HP:0001250") + .param("prioritiser", "phenix") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results").exists()); + } + + @Test + @DisplayName("Should return 400 when required parameters are missing") + void shouldReturn400WhenMissingRequiredParams() throws Exception { + mockMvc.perform(get(API_V_1_PRIORITISE_GENE) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("POST prioritisation endpoint tests") + class PostPrioritisationTests { + + @Test + @DisplayName("Should process valid POST request") + void shouldProcessValidPostRequest() throws Exception { + when(prioritiserService.prioritiseGenes(any(PrioritiserRequest.class))) + .thenReturn(sampleResultSet); + + mockMvc.perform(post(API_V_1_PRIORITISE_GENE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleRequest)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.params.phenotypes", hasSize(2))) + .andExpect(jsonPath("$.results", hasSize(2))) + .andExpect(jsonPath("$.queryTime", is(100))); + } + + @Test + @DisplayName("Should handle POST request with minimal required fields") + void shouldHandleMinimalPostRequest() throws Exception { + PrioritiserRequest minimalRequest = new PrioritiserRequest( + List.of("HP:0001250"), + List.of(), + "hiphive", + "", + 0 + ); + + when(prioritiserService.prioritiseGenes(any(PrioritiserRequest.class))) + .thenReturn(new PrioritiserResultSet(minimalRequest, 50L, List.of())); + + mockMvc.perform(post(API_V_1_PRIORITISE_GENE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(minimalRequest)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.params.phenotypes", hasSize(1))) + .andExpect(jsonPath("$.results", hasSize(0))); + } + + @Test + @DisplayName("Should return 400 for invalid request body") + void shouldReturn400ForInvalidRequestBody() throws Exception { + String invalidJson = "{\"phenotypes\": null, \"prioritiser\": null}"; + + mockMvc.perform(post(API_V_1_PRIORITISE_GENE) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Should handle empty results") + void shouldHandleEmptyResults() throws Exception { + PrioritiserResultSet emptyResultSet = new PrioritiserResultSet( + sampleRequest, + 50L, + List.of() + ); + + when(prioritiserService.prioritiseGenes(any(PrioritiserRequest.class))) + .thenReturn(emptyResultSet); + + mockMvc.perform(post(API_V_1_PRIORITISE_GENE) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(sampleRequest)) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.results", hasSize(0))); + } + } + + @Disabled + @Nested + @DisplayName("OpenAPI documentation endpoint tests") + class OpenApiEndpointTests { + + @Test + @DisplayName("Should serve OpenAPI documentation JSON") + void shouldServeOpenApiDocs() throws Exception { + mockMvc.perform(get("/api-docs")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.openapi", is("3.0.1"))) + .andExpect(jsonPath("$.info.title", is("Prioritiser"))) + .andExpect(jsonPath("$.paths.api.v1", notNullValue())) + .andExpect(jsonPath("$.paths.api.v1.prioritise", notNullValue())) + ; + } + + @Test + @DisplayName("Should serve Swagger UI page") + void shouldServeSwaggerUi() throws Exception { + mockMvc.perform(get("/swagger-ui/index.html")) + .andExpect(status().isOk()) + .andExpect(content().contentType("text/html")) + .andExpect(content().string(containsString("swagger-ui"))); + } + } + +} \ No newline at end of file diff --git a/exomiser-rest-prioritiser/src/test/resources/application.properties b/exomiser-rest-prioritiser/src/test/resources/application.properties index b12344241..e30bbfbb2 100644 --- a/exomiser-rest-prioritiser/src/test/resources/application.properties +++ b/exomiser-rest-prioritiser/src/test/resources/application.properties @@ -24,3 +24,10 @@ server.display-name=Exomiser Prioritiser Server exomiser.data-directory=${project.build.testOutputDirectory}/data exomiser.h2.url=jdbc:h2:mem:exomiser + + +springdoc.api-docs.path=/api-docs +springdoc.swagger-ui.path=/swagger-ui +# Enable or disable API docs +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true \ No newline at end of file