From cc5f68866c2d9f64ebc540814b47c431bf1fba00 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Wed, 17 Aug 2022 18:35:57 -0400 Subject: [PATCH 1/7] Added a TRAPI test for extra QNode and QEdge properties. As described in: - https://github.com/NCATSTranslator/ReasonerAPI/pull/322 --- .../scala/org/renci/cam/test/TRAPITest.scala | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/test/scala/org/renci/cam/test/TRAPITest.scala diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala new file mode 100644 index 00000000..46827429 --- /dev/null +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -0,0 +1,92 @@ +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.http4s.headers.{`Content-Type`, Accept} +import org.http4s.{EntityDecoder, MediaType, Method, Request, Uri} +import org.renci.cam.Biolink.biolinkData +import org.renci.cam._ +import org.renci.cam.domain.{BiolinkClass, BiolinkPredicate, IRI, TRAPIAttribute, TRAPIResponse} +import zio.config.ZConfig +import zio.config.typesafe.TypesafeConfig +import zio.interop.catz.concurrentInstance +import zio.test._ +import zio.{Layer, ZIO} + +object TRAPITest extends DefaultRunnableSpec with LazyLogging { + + /** This query contains two unexpected properties: + * - unknown_qnode_property on the "n0" QNode + * - unknown_qedge_property on the "e0" QEdge As per https://github.com/NCATSTranslator/ReasonerAPI/pull/322, a server receiving an + * unknown property SHOULD generate a warning and MAY continue processing. + */ + val testUnexpectedPropertiesOnQNodeAndQEdge = suite("testUnexpectedPropertiesOnQNodeAndQEdge") { + // + val query = """{ + "message": { + "query_graph": { + "nodes": { + "n0": { + "categories": ["biolink:GeneOrGeneProduct"], + "unknown_qnode_property": "A1" + }, + "n1": { "categories": ["biolink:AnatomicalEntity"], "ids": ["GO:0005634"] } + }, + "edges": { + "e0": { + "subject": "n0", + "object": "n1", + "predicates": ["biolink:part_of"], + "unknown_qedge_property": "B2" + } + } + } + } + }""" + + testM("test unexpected properties on QNode and 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 + } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) && + assert(content)(Assertion.isNonEmptyString) && + assert(logs)(Assertion.isSome(Assertion.isNonEmpty)) + } + } + + val configLayer: Layer[Throwable, ZConfig[AppConfig]] = TypesafeConfig.fromDefaultLoader(AppConfig.config) + val testLayer = HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer + + def spec = suite("OpenAPI tests")( + testUnexpectedPropertiesOnQNodeAndQEdge + ).provideCustomLayer(testLayer.mapError(TestFailure.die)) + +} From b9dcd317b1be47760f7afc8a58bfbb30e045ad7f Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 18 Aug 2022 17:47:01 -0400 Subject: [PATCH 2/7] Renamed test spec. --- src/test/scala/org/renci/cam/test/TRAPITest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index 46827429..240ce401 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -85,7 +85,7 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { val configLayer: Layer[Throwable, ZConfig[AppConfig]] = TypesafeConfig.fromDefaultLoader(AppConfig.config) val testLayer = HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer - def spec = suite("OpenAPI tests")( + def spec = suite("TRAPI tests")( testUnexpectedPropertiesOnQNodeAndQEdge ).provideCustomLayer(testLayer.mapError(TestFailure.die)) From 9bd3a49e3ba7f537ea055211fb678b1a3d8d4254 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 18 Aug 2022 18:27:37 -0400 Subject: [PATCH 3/7] Added checks for attribute and qualifier constraints. --- .../scala/org/renci/cam/test/TRAPITest.scala | 170 +++++++++++++++++- 1 file changed, 167 insertions(+), 3 deletions(-) diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index 240ce401..61f1af7b 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -4,11 +4,11 @@ import com.typesafe.scalalogging.LazyLogging import io.circe._ import io.circe.generic.auto._ import io.circe.generic.semiauto._ -import org.http4s.headers.{`Content-Type`, Accept} +import org.http4s.headers.{Accept, `Content-Type`} import org.http4s.{EntityDecoder, MediaType, Method, Request, Uri} import org.renci.cam.Biolink.biolinkData import org.renci.cam._ -import org.renci.cam.domain.{BiolinkClass, BiolinkPredicate, IRI, TRAPIAttribute, TRAPIResponse} +import org.renci.cam.domain.{BiolinkClass, BiolinkPredicate, IRI, LogEntry, TRAPIAttribute, TRAPIResponse} import zio.config.ZConfig import zio.config.typesafe.TypesafeConfig import zio.interop.catz.concurrentInstance @@ -17,6 +17,168 @@ import zio.{Layer, ZIO} 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 = 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)) + } + } + /** This query contains two unexpected properties: * - unknown_qnode_property on the "n0" QNode * - unknown_qedge_property on the "e0" QEdge As per https://github.com/NCATSTranslator/ReasonerAPI/pull/322, a server receiving an @@ -86,7 +248,9 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { val testLayer = HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer def spec = suite("TRAPI tests")( - testUnexpectedPropertiesOnQNodeAndQEdge + testUnexpectedPropertiesOnQNodeAndQEdge, + testUnsupportedAttributeConstraints, + testUnsupportedQualifierConstraints ).provideCustomLayer(testLayer.mapError(TestFailure.die)) } From 7dc91681b6ba96b4ff9be35d51e708f1a1496170 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 25 Aug 2022 23:29:37 -0400 Subject: [PATCH 4/7] Removed QNode/QEdge additional property test. This test is now implemented in PR #560. --- .../scala/org/renci/cam/test/TRAPITest.scala | 99 +++---------------- 1 file changed, 15 insertions(+), 84 deletions(-) diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index 61f1af7b..b378124b 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -4,7 +4,7 @@ import com.typesafe.scalalogging.LazyLogging import io.circe._ import io.circe.generic.auto._ import io.circe.generic.semiauto._ -import org.http4s.headers.{Accept, `Content-Type`} +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._ @@ -17,10 +17,9 @@ import zio.{Layer, ZIO} 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". - */ + /** If CAM-KP-API receives a attribute constraint that it does not support, it MUST respond with an error Code + * "UnsupportedAttributeConstraint". + */ val testUnsupportedAttributeConstraints = suite("testUnsupportedAttributeConstraints") { // Examples taken from https://github.com/NCATSTranslator/ReasonerAPI/blob/1e0795a1c4ff5bcac3ccd5f188fdc09ec6bd27c3/ImplementationRules.md#specifying-permitted-and-excluded-kps-to-an-ara val query = """{ @@ -80,10 +79,10 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { ) logs = trapiResponse.logs - logsWithUnsupportedAttributeConstraint = logs.getOrElse(List()).filter({ + logsWithUnsupportedAttributeConstraint = logs.getOrElse(List()).filter { case LogEntry(_, Some("Error"), Some("UnsupportedAttributeConstraint"), _) => true - case _ => false - }) + case _ => false + } } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) && assert(content)(Assertion.isNonEmptyString) && // Should return an UnsupportedAttributeConstraint as the status ... @@ -94,12 +93,11 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { } } - /** - * 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) - */ + /** 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 = """{ @@ -131,7 +129,6 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { } }""" - testM("test unsupported qualifier constraint on QEdge") { for { biolinkData <- biolinkData @@ -162,11 +159,11 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { ) logs = trapiResponse.logs - logWarningOfQualifierConstraints = logs.getOrElse(List()).filter({ + logWarningOfQualifierConstraints = logs.getOrElse(List()).filter { // We've made this up ourselves. case LogEntry(_, Some("Warning"), Some("UnsupportedQualifierConstraint"), _) => true - case _ => false - }) + case _ => false + } } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) && assert(content)(Assertion.isNonEmptyString) && // Should return an overall status of Success @@ -179,76 +176,10 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { } } - /** This query contains two unexpected properties: - * - unknown_qnode_property on the "n0" QNode - * - unknown_qedge_property on the "e0" QEdge As per https://github.com/NCATSTranslator/ReasonerAPI/pull/322, a server receiving an - * unknown property SHOULD generate a warning and MAY continue processing. - */ - val testUnexpectedPropertiesOnQNodeAndQEdge = suite("testUnexpectedPropertiesOnQNodeAndQEdge") { - // - val query = """{ - "message": { - "query_graph": { - "nodes": { - "n0": { - "categories": ["biolink:GeneOrGeneProduct"], - "unknown_qnode_property": "A1" - }, - "n1": { "categories": ["biolink:AnatomicalEntity"], "ids": ["GO:0005634"] } - }, - "edges": { - "e0": { - "subject": "n0", - "object": "n1", - "predicates": ["biolink:part_of"], - "unknown_qedge_property": "B2" - } - } - } - } - }""" - - testM("test unexpected properties on QNode and 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 - } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) && - assert(content)(Assertion.isNonEmptyString) && - assert(logs)(Assertion.isSome(Assertion.isNonEmpty)) - } - } - val configLayer: Layer[Throwable, ZConfig[AppConfig]] = TypesafeConfig.fromDefaultLoader(AppConfig.config) val testLayer = HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer def spec = suite("TRAPI tests")( - testUnexpectedPropertiesOnQNodeAndQEdge, testUnsupportedAttributeConstraints, testUnsupportedQualifierConstraints ).provideCustomLayer(testLayer.mapError(TestFailure.die)) From 6db115924f96fbfdcdfde9710668734f516a9288 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 25 Aug 2022 23:31:24 -0400 Subject: [PATCH 5/7] Added types as required. --- .../scala/org/renci/cam/test/TRAPITest.scala | 100 ++++++++++-------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index b378124b..67248d19 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -4,25 +4,30 @@ 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} +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 = suite("testUnsupportedAttributeConstraints") { - // Examples taken from https://github.com/NCATSTranslator/ReasonerAPI/blob/1e0795a1c4ff5bcac3ccd5f188fdc09ec6bd27c3/ImplementationRules.md#specifying-permitted-and-excluded-kps-to-an-ara - val query = """{ + 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": { @@ -49,49 +54,49 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { } }""" - testM("test unsupported attribute constraint") { - for { - biolinkData <- biolinkData - - server <- Server.httpApp - response <- server( - Request( - method = Method.POST, - uri = Uri.unsafeFromString("/query") + 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] + } ) - .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 } - ) - - 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) + } 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). @@ -177,9 +182,14 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { } val configLayer: Layer[Throwable, ZConfig[AppConfig]] = TypesafeConfig.fromDefaultLoader(AppConfig.config) - val testLayer = HttpClient.makeHttpClientLayer ++ Biolink.makeUtilitiesLayer ++ configLayer >+> SPARQLQueryExecutor.makeCache.toLayer - def spec = suite("TRAPI tests")( + 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)) From 4f9623ad5538cb1428ffefff6a309e20fc28c1f6 Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 25 Aug 2022 23:54:26 -0400 Subject: [PATCH 6/7] Updated QueryService.run() to return a TRAPIResponse. It previously returned a TRAPIMessage, but this prevented us from being able to return log messages or other statuses. --- .../scala/org/renci/cam/QueryService.scala | 114 +++++++++++++----- .../scala/org/renci/cam/test/TRAPITest.scala | 2 +- 2 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/main/scala/org/renci/cam/QueryService.scala b/src/main/scala/org/renci/cam/QueryService.scala index 8230ea64..2df6e398 100644 --- a/src/main/scala/org/renci/cam/QueryService.scala +++ b/src/main/scala/org/renci/cam/QueryService.scala @@ -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] = diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index 67248d19..05e710ec 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -85,7 +85,7 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { logs = trapiResponse.logs logsWithUnsupportedAttributeConstraint = logs.getOrElse(List()).filter { - case LogEntry(_, Some("Error"), Some("UnsupportedAttributeConstraint"), _) => true + case LogEntry(_, Some("ERROR"), Some("UnsupportedAttributeConstraint"), _) => true case _ => false } } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) && From e6173909a6019876a6024e114eb0416a05278b4c Mon Sep 17 00:00:00 2001 From: Gaurav Vaidya Date: Thu, 25 Aug 2022 23:57:51 -0400 Subject: [PATCH 7/7] Updated methods that call QueryService.run(). --- src/main/scala/org/renci/cam/Server.scala | 4 ++-- src/test/scala/org/renci/cam/test/LimitTest.scala | 3 ++- src/test/scala/org/renci/cam/test/TRAPITest.scala | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/scala/org/renci/cam/Server.scala b/src/main/scala/org/renci/cam/Server.scala index b54c2803..742d4524 100644 --- a/src/main/scala/org/renci/cam/Server.scala +++ b/src/main/scala/org/renci/cam/Server.scala @@ -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 diff --git a/src/test/scala/org/renci/cam/test/LimitTest.scala b/src/test/scala/org/renci/cam/test/LimitTest.scala index c5bd6ef6..a24ed763 100644 --- a/src/test/scala/org/renci/cam/test/LimitTest.scala +++ b/src/test/scala/org/renci/cam/test/LimitTest.scala @@ -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 { diff --git a/src/test/scala/org/renci/cam/test/TRAPITest.scala b/src/test/scala/org/renci/cam/test/TRAPITest.scala index 05e710ec..60812077 100644 --- a/src/test/scala/org/renci/cam/test/TRAPITest.scala +++ b/src/test/scala/org/renci/cam/test/TRAPITest.scala @@ -166,7 +166,7 @@ object TRAPITest extends DefaultRunnableSpec with LazyLogging { logs = trapiResponse.logs logWarningOfQualifierConstraints = logs.getOrElse(List()).filter { // We've made this up ourselves. - case LogEntry(_, Some("Warning"), Some("UnsupportedQualifierConstraint"), _) => true + case LogEntry(_, Some("WARNING"), Some("UnsupportedQualifierConstraint"), _) => true case _ => false } } yield assert(response.status)(Assertion.hasField("isSuccess", _.isSuccess, Assertion.isTrue)) &&