Skip to content
This repository has been archived by the owner on May 23, 2024. It is now read-only.

Commit

Permalink
Check for unsupported constraints (#566)
Browse files Browse the repository at this point in the history
This PR adds TRAPI Tests to ensure that CAM-KP-API responds correctly
to:
- An attribute constraint it does not understand
- A qualifier constraint it does not understand (as per PR
NCATSTranslator/ReasonerAPI#364)

Closes #565
  • Loading branch information
gaurav authored Aug 29, 2022
2 parents 82ca402 + ca4d5d2 commit 0956c32
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 35 deletions.
114 changes: 82 additions & 32 deletions src/main/scala/org/renci/cam/QueryService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,40 +259,90 @@ object QueryService extends LazyLogging {
* @param submittedQueryGraph
* The query graph to search the triplestore with.
* @return
* A TRAPIMessage displaying the results.
* A TRAPIResponse to return to the client.
*/
def run(limit: Int, submittedQueryGraph: TRAPIQueryGraph)
: RIO[ZConfig[AppConfig] with HttpClient with Has[BiolinkData] with Has[SPARQLCache], TRAPIMessage] =
for {
// Get the Biolink data.
biolinkData <- biolinkData
_ = logger.debug("limit: {}", limit)

// Prepare the query graph for processing.
queryGraph = enforceQueryEdgeTypes(submittedQueryGraph, biolinkData.predicates)

// Generate the relationsToLabelAndBiolinkPredicate.
allPredicatesInQuery = queryGraph.edges.values.flatMap(_.predicates.getOrElse(Nil)).to(Set)
predicatesToRelations <- mapQueryBiolinkPredicatesToRelations(allPredicatesInQuery)
allRelationsInQuery = predicatesToRelations.values.flatten.to(Set)
relationsToLabelAndBiolinkPredicate <- mapRelationsToLabelAndBiolink(allRelationsInQuery)

// Generate query solutions.
_ = logger.debug(s"findInitialQuerySolutions($queryGraph, $predicatesToRelations, $limit)")
initialQuerySolutions <- findInitialQuerySolutions(queryGraph, predicatesToRelations, limit)
results = initialQuerySolutions.zipWithIndex.map { case (qs, index) =>
Result.fromQuerySolution(qs, index, queryGraph)
}
_ = logger.debug(s"Results: $results")

// From the results, generate the TRAPI nodes, edges and results.
nodes <- generateTRAPINodes(results)
_ = logger.debug(s"Nodes: $nodes")
edges <- generateTRAPIEdges(results, relationsToLabelAndBiolinkPredicate)
_ = logger.debug(s"Edges: $edges")
trapiResults = generateTRAPIResults(results)
_ = logger.debug(s"Results: $trapiResults")
} yield TRAPIMessage(Some(queryGraph), Some(TRAPIKnowledgeGraph(nodes, edges)), Some(trapiResults.distinct))
: RIO[ZConfig[AppConfig] with HttpClient with Has[BiolinkData] with Has[SPARQLCache], TRAPIResponse] = {
val emptyTRAPIMessage = TRAPIMessage(Some(submittedQueryGraph), None, Some(List()))

val allAttributeConstraints = submittedQueryGraph.nodes.values.flatMap(
_.constraints.getOrElse(List())) ++ submittedQueryGraph.edges.values.flatMap(_.attribute_constraints.getOrElse(List()))
val allQualifierConstraints = submittedQueryGraph.edges.values.flatMap(_.qualifier_constraints.getOrElse(List()))

if (allAttributeConstraints.nonEmpty) {
ZIO.succeed(
TRAPIResponse(
emptyTRAPIMessage,
Some("UnsupportedAttributeConstraint"),
None,
Some(
List(
LogEntry(
Some(java.time.Instant.now().toString),
Some("ERROR"),
Some("UnsupportedAttributeConstraint"),
Some(s"The following attributes are not supported: ${allAttributeConstraints}")
)
)
)
)
)
} else if (allQualifierConstraints.nonEmpty) {
// Are there any qualifier constraints? If so, we can't match them, so we should return an empty list of results.
ZIO.succeed(
TRAPIResponse(
emptyTRAPIMessage,
Some("Success"),
None,
Some(
List(
LogEntry(
Some(java.time.Instant.now().toString),
Some("WARNING"),
Some("UnsupportedQualifierConstraint"),
Some(s"The following qualifier constraints are not supported: ${allQualifierConstraints}")
)
)
)
)
)
} else
for {
// Get the Biolink data.
biolinkData <- biolinkData
_ = logger.debug("limit: {}", limit)

// Prepare the query graph for processing.
queryGraph = enforceQueryEdgeTypes(submittedQueryGraph, biolinkData.predicates)

// Generate the relationsToLabelAndBiolinkPredicate.
allPredicatesInQuery = queryGraph.edges.values.flatMap(_.predicates.getOrElse(Nil)).to(Set)
predicatesToRelations <- mapQueryBiolinkPredicatesToRelations(allPredicatesInQuery)
allRelationsInQuery = predicatesToRelations.values.flatten.to(Set)
relationsToLabelAndBiolinkPredicate <- mapRelationsToLabelAndBiolink(allRelationsInQuery)

// Generate query solutions.
_ = logger.debug(s"findInitialQuerySolutions($queryGraph, $predicatesToRelations, $limit)")
initialQuerySolutions <- findInitialQuerySolutions(queryGraph, predicatesToRelations, limit)
results = initialQuerySolutions.zipWithIndex.map { case (qs, index) =>
Result.fromQuerySolution(qs, index, queryGraph)
}
_ = logger.debug(s"Results: $results")

// From the results, generate the TRAPI nodes, edges and results.
nodes <- generateTRAPINodes(results)
_ = logger.debug(s"Nodes: $nodes")
edges <- generateTRAPIEdges(results, relationsToLabelAndBiolinkPredicate)
_ = logger.debug(s"Edges: $edges")
trapiResults = generateTRAPIResults(results)
_ = logger.debug(s"Results: $trapiResults")
} yield TRAPIResponse(
TRAPIMessage(Some(queryGraph), Some(TRAPIKnowledgeGraph(nodes, edges)), Some(trapiResults.distinct)),
Some("Success"),
None,
None
)
}

def oldRun(limit: Int, includeExtraEdges: Boolean, submittedQueryGraph: TRAPIQueryGraph)
: RIO[ZConfig[AppConfig] with HttpClient with Has[BiolinkData] with Has[SPARQLCache], TRAPIMessage] =
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/org/renci/cam/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ object Server extends App with LazyLogging {
.fromOption(body.message.query_graph)
.orElseFail(new InvalidBodyException("A query graph is required, but hasn't been provided."))
limitValue <- ZIO.fromOption(limit).orElse(ZIO.effect(1000))
message <- QueryService.run(limitValue, queryGraph)
} yield TRAPIResponse(message, Some("Success"), None, None)
response <- QueryService.run(limitValue, queryGraph)
} yield response
program.mapError(error => error.getMessage)
}
.toRoutes
Expand Down
3 changes: 2 additions & 1 deletion src/test/scala/org/renci/cam/test/LimitTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ object LimitTest extends DefaultRunnableSpec with LazyLogging {
.map(limit =>
testM(s"Test query with limit of $limit expecting $queryGraphExpectedResults results") {
for {
message <- QueryService.run(limit, testQueryGraph)
response <- QueryService.run(limit, testQueryGraph)
message = response.message
_ = logger.info(s"Retrieved ${message.results.get.size} results when limit=$limit")
results = message.results.get
} yield {
Expand Down
197 changes: 197 additions & 0 deletions src/test/scala/org/renci/cam/test/TRAPITest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package org.renci.cam.test

import com.typesafe.scalalogging.LazyLogging
import io.circe._
import io.circe.generic.auto._
import io.circe.generic.semiauto._
import org.apache.jena.query.{Query, QuerySolution}
import org.http4s.headers.{`Content-Type`, Accept}
import org.http4s.{EntityDecoder, MediaType, Method, Request, Uri}
import org.renci.cam.Biolink.biolinkData
import org.renci.cam.HttpClient.HttpClient
import org.renci.cam.Server.EndpointEnv
import org.renci.cam._
import org.renci.cam.domain.{BiolinkClass, BiolinkPredicate, IRI, LogEntry, TRAPIAttribute, TRAPIResponse}
import zio.cache.Cache
import zio.config.ZConfig
import zio.config.typesafe.TypesafeConfig
import zio.interop.catz.concurrentInstance
import zio.test._
import zio.{Layer, ZIO, ZLayer}

object TRAPITest extends DefaultRunnableSpec with LazyLogging {

/** If CAM-KP-API receives a attribute constraint that it does not support, it MUST respond with an error Code
* "UnsupportedAttributeConstraint".
*/
val testUnsupportedAttributeConstraints: Spec[EndpointEnv, TestFailure[Throwable], TestSuccess] =
suite("testUnsupportedAttributeConstraints") {
// Examples taken from https://github.com/NCATSTranslator/ReasonerAPI/blob/1e0795a1c4ff5bcac3ccd5f188fdc09ec6bd27c3/ImplementationRules.md#specifying-permitted-and-excluded-kps-to-an-ara
val query = """{
"message": {
"query_graph": {
"nodes": {
"n0": {
"categories": ["biolink:GeneOrGeneProduct"]
},
"n1": { "categories": ["biolink:AnatomicalEntity"], "ids": ["GO:0005634"] }
},
"edges": {
"e0": {
"subject": "n0",
"object": "n1",
"predicates": ["biolink:part_of"],
"attribute_constraints": [{
"id": "biolink:knowledge_source",
"name": "knowledge source",
"value": "infores:semmeddb",
"not": true,
"operator": "=="
}]
}
}
}
}
}"""

testM("test unsupported attribute constraint") {
for {
biolinkData <- biolinkData

server <- Server.httpApp
response <- server(
Request(
method = Method.POST,
uri = Uri.unsafeFromString("/query")
)
.withHeaders(Accept(MediaType.application.json), `Content-Type`(MediaType.application.json))
.withEntity(query)
)
content <- EntityDecoder.decodeText(response)
trapiResponseJson <- ZIO.fromEither(io.circe.parser.parse(content))

trapiResponse <- ZIO.fromEither(
{
implicit val decoderIRI: Decoder[IRI] = Implicits.iriDecoder(biolinkData.prefixes)
implicit val keyDecoderIRI: KeyDecoder[IRI] = Implicits.iriKeyDecoder(biolinkData.prefixes)
implicit val decoderBiolinkClass: Decoder[BiolinkClass] = Implicits.biolinkClassDecoder(biolinkData.classes)
implicit val decoderBiolinkPredicate: Decoder[BiolinkPredicate] =
Implicits.biolinkPredicateDecoder(biolinkData.predicates)
implicit lazy val decoderTRAPIAttribute: Decoder[TRAPIAttribute] = deriveDecoder[TRAPIAttribute]

trapiResponseJson.as[TRAPIResponse]
}
)

logs = trapiResponse.logs
logsWithUnsupportedAttributeConstraint = logs.getOrElse(List()).filter {
case LogEntry(_, Some("ERROR"), Some("UnsupportedAttributeConstraint"), _) => true
case _ => false
}
} yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) &&
assert(content)(Assertion.isNonEmptyString) &&
// Should return an UnsupportedAttributeConstraint as the status ...
assert(trapiResponse.status)(Assertion.isSome(Assertion.equalTo("UnsupportedAttributeConstraint"))) &&
// ... and in the logs.
assert(logs)(Assertion.isSome(Assertion.isNonEmpty)) &&
assert(logsWithUnsupportedAttributeConstraint)(Assertion.isNonEmpty)
}
}

/** If CAM-KP-API receives a qualifier constraint that it does not support, it MUST return an empty response (since no edges meet the
* constraint).
*
* (This is still in development at https://github.com/NCATSTranslator/ReasonerAPI/pull/364)
*/
val testUnsupportedQualifierConstraints = suite("testUnsupportedConstraints") {
// Examples taken from https://github.com/NCATSTranslator/ReasonerAPI/blob/7520ac564e63289dffe092d4c7affd6db4ba22f1/examples/Message/subject_and_object_qualifiers.json
val query = """{
"message": {
"query_graph": {
"nodes": {
"n0": {
"categories": ["biolink:GeneOrGeneProduct"]
},
"n1": { "categories": ["biolink:AnatomicalEntity"], "ids": ["GO:0005634"] }
},
"edges": {
"e0": {
"subject": "n0",
"object": "n1",
"predicates": ["biolink:part_of"],
"qualifier_constraints": [{
"qualifier_set": [{
"qualifier_type_id": "biolink:subject_aspect_qualifier",
"qualifier_value": "abundance"
}, {
"qualifier_type_id": "biolink:subject_direction_qualifier",
"qualifier_value": "decreased"
}]
}]
}
}
}
}
}"""

testM("test unsupported qualifier constraint on QEdge") {
for {
biolinkData <- biolinkData

server <- Server.httpApp
response <- server(
Request(
method = Method.POST,
uri = Uri.unsafeFromString("/query")
)
.withHeaders(Accept(MediaType.application.json), `Content-Type`(MediaType.application.json))
.withEntity(query)
)
content <- EntityDecoder.decodeText(response)
trapiResponseJson <- ZIO.fromEither(io.circe.parser.parse(content))

trapiResponse <- ZIO.fromEither(
{
implicit val decoderIRI: Decoder[IRI] = Implicits.iriDecoder(biolinkData.prefixes)
implicit val keyDecoderIRI: KeyDecoder[IRI] = Implicits.iriKeyDecoder(biolinkData.prefixes)
implicit val decoderBiolinkClass: Decoder[BiolinkClass] = Implicits.biolinkClassDecoder(biolinkData.classes)
implicit val decoderBiolinkPredicate: Decoder[BiolinkPredicate] =
Implicits.biolinkPredicateDecoder(biolinkData.predicates)
implicit lazy val decoderTRAPIAttribute: Decoder[TRAPIAttribute] = deriveDecoder[TRAPIAttribute]

trapiResponseJson.as[TRAPIResponse]
}
)

logs = trapiResponse.logs
logWarningOfQualifierConstraints = logs.getOrElse(List()).filter {
// We've made this up ourselves.
case LogEntry(_, Some("WARNING"), Some("UnsupportedQualifierConstraint"), _) => true
case _ => false
}
} yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) &&
assert(content)(Assertion.isNonEmptyString) &&
// Should return an overall status of Success
assert(trapiResponse.status)(Assertion.isSome(Assertion.equalTo("Success"))) &&
// ... and in the logs
assert(logs)(Assertion.isSome(Assertion.isNonEmpty)) &&
assert(logWarningOfQualifierConstraints)(Assertion.isNonEmpty) &&
// ... and with no results.
assert(trapiResponse.message.results)(Assertion.isSome(Assertion.isEmpty))
}
}

val configLayer: Layer[Throwable, ZConfig[AppConfig]] = TypesafeConfig.fromDefaultLoader(AppConfig.config)

val testLayer: ZLayer[
Any,
Throwable,
HttpClient with ZConfig[Biolink.BiolinkData] with ZConfig[AppConfig] with ZConfig[Cache[Query, Throwable, List[QuerySolution]]]] =
HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer

def spec: Spec[environment.TestEnvironment, TestFailure[Throwable], TestSuccess] = suite("TRAPI tests")(
testUnsupportedAttributeConstraints,
testUnsupportedQualifierConstraints
).provideCustomLayer(testLayer.mapError(TestFailure.die))

}

0 comments on commit 0956c32

Please sign in to comment.