From 68537891ca8f1b06301bbe7b996e5eb4ed680e39 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Wed, 18 Oct 2017 14:15:45 +0700 Subject: Added REST API methods to HypothesisService --- .../pdsuidomain/services/HypothesisService.scala | 39 ++++++++++++++++++++++ .../services/rest/RestHypothesisService.scala | 35 +++++++++++++++---- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala index 52cd6c8..9dc8d33 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala @@ -2,12 +2,21 @@ package xyz.driver.pdsuidomain.services import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.domain.UuidId import xyz.driver.pdsuicommon.error.DomainError import xyz.driver.pdsuidomain.entities.Hypothesis import scala.concurrent.Future object HypothesisService { + trait DefaultNotFoundError { + def userMessage: String = "Intervention not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + sealed trait GetListReply object GetListReply { final case class EntityList(xs: Seq[Hypothesis], totalFound: Int) extends GetListReply @@ -16,6 +25,32 @@ object HypothesisService { def userMessage: String = "Access denied" } } + + sealed trait CreateReply + object CreateReply { + final case class Created(x: Hypothesis) extends CreateReply + + type Error = CreateReply with DomainError + + case object AuthorizationError + extends CreateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + final case class CommonError(userMessage: String) extends CreateReply with DomainError + } + + sealed trait DeleteReply + object DeleteReply { + case object Deleted extends DeleteReply + + type Error = DeleteReply with DomainError + + case object NotFoundError extends DeleteReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends DeleteReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + final case class CommonError(userMessage: String) extends DeleteReply with DomainError + } } trait HypothesisService { @@ -24,4 +59,8 @@ trait HypothesisService { def getAll(sorting: Option[Sorting] = None)( implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def create(draftHypothesis: Hypothesis)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + def delete(id: UuidId[Hypothesis])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala index 1b8c943..9cd0847 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala @@ -1,14 +1,17 @@ package xyz.driver.pdsuidomain.services.rest import scala.concurrent.{ExecutionContext, Future} - +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.marshalling.Marshal import akka.http.scaladsl.model._ import akka.stream.Materializer import xyz.driver.core.rest._ import xyz.driver.pdsuicommon.auth._ import xyz.driver.pdsuicommon.db._ -import xyz.driver.pdsuidomain.formats.json.ListResponse -import xyz.driver.pdsuidomain.formats.json.hypothesis.ApiHypothesis +import xyz.driver.pdsuicommon.domain.UuidId +import xyz.driver.pdsuidomain.entities.Hypothesis +import xyz.driver.pdsuidomain.formats.json.sprayformats.ListResponse +import xyz.driver.pdsuidomain.formats.json.sprayformats.hypothesis._ import xyz.driver.pdsuidomain.services.HypothesisService class RestHypothesisService(transport: ServiceTransport, baseUri: Uri)( @@ -16,7 +19,6 @@ class RestHypothesisService(transport: ServiceTransport, baseUri: Uri)( protected val exec: ExecutionContext) extends HypothesisService with RestHelper { - import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._ import xyz.driver.pdsuidomain.services.HypothesisService._ def getAll(sorting: Option[Sorting] = None)( @@ -24,9 +26,30 @@ class RestHypothesisService(transport: ServiceTransport, baseUri: Uri)( val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, "/v1/hypothesis", sortingQuery(sorting))) for { response <- transport.sendRequestGetResponse(requestContext)(request) - reply <- apiResponse[ListResponse[ApiHypothesis]](response) + reply <- apiResponse[ListResponse[Hypothesis]](response) + } yield { + GetListReply.EntityList(reply.items, reply.meta.itemsCount) + } + } + + def create(draftHypothesis: Hypothesis)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] = { + for { + entity <- Marshal(draftHypothesis).to[RequestEntity] + request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, "/v1/hypothesis")).withEntity(entity) + response <- transport.sendRequestGetResponse(requestContext)(request) + reply <- apiResponse[Hypothesis](response) + } yield { + CreateReply.Created(reply) + } + } + + def delete(id: UuidId[Hypothesis])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] = { + val request = HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, s"/v1/hypothesis/$id")) + for { + response <- transport.sendRequestGetResponse(requestContext)(request) + _ <- apiResponse[HttpEntity](response) } yield { - GetListReply.EntityList(reply.items.map(_.toDomain), reply.meta.itemsCount) + DeleteReply.Deleted } } -- cgit v1.2.3 From 53fa22f0b6477e518fce5df6d631e848ecd34922 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 19 Oct 2017 14:49:04 +0700 Subject: Fixed ACL for Hypothesis --- src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala index 180ebf9..3eb1a65 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala @@ -144,7 +144,9 @@ object ACL extends PhiLogging { object Hypothesis extends BaseACL( label = "hypothesis", - read = Set(TrialSummarizer, TrialAdmin) ++ TreatmentMatchingRoles + read = Set(TrialSummarizer, TrialAdmin) ++ TreatmentMatchingRoles, + create = Set(TrialAdmin), + delete = Set(TrialAdmin) ) object Criterion -- cgit v1.2.3 From 997407855c2293264dd160e6841f342aeaccf02a Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Fri, 20 Oct 2017 10:22:28 +0700 Subject: Fixed typo in HypothesisService --- src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala index 9dc8d33..572edb6 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala @@ -10,7 +10,7 @@ import scala.concurrent.Future object HypothesisService { trait DefaultNotFoundError { - def userMessage: String = "Intervention not found" + def userMessage: String = "Hypothesis not found" } trait DefaultAccessDeniedError { -- cgit v1.2.3 From e4ef78966ccb9af789c76339812b860395f0a9de Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Fri, 20 Oct 2017 15:02:19 +0700 Subject: Resolved breaking changes --- .../xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala index 9cd0847..9cef4c8 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala @@ -10,7 +10,8 @@ import xyz.driver.pdsuicommon.auth._ import xyz.driver.pdsuicommon.db._ import xyz.driver.pdsuicommon.domain.UuidId import xyz.driver.pdsuidomain.entities.Hypothesis -import xyz.driver.pdsuidomain.formats.json.sprayformats.ListResponse +import xyz.driver.pdsuidomain.ListResponse +import xyz.driver.pdsuidomain.formats.json.sprayformats.listresponse._ import xyz.driver.pdsuidomain.formats.json.sprayformats.hypothesis._ import xyz.driver.pdsuidomain.services.HypothesisService -- cgit v1.2.3 From 2ebb8f0a91f57259297f7ef1128d8d07e3146621 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 20 Oct 2017 15:51:10 -0700 Subject: Fix default version in ApiPartialDocument --- .../driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala index ecbdaed..34bebab 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala @@ -77,7 +77,7 @@ final case class ApiPartialDocument(recordId: Option[Long], previousAssignee = None, lastActiveUserId = None, lastUpdate = LocalDateTime.MIN, - labelVersion = 0 + labelVersion = 1 ) } else { throw new JsonValidationException(validationErrors) -- cgit v1.2.3 From d1537fa8bbf7c7097fe7ddb410e40c82381d79f2 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 20 Oct 2017 15:52:07 -0700 Subject: Use core rejection handler --- build.sbt | 2 +- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index d68a8f5..e9613ab 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ lazy val core = (project in file(".")) "io.github.cloudify" %% "spdf" % "1.4.0", "org.davidbild" %% "tristate-core" % "0.2.0", "org.davidbild" %% "tristate-play" % "0.2.0" exclude ("com.typesafe.play", "play-json"), - "xyz.driver" %% "core" % "1.2.2", + "xyz.driver" %% "core" % "1.2.3", "xyz.driver" %% "domain-model" % "0.17.8", "ch.qos.logback" % "logback-classic" % "1.1.7", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.8.4", diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index e9a4132..67da717 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -3,6 +3,7 @@ package xyz.driver.pdsuicommon.http import akka.http.scaladsl.server._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.model._ +import xyz.driver.core.app.DriverApp import xyz.driver.core.rest.ContextHeaders import xyz.driver.entities.users.AuthUserInfo import xyz.driver.pdsuicommon.auth._ @@ -88,7 +89,7 @@ trait Directives { val text = Json.stringify(implicitly[Writes[ErrorsResponse]].writes(err)) HttpEntity(ContentTypes.`application/json`, text) } - RejectionHandler.default.mapRejectionResponse { + DriverApp.rejectionHandler.mapRejectionResponse { case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) => res.copy(entity = wrapContent(ent.data.utf8String)) case x => x // pass through all other types of responses -- cgit v1.2.3 From 133b4fe8487db9df80c1f39f64bbe7d6fc039952 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Mon, 23 Oct 2017 14:46:32 +0700 Subject: Added 'inclusion' field to PatientCriterion --- .../scala/xyz/driver/pdsuidomain/entities/PatientCriterion.scala | 7 ++++--- .../driver/pdsuidomain/fakes/entities/treatmentmatching.scala | 3 ++- .../formats/json/patient/trial/ApiPatientCriterion.scala | 9 ++++++--- .../pdsuidomain/formats/json/sprayformats/patientcriterion.scala | 3 ++- .../formats/json/sprayformats/PatientCriterionFormatSuite.scala | 5 +++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientCriterion.scala index 79a19ed..7027eef 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientCriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientCriterion.scala @@ -9,11 +9,11 @@ import xyz.driver.pdsuicommon.logging._ object PatientCriterion { implicit def toPhiString(x: PatientCriterion): PhiString = { import x._ - phi"PatientCriterion(id=$id, patientLabelId=$patientLabelId, trialId=${Unsafe(trialId)}, nctId=${Unsafe(nctId)}, " + + phi"PatientCriterion(id=$id, patientLabelId=$patientLabelId, trialId=${Unsafe(trialId)}, nctId=$nctId, " + phi"criterionId=$criterionId, criterionValue=${Unsafe(criterionValue)}, " + phi"isImplicitMatch=$criterionIsDefining), criterionIsDefining=${Unsafe(criterionIsDefining)}, " + phi"eligibilityStatus=${Unsafe(eligibilityStatus)}, verifiedEligibilityStatus=${Unsafe(verifiedEligibilityStatus)}, " + - phi"isVerified=${Unsafe(isVerified)}, lastUpdate=${Unsafe(lastUpdate)}" + phi"isVerified=${Unsafe(isVerified)}, lastUpdate=${Unsafe(lastUpdate)}, inclusion=${Unsafe(inclusion)}" } /** @@ -52,7 +52,8 @@ final case class PatientCriterion(id: LongId[PatientCriterion], verifiedEligibilityStatus: Option[LabelValue], isVerified: Boolean, isVisible: Boolean, - lastUpdate: LocalDateTime) { + lastUpdate: LocalDateTime, + inclusion: Option[Boolean]) { def isIneligibleForEv: Boolean = eligibilityStatus.contains(LabelValue.No) && isVerified } diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/treatmentmatching.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/treatmentmatching.scala index b0ca136..b10c7de 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/treatmentmatching.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/treatmentmatching.scala @@ -66,7 +66,8 @@ object treatmentmatching { verifiedEligibilityStatus = generators.nextOption(fakes.entities.labels.nextLabelValue()), isVerified = generators.nextBoolean(), isVisible = generators.nextBoolean(), - lastUpdate = nextLocalDateTime + lastUpdate = nextLocalDateTime, + inclusion = generators.nextOption(generators.nextBoolean()) ) def nextDraftPatientCriterion(): DraftPatientCriterion = DraftPatientCriterion( diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala index b9bf772..5e44413 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala @@ -22,7 +22,8 @@ final case class ApiPatientCriterion(id: Long, verifiedEligibilityStatus: Option[String], isVerified: Boolean, isVisible: Boolean, - lastUpdate: ZonedDateTime) + lastUpdate: ZonedDateTime, + inclusion: Option[Boolean]) object ApiPatientCriterion { @@ -49,7 +50,8 @@ object ApiPatientCriterion { }), Writes.of[String])) and (JsPath \ "isVerified").format[Boolean] and (JsPath \ "isVisible").format[Boolean] and - (JsPath \ "lastUpdate").format[ZonedDateTime] + (JsPath \ "lastUpdate").format[ZonedDateTime] and + (JsPath \ "inclusion").formatNullable[Boolean] ) (ApiPatientCriterion.apply, unlift(ApiPatientCriterion.unapply)) def fromDomain(patientCriterion: PatientCriterion, @@ -70,6 +72,7 @@ object ApiPatientCriterion { verifiedEligibilityStatus = patientCriterion.verifiedEligibilityStatus.map(_.toString), isVerified = patientCriterion.isVerified, isVisible = patientCriterion.isVisible, - lastUpdate = ZonedDateTime.of(patientCriterion.lastUpdate, ZoneId.of("Z")) + lastUpdate = ZonedDateTime.of(patientCriterion.lastUpdate, ZoneId.of("Z")), + inclusion = patientCriterion.inclusion ) } diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/patientcriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/patientcriterion.scala index f41c846..2cad78c 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/patientcriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/patientcriterion.scala @@ -56,7 +56,8 @@ object patientcriterion { "verifiedEligibilityStatus" -> obj.patientCriterion.verifiedEligibilityStatus.toJson, "isVerified" -> obj.patientCriterion.isVerified.toJson, "isVisible" -> obj.patientCriterion.isVisible.toJson, - "lastUpdate" -> obj.patientCriterion.lastUpdate.toJson + "lastUpdate" -> obj.patientCriterion.lastUpdate.toJson, + "inclusion" -> obj.patientCriterion.inclusion.toJson ) } } diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/PatientCriterionFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/PatientCriterionFormatSuite.scala index b254013..27e27c2 100644 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/PatientCriterionFormatSuite.scala +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/PatientCriterionFormatSuite.scala @@ -26,7 +26,8 @@ class PatientCriterionFormatSuite extends FlatSpec with Matchers { verifiedEligibilityStatus = None, isVisible = true, isVerified = true, - lastUpdate = LocalDateTime.parse("2017-08-10T18:00:00") + lastUpdate = LocalDateTime.parse("2017-08-10T18:00:00"), + inclusion = Some(true) ) val arms = List( PatientCriterionArm(patientCriterionId = LongId(1), armId = LongId(31), armName = "arm 31"), @@ -38,7 +39,7 @@ class PatientCriterionFormatSuite extends FlatSpec with Matchers { writtenJson should be( """{"id":1,"labelId":21,"nctId":"NCT00001","criterionId":101,"criterionText":"criterion text","criterionValue":"Yes", "criterionIsDefining":false,"criterionIsCompound":false,"eligibilityStatus":"Yes","verifiedEligibilityStatus":null, - "isVisible":true,"isVerified":true,"lastUpdate":"2017-08-10T18:00Z","arms":["arm 31","arm 32"]}""".parseJson) + "isVisible":true,"isVerified":true,"lastUpdate":"2017-08-10T18:00Z","arms":["arm 31","arm 32"],"inclusion":true}""".parseJson) val updatePatientCriterionJson = """{"verifiedEligibilityStatus":"No"}""".parseJson val expectedUpdatedPatientCriterion = orig.copy(verifiedEligibilityStatus = Some(LabelValue.No)) -- cgit v1.2.3 From e9f8fbb7f8f379e6885331c94364f6e7d41ffbb2 Mon Sep 17 00:00:00 2001 From: Kseniya Tomskikh Date: Mon, 23 Oct 2017 15:49:45 +0700 Subject: Added custom set parameters to slick qb --- .../scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala index dc03a52..7d2f085 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala @@ -6,6 +6,7 @@ import java.time.LocalDateTime import slick.jdbc.{JdbcProfile, PositionedParameters, SQLActionBuilder, SetParameter} import xyz.driver.pdsuicommon.db.Sorting.{Dimension, Sequential} import xyz.driver.pdsuicommon.db.SortingOrder.{Ascending, Descending} +import xyz.driver.pdsuicommon.domain.{LongId, StringId, UuidId} import scala.concurrent.{ExecutionContext, Future} @@ -44,6 +45,18 @@ object SlickQueryBuilder { pp.setObject(v, JDBCType.BINARY.getVendorTypeNumber) } } + + implicit def setLongIdQueryParameter[T]: SetParameter[LongId[T]] = SetParameter[LongId[T]] { (v, pp) => + pp.setLong(v.id) + } + + implicit def setStringIdQueryParameter[T]: SetParameter[StringId[T]] = SetParameter[StringId[T]] { (v, pp) => + pp.setString(v.id) + } + + implicit def setUuidIdQueryParameter[T]: SetParameter[UuidId[T]] = SetParameter[UuidId[T]] { (v, pp) => + pp.setObject(v.id, JDBCType.BINARY.getVendorTypeNumber) + } } final case class SlickTableLink(keyColumnName: String, foreignTableName: String, foreignKeyColumnName: String) -- cgit v1.2.3 From b99235a22bfabeb175c297660d9dccc27c7b4daf Mon Sep 17 00:00:00 2001 From: Kseniya Tomskikh Date: Mon, 23 Oct 2017 19:40:57 +0700 Subject: Fixed update delivery method --- .../pdsuidomain/formats/json/sprayformats/intervention.scala | 5 +++-- .../formats/json/sprayformats/InterventionFormatSuite.scala | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala index e557e47..fccdda4 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala @@ -99,7 +99,8 @@ object intervention { val deliveryMethod = fields .get("deliveryMethod") - .map(_.convertTo[String]) + .map(_.convertTo[Option[String]]) + .getOrElse(orig.intervention.deliveryMethod) val origIntervention = orig.intervention val arms = fields @@ -112,7 +113,7 @@ object intervention { typeId = typeId.orElse(origIntervention.typeId), dosage = dosage.getOrElse(origIntervention.dosage), isActive = isActive.getOrElse(origIntervention.isActive), - deliveryMethod = deliveryMethod.orElse(origIntervention.deliveryMethod) + deliveryMethod = deliveryMethod ), arms = arms.getOrElse(orig.arms) ) diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala index 4532da4..edce9c1 100644 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala @@ -19,7 +19,7 @@ class InterventionFormatSuite extends FlatSpec with Matchers { dosage = "", originalDosage = "", isActive = true, - deliveryMethod = Some("pill") + deliveryMethod = Some("Inhalation") ) val arms = List( InterventionArm(interventionId = intervention.id, armId = LongId(20)), @@ -34,11 +34,11 @@ class InterventionFormatSuite extends FlatSpec with Matchers { writtenJson should be( """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22], - "trialId":"NCT000001","deliveryMethod":"pill","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson) + "trialId":"NCT000001","deliveryMethod":"Inhalation","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson) val createInterventionJson = """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22], - "trialId":"NCT000001","deliveryMethod":"pill"}""".parseJson + "trialId":"NCT000001","deliveryMethod":"Inhalation"}""".parseJson val parsedCreateIntervention = interventionFormat.read(createInterventionJson) val expectedCreateIntervention = parsedCreateIntervention.copy( intervention = intervention.copy(id = LongId(0), originalType = None, originalName = "intervention name"), @@ -46,9 +46,9 @@ class InterventionFormatSuite extends FlatSpec with Matchers { ) parsedCreateIntervention should be(expectedCreateIntervention) - val updateInterventionJson = """{"dosage":"descr","arms":[21,22]}""".parseJson + val updateInterventionJson = """{"dosage":"descr","deliveryMethod":null,"arms":[21,22]}""".parseJson val expectedUpdatedIntervention = orig.copy( - intervention = intervention.copy(dosage = "descr"), + intervention = intervention.copy(dosage = "descr", deliveryMethod = None), arms = List( InterventionArm(interventionId = intervention.id, armId = LongId(21)), InterventionArm(interventionId = intervention.id, armId = LongId(22)) -- cgit v1.2.3 From cb76f20dcd9a9519bffb3814a03c18c424f5e6a0 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 20 Oct 2017 17:44:51 -0700 Subject: Add PreCleaning format --- .../scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala index 09517e6..d00aa14 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala @@ -15,6 +15,7 @@ object record { import common._ implicit val recordStatusFormat = new EnumJsonFormat[Status]( + "PreCleaning" -> Status.PreCleaning, "Unprocessed" -> Status.Unprocessed, "PreOrganized" -> Status.PreOrganized, "New" -> Status.New, -- cgit v1.2.3 From 5a6b7a7a3013035d692eb6d8ac369c1eb6dac0f3 Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 23 Oct 2017 17:49:18 -0700 Subject: Support for `noteq` for filters on numbers --- src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 85e9149..41533fe 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -79,7 +79,7 @@ object SearchFilterParser { } private val numericOperatorParser: Parser[String] = { - P(IgnoreCase("eq") | ((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?)).! + P(IgnoreCase("eq") | IgnoreCase("noteq") | ((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?)).! } private val naryOperatorParser: Parser[String] = P(IgnoreCase("in")).! -- cgit v1.2.3 From 3c2a7fdfccc87cb8ec9e8e48c31c622555078c54 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Tue, 24 Oct 2017 11:33:55 +0700 Subject: Added field 'inclusion' to ExportTrialLabelCriterion --- .../export/trial/ExportTrialLabelCriterion.scala | 5 +- .../driver/pdsuidomain/fakes/entities/export.scala | 3 +- .../formats/json/sprayformats/export.scala | 86 ++++++++++++++++------ .../services/fake/FakeTrialService.scala | 3 +- .../json/sprayformats/ExportFormatSuite.scala | 12 ++- 5 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala index 8376e34..98bd084 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala @@ -11,13 +11,14 @@ final case class ExportTrialLabelCriterion(criterionId: LongId[Criterion], armIds: Set[LongId[EligibilityArm]], criteria: String, isCompound: Boolean, - isDefining: Boolean) + isDefining: Boolean, + inclusion: Option[Boolean]) object ExportTrialLabelCriterion { implicit def toPhiString(x: ExportTrialLabelCriterion): PhiString = { import x._ phi"TrialLabelCriterion(criterionId=$criterionId, value=$value, labelId=$labelId, " + - phi"criteria=${Unsafe(criteria)}, isCompound=$isCompound, isDefining=$isDefining)" + phi"criteria=${Unsafe(criteria)}, isCompound=$isCompound, isDefining=$isDefining), inclusion=${Unsafe(inclusion)}" } } diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/export.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/export.scala index 2c7d0e0..33da392 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/export.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/export.scala @@ -28,7 +28,8 @@ object export { armIds = setOf(nextLongId[EligibilityArm]), criteria = nextString(100), isCompound = nextBoolean(), - isDefining = nextBoolean() + isDefining = nextBoolean(), + inclusion = nextOption(nextBoolean()) ) def nextExportTrialWithLabels(): ExportTrialWithLabels = diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/export.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/export.scala index 4391453..9579288 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/export.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/export.scala @@ -3,6 +3,7 @@ package xyz.driver.pdsuidomain.formats.json.sprayformats import spray.json._ import xyz.driver.entities.labels.Label import xyz.driver.formats.json.labels._ +import xyz.driver.pdsuicommon.domain.LongId import xyz.driver.pdsuidomain.entities.export.patient._ import xyz.driver.pdsuidomain.entities.export.trial.{ExportTrialArm, ExportTrialLabelCriterion, ExportTrialWithLabels} import xyz.driver.pdsuidomain.entities.{Criterion, EligibilityArm} @@ -13,6 +14,14 @@ object export { import document._ import record._ + private def deserializationErrorFieldMessage(field: String, json: JsValue)(implicit className: String) = { + deserializationError(s"$className json object do not contain '$field' field: $json") + } + + private def deserializationErrorEntityMessage(json: JsValue)(implicit className: String) = { + deserializationError(s"Expected Json Object as $className, but got $json") + } + implicit val patientLabelEvidenceDocumentFormat: RootJsonFormat[ExportPatientLabelEvidenceDocument] = jsonFormat5(ExportPatientLabelEvidenceDocument.apply) @@ -29,6 +38,8 @@ object export { implicit val trialLabelCriterionFormat: RootJsonFormat[ExportTrialLabelCriterion] = new RootJsonFormat[ExportTrialLabelCriterion] { + implicit val className: String = "ExportTrialLabelCriterion" + override def write(obj: ExportTrialLabelCriterion): JsValue = JsObject( "value" -> obj.value @@ -43,40 +54,69 @@ object export { "criterionText" -> obj.criteria.toJson, "armIds" -> obj.armIds.toJson, "isCompound" -> obj.isCompound.toJson, - "isDefining" -> obj.isDefining.toJson + "isDefining" -> obj.isDefining.toJson, + "inclusion" -> obj.inclusion.toJson ) override def read(json: JsValue): ExportTrialLabelCriterion = { + json match { + case JsObject(fields) => + val value = fields + .get("value") + .map(_.convertTo[String]) + .map { + case "Yes" => Option(true) + case "No" => Option(false) + case "Unknown" => Option.empty[Boolean] + } + .getOrElse(deserializationErrorFieldMessage("value", json)) - val fields = Seq("value", "labelId", "criterionId", "criterionText", "armIds", "isCompound", "isDefining") - - json.asJsObject.getFields(fields: _*) match { - case Seq(JsString(valueString), - labelId, - criterionId, - JsString(criterionText), - JsArray(armIdsVector), - JsBoolean(isCompound), - JsBoolean(isDefining)) => - val value = valueString match { - case "Yes" => Option(true) - case "No" => Option(false) - case "Unknown" => Option.empty[Boolean] - } + val labelId = fields + .get("labelId") + .map(_.convertTo[LongId[Label]]) + .getOrElse(deserializationErrorFieldMessage("labelId", json)) + + val criterionId = fields + .get("criterionId") + .map(_.convertTo[LongId[Criterion]]) + .getOrElse(deserializationErrorFieldMessage("criterionId", json)) + + val criterionText = fields + .get("criterionText") + .map(_.convertTo[String]) + .getOrElse(deserializationErrorFieldMessage("criterionText", json)) + + val armIds = fields + .get("armIds") + .map(_.convertTo[Set[LongId[EligibilityArm]]]) + .getOrElse(deserializationErrorFieldMessage("armIds", json)) + + val isCompound = fields + .get("isCompound") + .map(_.convertTo[Boolean]) + .getOrElse(deserializationErrorFieldMessage("isCompound", json)) + + val isDefining = fields + .get("isDefining") + .map(_.convertTo[Boolean]) + .getOrElse(deserializationErrorFieldMessage("isDefining", json)) + + val inclusion = fields + .get("inclusion") + .flatMap(_.convertTo[Option[Boolean]]) ExportTrialLabelCriterion( - longIdFormat[Criterion].read(criterionId), + criterionId, value, - longIdFormat[Label].read(labelId), - armIdsVector.map(longIdFormat[EligibilityArm].read).toSet, + labelId, + armIds, criterionText, isCompound, - isDefining + isDefining, + inclusion ) - case _ => - deserializationError( - s"Cannot find required fields ${fields.mkString(", ")} in ExportTrialLabelCriterion object!") + case _ => deserializationErrorEntityMessage(json) } } } diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala index e0efcd1..e23449c 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala @@ -85,7 +85,8 @@ class FakeTrialService extends TrialService { generators.setOf(LongId[EligibilityArm](generators.nextInt(999999).toLong)), generators.nextName().value, generators.nextBoolean(), - generators.nextBoolean() + generators.nextBoolean(), + generators.nextOption(generators.nextBoolean()) )) ) diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExportFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExportFormatSuite.scala index d78e754..767f832 100644 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExportFormatSuite.scala +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExportFormatSuite.scala @@ -92,7 +92,8 @@ class ExportFormatSuite extends FlatSpec with Matchers { armIds = Set(LongId(1), LongId(2)), criteria = "criteria 10 text", isCompound = false, - isDefining = false + isDefining = false, + inclusion = Some(false) ), ExportTrialLabelCriterion( criterionId = LongId(11), @@ -101,7 +102,8 @@ class ExportFormatSuite extends FlatSpec with Matchers { armIds = Set(LongId(2)), criteria = "criteria 11 text", isCompound = true, - isDefining = false + isDefining = false, + inclusion = None ) ) val trialWithLabels = ExportTrialWithLabels( @@ -117,8 +119,10 @@ class ExportFormatSuite extends FlatSpec with Matchers { writtenJson should be( """{"nctId":"NCT000001","trialId":"40892a07-c638-49d2-9795-1edfefbbcc7c","lastReviewed":"2017-08-10T18:00Z", "labelVersion":1,"arms":[{"armId":1,"armName":"arm 1","diseaseList":["Breast"]},{"armId":2,"armName":"arm 2","diseaseList":["Breast"]}],"criteria":[ - {"value":"Yes","labelId":21,"criterionId":10,"criterionText":"criteria 10 text","armIds":[1,2],"isCompound":false,"isDefining":false}, - {"value":"Unknown","labelId":21,"criterionId":11,"criterionText":"criteria 11 text","armIds":[2],"isCompound":true,"isDefining":false}]}""".parseJson) + {"value":"Yes","labelId":21,"criterionId":10,"criterionText":"criteria 10 text","armIds":[1,2],"isCompound":false, + "isDefining":false,"inclusion":false}, + {"value":"Unknown","labelId":21,"criterionId":11,"criterionText":"criteria 11 text","armIds":[2],"isCompound":true, + "isDefining":false,"inclusion":null}]}""".parseJson) } } -- cgit v1.2.3 From c652553ca640f710f0182d9f51b3b87c954e24ac Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 24 Oct 2017 02:34:21 -0700 Subject: Fixing AllowAll, DenyAll filters in SlickQueryBuilder --- src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala index 7d2f085..c561d6d 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala @@ -209,10 +209,10 @@ sealed trait SlickQueryBuilderParameters { sql"" case AllowAll => - sql"1" + sql"1=1" case DenyAll => - sql"0" + sql"1=0" case Atom.Binary(dimension, Eq, value) if isNull(value) => sql"#${escapeDimension(dimension)} is NULL" -- cgit v1.2.3 From 5d4b85d9991abe63920e4f4d0e2cad6e6aa09f33 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 24 Oct 2017 03:25:48 -0700 Subject: Fixing dimensions table name in QueryBuilder and empty IN handling --- .../driver/pdsuicommon/db/SlickQueryBuilder.scala | 24 ++++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala index c561d6d..3011f6a 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala @@ -173,7 +173,7 @@ sealed trait SlickQueryBuilderParameters { def isNull(string: AnyRef) = Option(string).isEmpty || string.toString.toLowerCase == "null" def escapeDimension(dimension: SearchFilterExpr.Dimension) = { - s"$escapedTableName.$qs${dimension.name}$qs" + s"${dimension.tableName.map(t => s"$qs$databaseName$qs.$qs$t$qs").getOrElse(escapedTableName)}.$qs${dimension.name}$qs" } def filterToSqlMultiple(operands: Seq[SearchFilterExpr]) = operands.collect { @@ -187,9 +187,9 @@ sealed trait SlickQueryBuilderParameters { if (conditions.nonEmpty) { val condition = conditions.head if (first) { - multipleSqlToAction(false, op, conditions.tail, condition) + multipleSqlToAction(first = false, op, conditions.tail, condition) } else { - multipleSqlToAction(false, op, conditions.tail, sql concat sql" #${op} " concat condition) + multipleSqlToAction(first = false, op, conditions.tail, sql concat sql" #${op} " concat condition) } } else sql } @@ -197,9 +197,9 @@ sealed trait SlickQueryBuilderParameters { def concatenateParameters(sql: SQLActionBuilder, first: Boolean, tail: Seq[AnyRef]): SQLActionBuilder = { if (tail.nonEmpty) { if (!first) { - concatenateParameters(sql concat sql""",${tail.head}""", false, tail.tail) + concatenateParameters(sql concat sql""",${tail.head}""", first = false, tail.tail) } else { - concatenateParameters(sql"""(${tail.head}""", false, tail.tail) + concatenateParameters(sql"""(${tail.head}""", first = false, tail.tail) } } else sql concat sql")" } @@ -245,17 +245,19 @@ sealed trait SlickQueryBuilderParameters { case SearchFilterNAryOperation.NotIn => sql" not in " } - val formattedValues = if (values.nonEmpty) { - concatenateParameters(sql"", true, values) - } else sql"NULL" - sql"#${escapeDimension(dimension)}" concat sqlOp concat formattedValues + if (values.nonEmpty) { + val formattedValues = concatenateParameters(sql"", first = true, values) + sql"#${escapeDimension(dimension)}" concat sqlOp concat formattedValues + } else { + sql"1=0" + } case Intersection(operands) => - val filter = multipleSqlToAction(true, "and", filterToSqlMultiple(operands), sql"") + val filter = multipleSqlToAction(first = true, "and", filterToSqlMultiple(operands), sql"") sql"(" concat filter concat sql")" case Union(operands) => - val filter = multipleSqlToAction(true, "or", filterToSqlMultiple(operands), sql"") + val filter = multipleSqlToAction(first = true, "or", filterToSqlMultiple(operands), sql"") sql"(" concat filter concat sql")" } } -- cgit v1.2.3 From 72e4fe43bb03d2922a46f54e5a2f51a13c6df63a Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Tue, 24 Oct 2017 17:35:37 +0700 Subject: Implemented tests for MedicalRecord.Meta; Fixed MedicalRecord.Meta.Duplicate spray format --- .../formats/json/sprayformats/record.scala | 2 +- .../MedicalRecordMetaFormatSuite.scala | 85 ++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala index d00aa14..60ae437 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/record.scala @@ -94,7 +94,7 @@ object record { val endOriginalPage = fields .get("endOriginalPage") - .map(_.convertTo[Double]) + .flatMap(_.convertTo[Option[Double]]) Duplicate( startPage = startPage, diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala new file mode 100644 index 0000000..845bffc --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala @@ -0,0 +1,85 @@ +package xyz.driver.pdsuidomain.formats.json.sprayformats + +import org.scalatest.{FlatSpec, Matchers} +import spray.json._ +import xyz.driver.pdsuidomain.entities.MedicalRecord.Meta +import record._ + +class MedicalRecordMetaFormatSuite + extends FlatSpec + with Matchers { + + "Json format for MedicalRecord.Meta" should "read and write correct JSON" in { + val duplicate1 = Meta.Duplicate( + startPage = 1.0d, + endPage = 2.0d, + startOriginalPage = 1.0d, + endOriginalPage = Some(2.0d) + ) + + val duplicate2 = Meta.Duplicate( + startPage = 1.0d, + endPage = 2.0d, + startOriginalPage = 1.0d, + endOriginalPage = None + ) + + val reorder = Meta.Reorder( + Seq(1, 2) + ) + + val rotation = Meta.Rotation( + Map("item1" -> 1, "item2" -> 2) + ) + + val writtenDuplicateJson1 = + duplicateMetaFormat.write(duplicate1) + + val writtenDuplicateJson2 = + duplicateMetaFormat.write(duplicate2) + + val writtenReorderJson = + reorderMetaFormat.write(reorder) + + val writtenRotationJson = + rotateMetaFormat.write(rotation) + + writtenDuplicateJson1 should be( + """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":2.0}""".parseJson) + + writtenDuplicateJson2 should be( + """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":null}""".parseJson) + + writtenReorderJson should be( + """{"type":"reorder","items":[1,2]}""".parseJson) + + writtenRotationJson should be( + """{"type":"rotation","items":{"item1":1,"item2":2}}""".parseJson) + + val parsedDuplicateJson1 = + duplicateMetaFormat.read(writtenDuplicateJson1) + + val parsedDuplicateJson2 = + duplicateMetaFormat.read(writtenDuplicateJson2) + + val parsedReorderJson = + reorderMetaFormat.read(writtenReorderJson) + + val parsedRotationJson = + rotateMetaFormat.read(writtenRotationJson) + + duplicate1 should be(parsedDuplicateJson1) + + duplicate2 should be(parsedDuplicateJson2) + + reorder should be(parsedReorderJson) + + rotation should be(parsedRotationJson) + + duplicate1 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate1))) + duplicate2 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate2))) + reorder should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(reorder))) + rotation should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(rotation))) + } + +} -- cgit v1.2.3 From f8eb20e07891203c7ac7c9fa460c6db930acd06a Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 25 Oct 2017 17:51:34 -0700 Subject: Fixing filters parsing for UUIDs --- .../scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala | 7 +++---- .../xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala | 7 +++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 41533fe..a19945a 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -108,10 +108,9 @@ object SearchFilterParser { P((IgnoreCase("true") | IgnoreCase("false")).!.map(_.toBoolean)) private val binaryAtomParser: Parser[SearchFilterExpr.Atom.Binary] = P( - dimensionParser ~ whitespaceParser ~ ( - (numericOperatorParser.! ~ whitespaceParser ~ (longParser | booleanParser | numberParser.!)) | - (commonOperatorParser.! ~ whitespaceParser ~ AnyChar.rep(min = 1).!) - ) ~ End + dimensionParser ~ whitespaceParser ~ + ((numericOperatorParser.! ~ whitespaceParser ~ (longParser | booleanParser | numberParser.!) ~ End) | + (commonOperatorParser.! ~ whitespaceParser ~ AnyChar.rep(min = 1).! ~ End)) ).map { case BinaryAtomFromTuple(atom) => atom } diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala index ac5eec1..ac0010f 100644 --- a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala +++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala @@ -126,6 +126,13 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers { } } + "actual patientId uuid" - { + "should parse the full UUID" in { + val filter = SearchFilterParser.parse(Seq("filters" -> "patientId EQ 4b4879f7-42b3-4b7c-a685-5c97d9e69e7c")) + assert(filter === Success(SearchFilterExpr.Atom.Binary(Dimension(None, "patient_id"), Eq, "4b4879f7-42b3-4b7c-a685-5c97d9e69e7c"))) + } + } + "all operators" - { "should be parsed with numeric values" in check { val testQueryGen = queryGen( -- cgit v1.2.3 From 4844d7c785cf71b8e6a1c151195b112f9dce6d0d Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 25 Oct 2017 18:50:54 -0700 Subject: UUID type support in filters parsing --- .../pdsuicommon/parsers/SearchFilterParser.scala | 26 +++++++++++++++------- .../parsers/SearchFilterParserSuite.scala | 7 ++++-- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index a19945a..718a42d 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -1,5 +1,7 @@ package xyz.driver.pdsuicommon.parsers +import java.util.UUID + import xyz.driver.pdsuicommon.utils.Implicits.{toCharOps, toStringOps} import fastparse.all._ import fastparse.core.Parsed @@ -89,10 +91,8 @@ object SearchFilterParser { case _ => true } - // Exclude Unicode "digits" - private val digitsParser: Parser[String] = P(CharIn('0' to '9').rep(min = 1).!) + private val digitsParser: Parser[String] = P(CharIn('0' to '9').rep(min = 1).!) // Exclude Unicode "digits" - // @TODO Make complex checking here private val numberParser: Parser[String] = P(isPositiveParser ~ digitsParser.! ~ ("." ~ digitsParser).!.?).map { case (false, intPart, Some(fracPart)) => s"-$intPart.${fracPart.tail}" case (false, intPart, None) => s"-$intPart" @@ -107,10 +107,20 @@ object SearchFilterParser { private val booleanParser: Parser[Boolean] = P((IgnoreCase("true") | IgnoreCase("false")).!.map(_.toBoolean)) + private val hexDigit: Parser[String] = P((CharIn('a' to 'f') | CharIn('A' to 'F') | CharIn('0' to '9')).!) + + private val uuidParser: Parser[UUID] = + P( + hexDigit.rep(8).! ~ "-" ~ hexDigit.rep(4).! ~ "-" ~ hexDigit.rep(4).! ~ "-" ~ hexDigit.rep(4).! ~ "-" ~ hexDigit + .rep(12) + .!).map { + case (group1, group2, group3, group4, group5) => UUID.fromString(s"$group1-$group2-$group3-$group4-$group5") + } + private val binaryAtomParser: Parser[SearchFilterExpr.Atom.Binary] = P( dimensionParser ~ whitespaceParser ~ - ((numericOperatorParser.! ~ whitespaceParser ~ (longParser | booleanParser | numberParser.!) ~ End) | - (commonOperatorParser.! ~ whitespaceParser ~ AnyChar.rep(min = 1).! ~ End)) + ((numericOperatorParser.! ~ whitespaceParser ~ (longParser | numberParser.!) ~ End) | + (commonOperatorParser.! ~ whitespaceParser ~ (uuidParser | booleanParser | AnyChar.rep(min = 1).!) ~ End)) ).map { case BinaryAtomFromTuple(atom) => atom } @@ -118,9 +128,9 @@ object SearchFilterParser { private val nAryAtomParser: Parser[SearchFilterExpr.Atom.NAry] = P( dimensionParser ~ whitespaceParser ~ ( naryOperatorParser ~ whitespaceParser ~ - (longParser.rep(min = 1, sep = ",") | booleanParser.rep(min = 1, sep = ",") | nAryValueParser.!.rep(min = 1, - sep = ",")) - ) ~ End + ((longParser.rep(min = 1, sep = ",") ~ End) | (booleanParser.rep(min = 1, sep = ",") ~ End) | + (nAryValueParser.!.rep(min = 1, sep = ",") ~ End)) + ) ).map { case NAryAtomFromTuple(atom) => atom } diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala index ac0010f..0209222 100644 --- a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala +++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala @@ -1,5 +1,7 @@ package xyz.driver.pdsuicommon.parsers +import java.util.UUID + import xyz.driver.pdsuicommon.db.SearchFilterExpr.Dimension import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation} import xyz.driver.pdsuicommon.utils.Implicits.toStringOps @@ -127,9 +129,10 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers { } "actual patientId uuid" - { - "should parse the full UUID" in { + "should parse the full UUID as java.util.UUID type" in { val filter = SearchFilterParser.parse(Seq("filters" -> "patientId EQ 4b4879f7-42b3-4b7c-a685-5c97d9e69e7c")) - assert(filter === Success(SearchFilterExpr.Atom.Binary(Dimension(None, "patient_id"), Eq, "4b4879f7-42b3-4b7c-a685-5c97d9e69e7c"))) + assert(filter === Success(SearchFilterExpr.Atom.Binary( + Dimension(None, "patient_id"), Eq, UUID.fromString("4b4879f7-42b3-4b7c-a685-5c97d9e69e7c")))) } } -- cgit v1.2.3 From bb4176645aed22311b372f8f7f4897f9012322bd Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 25 Oct 2017 19:18:48 -0700 Subject: Support for table names in filters called over REST --- src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala index 0ff29ef..7275e3c 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala @@ -57,9 +57,9 @@ trait RestHelper { def exprToQuery(expr: SearchFilterExpr): Seq[(String, String)] = expr match { case SearchFilterExpr.Empty => Seq.empty case SearchFilterExpr.Atom.Binary(dimension, op, value) => - Seq("filters" -> s"${dimension.name} ${opToString(op)} $value") + Seq("filters" -> s"${dimension.tableName.fold("")(t => s"$t.") + dimension.name} ${opToString(op)} $value") case SearchFilterExpr.Atom.NAry(dimension, SearchFilterNAryOperation.In, values) => - Seq("filters" -> s"${dimension.name} in ${values.mkString(",")}") + Seq("filters" -> s"${dimension.tableName.fold("")(t => s"$t.") + dimension.name} in ${values.mkString(",")}") case SearchFilterExpr.Intersection(ops) => ops.flatMap(op => exprToQuery(op)) case expr => sys.error(s"No parser available for filter expression $expr.") -- cgit v1.2.3 From 91d58df031f5fdac59d70289bf9a7377a3529a9a Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 26 Oct 2017 09:55:50 +0700 Subject: Moved MedicalRecord.Meta tests to MedicalRecordFormatSuite --- .../sprayformats/MedicalRecordFormatSuite.scala | 74 +++++++++++++++++++ .../MedicalRecordMetaFormatSuite.scala | 85 ---------------------- 2 files changed, 74 insertions(+), 85 deletions(-) delete mode 100644 src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordFormatSuite.scala index 9616e70..363e352 100644 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordFormatSuite.scala +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordFormatSuite.scala @@ -12,6 +12,80 @@ class MedicalRecordFormatSuite extends FlatSpec with Matchers { import record._ import MedicalRecord._ + "Json format for MedicalRecord.Meta" should "read and write correct JSON" in { + val duplicate1 = Meta.Duplicate( + startPage = 1.0d, + endPage = 2.0d, + startOriginalPage = 1.0d, + endOriginalPage = Some(2.0d) + ) + + val duplicate2 = Meta.Duplicate( + startPage = 1.0d, + endPage = 2.0d, + startOriginalPage = 1.0d, + endOriginalPage = None + ) + + val reorder = Meta.Reorder( + Seq(1, 2) + ) + + val rotation = Meta.Rotation( + Map("item1" -> 1, "item2" -> 2) + ) + + val writtenDuplicateJson1 = + duplicateMetaFormat.write(duplicate1) + + val writtenDuplicateJson2 = + duplicateMetaFormat.write(duplicate2) + + val writtenReorderJson = + reorderMetaFormat.write(reorder) + + val writtenRotationJson = + rotateMetaFormat.write(rotation) + + writtenDuplicateJson1 should be( + """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":2.0}""".parseJson) + + writtenDuplicateJson2 should be( + """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":null}""".parseJson) + + writtenReorderJson should be( + """{"type":"reorder","items":[1,2]}""".parseJson) + + writtenRotationJson should be( + """{"type":"rotation","items":{"item1":1,"item2":2}}""".parseJson) + + val parsedDuplicateJson1 = + duplicateMetaFormat.read(writtenDuplicateJson1) + + val parsedDuplicateJson2 = + duplicateMetaFormat.read(writtenDuplicateJson2) + + val parsedReorderJson = + reorderMetaFormat.read(writtenReorderJson) + + val parsedRotationJson = + rotateMetaFormat.read(writtenRotationJson) + + duplicate1 should be(parsedDuplicateJson1) + + duplicate2 should be(parsedDuplicateJson2) + + reorder should be(parsedReorderJson) + + rotation should be(parsedRotationJson) + + duplicate1 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate1))) + duplicate2 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate2))) + reorder should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(reorder))) + rotation should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(rotation))) + } + + "Json format for MedicalRecord" should "read and write correct JSON" in { val orig = MedicalRecord( id = LongId(1), diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala deleted file mode 100644 index 845bffc..0000000 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordMetaFormatSuite.scala +++ /dev/null @@ -1,85 +0,0 @@ -package xyz.driver.pdsuidomain.formats.json.sprayformats - -import org.scalatest.{FlatSpec, Matchers} -import spray.json._ -import xyz.driver.pdsuidomain.entities.MedicalRecord.Meta -import record._ - -class MedicalRecordMetaFormatSuite - extends FlatSpec - with Matchers { - - "Json format for MedicalRecord.Meta" should "read and write correct JSON" in { - val duplicate1 = Meta.Duplicate( - startPage = 1.0d, - endPage = 2.0d, - startOriginalPage = 1.0d, - endOriginalPage = Some(2.0d) - ) - - val duplicate2 = Meta.Duplicate( - startPage = 1.0d, - endPage = 2.0d, - startOriginalPage = 1.0d, - endOriginalPage = None - ) - - val reorder = Meta.Reorder( - Seq(1, 2) - ) - - val rotation = Meta.Rotation( - Map("item1" -> 1, "item2" -> 2) - ) - - val writtenDuplicateJson1 = - duplicateMetaFormat.write(duplicate1) - - val writtenDuplicateJson2 = - duplicateMetaFormat.write(duplicate2) - - val writtenReorderJson = - reorderMetaFormat.write(reorder) - - val writtenRotationJson = - rotateMetaFormat.write(rotation) - - writtenDuplicateJson1 should be( - """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":2.0}""".parseJson) - - writtenDuplicateJson2 should be( - """{"startOriginalPage":1.0,"endPage":2.0,"startPage":1.0,"type":"duplicate","endOriginalPage":null}""".parseJson) - - writtenReorderJson should be( - """{"type":"reorder","items":[1,2]}""".parseJson) - - writtenRotationJson should be( - """{"type":"rotation","items":{"item1":1,"item2":2}}""".parseJson) - - val parsedDuplicateJson1 = - duplicateMetaFormat.read(writtenDuplicateJson1) - - val parsedDuplicateJson2 = - duplicateMetaFormat.read(writtenDuplicateJson2) - - val parsedReorderJson = - reorderMetaFormat.read(writtenReorderJson) - - val parsedRotationJson = - rotateMetaFormat.read(writtenRotationJson) - - duplicate1 should be(parsedDuplicateJson1) - - duplicate2 should be(parsedDuplicateJson2) - - reorder should be(parsedReorderJson) - - rotation should be(parsedRotationJson) - - duplicate1 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate1))) - duplicate2 should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(duplicate2))) - reorder should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(reorder))) - rotation should be(recordMetaTypeFormat.read(recordMetaTypeFormat.write(rotation))) - } - -} -- cgit v1.2.3 From b6943690e5e572f6edc9fe985d33a6349a4a6812 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Thu, 26 Oct 2017 17:44:42 +0700 Subject: Reimplemented entity ExtractedData.Meta --- .../pdsuidomain/entities/ExtractedData.scala | 2 +- .../fakes/entities/recordprocessing.scala | 6 ++--- .../sprayformats/ExtractedDataFormatSuite.scala | 27 ++++++++++++++++------ 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala index 352cf55..569375a 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/ExtractedData.scala @@ -17,7 +17,7 @@ final case class ExtractedData(id: LongId[ExtractedData] = LongId(0L), object ExtractedData { - final case class Meta(keyword: Meta.Keyword, evidence: Meta.Evidence) + final case class Meta(keyword: Option[Meta.Keyword], evidence: Option[Meta.Evidence]) object Meta { final case class Evidence(pageRatio: Double, start: TextLayerPosition, end: TextLayerPosition) diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/recordprocessing.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/recordprocessing.scala index 86583c1..3a018fa 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/recordprocessing.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/recordprocessing.scala @@ -245,13 +245,13 @@ object recordprocessing { def nextExtractedDataMeta(): Meta = { ExtractedData.Meta( - nextExtractedDataMetaKeyword(), - nextExtractedDataMetaEvidence() + nextOption(nextExtractedDataMetaKeyword()), + nextOption(nextExtractedDataMetaEvidence()) ) } def nextExtractedDataMetaJson(): TextJson[Meta] = - nextTextJson(ExtractedData.Meta(nextExtractedDataMetaKeyword(), nextExtractedDataMetaEvidence())) + nextTextJson(nextExtractedDataMeta()) def nextExtractedData(): ExtractedData = { ExtractedData( diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExtractedDataFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExtractedDataFormatSuite.scala index 1feca6a..d87b6c7 100644 --- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExtractedDataFormatSuite.scala +++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/ExtractedDataFormatSuite.scala @@ -9,6 +9,7 @@ import xyz.driver.pdsuidomain.entities.{ExtractedData, ExtractedDataLabel} import xyz.driver.pdsuidomain.services.ExtractedDataService.RichExtractedData class ExtractedDataFormatSuite extends FlatSpec with Matchers { + import extracteddata._ "Json format for ExtractedData" should "read and write correct JSON" in { @@ -17,8 +18,18 @@ class ExtractedDataFormatSuite extends FlatSpec with Matchers { documentId = LongId(101), keywordId = Some(LongId(201)), evidenceText = Some("evidence text"), - meta = None - ) + meta = Option(TextJson(Meta( + evidence = None, + keyword = + Some(Meta.Keyword( + page = 1, + pageRatio = Some(1.6161616161616161d), + index = 0, + sortIndex = "1080000" + )) + ) + ))) + val extractedDataLabels = List( ExtractedDataLabel( id = LongId(1), @@ -43,11 +54,13 @@ class ExtractedDataFormatSuite extends FlatSpec with Matchers { writtenJson should be( """{"id":1,"documentId":101,"keywordId":201,"evidence":"evidence text","meta":null, - "labels":[{"id":null,"categoryId":null,"value":"Yes"},{"id":12,"categoryId":1,"value":"No"}]}""".parseJson) + "labels":[{"id":null,"categoryId":null,"value":"Yes"},{"id":12,"categoryId":1,"value":"No"}], + "meta":{"keyword":{"index":0,"page":1,"pageRatio":1.6161616161616161,"sortIndex":"1080000"}}}""".parseJson) val createExtractedDataJson = """{"documentId":101,"keywordId":201,"evidence":"evidence text", - "labels":[{"value":"Yes"},{"id":12,"categoryId":1,"value":"No"}]}""".parseJson + "labels":[{"value":"Yes"},{"id":12,"categoryId":1,"value":"No"}], + "meta":{"keyword":{"index":0,"page":1,"pageRatio":1.6161616161616161,"sortIndex":"1080000"}}}""".parseJson val expectedCreatedExtractedData = origRichExtractedData.copy( extractedData = extractedData.copy(id = LongId(0)), labels = extractedDataLabels.map(_.copy(id = LongId(0), dataId = LongId(0))) @@ -80,12 +93,12 @@ class ExtractedDataFormatSuite extends FlatSpec with Matchers { evidenceText = Some("new evidence text"), meta = Some( TextJson(Meta( - keyword = Meta.Keyword(page = 1, pageRatio = None, index = 2, sortIndex = "ASC"), - evidence = Meta.Evidence( + keyword = Some(Meta.Keyword(page = 1, pageRatio = None, index = 2, sortIndex = "ASC")), + evidence = Some(Meta.Evidence( pageRatio = 1.0, start = Meta.TextLayerPosition(page = 1, index = 3, offset = 2), end = Meta.TextLayerPosition(page = 2, index = 3, offset = 10) - ) + )) ))) ), labels = updatedExtractedDataLabels -- cgit v1.2.3