From 5832f63b84d7388441d1200f2442dc1e9de0225c Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 27 Jun 2017 17:13:02 -0700 Subject: All PDS UI domain models, API case classes, service traits and necessary utils moved to pdsui-common --- build.sbt | 3 + .../pdsuicommon/computation/Computation.scala | 110 +++++++++++++++ .../computation/FutureToComputationOps.scala | 24 ++++ .../driver/pdsuicommon/computation/Implicits.scala | 15 ++ .../computation/TryToComputationOps.scala | 15 ++ .../xyz/driver/pdsuicommon/domain/FuzzyValue.scala | 12 ++ .../driver/pdsuicommon/json/JsonSerializer.scala | 26 ++++ .../pdsuicommon/json/JsonValidationException.scala | 5 + .../driver/pdsuicommon/json/Serialization.scala | 42 ++++++ .../xyz/driver/pdsuicommon/utils/Computation.scala | 110 --------------- .../driver/pdsuicommon/utils/JsonSerializer.scala | 26 ---- .../validation/AdditionalConstraints.scala | 25 ++++ .../driver/pdsuicommon/validation/Validators.scala | 2 +- .../driver/pdsuicommon/validation/package.scala | 8 ++ .../xyz/driver/pdsuidomain/entities/Arm.scala | 10 +- .../xyz/driver/pdsuidomain/entities/Document.scala | 30 ++-- .../xyz/driver/pdsuidomain/entities/Patient.scala | 22 +-- .../pdsuidomain/entities/PatientHypothesis.scala | 10 +- .../driver/pdsuidomain/entities/PatientLabel.scala | 14 +- .../pdsuidomain/entities/RawPatientLabel.scala | 27 ++-- .../driver/pdsuidomain/entities/ScrapedTrial.scala | 105 ++++++++++++++ .../export/patient/ExportPatientLabel.scala | 28 ---- .../patient/ExportPatientLabelEvidence.scala | 32 ----- .../ExportPatientLabelEvidenceDocument.scala | 30 ---- .../export/patient/ExportPatientWithLabels.scala | 25 ---- .../entities/export/trial/ExportTrial.scala | 27 ---- .../entities/export/trial/ExportTrialArm.scala | 15 -- .../export/trial/ExportTrialLabelCriterion.scala | 32 ----- .../export/trial/ExportTrialWithLabels.scala | 56 -------- .../pdsuidomain/formats/json/arm/ApiArm.scala | 22 +++ .../formats/json/arm/ApiCreateArm.scala | 20 +++ .../formats/json/arm/ApiPartialArm.scala | 14 ++ .../formats/json/category/ApiCategory.scala | 23 +++ .../formats/json/criterion/ApiCriterion.scala | 40 ++++++ .../formats/json/criterion/ApiNewCriterion.scala | 41 ++++++ .../json/criterion/ApiUpdateCriterion.scala | 56 ++++++++ .../formats/json/document/ApiDocument.scala | 71 ++++++++++ .../formats/json/document/ApiDocumentType.scala | 20 +++ .../formats/json/document/ApiPartialDocument.scala | 114 +++++++++++++++ .../formats/json/document/ApiProviderType.scala | 20 +++ .../formats/json/document/DocumentUtils.scala | 24 ++++ .../json/evidence/ApiPatientLabelEvidence.scala | 38 +++++ .../json/extracteddata/ApiExtractedData.scala | 44 ++++++ .../extracteddata/ApiPartialExtractedData.scala | 80 +++++++++++ .../formats/json/hypothesis/ApiHypothesis.scala | 26 ++++ .../json/intervention/ApiIntervention.scala | 41 ++++++ .../json/intervention/ApiInterventionType.scala | 20 +++ .../json/intervention/ApiPartialIntervention.scala | 44 ++++++ .../formats/json/keyword/ApiKeyword.scala | 23 +++ .../formats/json/label/ApiCriterionLabel.scala | 49 +++++++ .../formats/json/label/ApiExtractedDataLabel.scala | 36 +++++ .../pdsuidomain/formats/json/label/ApiLabel.scala | 22 +++ .../formats/json/message/ApiMessage.scala | 59 ++++++++ .../formats/json/message/ApiPartialMessage.scala | 84 +++++++++++ .../json/password/PasswordCreateRequest.scala | 9 ++ .../json/password/PasswordUpdateRequest.scala | 9 ++ .../formats/json/patient/ApiPatient.scala | 44 ++++++ .../formats/json/patient/PatientStatus.scala | 24 ++++ .../eligible/ApiPartialPatientEligibleTrial.scala | 18 +++ .../patient/eligible/ApiPatientEligibleTrial.scala | 46 ++++++ .../hypothesis/ApiPartialPatientHypothesis.scala | 27 ++++ .../patient/hypothesis/ApiPatientHypothesis.scala | 32 +++++ .../patient/label/ApiPartialPatientLabel.scala | 38 +++++ .../json/patient/label/ApiPatientLabel.scala | 47 ++++++ .../label/ApiPatientLabelDefiningCriteria.scala | 25 ++++ .../patient/trial/ApiPartialPatientCriterion.scala | 40 ++++++ .../trial/ApiPartialPatientCriterionList.scala | 32 +++++ .../json/patient/trial/ApiPatientCriterion.scala | 72 ++++++++++ .../formats/json/record/ApiCreateRecord.scala | 37 +++++ .../formats/json/record/ApiRecord.scala | 57 ++++++++ .../formats/json/record/ApiUpdateRecord.scala | 47 ++++++ .../formats/json/record/MedicalRecordStatus.scala | 34 +++++ .../formats/json/session/NewSessionRequest.scala | 12 ++ .../formats/json/session/NewSessionResponse.scala | 11 ++ .../formats/json/studydesign/ApiStudyDesign.scala | 20 +++ .../formats/json/trial/ApiPartialTrial.scala | 44 ++++++ .../pdsuidomain/formats/json/trial/ApiTrial.scala | 63 +++++++++ .../formats/json/trial/TrialStatus.scala | 30 ++++ .../formats/json/user/ApiPartialUser.scala | 83 +++++++++++ .../pdsuidomain/formats/json/user/ApiUser.scala | 32 +++++ .../pdsuidomain/formats/json/user/UserRole.scala | 33 +++++ .../driver/pdsuidomain/services/ArmService.scala | 133 +++++++++++++++++ .../pdsuidomain/services/CriterionService.scala | 131 +++++++++++++++++ .../pdsuidomain/services/DocumentService.scala | 157 +++++++++++++++++++++ .../pdsuidomain/services/DocumentTypeService.scala | 26 ++++ .../services/ExtractedDataService.scala | 116 +++++++++++++++ .../pdsuidomain/services/HypothesisService.scala | 28 ++++ .../pdsuidomain/services/InterventionService.scala | 85 +++++++++++ .../services/InterventionTypeService.scala | 28 ++++ .../pdsuidomain/services/KeywordService.scala | 29 ++++ .../services/MedicalRecordService.scala | 137 ++++++++++++++++++ .../services/PatientCriterionService.scala | 126 +++++++++++++++++ .../services/PatientEligibleTrialService.scala | 137 ++++++++++++++++++ .../services/PatientHypothesisService.scala | 105 ++++++++++++++ .../services/PatientLabelEvidenceService.scala | 70 +++++++++ .../pdsuidomain/services/PatientLabelService.scala | 124 ++++++++++++++++ .../pdsuidomain/services/PatientService.scala | 105 ++++++++++++++ .../pdsuidomain/services/ProviderTypeService.scala | 27 ++++ .../services/ScrapedTrialsService.scala | 54 +++++++ .../pdsuidomain/services/StudyDesignService.scala | 28 ++++ .../driver/pdsuidomain/services/TrialService.scala | 132 +++++++++++++++++ 101 files changed, 4222 insertions(+), 439 deletions(-) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/computation/Implicits.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/json/JsonSerializer.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/json/JsonValidationException.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/utils/Computation.scala delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/utils/JsonSerializer.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/validation/package.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/ScrapedTrial.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala delete mode 100644 src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiArm.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiCreateArm.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiPartialArm.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/category/ApiCategory.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiUpdateCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiPartialExtractedData.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/hypothesis/ApiHypothesis.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/keyword/ApiKeyword.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiCriterionLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiExtractedDataLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiMessage.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordCreateRequest.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordUpdateRequest.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/PatientStatus.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPartialPatientEligibleTrial.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPartialPatientHypothesis.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabel.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabelDefiningCriteria.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterionList.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/record/MedicalRecordStatus.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionRequest.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionResponse.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/studydesign/ApiStudyDesign.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiPartialTrial.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/TrialStatus.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiUser.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/user/UserRole.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/CriterionService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/DocumentTypeService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/ExtractedDataService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/InterventionTypeService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/KeywordService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientEligibleTrialService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/PatientService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/ProviderTypeService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/ScrapedTrialsService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/StudyDesignService.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala diff --git a/build.sbt b/build.sbt index 90e1a2c..0baf2f0 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,9 @@ lazy val core = (project in file(".")) "com.typesafe" % "config" % "1.3.0", "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.3", "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.8.4", + "com.typesafe.play" %% "play" % "2.5.15", + "org.davidbild" %% "tristate-core" % "0.2.0", + "org.davidbild" %% "tristate-play" % "0.2.0" exclude ("com.typesafe.play", "play-json"), "org.asynchttpclient" % "async-http-client" % "2.0.24", "io.getquill" %% "quill-jdbc" % "1.2.1", "io.github.cloudify" %% "spdf" % "1.4.0", diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala new file mode 100644 index 0000000..ad458de --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala @@ -0,0 +1,110 @@ +package xyz.driver.pdsuicommon.computation + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Takes care of computations + * + * Success(either) - the computation will be continued. + * Failure(error) - the computation was failed with unhandled error. + * + * Either[Result, T]: + * Left(result) is a final and handled result, another computations (map, flatMap) will be ignored. + * Right(T) is a current result. Functions in map/flatMap will continue the computation. + * + * Example: + * {{{ + * import scala.concurrent.ExecutionContext.Implicits.global + * import scala.concurrent.{ExecutionContext, Future} + * import com.drivergrp.server.com.drivergrp.server.common.utils.Computation + * + * def successful = for { + * x <- Computation.continue(1) + * y <- Computation.continue(2) + * } yield s"\$x + \$y" + * + * // Prints "Success(1 + 2)" + * successful.join.onComplete(print) + * + * def failed = for { + * x <- Computation.abort("Failed on x") + * _ = print("Second step") + * y <- Computation.continue(2) + * } yield s"\$x + \$y" + * + * // Prints "Success(Failed on x)" + * failed.join.onComplete(print) + * }}} + * + * TODO: Make future private + * + * @param future The final flow in a future. + * @tparam R Type of result for aborted computation. + * @tparam T Type of result for continued computation. + */ +final case class Computation[+R, +T](future: Future[Either[R, T]]) { + + def flatMap[R2, T2](f: T => Computation[R2, T2])(implicit ec: ExecutionContext, ev: R <:< R2): Computation[R2, T2] = { + Computation(future.flatMap { + case Left(x) => Future.successful(Left(x)) + case Right(x) => f(x).future + }) + } + + def processExceptions[R2](f: PartialFunction[Throwable, R2])(implicit ev1: R <:< R2, + ec: ExecutionContext): Computation[R2, T] = { + val strategy = f.andThen(x => Left(x): Either[R2, T]) + val castedFuture: Future[Either[R2, T]] = future.map { + case Left(x) => Left(x) + case Right(x) => Right(x) + } + Computation(castedFuture.recover(strategy)) + } + + def map[T2](f: T => T2)(implicit ec: ExecutionContext): Computation[R, T2] = flatMap { a => + Computation.continue(f(a)) + } + + def andThen(f: T => Any)(implicit ec: ExecutionContext): Computation[R, T] = map { a => + f(a) + a + } + + def filter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = map { a => + if (f(a)) a + else throw new NoSuchElementException("When filtering") + } + + def withFilter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = filter(f) + + def foreach[T2](f: T => T2)(implicit ec: ExecutionContext): Unit = future.foreach { + case Right(x) => f(x) + case _ => + } + + def toFuture[R2](resultFormatter: T => R2)(implicit ec: ExecutionContext, ev: R <:< R2): Future[R2] = future.map { + case Left(x) => x + case Right(x) => resultFormatter(x) + } + + def toFuture[R2](implicit ec: ExecutionContext, ev1: R <:< R2, ev2: T <:< R2): Future[R2] = future.map { + case Left(x) => x + case Right(x) => x + } + +} + +object Computation { + + def continue[T](x: T): Computation[Nothing, T] = Computation(Future.successful(Right(x))) + + def abort[R](result: R): Computation[R, Nothing] = Computation(Future.successful(Left(result))) + + def fail(exception: Throwable): Computation[Nothing, Nothing] = Computation(Future.failed(exception)) + + def fromFuture[T](input: Future[T])(implicit ec: ExecutionContext): Computation[Nothing, T] = Computation { + input.map { x => + Right(x) + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala new file mode 100644 index 0000000..c5800dc --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/FutureToComputationOps.scala @@ -0,0 +1,24 @@ +package xyz.driver.pdsuicommon.computation + +import xyz.driver.pdsuicommon.error.DomainError + +import scala.concurrent.{ExecutionContext, Future} + +final class FutureToComputationOps[T](val self: Future[T]) extends AnyVal { + + def handleDomainError[U, ER](f: PartialFunction[T, U]) + (implicit unsuitableToErrorsResponse: DomainError => ER, + ec: ExecutionContext): Future[Either[ER, U]] = { + self.map { + case x if f.isDefinedAt(x) => Right(f(x)) + case x: DomainError => Left(unsuitableToErrorsResponse(x)) + case x => throw new RuntimeException(s"Can not process $x") + } + } + + def toComputation[U, ER](f: PartialFunction[T, U]) + (implicit unsuitableToErrorsResponse: DomainError => ER, + ec: ExecutionContext): Computation[ER, U] = { + Computation(handleDomainError(f)) + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/Implicits.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/Implicits.scala new file mode 100644 index 0000000..d5acc2d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/Implicits.scala @@ -0,0 +1,15 @@ +package xyz.driver.pdsuicommon.computation + +import scala.concurrent.Future +import scala.util.Try + +trait Implicits { + + implicit def futureToFutureComputationOps[T](self: Future[T]): FutureToComputationOps[T] = { + new FutureToComputationOps[T](self) + } + + implicit def tryToTryComputationOps[T](self: Try[T]): TryToComputationOps[T] = { + new TryToComputationOps[T](self) + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala b/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala new file mode 100644 index 0000000..8282bc6 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/computation/TryToComputationOps.scala @@ -0,0 +1,15 @@ +package xyz.driver.pdsuicommon.computation + +import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} + +final class TryToComputationOps[T](val self: Try[T]) extends AnyVal { + + def toComputation[ER](implicit exceptionToErrorResponse: Throwable => ER, + ec: ExecutionContext): Computation[ER, T] = self match { + case Success(x) => Computation.continue(x) + case Failure(NonFatal(e)) => Computation.abort(exceptionToErrorResponse(e)) + case Failure(e) => Computation.fail(e) + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala b/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala index 4c6bf3f..4e98f40 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala @@ -15,4 +15,16 @@ object FuzzyValue { def fromBoolean(x: Boolean): FuzzyValue = if (x) Yes else No implicit def toPhiString(x: FuzzyValue): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass)) + + val fromString: PartialFunction[String, FuzzyValue] = { + case "Yes" => Yes + case "No" => No + case "Maybe" => Maybe + } + + def valueToString(x: FuzzyValue): String = x match { + case Yes => "Yes" + case No => "No" + case Maybe => "Maybe" + } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/JsonSerializer.scala b/src/main/scala/xyz/driver/pdsuicommon/json/JsonSerializer.scala new file mode 100644 index 0000000..a53f1dd --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/json/JsonSerializer.scala @@ -0,0 +1,26 @@ +package xyz.driver.pdsuicommon.json + +import java.text.SimpleDateFormat + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.scala.DefaultScalaModule +import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper + +object JsonSerializer { + + private val mapper = new ObjectMapper() with ScalaObjectMapper + mapper.registerModule(DefaultScalaModule) + mapper.registerModule(new JavaTimeModule) + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) + + def serialize(value: Any): String = { + mapper.writeValueAsString(value) + } + + def deserialize[T](value: String)(implicit m: Manifest[T]): T = { + mapper.readValue(value) + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/JsonValidationException.scala b/src/main/scala/xyz/driver/pdsuicommon/json/JsonValidationException.scala new file mode 100644 index 0000000..21750b4 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/json/JsonValidationException.scala @@ -0,0 +1,5 @@ +package xyz.driver.pdsuicommon.json + +import xyz.driver.pdsuicommon.validation.JsonValidationErrors + +class JsonValidationException(val errors: JsonValidationErrors) extends Exception diff --git a/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala new file mode 100644 index 0000000..a6d3ee9 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/json/Serialization.scala @@ -0,0 +1,42 @@ +package xyz.driver.pdsuicommon.json + +import java.net.URI + +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain._ + +object Serialization { + + // @TODO Test and check all items in an array + private def seqJsonReads[T](implicit argFormat: Reads[T]): Reads[Seq[T]] = Reads { + case JsArray(xs) => JsSuccess(xs.map { x => argFormat.reads(x).get }) + case x => JsError(s"Expected JsArray, but got $x") + } + + private def seqJsonWrites[T](implicit argFormat: Writes[T]): Writes[Seq[T]] = Writes { xs => + JsArray(xs.map(argFormat.writes)) + } + + implicit def seqJsonFormat[T](implicit f: Format[T]): Format[Seq[T]] = Format(seqJsonReads[T], seqJsonWrites[T]) + + private val uriJsonReads: Reads[URI] = Reads.StringReads.map(URI.create) + private val uriJsonWrites: Writes[URI] = Writes(uri => JsString(uri.toString)) + implicit val uriJsonFormat: Format[URI] = Format(uriJsonReads, uriJsonWrites) + + private def uuidIdJsonReads[T]: Reads[UuidId[T]] = Reads.uuidReads.map(x => UuidId[T](x)) + private def uuidIdJsonWrites[T]: Writes[UuidId[T]] = Writes.UuidWrites.contramap(_.id) + implicit def uuidIdJsonFormat[T]: Format[UuidId[T]] = Format(uuidIdJsonReads, uuidIdJsonWrites) + + private def longIdJsonReads[T]: Reads[LongId[T]] = Reads.LongReads.map(x => LongId[T](x)) + private def longIdJsonWrites[T]: Writes[LongId[T]] = Writes.LongWrites.contramap(_.id) + implicit def longIdJsonFormat[T]: Format[LongId[T]] = Format(longIdJsonReads, longIdJsonWrites) + + private val emailJsonReads: Reads[Email] = Reads.email.map(Email.apply) + private val emailJsonWrites: Writes[Email] = Writes(email => JsString(email.value)) + implicit val emailJsonFormat: Format[Email] = Format(emailJsonReads, emailJsonWrites) + + private val passwordHashJsonReads: Reads[PasswordHash] = Reads.StringReads.map(hash => PasswordHash(hash.getBytes("UTF-8"))) + private val passwordHashJsonWrites: Writes[PasswordHash] = Writes(passwordHash => JsString(passwordHash.value.toString)) + implicit val passwordHashJsonFormat: Format[PasswordHash] = Format(passwordHashJsonReads, passwordHashJsonWrites) +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/Computation.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/Computation.scala deleted file mode 100644 index 9e6f3bd..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/Computation.scala +++ /dev/null @@ -1,110 +0,0 @@ -package xyz.driver.pdsuicommon.utils - -import scala.concurrent.{ExecutionContext, Future} - -/** - * Takes care of computations - * - * Success(either) - the computation will be continued. - * Failure(error) - the computation was failed with unhandled error. - * - * Either[Result, T]: - * Left(result) is a final and handled result, another computations (map, flatMap) will be ignored. - * Right(T) is a current result. Functions in map/flatMap will continue the computation. - * - * Example: - * {{{ - * import scala.concurrent.ExecutionContext.Implicits.global - * import scala.concurrent.{ExecutionContext, Future} - * import com.drivergrp.server.com.drivergrp.server.common.utils.Computation - * - * def successful = for { - * x <- Computation.continue(1) - * y <- Computation.continue(2) - * } yield s"\$x + \$y" - * - * // Prints "Success(1 + 2)" - * successful.join.onComplete(print) - * - * def failed = for { - * x <- Computation.abort("Failed on x") - * _ = print("Second step") - * y <- Computation.continue(2) - * } yield s"\$x + \$y" - * - * // Prints "Success(Failed on x)" - * failed.join.onComplete(print) - * }}} - * - * TODO: Make future private - * - * @param future The final flow in a future. - * @tparam R Type of result for aborted computation. - * @tparam T Type of result for continued computation. - */ -final case class Computation[+R, +T](future: Future[Either[R, T]]) { - - def flatMap[R2, T2](f: T => Computation[R2, T2])(implicit ec: ExecutionContext, ev: R <:< R2): Computation[R2, T2] = { - Computation(future.flatMap { - case Left(x) => Future.successful(Left(x)) - case Right(x) => f(x).future - }) - } - - def processExceptions[R2](f: PartialFunction[Throwable, R2])(implicit ev1: R <:< R2, - ec: ExecutionContext): Computation[R2, T] = { - val strategy = f.andThen(x => Left(x): Either[R2, T]) - val castedFuture: Future[Either[R2, T]] = future.map { - case Left(x) => Left(x) - case Right(x) => Right(x) - } - Computation(castedFuture.recover(strategy)) - } - - def map[T2](f: T => T2)(implicit ec: ExecutionContext): Computation[R, T2] = flatMap { a => - Computation.continue(f(a)) - } - - def andThen(f: T => Any)(implicit ec: ExecutionContext): Computation[R, T] = map { a => - f(a) - a - } - - def filter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = map { a => - if (f(a)) a - else throw new NoSuchElementException("When filtering") - } - - def withFilter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = filter(f) - - def foreach[T2](f: T => T2)(implicit ec: ExecutionContext): Unit = future.foreach { - case Right(x) => f(x) - case _ => - } - - def toFuture[R2](resultFormatter: T => R2)(implicit ec: ExecutionContext, ev: R <:< R2): Future[R2] = future.map { - case Left(x) => x - case Right(x) => resultFormatter(x) - } - - def toFuture[R2](implicit ec: ExecutionContext, ev1: R <:< R2, ev2: T <:< R2): Future[R2] = future.map { - case Left(x) => x - case Right(x) => x - } - -} - -object Computation { - - def continue[T](x: T): Computation[Nothing, T] = Computation(Future.successful(Right(x))) - - def abort[R](result: R): Computation[R, Nothing] = Computation(Future.successful(Left(result))) - - def fail(exception: Throwable): Computation[Nothing, Nothing] = Computation(Future.failed(exception)) - - def fromFuture[T](input: Future[T])(implicit ec: ExecutionContext): Computation[Nothing, T] = Computation { - input.map { x => - Right(x) - } - } -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/JsonSerializer.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/JsonSerializer.scala deleted file mode 100644 index 8c8fd4e..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/utils/JsonSerializer.scala +++ /dev/null @@ -1,26 +0,0 @@ -package xyz.driver.pdsuicommon.utils - -import java.text.SimpleDateFormat - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.module.scala.DefaultScalaModule -import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper - -object JsonSerializer { - - private val mapper = new ObjectMapper() with ScalaObjectMapper - mapper.registerModule(DefaultScalaModule) - mapper.registerModule(new JavaTimeModule) - mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")) - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - - def serialize(value: Any): String = { - mapper.writeValueAsString(value) - } - - def deserialize[T](value: String)(implicit m: Manifest[T]): T = { - mapper.readValue(value) - } -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala b/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala new file mode 100644 index 0000000..115163c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/validation/AdditionalConstraints.scala @@ -0,0 +1,25 @@ +package xyz.driver.pdsuicommon.validation + +import org.davidbild.tristate.Tristate +import play.api.data.validation._ + +object AdditionalConstraints { + + val optionNonEmptyConstraint: Constraint[Option[Any]] = { + Constraint("option.nonEmpty") { + case Some(x) => Valid + case None => Invalid("is empty") + } + } + + val tristateSpecifiedConstraint: Constraint[Tristate[Any]] = { + Constraint("tristate.specified") { + case Tristate.Unspecified => Invalid("unspecified") + case _ => Valid + } + } + + val uuid: Constraint[String] = { + Constraints.pattern("""[\da-z]{8}-[\da-z]{4}-[\da-z]{4}-[\da-z]{4}-[\da-z]{12}""".r, "uuid", "invalid uuid") + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/validation/Validators.scala b/src/main/scala/xyz/driver/pdsuicommon/validation/Validators.scala index fca726c..a41f87a 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/validation/Validators.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/validation/Validators.scala @@ -1,7 +1,7 @@ package xyz.driver.pdsuicommon.validation +import xyz.driver.pdsuicommon.json.JsonSerializer import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuicommon.utils.JsonSerializer import scala.util.control.NonFatal diff --git a/src/main/scala/xyz/driver/pdsuicommon/validation/package.scala b/src/main/scala/xyz/driver/pdsuicommon/validation/package.scala new file mode 100644 index 0000000..9a31a93 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/validation/package.scala @@ -0,0 +1,8 @@ +package xyz.driver.pdsuicommon + +import play.api.data.validation.{ValidationError => PlayValidationError} +import play.api.libs.json.JsPath + +package object validation { + type JsonValidationErrors = Seq[(JsPath, Seq[PlayValidationError])] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Arm.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Arm.scala index 70b84ff..2190b8d 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Arm.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Arm.scala @@ -5,11 +5,11 @@ import java.time.LocalDateTime import xyz.driver.pdsuicommon.domain.{LongId, StringId} import xyz.driver.pdsuicommon.logging._ -case class Arm(id: LongId[Arm], - name: String, - originalName: String, - trialId: StringId[Trial], - deleted: Option[LocalDateTime] = None) +final case class Arm(id: LongId[Arm], + name: String, + originalName: String, + trialId: StringId[Trial], + deleted: Option[LocalDateTime] = None) object Arm { diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala index 19c0021..5af00bc 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala @@ -34,7 +34,7 @@ object DocumentType { object Document { - case class Meta(predicted: Option[Boolean], startPage: Double, endPage: Double) { + final case class Meta(predicted: Option[Boolean], startPage: Double, endPage: Double) { /** * Return a regular meta: this meta is considered as not predicted @@ -121,20 +121,20 @@ object Document { } @JsonIgnoreProperties(value = Array("valid")) -case class Document(id: LongId[Document] = LongId(0L), - status: Document.Status, - previousStatus: Option[Document.Status], - assignee: Option[LongId[User]], - previousAssignee: Option[LongId[User]], - recordId: LongId[MedicalRecord], - physician: Option[String], - typeId: Option[LongId[DocumentType]], // not null - providerName: Option[String], // not null - providerTypeId: Option[LongId[ProviderType]], // not null - meta: Option[TextJson[Meta]], // not null - startDate: Option[LocalDate], // not null - endDate: Option[LocalDate], - lastUpdate: LocalDateTime) { +final case class Document(id: LongId[Document] = LongId(0L), + status: Document.Status, + previousStatus: Option[Document.Status], + assignee: Option[LongId[User]], + previousAssignee: Option[LongId[User]], + recordId: LongId[MedicalRecord], + physician: Option[String], + typeId: Option[LongId[DocumentType]], // not null + providerName: Option[String], // not null + providerTypeId: Option[LongId[ProviderType]], // not null + meta: Option[TextJson[Meta]], // not null + startDate: Option[LocalDate], // not null + endDate: Option[LocalDate], + lastUpdate: LocalDateTime) { import Document.Status._ diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala index bc80ce3..93262a4 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Patient.scala @@ -50,17 +50,17 @@ object Patient { } } -case class Patient(id: UuidId[Patient], - status: Patient.Status, - name: String, - dob: LocalDate, - assignee: Option[LongId[User]], - previousStatus: Option[Patient.Status], - previousAssignee: Option[LongId[User]], - isUpdateRequired: Boolean, - condition: String, - orderId: PatientOrderId, - lastUpdate: LocalDateTime) { +final case class Patient(id: UuidId[Patient], + status: Patient.Status, + name: String, + dob: LocalDate, + assignee: Option[LongId[User]], + previousStatus: Option[Patient.Status], + previousAssignee: Option[LongId[User]], + isUpdateRequired: Boolean, + condition: String, + orderId: PatientOrderId, + lastUpdate: LocalDateTime) { import Patient.Status._ diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientHypothesis.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientHypothesis.scala index 146b1c3..23bb546 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientHypothesis.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientHypothesis.scala @@ -11,8 +11,8 @@ object PatientHypothesis { } } -case class PatientHypothesis(id: UuidId[PatientHypothesis], - patientId: UuidId[Patient], - hypothesisId: UuidId[Hypothesis], - rationale: Option[String], - matchedTrials: Long) +final case class PatientHypothesis(id: UuidId[PatientHypothesis], + patientId: UuidId[Patient], + hypothesisId: UuidId[Hypothesis], + rationale: Option[String], + matchedTrials: Long) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabel.scala index 63d38f8..633a347 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabel.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/PatientLabel.scala @@ -29,10 +29,10 @@ object PatientLabelEvidence { } } -case class PatientLabelEvidence(id: LongId[PatientLabelEvidence], - patientLabelId: LongId[PatientLabel], - value: FuzzyValue, - evidenceText: String, - reportId: Option[UuidId[DirectReport]], - documentId: Option[LongId[Document]], - evidenceId: Option[LongId[ExtractedData]]) +final case class PatientLabelEvidence(id: LongId[PatientLabelEvidence], + patientLabelId: LongId[PatientLabel], + value: FuzzyValue, + evidenceText: String, + reportId: Option[UuidId[DirectReport]], + documentId: Option[LongId[Document]], + evidenceId: Option[LongId[ExtractedData]]) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/RawPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/RawPatientLabel.scala index 052b2fa..e7956a8 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/RawPatientLabel.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/RawPatientLabel.scala @@ -5,19 +5,19 @@ import java.time.LocalDate import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId, UuidId} import xyz.driver.pdsuicommon.logging._ -case class RawPatientLabel(patientId: UuidId[Patient], - labelId: LongId[Label], - label: String, - value: FuzzyValue, - evidenceId: LongId[ExtractedData], - evidenceText: String, - disease: String, - documentId: LongId[Document], - requestId: RecordRequestId, - documentType: String, - providerType: String, - startDate: LocalDate, - endDate: Option[LocalDate]) +final case class RawPatientLabel(patientId: UuidId[Patient], + labelId: LongId[Label], + label: String, + value: FuzzyValue, + evidenceId: LongId[ExtractedData], + evidenceText: String, + disease: String, + documentId: LongId[Document], + requestId: RecordRequestId, + documentType: String, + providerType: String, + startDate: LocalDate, + endDate: Option[LocalDate]) object RawPatientLabel { @@ -29,5 +29,4 @@ object RawPatientLabel { phi"documentType=${Unsafe(documentType)}, providerType=${Unsafe(providerType)}, " + phi"startDate=$startDate, endDate=$endDate)" } - } diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/ScrapedTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/ScrapedTrial.scala new file mode 100644 index 0000000..40e84c9 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/ScrapedTrial.scala @@ -0,0 +1,105 @@ +package xyz.driver.pdsuidomain.entities + +import java.time.LocalDateTime +import java.util.UUID + +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuicommon.domain.UuidId + +final case class ScrapedStudyDesign(value: String) + +final case class ScrapedOverall(affiliation: String, + status: String, + facilityName: Option[String], + firstName: Option[String], + lastName: Option[String], + phone: Option[String], + phoneExt: Option[String], + email: Option[String], + isBackup: Boolean) + +final case class ScrapedLocationContact(firstName: Option[String], + lastName: Option[String], + phone: Option[String], + phoneExt: Option[String], + email: Option[String], + isBackup: Boolean) + +final case class ScrapedLocation(id: UuidId[ScrapedLocation], + createdAt: LocalDateTime, + facilityName: Option[String], + city: Option[String], + state: Option[String], + zip: Option[String], + country: Option[String], + latitude: Option[Double], + longitude: Option[Double], + preferredName: Option[String], + partnershipStatus: Option[String], + lastReviewed: LocalDateTime, + contacts: Set[ScrapedLocationContact]) + +final case class ScrapedInterventionType(value: String) + +final case class ScrapedIntervention(name: String, + kind: ScrapedInterventionType, + description: Option[String], + isSynonym: Boolean) + +object ScrapedIntervention { + + implicit def toPhiString(x: ScrapedIntervention): PhiString = phi"ScrapedIntervention(${Unsafe(x.name)})" +} + +final case class ScrapedArm(name: String, + kind: Option[String], + interventions: Set[ScrapedIntervention]) + +object ScrapedArm { + + implicit def toPhiString(x: ScrapedArm): PhiString = { + import x._ + phi"ScrapedArm(name=${Unsafe(name)}, inverventions=$interventions)" + } +} + +final case class ScrapedTrialChecksum(eligibilityCriteria: String, + briefSummary: String, + detailedDescription: String, + armDescription: String) + +object ScrapedTrialChecksum { + + implicit def toPhiString(x: ScrapedTrialChecksum): PhiString = { + import x._ + phi"ScrapedTrialChecksum(eligibilityCriteria=${Unsafe(eligibilityCriteria)}, briefSummary=${Unsafe(briefSummary)}, " + + phi"detailedDescription=${Unsafe(detailedDescription)}, armDescription=${Unsafe(armDescription)}" + } +} + +object ScrapedTrial { + + implicit def toPhiString(x: ScrapedTrial): PhiString = { + import x._ + phi"ScrapedTrial(rawId=$rawId, nctId=${Unsafe(nctId)}, " + + phi"location.size=${Unsafe(locations.size)}, arms=$arms, checksum=$checksum)" + } +} + +final case class ScrapedTrial(rawId: UuidId[ScrapedTrial], + createdAt: LocalDateTime, + disease: String, + nctId: String, + nctUuid: UUID, + title: Option[String], + startDate: Option[LocalDateTime], + phase: String, + studyDesign: Option[ScrapedStudyDesign], + overall: Set[ScrapedOverall], + locations: Set[ScrapedLocation], + // // see ClinicalTrialRaw + // trialHtml: String, + // eligibilityText: String, + lastReviewed: LocalDateTime, + arms: Set[ScrapedArm], + checksum: ScrapedTrialChecksum) diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala deleted file mode 100644 index 3b7a6ad..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabel.scala +++ /dev/null @@ -1,28 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.patient - -import xyz.driver.pdsuicommon.domain.LongId -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{Label, RawPatientLabel} - -case class ExportPatientLabel(id: LongId[Label], name: String, evidences: List[ExportPatientLabelEvidence]) - -object ExportPatientLabel extends PhiLogging { - - implicit def toPhiString(x: ExportPatientLabel): PhiString = { - import x._ - phi"ExportPatientLabel(id=$id, evidences=$evidences)" - } - - def fromRaw(labelId: LongId[Label], rawPatientLabels: List[RawPatientLabel]): ExportPatientLabel = { - val firstLabel = rawPatientLabels.headOption - if (firstLabel.isEmpty) { - logger.warn(phi"rawPatientLabels is empty, labelId: $labelId") - } - - ExportPatientLabel( - id = labelId, - name = firstLabel.map(_.label).getOrElse(""), - evidences = rawPatientLabels.map(ExportPatientLabelEvidence.fromRaw) - ) - } -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala deleted file mode 100644 index ff0fb6c..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidence.scala +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.patient - -import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{ExtractedData, RawPatientLabel} - -case class ExportPatientLabelEvidence(id: LongId[ExtractedData], - value: FuzzyValue, - evidenceText: String, - document: ExportPatientLabelEvidenceDocument) - -object ExportPatientLabelEvidence { - - implicit def toPhiString(x: ExportPatientLabelEvidence): PhiString = { - import x._ - phi"ExportPatientLabelEvidence(id=${Unsafe(id)}, value=$value, " + - phi"evidenceText=${Unsafe(evidenceText)}, document=$document)" - } - - def fromRaw(x: RawPatientLabel) = ExportPatientLabelEvidence( - id = x.evidenceId, - value = x.value, - evidenceText = x.evidenceText, - document = ExportPatientLabelEvidenceDocument( - x.documentId, - x.requestId, - x.documentType, - x.providerType, - x.startDate - ) - ) -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala deleted file mode 100644 index 978c4b8..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientLabelEvidenceDocument.scala +++ /dev/null @@ -1,30 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.patient - -import java.time.LocalDate - -import xyz.driver.pdsuicommon.domain.LongId -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{Document, RawPatientLabel, RecordRequestId} - -case class ExportPatientLabelEvidenceDocument(documentId: LongId[Document], - requestId: RecordRequestId, - documentType: String, - providerType: String, - date: LocalDate) - -object ExportPatientLabelEvidenceDocument extends PhiLogging { - - implicit def toPhiString(x: ExportPatientLabelEvidenceDocument): PhiString = { - import x._ - phi"ExportPatientLabelEvidenceDocument(documentId=$documentId, requestId=$requestId, " + - phi"documentType=${Unsafe(documentType)}, providerType=${Unsafe(providerType)}, date=$date)" - } - - def fromRaw(x: RawPatientLabel) = ExportPatientLabelEvidenceDocument( - documentId = x.documentId, - requestId = x.requestId, - documentType = x.documentType, - providerType = x.providerType, - date = x.startDate - ) -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala deleted file mode 100644 index 718255b..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/patient/ExportPatientWithLabels.scala +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.patient - -import xyz.driver.pdsuicommon.domain.UuidId -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{Patient, RawPatientLabel} - -import scala.collection.breakOut - -case class ExportPatientWithLabels(patientId: UuidId[Patient], labelVersion: Long, labels: List[ExportPatientLabel]) - -object ExportPatientWithLabels { - - implicit def toPhiString(x: ExportPatientWithLabels): PhiString = { - import x._ - phi"ExportPatientWithLabels(patientId=$patientId, version=${Unsafe(labelVersion)}, labels=$labels)" - } - - def fromRaw(patientId: UuidId[Patient], rawPatientRefs: List[RawPatientLabel]) = ExportPatientWithLabels( - patientId = patientId, - labelVersion = 1L, // TODO It is needed to replace this mock label version. - labels = rawPatientRefs - .groupBy(_.labelId) - .map(Function.tupled(ExportPatientLabel.fromRaw))(breakOut) - ) -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala deleted file mode 100644 index a3a0e90..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrial.scala +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.trial - -import java.time.LocalDateTime - -import xyz.driver.pdsuicommon.domain.{StringId, UuidId} -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.Trial - -case class ExportTrial(nctId: StringId[Trial], - trialId: UuidId[Trial], - condition: Trial.Condition, - lastReviewed: LocalDateTime) - -object ExportTrial { - - implicit def toPhiString(x: ExportTrial): PhiString = { - import x._ - phi"ExportTrial(nctId=$nctId, trialId=$trialId, condition=${Unsafe(condition)}, lastReviewed=$lastReviewed)" - } - - def fromDomain(x: Trial) = ExportTrial( - nctId = x.id, - trialId = x.externalId, - condition = x.condition, - lastReviewed = x.lastUpdate - ) -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala deleted file mode 100644 index abdade4..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialArm.scala +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.trial - -import xyz.driver.pdsuicommon.domain.LongId -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.Arm - -case class ExportTrialArm(armId: LongId[Arm], armName: String) - -object ExportTrialArm { - - implicit def toPhiString(x: ExportTrialArm): PhiString = { - import x._ - phi"ExportTrialArm(armId=$armId, armName=${Unsafe(armName)})" - } -} 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 deleted file mode 100644 index 1dccaa5..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialLabelCriterion.scala +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.trial - -import xyz.driver.pdsuicommon.domain.LongId -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{Arm, Criterion, Label, RawTrialLabel} - -case class ExportTrialLabelCriterion(criterionId: LongId[Criterion], - value: Option[Boolean], - labelId: LongId[Label], - armIds: Set[LongId[Arm]], - criteria: String, - isCompound: Boolean, - isDefining: 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)" - } - - def fromRaw(x: RawTrialLabel, armIds: Set[LongId[Arm]]) = ExportTrialLabelCriterion( - criterionId = x.criterionId, - value = x.value, - labelId = x.labelId, - armIds = armIds, - criteria = x.criteria, - isCompound = x.isCompound, - isDefining = x.isDefining - ) -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala deleted file mode 100644 index 2580e54..0000000 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala +++ /dev/null @@ -1,56 +0,0 @@ -package xyz.driver.pdsuidomain.entities.export.trial - -import java.time.LocalDateTime - -import xyz.driver.pdsuicommon.domain.{StringId, UuidId} -import xyz.driver.pdsuicommon.logging._ -import xyz.driver.pdsuidomain.entities.{RawTrialLabel, Trial} - -import scala.collection.breakOut - -case class ExportTrialWithLabels(nctId: StringId[Trial], - trialId: UuidId[Trial], - condition: String, - lastReviewed: LocalDateTime, - labelVersion: Long, - arms: List[ExportTrialArm], - criteria: List[ExportTrialLabelCriterion]) - -object ExportTrialWithLabels { - - implicit def toPhiString(x: ExportTrialWithLabels): PhiString = { - import x._ - phi"TrialWithLabels(nctId=$nctId, trialId=$trialId, condition=${Unsafe(condition)}, " + - phi"lastReviewed=$lastReviewed, labelVersion=${Unsafe(labelVersion)}, arms=$arms, criteria=$criteria)" - } - - def fromRaw(rawData: List[RawTrialLabel]): ExportTrialWithLabels = { - val trials: Set[StringId[Trial]] = rawData.map(_.nctId)(breakOut) - - assert(trials.size == 1, "There are more than one trials in the rawData") - val trial = rawData.head - - ExportTrialWithLabels( - nctId = trial.nctId, - trialId = trial.trialId, - condition = trial.condition, - lastReviewed = trial.lastReviewed, - labelVersion = 1, // TODO It is needed to replace this mock label version. - arms = rawData - .groupBy(_.armId) - .map { - case (armId, rawTrials) => - ExportTrialArm(armId, rawTrials.head.armName) - }(breakOut), - criteria = rawData - .groupBy { x => - (x.criterionId, x.labelId) - } - .map { - case (_, rawTrialLabels) => - val armIds = rawTrialLabels.map(_.criterionArmId).toSet - ExportTrialLabelCriterion.fromRaw(rawTrialLabels.head, armIds) - }(breakOut) - ) - } -} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiArm.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiArm.scala new file mode 100644 index 0000000..509ea35 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiArm.scala @@ -0,0 +1,22 @@ +package xyz.driver.pdsuidomain.formats.json.arm + +import xyz.driver.pdsuidomain.entities.Arm +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiArm(id: Long, name: String, trialId: String) + +object ApiArm { + + implicit val format: Format[ApiArm] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] and + (JsPath \ "trialId").format[String] + ) (ApiArm.apply, unlift(ApiArm.unapply)) + + def fromDomain(arm: Arm): ApiArm = ApiArm( + id = arm.id.id, + name = arm.name, + trialId = arm.trialId.id + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiCreateArm.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiCreateArm.scala new file mode 100644 index 0000000..5168e94 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiCreateArm.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuidomain.formats.json.arm + +import xyz.driver.pdsuicommon.domain.{LongId, StringId} +import xyz.driver.pdsuidomain.entities.Arm +import play.api.libs.json.{Format, Json} + +final case class ApiCreateArm(name: String, trialId: String) { + + def toDomain = Arm( + id = LongId(0), + name = name, + trialId = StringId(trialId), + originalName = name + ) +} + +object ApiCreateArm { + + implicit val format: Format[ApiCreateArm] = Json.format[ApiCreateArm] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiPartialArm.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiPartialArm.scala new file mode 100644 index 0000000..f85d7ff --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/arm/ApiPartialArm.scala @@ -0,0 +1,14 @@ +package xyz.driver.pdsuidomain.formats.json.arm + +import xyz.driver.pdsuidomain.entities.Arm +import play.api.libs.json.{Format, Json} + +final case class ApiPartialArm(name: String) { + + def applyTo(arm: Arm): Arm = arm.copy(name = name) +} + +object ApiPartialArm { + + implicit val format: Format[ApiPartialArm] = Json.format +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/category/ApiCategory.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/category/ApiCategory.scala new file mode 100644 index 0000000..fabdaa2 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/category/ApiCategory.scala @@ -0,0 +1,23 @@ +package xyz.driver.pdsuidomain.formats.json.category + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.CategoryWithLabels +import xyz.driver.pdsuidomain.formats.json.label.ApiLabel + +final case class ApiCategory(id: Long, name: String, labels: List[ApiLabel]) + +object ApiCategory { + + implicit val format: Format[ApiCategory] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] and + (JsPath \ "labels").format[List[ApiLabel]] + ) (ApiCategory.apply, unlift(ApiCategory.unapply)) + + def fromDomain(categoryWithLabels: CategoryWithLabels) = ApiCategory( + id = categoryWithLabels.category.id.id, + name = categoryWithLabels.category.name, + labels = categoryWithLabels.labels.map(x => ApiLabel(x.id.id, x.name, x.categoryId.id)) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiCriterion.scala new file mode 100644 index 0000000..0f3b76e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiCriterion.scala @@ -0,0 +1,40 @@ +package xyz.driver.pdsuidomain.formats.json.criterion + +import xyz.driver.pdsuicommon.json.Serialization.seqJsonFormat +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuidomain.formats.json.label.ApiCriterionLabel +import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion + +final case class ApiCriterion(id: Long, + meta: Option[String], + arms: Seq[Long], + text: Option[String], + isCompound: Boolean, + labels: Seq[ApiCriterionLabel], + trialId: String) + +object ApiCriterion { + + implicit val format: Format[ApiCriterion] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "meta").formatNullable(Format(Reads { x => + JsSuccess(Json.stringify(x)) + }, Writes[String](Json.parse))) and + (JsPath \ "arms").format(seqJsonFormat[Long]) and + (JsPath \ "text").formatNullable[String] and + (JsPath \ "isCompound").format[Boolean] and + (JsPath \ "labels").format(seqJsonFormat[ApiCriterionLabel]) and + (JsPath \ "trialId").format[String] + ) (ApiCriterion.apply, unlift(ApiCriterion.unapply)) + + def fromDomain(richCriterion: RichCriterion) = ApiCriterion( + id = richCriterion.criterion.id.id, + meta = Option(richCriterion.criterion.meta), + arms = richCriterion.armIds.map(_.id), + text = richCriterion.criterion.text, + isCompound = richCriterion.criterion.isCompound, + labels = richCriterion.labels.map(ApiCriterionLabel.fromDomain), + trialId = richCriterion.criterion.trialId.id + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala new file mode 100644 index 0000000..85c91d5 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiNewCriterion.scala @@ -0,0 +1,41 @@ +package xyz.driver.pdsuidomain.formats.json.criterion + +import xyz.driver.pdsuicommon.domain.{LongId, StringId} +import xyz.driver.pdsuicommon.json.Serialization.seqJsonFormat +import xyz.driver.pdsuidomain.entities._ +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuidomain.formats.json.label.ApiCriterionLabel +import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion + +final case class ApiNewCriterion(meta: Option[String], + arms: Option[Seq[Long]], + text: Option[String], + labels: Seq[ApiCriterionLabel], + trialId: String) { + + def toDomain = RichCriterion( + criterion = Criterion( + id = LongId(0L), + meta = meta.getOrElse(""), + trialId = StringId(trialId), + isCompound = false, + text = text + ), + armIds = arms.getOrElse(Seq.empty).map(LongId[Arm]), + labels = labels.map(_.toDomain(LongId(Long.MaxValue))) // A developer should specify right criterionId himself + ) +} + +object ApiNewCriterion { + + implicit val format: Format[ApiNewCriterion] = ( + (JsPath \ "meta").formatNullable(Format(Reads { x => + JsSuccess(Json.stringify(x)) + }, Writes[String](Json.parse))) and + (JsPath \ "arms").formatNullable(seqJsonFormat[Long]) and + (JsPath \ "text").formatNullable[String] and + (JsPath \ "labels").format(seqJsonFormat[ApiCriterionLabel]) and + (JsPath \ "trialId").format[String] + ) (ApiNewCriterion.apply, unlift(ApiNewCriterion.unapply)) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiUpdateCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiUpdateCriterion.scala new file mode 100644 index 0000000..c73c3ef --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/criterion/ApiUpdateCriterion.scala @@ -0,0 +1,56 @@ +package xyz.driver.pdsuidomain.formats.json.criterion + +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.json.Serialization.seqJsonFormat +import xyz.driver.pdsuidomain.entities.{Arm, Criterion} +import org.davidbild.tristate._ +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuidomain.formats.json.label.ApiCriterionLabel +import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion + +final case class ApiUpdateCriterion(meta: Tristate[String], + arms: Tristate[Seq[Long]], + text: Option[String], + isCompound: Option[Boolean], + labels: Tristate[Seq[ApiCriterionLabel]]) { + + def applyTo(orig: RichCriterion): RichCriterion = RichCriterion( + criterion = applyTo(orig.criterion), + armIds = arms.cata(_.map(LongId[Arm]), Seq.empty, orig.armIds), + labels = labels.cata(_.map(_.toDomain(orig.criterion.id)), Seq.empty, orig.labels) + ) + + private def applyTo(orig: Criterion): Criterion = Criterion( + id = orig.id, + meta = meta.cata(identity, "{}", orig.meta), + text = text.orElse(orig.text), + isCompound = isCompound.getOrElse(orig.isCompound), + trialId = orig.trialId + ) +} + +object ApiUpdateCriterion { + + private val reads: Reads[ApiUpdateCriterion] = ( + (JsPath \ "meta").readTristate(Reads { x => JsSuccess(Json.stringify(x)) }).map { + case Tristate.Present("{}") => Tristate.Absent + case x => x + } and + (JsPath \ "arms").readTristate(seqJsonFormat[Long]) and + (JsPath \ "text").readNullable[String] and + (JsPath \ "isCompound").readNullable[Boolean] and + (JsPath \ "labels").readTristate(seqJsonFormat[ApiCriterionLabel]) + ) (ApiUpdateCriterion.apply _) + + private val writes: Writes[ApiUpdateCriterion] = ( + (JsPath \ "meta").writeTristate(Writes[String](Json.parse)) and + (JsPath \ "arms").writeTristate(seqJsonFormat[Long]) and + (JsPath \ "text").writeNullable[String] and + (JsPath \ "isCompound").writeNullable[Boolean] and + (JsPath \ "labels").writeTristate(seqJsonFormat[ApiCriterionLabel]) + ) (unlift(ApiUpdateCriterion.unapply)) + + implicit val format: Format[ApiUpdateCriterion] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala new file mode 100644 index 0000000..be9c65b --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocument.scala @@ -0,0 +1,71 @@ +package xyz.driver.pdsuidomain.formats.json.document + +import java.time.{LocalDate, ZoneId, ZonedDateTime} + +import xyz.driver.pdsuidomain.entities._ +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.JsonSerializer + +final case class ApiDocument(id: Long, + recordId: Long, + physician: Option[String], + lastUpdate: Option[ZonedDateTime], + typeId: Option[Long], + startDate: Option[LocalDate], + endDate: Option[LocalDate], + provider: Option[String], + providerTypeId: Option[Long], + status: Option[String], + previousStatus: Option[String], + assignee: Option[Long], + previousAssignee: Option[Long], + meta: Option[String]) + +object ApiDocument { + + private val statusFormat = Format( + Reads.StringReads.filter(ValidationError("unknown status")) { + case x if DocumentUtils.statusFromString.isDefinedAt(x) => true + case _ => false + }, + Writes.StringWrites + ) + + implicit val format: Format[ApiDocument] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "recordId").format[Long] and + (JsPath \ "physician").formatNullable[String] and + (JsPath \ "lastUpdate").formatNullable[ZonedDateTime] and + (JsPath \ "typeId").formatNullable[Long] and + (JsPath \ "startDate").formatNullable[LocalDate] and + (JsPath \ "endDate").formatNullable[LocalDate] and + (JsPath \ "provider").formatNullable[String] and + (JsPath \ "providerTypeId").formatNullable[Long] and + (JsPath \ "status").formatNullable(statusFormat) and + (JsPath \ "previousStatus").formatNullable(statusFormat) and + (JsPath \ "assignee").formatNullable[Long] and + (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "meta").formatNullable(Format(Reads { x => JsSuccess(Json.stringify(x)) }, Writes[String](Json.parse))) + ) (ApiDocument.apply, unlift(ApiDocument.unapply)) + + def fromDomain(document: Document): ApiDocument = { + ApiDocument( + id = document.id.id, + recordId = document.recordId.id, + physician = document.physician, + lastUpdate = Option(document.lastUpdate).map(ZonedDateTime.of(_, ZoneId.of("Z"))), + typeId = document.typeId.map(_.id), + startDate = document.startDate, + endDate = document.endDate, + provider = document.providerName, + providerTypeId = document.providerTypeId.map(_.id), + status = Option(DocumentUtils.statusToString(document.status)), + previousStatus = document.previousStatus.map(DocumentUtils.statusToString), + assignee = document.assignee.map(_.id), + previousAssignee = document.previousAssignee.map(_.id), + meta = document.meta.map(meta => JsonSerializer.serialize(meta.content)) + ) + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala new file mode 100644 index 0000000..e00da20 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiDocumentType.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuidomain.formats.json.document + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.DocumentType + +final case class ApiDocumentType(id: Long, name: String) + +object ApiDocumentType { + + implicit val format: Format[ApiDocumentType] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] + ) (ApiDocumentType.apply, unlift(ApiDocumentType.unapply)) + + def fromDomain(documentType: DocumentType) = ApiDocumentType( + id = documentType.id.id, + name = documentType.name + ) +} 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 new file mode 100644 index 0000000..7682bb5 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiPartialDocument.scala @@ -0,0 +1,114 @@ +package xyz.driver.pdsuidomain.formats.json.document + +import java.time.{LocalDate, LocalDateTime} + +import xyz.driver.pdsuicommon.domain.{LongId, TextJson} +import xyz.driver.pdsuidomain.entities.Document.Meta +import xyz.driver.pdsuidomain.entities._ +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.data.validation._ +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.{JsonSerializer, JsonValidationException} +import xyz.driver.pdsuicommon.validation.{AdditionalConstraints, JsonValidationErrors} + +import scala.collection.breakOut +import scala.util.Try + +final case class ApiPartialDocument(recordId: Option[Long], + physician: Option[String], + typeId: Tristate[Long], + startDate: Tristate[LocalDate], + endDate: Tristate[LocalDate], + provider: Tristate[String], + providerTypeId: Tristate[Long], + status: Option[String], + assignee: Tristate[Long], + meta: Tristate[String]) { + + import xyz.driver.pdsuicommon.domain.User + + def applyTo(orig: Document): Document = Document( + id = orig.id, + status = status.map(DocumentUtils.statusFromString).getOrElse(orig.status), + previousStatus = orig.previousStatus, + assignee = assignee.map(LongId[User]).cata(Some(_), None, orig.assignee), + previousAssignee = orig.previousAssignee, + recordId = recordId.map(LongId[MedicalRecord]).getOrElse(orig.recordId), + physician = physician.orElse(orig.physician), + typeId = typeId.map(LongId[DocumentType]).cata(Some(_), None, orig.typeId), + providerName = provider.cata(Some(_), None, orig.providerName), + providerTypeId = providerTypeId.map(LongId[ProviderType]).cata(Some(_), None, orig.providerTypeId), + meta = meta.cata(x => Some(TextJson(JsonSerializer.deserialize[Meta](x))), None, orig.meta), + startDate = startDate.cata(Some(_), None, orig.startDate), + endDate = endDate.cata(Some(_), None, orig.endDate), + lastUpdate = LocalDateTime.MIN // Should update internally in a business logic module + ) + + def toDomain: Try[Document] = Try { + val validation = Map(JsPath \ "recordId" -> AdditionalConstraints.optionNonEmptyConstraint(recordId)) + + val validationErrors: JsonValidationErrors = validation.collect({ + case (fieldName, e: Invalid) => (fieldName, e.errors) + })(breakOut) + + if (validationErrors.isEmpty) { + Document( + id = LongId(0), + recordId = recordId.map(LongId[MedicalRecord]).get, + status = Document.Status.New, + physician = physician, + typeId = typeId.map(LongId[DocumentType]).toOption, + startDate = startDate.toOption, + endDate = endDate.toOption, + providerName = provider.toOption, + providerTypeId = providerTypeId.map(LongId[ProviderType]).toOption, + meta = meta.map(x => TextJson(JsonSerializer.deserialize[Meta](x))).toOption, + previousStatus = None, + assignee = None, + previousAssignee = None, + lastUpdate = LocalDateTime.MIN + ) + } else { + throw new JsonValidationException(validationErrors) + } + } +} + +object ApiPartialDocument { + + private val reads: Reads[ApiPartialDocument] = ( + (JsPath \ "recordId").readNullable[Long] and + (JsPath \ "physician").readNullable[String] and + (JsPath \ "typeId").readTristate[Long] and + (JsPath \ "startDate").readTristate[LocalDate] and + (JsPath \ "endDate").readTristate[LocalDate] and + (JsPath \ "provider").readTristate[String] and + (JsPath \ "providerTypeId").readTristate[Long] and + (JsPath \ "status").readNullable[String](Reads.of[String].filter(ValidationError("unknown status"))({ + case x if DocumentUtils.statusFromString.isDefinedAt(x) => true + case _ => false + })) and + (JsPath \ "assignee").readTristate[Long] and + (JsPath \ "meta").readTristate(Reads { x => JsSuccess(Json.stringify(x)) }).map { + case Tristate.Present("{}") => Tristate.Absent + case x => x + } + ) (ApiPartialDocument.apply _) + + private val writes: Writes[ApiPartialDocument] = ( + (JsPath \ "recordId").writeNullable[Long] and + (JsPath \ "physician").writeNullable[String] and + (JsPath \ "typeId").writeTristate[Long] and + (JsPath \ "startDate").writeTristate[LocalDate] and + (JsPath \ "endDate").writeTristate[LocalDate] and + (JsPath \ "provider").writeTristate[String] and + (JsPath \ "providerTypeId").writeTristate[Long] and + (JsPath \ "status").writeNullable[String] and + (JsPath \ "assignee").writeTristate[Long] and + (JsPath \ "meta").writeTristate(Writes[String](Json.parse)) + ) (unlift(ApiPartialDocument.unapply)) + + implicit val format: Format[ApiPartialDocument] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala new file mode 100644 index 0000000..7b370ba --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/ApiProviderType.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuidomain.formats.json.document + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.ProviderType + +final case class ApiProviderType(id: Long, name: String) + +object ApiProviderType { + + implicit val format: Format[ApiProviderType] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] + ) (ApiProviderType.apply, unlift(ApiProviderType.unapply)) + + def fromDomain(providerType: ProviderType) = ApiProviderType( + id = providerType.id.id, + name = providerType.name + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala new file mode 100644 index 0000000..87e449f --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/document/DocumentUtils.scala @@ -0,0 +1,24 @@ +package xyz.driver.pdsuidomain.formats.json.document + +import xyz.driver.pdsuidomain.entities.Document.Status + +object DocumentUtils { + + val statusFromString: PartialFunction[String, Status] = { + case "New" => Status.New + case "Organized" => Status.Organized + case "Extracted" => Status.Extracted + case "Done" => Status.Done + case "Flagged" => Status.Flagged + case "Archived" => Status.Archived + } + + def statusToString(x: Status): String = x match { + case Status.New => "New" + case Status.Organized => "Organized" + case Status.Extracted => "Extracted" + case Status.Done => "Done" + case Status.Flagged => "Flagged" + case Status.Archived => "Archived" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala new file mode 100644 index 0000000..2bb4945 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/evidence/ApiPatientLabelEvidence.scala @@ -0,0 +1,38 @@ +package xyz.driver.pdsuidomain.formats.json.evidence + +import java.time.LocalDate + +import xyz.driver.pdsuidomain.services.PatientLabelEvidenceService +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain.FuzzyValue + +final case class ApiPatientLabelEvidence(id: Long, + value: String, + evidenceText: String, + documentId: Option[Long], + evidenceId: Option[Long], + reportId: Option[String], + documentType: String, + date: LocalDate, + providerType: String) + +object ApiPatientLabelEvidence { + + implicit val format: Format[ApiPatientLabelEvidence] = Json.format + + def fromDomain(x: PatientLabelEvidenceService.Aggregated): ApiPatientLabelEvidence = { + import x._ + + ApiPatientLabelEvidence( + id = evidence.id.id, + value = FuzzyValue.valueToString(evidence.value), + evidenceText = evidence.evidenceText, + documentId = evidence.documentId.map(_.id), + evidenceId = evidence.evidenceId.map(_.id), + reportId = evidence.reportId.map(_.toString), + documentType = documentType, + date = date, + providerType = providerType + ) + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala new file mode 100644 index 0000000..dad7a1e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiExtractedData.scala @@ -0,0 +1,44 @@ +package xyz.driver.pdsuidomain.formats.json.extracteddata + +import xyz.driver.pdsuidomain.formats.json.label.ApiExtractedDataLabel +import play.api.libs.json._ +import play.api.data.validation._ +import play.api.libs.functional.syntax._ +import xyz.driver.pdsuicommon.json.JsonSerializer +import xyz.driver.pdsuidomain.services.ExtractedDataService.RichExtractedData + +// The specification: https://driverinc.atlassian.net/wiki/pages/viewpage.action?pageId=33423387 +// Note, that there is "Extracted data object or Temporary extracted data object" in specification +// ApiExtractedData represents both types +final case class ApiExtractedData(id: Long, + documentId: Long, + keywordId: Option[Long], + evidence: Option[String], + meta: Option[String], + // An empty list and no-existent list are different cases + labels: Option[List[ApiExtractedDataLabel]]) + +object ApiExtractedData { + + implicit val format: Format[ApiExtractedData] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "documentId").format[Long] and + (JsPath \ "keywordId").formatNullable[Long] and + (JsPath \ "evidence").formatNullable[String] and + (JsPath \ "meta").formatNullable[String] and + (JsPath \ "labels").formatNullable[List[ApiExtractedDataLabel]](Format( + Reads.of[List[ApiExtractedDataLabel]].filter(ValidationError("empty labels"))({ + case x if x.nonEmpty => true + case _ => false + }), Writes.of[List[ApiExtractedDataLabel]])) + ) (ApiExtractedData.apply, unlift(ApiExtractedData.unapply)) + + def fromDomain(extractedDataWithLabels: RichExtractedData) = ApiExtractedData( + id = extractedDataWithLabels.extractedData.id.id, + documentId = extractedDataWithLabels.extractedData.documentId.id, + keywordId = extractedDataWithLabels.extractedData.keywordId.map(_.id), + evidence = extractedDataWithLabels.extractedData.evidenceText, + meta = extractedDataWithLabels.extractedData.meta.map(x => JsonSerializer.serialize(x.content)), + labels = Option(extractedDataWithLabels.labels.map(ApiExtractedDataLabel.fromDomain)) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiPartialExtractedData.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiPartialExtractedData.scala new file mode 100644 index 0000000..69b5627 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/extracteddata/ApiPartialExtractedData.scala @@ -0,0 +1,80 @@ +package xyz.driver.pdsuidomain.formats.json.extracteddata + +import xyz.driver.pdsuicommon.domain.{LongId, TextJson} +import xyz.driver.pdsuidomain.entities.ExtractedData.Meta +import xyz.driver.pdsuidomain.entities._ +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.data.validation._ +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.{JsonSerializer, JsonValidationException} +import xyz.driver.pdsuicommon.validation.{AdditionalConstraints, JsonValidationErrors} +import xyz.driver.pdsuidomain.formats.json.label.ApiExtractedDataLabel +import xyz.driver.pdsuidomain.services.ExtractedDataService.RichExtractedData + +import scala.collection._ + +final case class ApiPartialExtractedData(documentId: Option[Long], + keywordId: Option[Long], + evidence: Tristate[String], + meta: Tristate[String], + labels: Tristate[List[ApiExtractedDataLabel]]) { + + def applyTo(orig: RichExtractedData): RichExtractedData = RichExtractedData( + extractedData = applyTo(orig.extractedData), + labels = labels.cata(_.map(_.toDomain(orig.extractedData.id)), List.empty, orig.labels) + ) + + private def applyTo(orig: ExtractedData): ExtractedData = ExtractedData( + id = orig.id, + documentId = orig.documentId, + keywordId = keywordId.map(LongId[Keyword]).orElse(orig.keywordId), + evidenceText = evidence.cata(Some(_), None, orig.evidenceText), + meta = meta.map(x => TextJson(JsonSerializer.deserialize[Meta](x))).cata(Some(_), None, orig.meta) + ) + + def toDomain: RichExtractedData = { + val validation = Map( + JsPath \ "documentId" -> AdditionalConstraints.optionNonEmptyConstraint(documentId) + ) + + val validationErrors: JsonValidationErrors = validation.collect({ + case (fieldName, e: Invalid) => (fieldName, e.errors) + })(breakOut) + + if (validationErrors.isEmpty) { + val extractedData = ExtractedData( + documentId = documentId.map(LongId[Document]).get, + keywordId = keywordId.map(LongId[Keyword]), + evidenceText = evidence.toOption, + meta = meta.map(x => TextJson(JsonSerializer.deserialize[Meta](x))).toOption + ) + val labelList = labels.map(_.map(_.toDomain())) + RichExtractedData(extractedData, labelList.getOrElse(List.empty)) + } else { + throw new JsonValidationException(validationErrors) + } + } +} + +object ApiPartialExtractedData { + + private val reads: Reads[ApiPartialExtractedData] = ( + (JsPath \ "documentId").readNullable[Long] and + (JsPath \ "keywordId").readNullable[Long] and + (JsPath \ "evidence").readTristate[String] and + (JsPath \ "meta").readTristate[String] and + (JsPath \ "labels").readTristate[List[ApiExtractedDataLabel]] + ) (ApiPartialExtractedData.apply _) + + private val writes: Writes[ApiPartialExtractedData] = ( + (JsPath \ "documentId").writeNullable[Long] and + (JsPath \ "keywordId").writeNullable[Long] and + (JsPath \ "evidence").writeTristate[String] and + (JsPath \ "meta").writeTristate[String] and + (JsPath \ "labels").writeTristate[List[ApiExtractedDataLabel]] + ) (unlift(ApiPartialExtractedData.unapply)) + + implicit val format: Format[ApiPartialExtractedData] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/hypothesis/ApiHypothesis.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/hypothesis/ApiHypothesis.scala new file mode 100644 index 0000000..0d6763c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/hypothesis/ApiHypothesis.scala @@ -0,0 +1,26 @@ +package xyz.driver.pdsuidomain.formats.json.hypothesis + +import java.util.UUID + +import xyz.driver.pdsuidomain.entities.Hypothesis +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} + +final case class ApiHypothesis(id: UUID, name: String, treatmentType: String, description: String) + +object ApiHypothesis { + + implicit val format: Format[ApiHypothesis] = ( + (JsPath \ "id").format[UUID] and + (JsPath \ "name").format[String] and + (JsPath \ "treatmentType").format[String] and + (JsPath \ "description").format[String] + ) (ApiHypothesis.apply, unlift(ApiHypothesis.unapply)) + + def fromDomain(hypothesis: Hypothesis) = ApiHypothesis( + id = hypothesis.id.id, + name = hypothesis.name, + treatmentType = hypothesis.treatmentType, + description = hypothesis.description + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala new file mode 100644 index 0000000..37a9758 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala @@ -0,0 +1,41 @@ +package xyz.driver.pdsuidomain.formats.json.intervention + +import xyz.driver.pdsuidomain.entities.InterventionWithArms +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} + +final case class ApiIntervention(id: Long, + name: String, + typeId: Option[Long], + description: String, + isActive: Boolean, + arms: List[Long], + trialId: String) + +object ApiIntervention { + + implicit val format: Format[ApiIntervention] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] and + (JsPath \ "typeId").formatNullable[Long] and + (JsPath \ "description").format[String] and + (JsPath \ "isActive").format[Boolean] and + (JsPath \ "arms").format[List[Long]] and + (JsPath \ "trialId").format[String] + ) (ApiIntervention.apply, unlift(ApiIntervention.unapply)) + + def fromDomain(interventionWithArms: InterventionWithArms): ApiIntervention = { + import interventionWithArms.intervention + import interventionWithArms.arms + + ApiIntervention( + id = intervention.id.id, + name = intervention.name, + typeId = intervention.typeId.map(_.id), + description = intervention.description, + isActive = intervention.isActive, + arms = arms.map(_.armId.id), + trialId = intervention.trialId.id + ) + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala new file mode 100644 index 0000000..ca444eb --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuidomain.formats.json.intervention + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.InterventionType + +final case class ApiInterventionType(id: Long, name: String) + +object ApiInterventionType { + + implicit val format: Format[ApiInterventionType] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] + ) (ApiInterventionType.apply, unlift(ApiInterventionType.unapply)) + + def fromDomain(interventionType: InterventionType) = ApiInterventionType( + id = interventionType.id.id, + name = interventionType.name + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala new file mode 100644 index 0000000..416237a --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala @@ -0,0 +1,44 @@ +package xyz.driver.pdsuidomain.formats.json.intervention + +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuidomain.entities.{InterventionArm, InterventionWithArms} +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiPartialIntervention(typeId: Option[Long], + description: Option[String], + isActive: Option[Boolean], + arms: Option[List[Long]]) { + + def applyTo(orig: InterventionWithArms): InterventionWithArms = { + val origIntervention = orig.intervention + val draftArmList = arms.map(_.map(x => InterventionArm(LongId(x), orig.intervention.id))) + orig.copy( + intervention = origIntervention.copy( + typeId = typeId.map(LongId(_)).orElse(origIntervention.typeId), + description = description.getOrElse(origIntervention.description), + isActive = isActive.getOrElse(origIntervention.isActive) + ), + arms = draftArmList.getOrElse(orig.arms) + ) + } +} + +object ApiPartialIntervention { + + private val reads: Reads[ApiPartialIntervention] = ( + (JsPath \ "typeId").readNullable[Long] and + (JsPath \ "description").readNullable[String] and + (JsPath \ "isActive").readNullable[Boolean] and + (JsPath \ "arms").readNullable[List[Long]] + ) (ApiPartialIntervention.apply _) + + private val writes: Writes[ApiPartialIntervention] = ( + (JsPath \ "typeId").writeNullable[Long] and + (JsPath \ "description").writeNullable[String] and + (JsPath \ "isActive").writeNullable[Boolean] and + (JsPath \ "arms").writeNullable[List[Long]] + ) (unlift(ApiPartialIntervention.unapply)) + + implicit val format: Format[ApiPartialIntervention] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/keyword/ApiKeyword.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/keyword/ApiKeyword.scala new file mode 100644 index 0000000..afd012d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/keyword/ApiKeyword.scala @@ -0,0 +1,23 @@ +package xyz.driver.pdsuidomain.formats.json.keyword + +import xyz.driver.pdsuidomain.entities.KeywordWithLabels +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.formats.json.label.ApiLabel + +final case class ApiKeyword(id: Long, keyword: String, labels: List[ApiLabel]) + +object ApiKeyword { + + implicit val format: Format[ApiKeyword] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "keyword").format[String] and + (JsPath \ "labels").format[List[ApiLabel]] + ) (ApiKeyword.apply, unlift(ApiKeyword.unapply)) + + def fromDomain(keywordWithLabels: KeywordWithLabels) = ApiKeyword( + id = keywordWithLabels.keyword.id.id, + keyword = keywordWithLabels.keyword.keyword, + labels = keywordWithLabels.labels.map(x => ApiLabel(x.id.id, x.name, x.categoryId.id)) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiCriterionLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiCriterionLabel.scala new file mode 100644 index 0000000..2788bf2 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiCriterionLabel.scala @@ -0,0 +1,49 @@ +package xyz.driver.pdsuidomain.formats.json.label + +import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} +import xyz.driver.pdsuidomain.entities.{Category, Criterion, CriterionLabel, Label} +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +/** + * @param value Yes|No + */ +final case class ApiCriterionLabel(labelId: Option[Long], + categoryId: Option[Long], + value: Option[String], + isDefining: Boolean) { + + def toDomain(criterionId: LongId[Criterion]) = CriterionLabel( + id = LongId(0L), + labelId = labelId.map(LongId[Label]), + criterionId = criterionId, + categoryId = categoryId.map(LongId[Category]), + value = value.map { + case "Yes" => true + case "No" => false + }, + isDefining = isDefining + ) +} + +object ApiCriterionLabel { + + def fromDomain(x: CriterionLabel) = ApiCriterionLabel( + labelId = x.labelId.map(_.id), + categoryId = x.categoryId.map(_.id), + value = x.value.map { x => + FuzzyValue.valueToString(FuzzyValue.fromBoolean(x)) + }, + isDefining = x.isDefining + ) + + implicit val format: Format[ApiCriterionLabel] = ( + (JsPath \ "labelId").formatNullable[Long] and + (JsPath \ "categoryId").formatNullable[Long] and + (JsPath \ "value").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ x => + x == "Yes" || x == "No" + }), Writes.of[String])) and + (JsPath \ "isDefining").format[Boolean] + ) (ApiCriterionLabel.apply, unlift(ApiCriterionLabel.unapply)) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiExtractedDataLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiExtractedDataLabel.scala new file mode 100644 index 0000000..9159d27 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiExtractedDataLabel.scala @@ -0,0 +1,36 @@ +package xyz.driver.pdsuidomain.formats.json.label + +import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} +import xyz.driver.pdsuidomain.entities.{Category, ExtractedData, ExtractedDataLabel, Label} +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiExtractedDataLabel(id: Option[Long], categoryId: Option[Long], value: Option[String]) { + + def toDomain(dataId: LongId[ExtractedData] = LongId(0)) = ExtractedDataLabel( + id = LongId(0), + dataId = dataId, + labelId = id.map(LongId[Label]), + categoryId = categoryId.map(LongId[Category]), + value = value.map(FuzzyValue.fromString) + ) +} + +object ApiExtractedDataLabel { + + implicit val format: Format[ApiExtractedDataLabel] = ( + (JsPath \ "id").formatNullable[Long] and + (JsPath \ "categoryId").formatNullable[Long] and + (JsPath \ "value").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) + ) (ApiExtractedDataLabel.apply, unlift(ApiExtractedDataLabel.unapply)) + + def fromDomain(dataLabel: ExtractedDataLabel) = ApiExtractedDataLabel( + id = dataLabel.labelId.map(_.id), + categoryId = dataLabel.categoryId.map(_.id), + value = dataLabel.value.map(FuzzyValue.valueToString) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiLabel.scala new file mode 100644 index 0000000..8c30f3a --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/label/ApiLabel.scala @@ -0,0 +1,22 @@ +package xyz.driver.pdsuidomain.formats.json.label + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.Label + +final case class ApiLabel(id: Long, name: String, categoryId: Long) + +object ApiLabel { + + implicit val format: Format[ApiLabel] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] and + (JsPath \ "categoryId").format[Long] + ) (ApiLabel.apply, unlift(ApiLabel.unapply)) + + def fromDomain(x: Label) = ApiLabel( + id = x.id.id, + name = x.name, + categoryId = x.categoryId.id + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiMessage.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiMessage.scala new file mode 100644 index 0000000..20b2607 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiMessage.scala @@ -0,0 +1,59 @@ +package xyz.driver.pdsuidomain.formats.json.message + +import java.time.{ZoneId, ZonedDateTime} + +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.Message + +final case class ApiMessage(id: Long, + text: String, + lastUpdate: ZonedDateTime, + userId: Long, + isDraft: Boolean, + recordId: Option[Long], + documentId: Option[Long], + patientId: Option[String], + trialId: Option[String], + startPage: Option[Double], + endPage: Option[Double], + evidence: Option[String], + archiveRequired: Option[Boolean], + meta: Option[String]) + +object ApiMessage { + + def fromDomain(domain: Message) = ApiMessage( + id = domain.id.id, + text = domain.text, + lastUpdate = ZonedDateTime.of(domain.lastUpdate, ZoneId.of("Z")), + userId = domain.userId.id, + isDraft = domain.isDraft, + recordId = domain.recordId.map(_.id), + documentId = domain.documentId.map(_.id), + patientId = domain.patientId.map(_.toString), + trialId = domain.trialId.map(_.toString), + startPage = domain.startPage, + endPage = domain.endPage, + evidence = domain.evidence, + archiveRequired = domain.archiveRequired, + meta = domain.meta + ) + + implicit val format: Format[ApiMessage] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "text").format[String] and + (JsPath \ "lastUpdate").format[ZonedDateTime] and + (JsPath \ "userId").format[Long] and + (JsPath \ "isDraft").format[Boolean] and + (JsPath \ "recordId").formatNullable[Long] and + (JsPath \ "documentId").formatNullable[Long] and + (JsPath \ "patientId").formatNullable[String] and + (JsPath \ "trialId").formatNullable[String] and + (JsPath \ "startPage").formatNullable[Double] and + (JsPath \ "endPage").formatNullable[Double] and + (JsPath \ "evidence").formatNullable[String] and + (JsPath \ "archiveRequired").formatNullable[Boolean] and + (JsPath \ "meta").formatNullable[String] + ) (ApiMessage.apply, unlift(ApiMessage.unapply)) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala new file mode 100644 index 0000000..151234c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/message/ApiPartialMessage.scala @@ -0,0 +1,84 @@ +package xyz.driver.pdsuidomain.formats.json.message + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.domain._ +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} +import xyz.driver.pdsuidomain.entities.Message + +final case class ApiPartialMessage(text: Option[String], + recordId: Option[Long], + documentId: Option[Long], + patientId: Option[String], + trialId: Option[String], + startPage: Option[Double], + endPage: Option[Double], + evidence: Option[String], + archiveRequired: Option[Boolean], + meta: Option[String]) { + + def toDomain(userId: LongId[User]) = Message( + id = LongId(0), + text = text.getOrElse(""), + userId = userId, + isDraft = true, + recordId = recordId.map(LongId(_)), + documentId = documentId.map(LongId(_)), + patientId = patientId.map(UuidId(_)), + trialId = trialId.map(StringId(_)), + startPage = startPage, + endPage = endPage, + evidence = evidence, + archiveRequired = archiveRequired, + meta = meta, + lastUpdate = LocalDateTime.MIN + ) + + def applyTo(orig: Message): Message = { + orig.copy( + text = text.getOrElse(""), + recordId = recordId.map(LongId(_)), + documentId = documentId.map(LongId(_)), + patientId = patientId.map(UuidId(_)), + trialId = trialId.map(StringId(_)), + startPage = startPage, + endPage = endPage, + evidence = evidence, + archiveRequired = archiveRequired, + meta = meta, + lastUpdate = LocalDateTime.MIN + ) + } + +} + +object ApiPartialMessage { + + implicit val format: Format[ApiPartialMessage] = ( + (JsPath \ "text").formatNullable[String] and + (JsPath \ "recordId").formatNullable[Long] and + (JsPath \ "documentId").formatNullable[Long] and + (JsPath \ "patientId").formatNullable[String] and + (JsPath \ "trialId").formatNullable[String] and + (JsPath \ "startPage").formatNullable[Double] and + (JsPath \ "endPage").formatNullable[Double] and + (JsPath \ "evidence").formatNullable[String] and + (JsPath \ "archiveRequired").formatNullable[Boolean] and + (JsPath \ "meta").formatNullable[String] + ) (ApiPartialMessage.apply, unlift(ApiPartialMessage.unapply)) + + def fromDomain(domain: Message) = ApiPartialMessage( + text = Some(domain.text), + recordId = domain.recordId.map(_.id), + documentId = domain.documentId.map(_.id), + patientId = domain.patientId.map(_.toString), + trialId = domain.trialId.map(_.toString), + startPage = domain.startPage, + endPage = domain.endPage, + evidence = domain.evidence, + archiveRequired = domain.archiveRequired, + meta = domain.meta + ) + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordCreateRequest.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordCreateRequest.scala new file mode 100644 index 0000000..5c12415 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordCreateRequest.scala @@ -0,0 +1,9 @@ +package xyz.driver.pdsuidomain.formats.json.password + +import play.api.libs.json.{Format, Json} + +final case class PasswordCreateRequest(password: String, key: String) + +object PasswordCreateRequest { + implicit val format: Format[PasswordCreateRequest] = Json.format[PasswordCreateRequest] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordUpdateRequest.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordUpdateRequest.scala new file mode 100644 index 0000000..07851ea --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/password/PasswordUpdateRequest.scala @@ -0,0 +1,9 @@ +package xyz.driver.pdsuidomain.formats.json.password + +import play.api.libs.json.{Format, Json} + +final case class PasswordUpdateRequest(password: String, oldPassword: String) + +object PasswordUpdateRequest { + implicit val format: Format[PasswordUpdateRequest] = Json.format[PasswordUpdateRequest] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala new file mode 100644 index 0000000..0a3938c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/ApiPatient.scala @@ -0,0 +1,44 @@ +package xyz.driver.pdsuidomain.formats.json.patient + +import java.time.{LocalDate, ZoneId, ZonedDateTime} + +import xyz.driver.pdsuidomain.entities.Patient +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} + +final case class ApiPatient(id: String, + status: String, + name: String, + dob: LocalDate, + assignee: Option[Long], + previousStatus: Option[String], + previousAssignee: Option[Long], + lastUpdate: ZonedDateTime, + condition: String) + +object ApiPatient { + + implicit val format: Format[ApiPatient] = ( + (JsPath \ "id").format[String] and + (JsPath \ "status").format[String] and + (JsPath \ "name").format[String] and + (JsPath \ "dob").format[LocalDate] and + (JsPath \ "assignee").formatNullable[Long] and + (JsPath \ "previousStatus").formatNullable[String] and + (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "lastUpdate").format[ZonedDateTime] and + (JsPath \ "condition").format[String] + ) (ApiPatient.apply, unlift(ApiPatient.unapply)) + + def fromDomain(patient: Patient) = ApiPatient( + id = patient.id.toString, + status = PatientStatus.statusToString(patient.status), + name = patient.name, + dob = patient.dob, + assignee = patient.assignee.map(_.id), + previousStatus = patient.previousStatus.map(PatientStatus.statusToString), + previousAssignee = patient.previousAssignee.map(_.id), + lastUpdate = ZonedDateTime.of(patient.lastUpdate, ZoneId.of("Z")), + condition = patient.condition + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/PatientStatus.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/PatientStatus.scala new file mode 100644 index 0000000..d906fc6 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/PatientStatus.scala @@ -0,0 +1,24 @@ +package xyz.driver.pdsuidomain.formats.json.patient + +import xyz.driver.pdsuidomain.entities.Patient.Status + +object PatientStatus { + + val statusFromString: PartialFunction[String, Status] = { + case "New" => Status.New + case "Verified" => Status.Verified + case "Reviewed" => Status.Reviewed + case "Curated" => Status.Curated + case "Flagged" => Status.Flagged + case "Done" => Status.Done + } + + def statusToString(x: Status): String = x match { + case Status.New => "New" + case Status.Verified => "Verified" + case Status.Reviewed => "Reviewed" + case Status.Curated => "Curated" + case Status.Flagged => "Flagged" + case Status.Done => "Done" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPartialPatientEligibleTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPartialPatientEligibleTrial.scala new file mode 100644 index 0000000..03ff275 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPartialPatientEligibleTrial.scala @@ -0,0 +1,18 @@ +package xyz.driver.pdsuidomain.formats.json.patient.eligible + +import xyz.driver.pdsuidomain.entities.PatientTrialArmGroupView +import play.api.libs.json.{Format, Json} + +final case class ApiPartialPatientEligibleTrial(isVerified: Option[Boolean]) { + + def applyTo(orig: PatientTrialArmGroupView): PatientTrialArmGroupView = { + orig.copy( + isVerified = isVerified.getOrElse(orig.isVerified) + ) + } +} + +object ApiPartialPatientEligibleTrial { + + implicit val format: Format[ApiPartialPatientEligibleTrial] = Json.format +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala new file mode 100644 index 0000000..c1a6e76 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/eligible/ApiPatientEligibleTrial.scala @@ -0,0 +1,46 @@ +package xyz.driver.pdsuidomain.formats.json.patient.eligible + +import java.util.UUID + +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain.FuzzyValue +import xyz.driver.pdsuidomain.services.PatientEligibleTrialService.RichPatientEligibleTrial + +final case class ApiPatientEligibleTrial(id: Long, + patientId: String, + trialId: String, + trialTitle: String, + arms: List[String], + hypothesisId: UUID, + eligibilityStatus: Option[String], + isVerified: Boolean) + +object ApiPatientEligibleTrial { + + implicit val apiEligibleTrialJsonFormat: Format[ApiPatientEligibleTrial] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "patientId").format[String] and + (JsPath \ "trialId").format[String] and + (JsPath \ "trialTitle").format[String] and + (JsPath \ "arms").format[List[String]] and + (JsPath \ "hypothesisId").format[UUID] and + (JsPath \ "eligibilityStatus").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown eligibility status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "isVerified").format[Boolean] + ) (ApiPatientEligibleTrial.apply, unlift(ApiPatientEligibleTrial.unapply)) + + def fromDomain(eligibleTrialWithTrial: RichPatientEligibleTrial) = ApiPatientEligibleTrial( + id = eligibleTrialWithTrial.group.id.id, + patientId = eligibleTrialWithTrial.group.patientId.toString, + trialId = eligibleTrialWithTrial.group.trialId.id, + trialTitle = eligibleTrialWithTrial.trial.title, + arms = eligibleTrialWithTrial.arms.map(_.name), + hypothesisId = eligibleTrialWithTrial.group.hypothesisId.id, + eligibleTrialWithTrial.group.eligibilityStatus.map(FuzzyValue.valueToString), + eligibleTrialWithTrial.group.isVerified + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPartialPatientHypothesis.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPartialPatientHypothesis.scala new file mode 100644 index 0000000..0858ce1 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPartialPatientHypothesis.scala @@ -0,0 +1,27 @@ +package xyz.driver.pdsuidomain.formats.json.patient.hypothesis + +import xyz.driver.pdsuidomain.entities.PatientHypothesis +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiPartialPatientHypothesis(rationale: Tristate[String]) { + + def applyTo(orig: PatientHypothesis): PatientHypothesis = { + orig.copy( + rationale = rationale.cata(Some(_), None, orig.rationale) + ) + } +} + +object ApiPartialPatientHypothesis { + + implicit val reads: Reads[ApiPartialPatientHypothesis] = + (__ \ "rationale").readTristate[String].map(x => ApiPartialPatientHypothesis(x)) + + implicit val writes: Writes[ApiPartialPatientHypothesis] = + (__ \ "rationale").writeTristate[String].contramap(_.rationale) + + implicit val format: Format[ApiPartialPatientHypothesis] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala new file mode 100644 index 0000000..1b0767d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/hypothesis/ApiPatientHypothesis.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.formats.json.patient.hypothesis + +import java.util.UUID + +import xyz.driver.pdsuidomain.entities.PatientHypothesis +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiPatientHypothesis(id: UUID, + patientId: String, + hypothesisId: UUID, + matchedTrials: Long, + rationale: Option[String]) + +object ApiPatientHypothesis { + + implicit val apiPatientHypothesisJsonFormat: Format[ApiPatientHypothesis] = ( + (JsPath \ "id").format[UUID] and + (JsPath \ "patientId").format[String] and + (JsPath \ "hypothesisId").format[UUID] and + (JsPath \ "matchedTrials").format[Long] and + (JsPath \ "rationale").formatNullable[String] + ) (ApiPatientHypothesis.apply, unlift(ApiPatientHypothesis.unapply)) + + def fromDomain(patientHypothesis: PatientHypothesis) = ApiPatientHypothesis( + id = patientHypothesis.id.id, + patientId = patientHypothesis.patientId.toString, + hypothesisId = patientHypothesis.hypothesisId.id, + matchedTrials = patientHypothesis.matchedTrials, + rationale = patientHypothesis.rationale + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala new file mode 100644 index 0000000..82e3a3f --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPartialPatientLabel.scala @@ -0,0 +1,38 @@ +package xyz.driver.pdsuidomain.formats.json.patient.label + +import xyz.driver.pdsuidomain.entities.PatientLabel +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain.FuzzyValue + +final case class ApiPartialPatientLabel(primaryValue: Option[String], + verifiedPrimaryValue: Tristate[String]) { + + def applyTo(orig: PatientLabel): PatientLabel = { + orig.copy( + primaryValue = primaryValue.map(FuzzyValue.fromString).orElse(orig.primaryValue), + verifiedPrimaryValue = verifiedPrimaryValue.cata(x => Some(FuzzyValue.fromString(x)), None, orig.verifiedPrimaryValue) + ) + } + +} + +object ApiPartialPatientLabel { + + implicit val format: Format[ApiPartialPatientLabel] = ( + (JsPath \ "primaryValue").formatNullable[String](Format( + Reads.of[String].filter(ValidationError("unknown primary value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "verifiedPrimaryValue").formatTristate[String](Format( + Reads.of[String].filter(ValidationError("unknown verified primary value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) + ) (ApiPartialPatientLabel.apply, unlift(ApiPartialPatientLabel.unapply)) + +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabel.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabel.scala new file mode 100644 index 0000000..fc8687b --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabel.scala @@ -0,0 +1,47 @@ +package xyz.driver.pdsuidomain.formats.json.patient.label + +import xyz.driver.pdsuidomain.entities.PatientLabel +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain.FuzzyValue + +final case class ApiPatientLabel(id: Long, + labelId: Long, + primaryValue: Option[String], + verifiedPrimaryValue: Option[String], + score: Int, + isImplicitMatch: Boolean, + isVisible: Boolean, + isVerified: Boolean) + +object ApiPatientLabel { + + implicit val apiPatientLabelJsonFormat: Format[ApiPatientLabel] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "labelId").format[Long] and + (JsPath \ "primaryValue").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "verifiedPrimaryValue").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "score").format[Int] and + (JsPath \ "isImplicitMatch").format[Boolean] and + (JsPath \ "isVisible").format[Boolean] and + (JsPath \ "isVerified").format[Boolean] + ) (ApiPatientLabel.apply, unlift(ApiPatientLabel.unapply)) + + def fromDomain(patientLabel: PatientLabel, isVerified: Boolean): ApiPatientLabel = ApiPatientLabel( + id = patientLabel.id.id, + labelId = patientLabel.labelId.id, + primaryValue = patientLabel.primaryValue.map(FuzzyValue.valueToString), + verifiedPrimaryValue = patientLabel.verifiedPrimaryValue.map(FuzzyValue.valueToString), + score = patientLabel.score, + isImplicitMatch = patientLabel.isImplicitMatch, + isVisible = patientLabel.isVisible, + isVerified = isVerified + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabelDefiningCriteria.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabelDefiningCriteria.scala new file mode 100644 index 0000000..3fe135e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/label/ApiPatientLabelDefiningCriteria.scala @@ -0,0 +1,25 @@ +package xyz.driver.pdsuidomain.formats.json.patient.label + +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.domain.FuzzyValue +import xyz.driver.pdsuidomain.entities.PatientLabel + +final case class ApiPatientLabelDefiningCriteria(labelId: Long, value: Option[String]) + +object ApiPatientLabelDefiningCriteria { + + implicit val format: Format[ApiPatientLabelDefiningCriteria] = ( + (JsPath \ "labelId").format[Long] and + (JsPath \ "value").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) + ) (ApiPatientLabelDefiningCriteria.apply, unlift(ApiPatientLabelDefiningCriteria.unapply)) + + def fromDomain(x: PatientLabel) = ApiPatientLabelDefiningCriteria( + labelId = x.labelId.id, + value = x.verifiedPrimaryValue.map(FuzzyValue.valueToString) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterion.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterion.scala new file mode 100644 index 0000000..b68dad5 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterion.scala @@ -0,0 +1,40 @@ +package xyz.driver.pdsuidomain.formats.json.patient.trial + +import xyz.driver.pdsuidomain.entities.PatientCriterion +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath, Reads, Writes} +import xyz.driver.pdsuicommon.domain.FuzzyValue + +final case class ApiPartialPatientCriterion(eligibilityStatus: Option[String], + verifiedEligibilityStatus: Tristate[String]) { + + def applyTo(orig: PatientCriterion): PatientCriterion = { + orig.copy( + eligibilityStatus = eligibilityStatus.map(FuzzyValue.fromString).orElse(orig.eligibilityStatus), + verifiedEligibilityStatus = verifiedEligibilityStatus.cata(x => + Some(FuzzyValue.fromString(x)), + None, + orig.verifiedEligibilityStatus + ) + ) + } +} + +object ApiPartialPatientCriterion { + + implicit val format: Format[ApiPartialPatientCriterion] = ( + (JsPath \ "eligibilityStatus").formatNullable[String](Format( + Reads.of[String].filter(ValidationError("unknown eligibility status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "verifiedEligibilityStatus").formatTristate[String](Format( + Reads.of[String].filter(ValidationError("unknown verified eligibility status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) + ) (ApiPartialPatientCriterion.apply, unlift(ApiPartialPatientCriterion.unapply)) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterionList.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterionList.scala new file mode 100644 index 0000000..71cb58f --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPartialPatientCriterionList.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.formats.json.patient.trial + +import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} +import xyz.driver.pdsuidomain.entities.PatientCriterion +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath, Reads, Writes} +import xyz.driver.pdsuidomain.services.PatientCriterionService.DraftPatientCriterion + +final case class ApiPartialPatientCriterionList(id: Long, + eligibilityStatus: Option[String], + isVerified: Option[Boolean]) { + + def toDomain: DraftPatientCriterion = DraftPatientCriterion( + id = LongId[PatientCriterion](id), + eligibilityStatus = eligibilityStatus.map(FuzzyValue.fromString), + isVerified = isVerified + ) +} + +object ApiPartialPatientCriterionList { + + implicit val format: Format[ApiPartialPatientCriterionList] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "eligibilityStatus").formatNullable[String](Format( + Reads.of[String].filter(ValidationError("unknown eligibility status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "isVerified").formatNullable[Boolean] + ) (ApiPartialPatientCriterionList.apply, unlift(ApiPartialPatientCriterionList.unapply)) +} 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 new file mode 100644 index 0000000..3e2de99 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/patient/trial/ApiPatientCriterion.scala @@ -0,0 +1,72 @@ +package xyz.driver.pdsuidomain.formats.json.patient.trial + +import java.time.{ZoneId, ZonedDateTime} + +import xyz.driver.pdsuicommon.domain.{FuzzyValue, LongId} +import xyz.driver.pdsuidomain.entities.{Arm, Label, PatientCriterion} +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath, Reads, Writes} + +final case class ApiPatientCriterion(id: Long, + labelId: Long, + nctId: String, + criterionText: String, + criterionValue: Option[String], + criterionIsDefining: Boolean, + criterionIsCompound: Boolean, + arms: List[String], + eligibilityStatus: Option[String], + verifiedEligibilityStatus: Option[String], + isVerified: Boolean, + isVisible: Boolean, + lastUpdate: ZonedDateTime) + +object ApiPatientCriterion { + + implicit val format: Format[ApiPatientCriterion] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "labelId").format[Long] and + (JsPath \ "nctId").format[String] and + (JsPath \ "criterionText").format[String] and + (JsPath \ "criterionValue").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown value"))({ x => + x == "Yes" || x == "No" + }), Writes.of[String])) and + (JsPath \ "criterionIsDefining").format[Boolean] and + (JsPath \ "criterionIsCompound").format[Boolean] and + (JsPath \ "arms").format[List[String]] and + (JsPath \ "eligibilityStatus").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "verifiedEligibilityStatus").formatNullable[String](Format( + Reads.of[String].filter(ValidationError("unknown status"))({ + case x if FuzzyValue.fromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "isVerified").format[Boolean] and + (JsPath \ "isVisible").format[Boolean] and + (JsPath \ "lastUpdate").format[ZonedDateTime] + ) (ApiPatientCriterion.apply, unlift(ApiPatientCriterion.unapply)) + + def fromDomain(patientCriterion: PatientCriterion, + labelId: LongId[Label], + arms: List[Arm], + criterionIsCompound: Boolean) = ApiPatientCriterion( + id = patientCriterion.id.id, + labelId = labelId.id, + nctId = patientCriterion.nctId.id, + criterionText = patientCriterion.criterionText, + criterionValue = patientCriterion.criterionValue.map { x => + FuzzyValue.valueToString(FuzzyValue.fromBoolean(x)) + }, + criterionIsDefining = patientCriterion.criterionIsDefining, + criterionIsCompound = criterionIsCompound, + arms = arms.map(_.name), + eligibilityStatus = patientCriterion.eligibilityStatus.map(FuzzyValue.valueToString), + verifiedEligibilityStatus = patientCriterion.verifiedEligibilityStatus.map(FuzzyValue.valueToString), + isVerified = patientCriterion.isVerified, + isVisible = patientCriterion.isVisible, + lastUpdate = ZonedDateTime.of(patientCriterion.lastUpdate, ZoneId.of("Z")) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala new file mode 100644 index 0000000..57c030b --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiCreateRecord.scala @@ -0,0 +1,37 @@ +package xyz.driver.pdsuidomain.formats.json.record + +import java.time.LocalDateTime +import java.util.UUID + +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuidomain.entities._ +import play.api.libs.json._ + +final case class ApiCreateRecord(disease: String, + patientId: String, + requestId: UUID, + filename: String) { + + def toDomain = MedicalRecord( + id = LongId(0), + status = MedicalRecord.Status.New, + previousStatus = None, + assignee = None, + previousAssignee = None, + patientId = UuidId(patientId), + requestId = RecordRequestId(requestId), + disease = disease, + caseId = None, + physician = None, + sourceName = filename, + meta = None, + predictedMeta = None, + predictedDocuments = None, + lastUpdate = LocalDateTime.now() + ) +} + +object ApiCreateRecord { + + implicit val format: Format[ApiCreateRecord] = Json.format[ApiCreateRecord] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala new file mode 100644 index 0000000..6096006 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala @@ -0,0 +1,57 @@ +package xyz.driver.pdsuidomain.formats.json.record + +import java.time.{ZoneId, ZonedDateTime} + +import xyz.driver.pdsuidomain.entities.MedicalRecord +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.JsonSerializer + +final case class ApiRecord(id: Long, + patientId: String, + caseId: Option[String], + physician: Option[String], + lastUpdate: ZonedDateTime, + status: String, + previousStatus: Option[String], + assignee: Option[Long], + previousAssignee: Option[Long], + meta: String) + +object ApiRecord { + + private val statusFormat = Format( + Reads.StringReads.filter(ValidationError("unknown status")) { + case x if MedicalRecordStatus.statusFromString.isDefinedAt(x) => true + case _ => false + }, + Writes.StringWrites + ) + + implicit val format: Format[ApiRecord] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "patientId").format[String] and + (JsPath \ "caseId").formatNullable[String] and + (JsPath \ "physician").formatNullable[String] and + (JsPath \ "lastUpdate").format[ZonedDateTime] and + (JsPath \ "status").format(statusFormat) and + (JsPath \ "previousStatus").formatNullable(statusFormat) and + (JsPath \ "assignee").formatNullable[Long] and + (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "meta").format(Format(Reads { x => JsSuccess(Json.stringify(x)) }, Writes[String](Json.parse))) + ) (ApiRecord.apply, unlift(ApiRecord.unapply)) + + def fromDomain(record: MedicalRecord) = ApiRecord( + id = record.id.id, + patientId = record.patientId.toString, + caseId = record.caseId.map(_.id), + physician = record.physician, + lastUpdate = ZonedDateTime.of(record.lastUpdate, ZoneId.of("Z")), + status = MedicalRecordStatus.statusToString(record.status), + previousStatus = record.previousStatus.map(MedicalRecordStatus.statusToString), + assignee = record.assignee.map(_.id), + previousAssignee = record.previousAssignee.map(_.id), + meta = record.meta.map(x => JsonSerializer.serialize(x.content)).getOrElse("[]") + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala new file mode 100644 index 0000000..b752a4c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala @@ -0,0 +1,47 @@ +package xyz.driver.pdsuidomain.formats.json.record + +import xyz.driver.pdsuidomain.entities.MedicalRecord.Meta +import xyz.driver.pdsuidomain.entities._ +import xyz.driver.pdsuicommon.domain.{LongId, TextJson, User} +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.JsonSerializer + +final case class ApiUpdateRecord(status: Option[String], + assignee: Tristate[Long], + meta: Tristate[String]) { + + def applyTo(orig: MedicalRecord): MedicalRecord = { + orig.copy( + status = status.map(MedicalRecordStatus.statusFromString).getOrElse(orig.status), + assignee = assignee.map(LongId[User]).cata(Some(_), None, orig.assignee), + meta = meta.cata(x => Some(TextJson(JsonSerializer.deserialize[List[Meta]](x))), None, orig.meta) + ) + } +} + +object ApiUpdateRecord { + + private val reads: Reads[ApiUpdateRecord] = ( + (JsPath \ "status").readNullable[String](Reads.of[String].filter(ValidationError("unknown status"))({ + case x if MedicalRecordStatus.statusFromString.isDefinedAt(x) => true + case _ => false + })) and + (JsPath \ "assignee").readTristate[Long] and + (JsPath \ "meta").readTristate(Reads { x => JsSuccess(Json.stringify(x)) }).map { + case Tristate.Present("{}") => Tristate.Absent + case x => x + } + ) (ApiUpdateRecord.apply _) + + private val writes: Writes[ApiUpdateRecord] = ( + (JsPath \ "status").writeNullable[String] and + (JsPath \ "assignee").writeTristate[Long] and + (JsPath \ "meta").writeTristate(Writes[String](Json.parse)) + ) (unlift(ApiUpdateRecord.unapply)) + + implicit val format: Format[ApiUpdateRecord] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/MedicalRecordStatus.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/MedicalRecordStatus.scala new file mode 100644 index 0000000..bde4af0 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/MedicalRecordStatus.scala @@ -0,0 +1,34 @@ +package xyz.driver.pdsuidomain.formats.json.record + +import xyz.driver.pdsuidomain.entities.MedicalRecord.Status + +object MedicalRecordStatus { + + val statusFromString: PartialFunction[String, Status] = { + case "Unprocessed" => Status.Unprocessed + case "PreCleaning" => Status.PreCleaning + case "New" => Status.New + case "Cleaned" => Status.Cleaned + case "PreOrganized" => Status.PreOrganized + case "PreOrganizing" => Status.PreOrganizing + case "Reviewed" => Status.Reviewed + case "Organized" => Status.Organized + case "Done" => Status.Done + case "Flagged" => Status.Flagged + case "Archived" => Status.Archived + } + + def statusToString(x: Status): String = x match { + case Status.Unprocessed => "Unprocessed" + case Status.PreCleaning => "PreCleaning" + case Status.New => "New" + case Status.Cleaned => "Cleaned" + case Status.PreOrganized => "PreOrganized" + case Status.PreOrganizing => "PreOrganizing" + case Status.Reviewed => "Reviewed" + case Status.Organized => "Organized" + case Status.Done => "Done" + case Status.Flagged => "Flagged" + case Status.Archived => "Archived" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionRequest.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionRequest.scala new file mode 100644 index 0000000..fbe9689 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionRequest.scala @@ -0,0 +1,12 @@ +package xyz.driver.pdsuidomain.formats.json.session + +import xyz.driver.pdsuicommon.domain.Email +import play.api.libs.json._ +import xyz.driver.pdsuicommon.json.Serialization._ + +final case class NewSessionRequest(email: Email, password: String) + +object NewSessionRequest { + + implicit val format: Format[NewSessionRequest] = Json.format[NewSessionRequest] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionResponse.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionResponse.scala new file mode 100644 index 0000000..55e05b1 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/session/NewSessionResponse.scala @@ -0,0 +1,11 @@ +package xyz.driver.pdsuidomain.formats.json.session + +import play.api.libs.json.Json +import xyz.driver.pdsuidomain.formats.json.user.ApiUser + +final case class NewSessionResponse(token: String, user: ApiUser) + +object NewSessionResponse { + + implicit val format = Json.format[NewSessionResponse] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/studydesign/ApiStudyDesign.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/studydesign/ApiStudyDesign.scala new file mode 100644 index 0000000..958ff5d --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/studydesign/ApiStudyDesign.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuidomain.formats.json.studydesign + +import xyz.driver.pdsuidomain.entities.StudyDesign +import play.api.libs.functional.syntax._ +import play.api.libs.json.{Format, JsPath} + +final case class ApiStudyDesign(id: Long, name: String) + +object ApiStudyDesign { + + implicit val format: Format[ApiStudyDesign] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "name").format[String] + ) (ApiStudyDesign.apply, unlift(ApiStudyDesign.unapply)) + + def fromDomain(studyDesign: StudyDesign) = ApiStudyDesign( + id = studyDesign.id.id, + name = studyDesign.name + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiPartialTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiPartialTrial.scala new file mode 100644 index 0000000..0dc1446 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiPartialTrial.scala @@ -0,0 +1,44 @@ +package xyz.driver.pdsuidomain.formats.json.trial + +import java.util.UUID + +import xyz.driver.pdsuicommon.domain.{LongId, UuidId} +import xyz.driver.pdsuidomain.entities.Trial +import org.davidbild.tristate.Tristate +import org.davidbild.tristate.contrib.play.ToJsPathOpsFromJsPath +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiPartialTrial(hypothesisId: Tristate[UUID], + studyDesignId: Tristate[Long], + overview: Tristate[String], + title: Tristate[String]) { + + def applyTo(orig: Trial): Trial = { + orig.copy( + hypothesisId = hypothesisId.map(UuidId(_)).cata(Some(_), None, orig.hypothesisId), + studyDesignId = studyDesignId.map(LongId(_)).cata(Some(_), None, orig.studyDesignId), + overview = overview.cata(Some(_), None, orig.overview), + title = title.cata(Some(_).getOrElse(""), "", orig.title) + ) + } +} + +object ApiPartialTrial { + + private val reads: Reads[ApiPartialTrial] = ( + (JsPath \ "hypothesisId").readTristate[UUID] and + (JsPath \ "studyDesignId").readTristate[Long] and + (JsPath \ "overview").readTristate[String] and + (JsPath \ "title").readTristate[String] + ) (ApiPartialTrial.apply _) + + private val writes: Writes[ApiPartialTrial] = ( + (JsPath \ "hypothesisId").writeTristate[UUID] and + (JsPath \ "studyDesignId").writeTristate[Long] and + (JsPath \ "overview").writeTristate[String] and + (JsPath \ "title").writeTristate[String] + ) (unlift(ApiPartialTrial.unapply)) + + implicit val format: Format[ApiPartialTrial] = Format(reads, writes) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala new file mode 100644 index 0000000..3267617 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/ApiTrial.scala @@ -0,0 +1,63 @@ +package xyz.driver.pdsuidomain.formats.json.trial + +import java.time.{ZoneId, ZonedDateTime} +import java.util.UUID + +import xyz.driver.pdsuidomain.entities.Trial +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiTrial(id: String, + lastUpdate: Option[ZonedDateTime], + status: String, + assignee: Option[Long], + previousStatus: Option[String], + previousAssignee: Option[Long], + condition: Option[String], + phase: Option[String], + hypothesisId: Option[UUID], + studyDesignId: Option[Long], + isPartner: Boolean, + overview: Option[String], + overviewTemplate: String, + isUpdated: Boolean, + title: String) + +object ApiTrial { + + implicit val format: Format[ApiTrial] = ( + (JsPath \ "id").format[String] and + (JsPath \ "lastUpdate").formatNullable[ZonedDateTime] and + (JsPath \ "status").format[String] and + (JsPath \ "assignee").formatNullable[Long] and + (JsPath \ "previousStatus").formatNullable[String] and + (JsPath \ "previousAssignee").formatNullable[Long] and + (JsPath \ "condition").formatNullable[String] and + (JsPath \ "phase").formatNullable[String] and + (JsPath \ "hypothesisId").formatNullable[UUID] and + (JsPath \ "studyDesignId").formatNullable[Long] and + (JsPath \ "isPartner").format[Boolean] and + (JsPath \ "overview").formatNullable[String] and + (JsPath \ "overviewTemplate").format[String] and + (JsPath \ "isUpdated").format[Boolean] and + (JsPath \ "title").format[String] + ) (ApiTrial.apply, unlift(ApiTrial.unapply)) + + def fromDomain(trial: Trial): ApiTrial = ApiTrial( + id = trial.id.id, + status = TrialStatus.statusToString(trial.status), + assignee = trial.assignee.map(_.id), + previousStatus = trial.previousStatus.map(TrialStatus.statusToString), + previousAssignee = trial.previousAssignee.map(_.id), + lastUpdate = Option(ZonedDateTime.of(trial.lastUpdate, ZoneId.of("Z"))), + condition = Option(trial.condition.toString), + phase = Option(trial.phase), + hypothesisId = trial.hypothesisId.map(_.id), + studyDesignId = trial.studyDesignId.map(_.id), + isPartner = trial.isPartner, + overview = trial.overview, + overviewTemplate = trial.overviewTemplate, + isUpdated = trial.isUpdated, + title = trial.title + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/TrialStatus.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/TrialStatus.scala new file mode 100644 index 0000000..49bcbcb --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trial/TrialStatus.scala @@ -0,0 +1,30 @@ +package xyz.driver.pdsuidomain.formats.json.trial + +import xyz.driver.pdsuidomain.entities.Trial.Status + +object TrialStatus { + + val statusFromString: PartialFunction[String, Status] = { + case "New" => Status.New + case "ReviewSummary" => Status.ReviewSummary + case "Summarized" => Status.Summarized + case "PendingUpdate" => Status.PendingUpdate + case "Update" => Status.Update + case "ReviewCriteria" => Status.ReviewCriteria + case "Done" => Status.Done + case "Flagged" => Status.Flagged + case "Archived" => Status.Archived + } + + def statusToString(x: Status): String = x match { + case Status.New => "New" + case Status.ReviewSummary => "ReviewSummary" + case Status.Summarized => "Summarized" + case Status.PendingUpdate => "PendingUpdate" + case Status.Update => "Update" + case Status.ReviewCriteria => "ReviewCriteria" + case Status.Done => "Done" + case Status.Flagged => "Flagged" + case Status.Archived => "Archived" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala new file mode 100644 index 0000000..977934b --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiPartialUser.scala @@ -0,0 +1,83 @@ +package xyz.driver.pdsuidomain.formats.json.user + +import java.math.BigInteger +import java.security.SecureRandom + +import xyz.driver.pdsuicommon.domain.{Email, LongId, PasswordHash, User} +import play.api.data.validation._ +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +import scala.collection._ +import scala.util.Try +import ApiPartialUser._ +import xyz.driver.pdsuicommon.json.JsonValidationException +import xyz.driver.pdsuicommon.validation.{AdditionalConstraints, JsonValidationErrors} + +final case class ApiPartialUser(email: Option[String], + name: Option[String], + roleId: Option[String]) { + + def applyTo(orig: User): Try[User] = Try { + val validation = Map( + JsPath \ "name" -> AdditionalConstraints.optionNonEmptyConstraint(name) + ) + + val validationErrors: JsonValidationErrors = validation.collect({ + case (fieldName, e: Invalid) => (fieldName, e.errors) + })(breakOut) + + if (validationErrors.isEmpty) { + orig.copy(name = name.get) + } else { + throw new JsonValidationException(validationErrors) + } + } + + def toDomain(id: LongId[User] = LongId(0L)): Try[User] = Try { + val validation = Map( + JsPath \ "email" -> AdditionalConstraints.optionNonEmptyConstraint(email), + JsPath \ "name" -> AdditionalConstraints.optionNonEmptyConstraint(name), + JsPath \ "roleId" -> AdditionalConstraints.optionNonEmptyConstraint(roleId) + ) + + val validationErrors: JsonValidationErrors = validation.collect({ + case (fieldName, e: Invalid) => (fieldName, e.errors) + })(breakOut) + + if (validationErrors.isEmpty) { + val userEmail = email.map(x => Email(x.toLowerCase)).get + User( + id = id, + email = userEmail, + name = name.get, + role = roleId.map(UserRole.roleFromString).get, + passwordHash = PasswordHash(createPassword), + latestActivity = None, + deleted = None + ) + } else { + throw new JsonValidationException(validationErrors) + } + } +} + +object ApiPartialUser { + + // SecureRandom is thread-safe, see the implementation + private val random = new SecureRandom() + + def createPassword: String = new BigInteger(240, random).toString(32) + + implicit val format: Format[ApiPartialUser] = ( + (JsPath \ "email").formatNullable[String](Format(Reads.email, Writes.StringWrites)) and + (JsPath \ "name").formatNullable[String](Format( + Reads.filterNot[String](ValidationError("Username is too long (max length is 255 chars)", 255))(_.length > 255), + Writes.StringWrites + )) and + (JsPath \ "roleId").formatNullable[String](Format(Reads.of[String].filter(ValidationError("unknown role"))({ + case x if UserRole.roleFromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) + ) (ApiPartialUser.apply, unlift(ApiPartialUser.unapply)) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiUser.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiUser.scala new file mode 100644 index 0000000..c2653ec --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/ApiUser.scala @@ -0,0 +1,32 @@ +package xyz.driver.pdsuidomain.formats.json.user + +import java.time.{ZoneId, ZonedDateTime} + +import xyz.driver.pdsuicommon.domain.User +import play.api.data.validation.ValidationError +import play.api.libs.functional.syntax._ +import play.api.libs.json._ + +final case class ApiUser(id: Long, email: String, name: String, roleId: String, latestActivity: Option[ZonedDateTime]) + +object ApiUser { + + implicit val format: Format[ApiUser] = ( + (JsPath \ "id").format[Long] and + (JsPath \ "email").format[String](Reads.email) and + (JsPath \ "name").format[String] and + (JsPath \ "roleId").format[String](Format(Reads.of[String].filter(ValidationError("unknown role"))({ + case x if UserRole.roleFromString.isDefinedAt(x) => true + case _ => false + }), Writes.of[String])) and + (JsPath \ "latestActivity").formatNullable[ZonedDateTime] + ) (ApiUser.apply, unlift(ApiUser.unapply)) + + def fromDomain(user: User) = ApiUser( + user.id.id, + user.email.value, + user.name, + UserRole.roleToString(user.role), + user.latestActivity.map(ZonedDateTime.of(_, ZoneId.of("Z"))) + ) +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/UserRole.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/UserRole.scala new file mode 100644 index 0000000..74acb81 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/user/UserRole.scala @@ -0,0 +1,33 @@ +package xyz.driver.pdsuidomain.formats.json.user + +import xyz.driver.pdsuicommon.domain.User.Role + +object UserRole { + + val roleFromString: PartialFunction[String, Role] = { + case "Cleaner" => Role.RecordCleaner + case "Organizer" => Role.RecordOrganizer + case "Extractor" => Role.DocumentExtractor + case "RecordAdmin" => Role.RecordAdmin + case "TrialSummarizer" => Role.TrialSummarizer + case "CriteriaCurator" => Role.CriteriaCurator + case "TrialAdmin" => Role.TrialAdmin + case "EligibilityVerifier" => Role.EligibilityVerifier + case "TreatmentMatchingAdmin" => Role.TreatmentMatchingAdmin + case "RoutesCurator" => Role.RoutesCurator + // No Mixed at this time + } + + def roleToString(x: Role): String = x match { + case Role.RecordCleaner => "Cleaner" + case Role.RecordOrganizer => "Organizer" + case Role.DocumentExtractor => "Extractor" + case Role.RecordAdmin => "RecordAdmin" + case Role.TrialSummarizer => "TrialSummarizer" + case Role.CriteriaCurator => "CriteriaCurator" + case Role.TrialAdmin => "TrialAdmin" + case Role.EligibilityVerifier => "EligibilityVerifier" + case Role.TreatmentMatchingAdmin => "TreatmentMatchingAdmin" + case Role.RoutesCurator => "RoutesCurator" + } +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala new file mode 100644 index 0000000..bc59f0c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ArmService.scala @@ -0,0 +1,133 @@ +package xyz.driver.pdsuidomain.services + + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.Arm + +import scala.concurrent.Future + +object ArmService { + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + trait DefaultNotFoundError { + def userMessage: String = "Arm not found" + } + + sealed trait GetByIdReply + object GetByIdReply { + + case class Entity(x: Arm) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + } + + sealed trait GetListReply + object GetListReply { + type Error = GetListReply with DomainError + + case class EntityList(xs: Seq[Arm], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + } + + sealed trait UpdateReply + object UpdateReply { + + case class Updated(updated: Arm) extends UpdateReply + + type Error = UpdateReply with DomainError + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + case class AlreadyExistsError(x: Arm) extends UpdateReply with DomainError { + val userMessage = s"The arm with such name of trial already exists." + } + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } + + sealed trait CreateReply + object CreateReply { + case class Created(x: Arm) extends CreateReply + + type Error = CreateReply with DomainError + + case object AuthorizationError + extends CreateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends CreateReply with DomainError + + case class AlreadyExistsError(x: Arm) extends CreateReply with DomainError { + val userMessage = s"The arm with this name of trial already exists." + } + + implicit def toPhiString(reply: CreateReply): PhiString = reply match { + case Created(x) => phi"Created($x)" + case x: Error => DomainError.toPhiString(x) + } + } + + 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 + + case class CommonError(userMessage: String) + extends DeleteReply with DomainError + } +} + +trait ArmService { + + import ArmService._ + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getById(armId: LongId[Arm])(implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def create(draftArm: Arm)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + def update(origArm: Arm, draftArm: Arm)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def delete(id: LongId[Arm])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/CriterionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/CriterionService.scala new file mode 100644 index 0000000..e3d806c --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/CriterionService.scala @@ -0,0 +1,131 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object CriterionService { + + trait DefaultNotFoundError { + def userMessage: String = "Criterion not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + case class RichCriterion(criterion: Criterion, + armIds: Seq[LongId[Arm]], + labels: Seq[CriterionLabel]) + object RichCriterion { + implicit def toPhiString(x: RichCriterion): PhiString = { + import x._ + phi"RichCriterion(criterion=$criterion, armIds=$armIds, labels=$labels)" + } + } + + sealed trait CreateReply + object CreateReply { + type Error = CreateReply with DomainError + + case class Created(x: RichCriterion) extends CreateReply + + case object AuthorizationError + extends CreateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends CreateReply with DomainError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: RichCriterion) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[RichCriterion], + totalFound: Int, + lastUpdate: Option[LocalDateTime]) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: RichCriterion) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply 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 + + case class CommonError(userMessage: String) + extends DeleteReply with DomainError + } +} + +trait CriterionService { + + import CriterionService._ + + def create(draftRichCriterion: RichCriterion) + (implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + def getById(id: LongId[Criterion]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def update(origRichCriterion: RichCriterion, + draftRichCriterion: RichCriterion) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def delete(id: LongId[Criterion]) + (implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala new file mode 100644 index 0000000..cd242c9 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentService.scala @@ -0,0 +1,157 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object DocumentService { + + trait DefaultNotFoundError { + def userMessage: String = "Can not find the document" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: Document) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[Document], + totalFound: Int, + lastUpdate: Option[LocalDateTime]) extends GetListReply + + type Error = GetListReply with DomainError + + case object AuthorizationError + extends GetListReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) extends GetListReply with DomainError + } + + sealed trait CreateReply + object CreateReply { + case class Created(x: Document) extends CreateReply + + type Error = CreateReply with DomainError + + case object NotFoundError + extends CreateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends CreateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends CreateReply with DomainError + } + + sealed trait UpdateReply + object UpdateReply { + case class Updated(updated: Document) extends UpdateReply + + type Error = UpdateReply with DomainError + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } + + + 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 + + case class CommonError(userMessage: String) + extends DeleteReply with DomainError + } + +} + +trait DocumentService { + + import DocumentService._ + + + def getById(id: LongId[Document]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def create(draftDocument: Document)(implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + // Update operations are validated in internal.*Command + def update(orig: Document, draft: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def delete(id: LongId[Document]) + (implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] + + def start(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def submit(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def restart(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def flag(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def resolve(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def unassign(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def archive(orig: Document) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/DocumentTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentTypeService.scala new file mode 100644 index 0000000..30fd348 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/DocumentTypeService.scala @@ -0,0 +1,26 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.DocumentType +import xyz.driver.pdsuidomain.services.DocumentTypeService.GetListReply + +import scala.concurrent.Future + +object DocumentTypeService { + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[DocumentType], totalFound: Int) extends GetListReply + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait DocumentTypeService { + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ExtractedDataService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ExtractedDataService.scala new file mode 100644 index 0000000..6cde411 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ExtractedDataService.scala @@ -0,0 +1,116 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object ExtractedDataService { + + trait DefaultNotFoundError { + def userMessage: String = "Extracted data hasn't been found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + case class RichExtractedData(extractedData: ExtractedData, + labels: List[ExtractedDataLabel]) + + object RichExtractedData { + implicit def toPhiString(x: RichExtractedData): PhiString = { + import x._ + phi"RichExtractedData(extractedData=$extractedData, labels=$labels)" + } + } + + sealed trait GetByIdReply + object GetByIdReply { + type Error = GetByIdReply with DomainError + case class Entity(x: RichExtractedData) extends GetByIdReply + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetByIdReply with DomainError + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[RichExtractedData], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait CreateReply + object CreateReply { + type Error = CreateReply with DomainError + case class Created(x: RichExtractedData) extends CreateReply + + case object AuthorizationError + extends CreateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends CreateReply with DomainError + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + case class Updated(updated: RichExtractedData) extends UpdateReply + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + } + + sealed trait DeleteReply + object DeleteReply { + type Error = DeleteReply with DomainError + case object Deleted extends DeleteReply + + case object AuthorizationError + extends DeleteReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case object NotFoundError + extends DeleteReply with DefaultNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends DeleteReply with DomainError + } +} + +trait ExtractedDataService { + + import ExtractedDataService._ + + def getById(id: LongId[ExtractedData]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def create(draftRichExtractedData: RichExtractedData) + (implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] + + def update(origRichExtractedData: RichExtractedData, + draftRichExtractedData: RichExtractedData) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def delete(id: LongId[ExtractedData]) + (implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala new file mode 100644 index 0000000..64ce3ea --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/HypothesisService.scala @@ -0,0 +1,28 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.Hypothesis + +import scala.concurrent.Future + +object HypothesisService { + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[Hypothesis], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait HypothesisService { + + import HypothesisService._ + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala new file mode 100644 index 0000000..d65c9f9 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala @@ -0,0 +1,85 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object InterventionService { + + trait DefaultNotFoundError { + def userMessage: String = "Intervention not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[InterventionWithArms], totalFound: Int) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: InterventionWithArms) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: InterventionWithArms) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + } + +} + +trait InterventionService { + + import InterventionService._ + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getById(id: LongId[Intervention]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def update(origIntervention: InterventionWithArms, + draftIntervention: InterventionWithArms) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/InterventionTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionTypeService.scala new file mode 100644 index 0000000..db7473e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionTypeService.scala @@ -0,0 +1,28 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.InterventionType + +import scala.concurrent.Future + +object InterventionTypeService { + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[InterventionType], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait InterventionTypeService { + + import InterventionTypeService._ + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/KeywordService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/KeywordService.scala new file mode 100644 index 0000000..5ee69a2 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/KeywordService.scala @@ -0,0 +1,29 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.KeywordWithLabels + +import scala.concurrent.Future + +object KeywordService { + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[KeywordWithLabels], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait KeywordService { + + import KeywordService._ + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala new file mode 100644 index 0000000..7b5aa66 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/MedicalRecordService.scala @@ -0,0 +1,137 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.LongId +import xyz.driver.pdsuicommon.error._ +import xyz.driver.pdsuidomain.entities.MedicalRecord.PdfSource +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object MedicalRecordService { + + type PdfSourceFetcher = (String, String) => Future[PdfSource] + + trait DefaultNotFoundError { + def userMessage: String = "Medical record hasn't been found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: MedicalRecord) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError extends GetByIdReply + with DomainError.NotFoundError with DefaultNotFoundError + + case class CommonError(userMessage: String) extends GetByIdReply + with DomainError + + case object AuthorizationError extends GetByIdReply + with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait GetPdfSourceReply + object GetPdfSourceReply { + type Error = GetPdfSourceReply with DomainError + + case class Entity(x: PdfSource.Channel) extends GetPdfSourceReply + + case object AuthorizationError + extends GetPdfSourceReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object NotFoundError + extends GetPdfSourceReply with DomainError.NotFoundError { + def userMessage: String = "Medical record PDF hasn't been found" + } + + case object RecordNotFoundError + extends GetPdfSourceReply with DomainError.NotFoundError with DefaultNotFoundError + + case class CommonError(userMessage: String) + extends GetPdfSourceReply with DomainError + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[MedicalRecord], totalFound: Int, lastUpdate: Option[LocalDateTime]) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait CreateReply + object CreateReply { + case class Created(x: MedicalRecord) extends CreateReply + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: MedicalRecord) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + } + + case class Settings(pdfSourceBucket: String) +} + +trait MedicalRecordService { + + import MedicalRecordService._ + + def getById(recordId: LongId[MedicalRecord]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getPdfSource(recordId: LongId[MedicalRecord]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def create(draft: MedicalRecord): Future[CreateReply] + + def update(origRecord: MedicalRecord, + draftRecord: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def start(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def submit(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def restart(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def flag(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def resolve(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def unassign(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def archive(orig: MedicalRecord) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala new file mode 100644 index 0000000..ff95ed0 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientCriterionService.scala @@ -0,0 +1,126 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object PatientCriterionService { + + case class DraftPatientCriterion(id: LongId[PatientCriterion], + eligibilityStatus: Option[FuzzyValue], + isVerified: Option[Boolean]) { + def applyTo(orig: PatientCriterion) = { + orig.copy( + eligibilityStatus = eligibilityStatus.orElse(orig.eligibilityStatus), + isVerified = isVerified.getOrElse(orig.isVerified) + ) + } + } + + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + + trait DefaultNotFoundError { + def userMessage: String = "Patient criterion not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + type Error = GetListReply with DomainError + + case class EntityList(xs: Seq[(PatientCriterion, LongId[Label], List[Arm], Boolean)], + totalFound: Int, + lastUpdate: Option[LocalDateTime]) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends GetListReply with DomainError.NotFoundError with DefaultPatientNotFoundError + + case class CommonError(userMessage: String) + extends GetListReply with DomainError + + } + + sealed trait GetByIdReply + object GetByIdReply { + type Error = GetByIdReply with DomainError + + case class Entity(x: PatientCriterion, + labelId: LongId[Label], + armList: List[Arm], + criterionIsCompound: Boolean) extends GetByIdReply + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object NotFoundError + extends GetByIdReply with DomainError.NotFoundError with DefaultNotFoundError + + case object PatientNotFoundError + extends GetByIdReply with DomainError.NotFoundError with DefaultPatientNotFoundError + + case class CommonError(userMessage: String) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x, labelId, armList, criterionIsCompound) => + phi"GetByIdReply.Entity(entity=$x, labelId=$labelId, " + + phi"armList=$armList, criterionIsCompound=$criterionIsCompound)" + } + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case object Updated extends UpdateReply + + case object AuthorizationError + extends UpdateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends UpdateReply with DomainError.NotFoundError with DefaultPatientNotFoundError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + } +} + +trait PatientCriterionService { + + import PatientCriterionService._ + + def getAll(patientId: UuidId[Patient], + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getById(patientId: UuidId[Patient], + id: LongId[PatientCriterion]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def updateList(patientId: UuidId[Patient], + draftEntities: List[DraftPatientCriterion]) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def update(origEntity: PatientCriterion, + draftEntity: PatientCriterion, + patientId: UuidId[Patient]) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientEligibleTrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientEligibleTrialService.scala new file mode 100644 index 0000000..64b4b81 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientEligibleTrialService.scala @@ -0,0 +1,137 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.domain.{LongId, UuidId} +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Arm, Trial, _} + +import scala.concurrent.Future + +object PatientEligibleTrialService { + + trait DefaultNotFoundError { + def userMessage: String = "Patient eligible trial not found" + } + + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + case class RichPatientEligibleTrial(trial: Trial, + group: PatientTrialArmGroupView, + arms: List[Arm]) + object RichPatientEligibleTrial { + implicit def toPhiString(x: RichPatientEligibleTrial): PhiString = { + phi"RichPatientEligibleTrial(group=${x.group}, trial=${x.trial}, arms=${x.arms})" + } + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[RichPatientEligibleTrial], totalFound: Int) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends GetListReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetListReply with DomainError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: RichPatientEligibleTrial) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends GetByIdReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait GetCriterionListOfGroupReply + object GetCriterionListOfGroupReply { + case class EntityList(xs: Seq[(PatientCriterion, LongId[Label], List[Arm], Boolean)], totalFound: Int) + extends GetCriterionListOfGroupReply + + type Error = GetCriterionListOfGroupReply with DomainError + + case object AuthorizationError + extends GetCriterionListOfGroupReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object NotFoundError + extends GetCriterionListOfGroupReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends GetCriterionListOfGroupReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetCriterionListOfGroupReply with DomainError + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: RichPatientEligibleTrial) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends UpdateReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } +} + +trait PatientEligibleTrialService { + + import PatientEligibleTrialService._ + + def getAll(patientId: UuidId[Patient], + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getById(patientId: UuidId[Patient], + id: LongId[PatientTrialArmGroup]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getCriterionListByGroupId(patientId: UuidId[Patient], + id: LongId[PatientTrialArmGroup]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetCriterionListOfGroupReply] + + def update(origEligibleTrialWithTrial: RichPatientEligibleTrial, + draftPatientTrialArmGroup: PatientTrialArmGroupView) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala new file mode 100644 index 0000000..dff595a --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientHypothesisService.scala @@ -0,0 +1,105 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import xyz.driver.pdsuicommon.domain.UuidId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.{Hypothesis, Patient, PatientHypothesis} + +import scala.concurrent.Future + +object PatientHypothesisService { + + trait DefaultNotFoundError { + def userMessage: String = "Patient hypothesis not found" + } + + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[PatientHypothesis], totalFound: Int) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends GetListReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetListReply with DomainError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: PatientHypothesis) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends GetByIdReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: PatientHypothesis) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends UpdateReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } +} + +trait PatientHypothesisService { + + import PatientHypothesisService._ + + def getAll(patientId: UuidId[Patient], + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getById(patientId: UuidId[Patient], + hypothesisId: UuidId[Hypothesis]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def update(origPatientHypothesis: PatientHypothesis, + draftPatientHypothesis: PatientHypothesis) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala new file mode 100644 index 0000000..8e791d4 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelEvidenceService.scala @@ -0,0 +1,70 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDate + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.{LongId, UuidId} +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object PatientLabelEvidenceService { + + case class Aggregated(evidence: PatientLabelEvidence, + date: LocalDate, + documentType: String, + providerType: String) + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: Aggregated) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case class NotFoundError(userMessage: String) extends GetByIdReply + with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetByIdReply + with DomainError + + case object AuthorizationError extends GetByIdReply + with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[Aggregated], totalFound: Int) + extends GetListReply + + type Error = GetListReply with DomainError + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends GetListReply + with DomainError + } +} + +trait PatientLabelEvidenceService { + + import PatientLabelEvidenceService._ + + def getById(patientId: UuidId[Patient], + labelId: LongId[Label], + id: LongId[PatientLabelEvidence]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(patientId: UuidId[Patient], + labelId: LongId[Label], + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelService.scala new file mode 100644 index 0000000..1d488ad --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientLabelService.scala @@ -0,0 +1,124 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object PatientLabelService { + + trait DefaultNotFoundError { + def userMessage: String = "Patient label not found" + } + + trait DefaultPatientNotFoundError { + def userMessage: String = "Patient not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[(PatientLabel, Boolean)], totalFound: Int) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends GetListReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetListReply with DomainError + } + + sealed trait GetDefiningCriteriaListReply + object GetDefiningCriteriaListReply { + case class EntityList(xs: Seq[PatientLabel], totalFound: Int) + extends GetDefiningCriteriaListReply + + case object AuthorizationError + extends GetDefiningCriteriaListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object PatientNotFoundError + extends GetDefiningCriteriaListReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case class CommonError(userMessage: String) extends GetDefiningCriteriaListReply with DomainError + } + + sealed trait GetByLabelIdReply + object GetByLabelIdReply { + case class Entity(x: PatientLabel, isVerified: Boolean) extends GetByLabelIdReply + + type Error = GetByLabelIdReply with DomainError + + case object NotFoundError + extends GetByLabelIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends GetByLabelIdReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByLabelIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) extends GetByLabelIdReply with DomainError + + implicit def toPhiString(reply: GetByLabelIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x, y) => phi"GetByIdReply.Entity($x, $y)" + } + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: PatientLabel, isVerified: Boolean) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object PatientNotFoundError + extends UpdateReply with DefaultPatientNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x, y) => phi"Updated($x, $y)" + case x: Error => DomainError.toPhiString(x) + } + } +} + +trait PatientLabelService { + + import PatientLabelService._ + + def getAll(patientId: UuidId[Patient], + filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def getDefiningCriteriaList(patientId: UuidId[Patient], + hypothesisId: UuidId[Hypothesis], + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetDefiningCriteriaListReply] + + def getByLabelIdOfPatient(patientId: UuidId[Patient], + labelId: LongId[Label]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByLabelIdReply] + + def update(origPatientLabel: PatientLabel, + draftPatientLabel: PatientLabel) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/PatientService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/PatientService.scala new file mode 100644 index 0000000..3f8b606 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/PatientService.scala @@ -0,0 +1,105 @@ +package xyz.driver.pdsuidomain.services + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain._ +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities._ + +import scala.concurrent.Future + +object PatientService { + + trait DefaultNotFoundError { + def userMessage: String = "Patient not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[Patient], totalFound: Int, lastUpdate: Option[LocalDateTime]) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: Patient) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: Patient) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } +} + +trait PatientService { + + import PatientService._ + + def getById(id: UuidId[Patient]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def unassign(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def start(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def submit(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def restart(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def flag(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def resolve(origPatient: Patient) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ProviderTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ProviderTypeService.scala new file mode 100644 index 0000000..49901a4 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ProviderTypeService.scala @@ -0,0 +1,27 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.ProviderType + +import scala.concurrent.Future + +object ProviderTypeService { + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[ProviderType], totalFound: Int) extends GetListReply + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait ProviderTypeService { + + import ProviderTypeService._ + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/ScrapedTrialsService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/ScrapedTrialsService.scala new file mode 100644 index 0000000..4417a54 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/ScrapedTrialsService.scala @@ -0,0 +1,54 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.ScrapedTrial + +import scala.concurrent.Future + +object ScrapedTrialsService { + + sealed trait GetRawTrialReply + object GetRawTrialReply { + type Error = GetRawTrialReply with DomainError + + case class TrialRawEntity(rawTrial: ScrapedTrial) extends GetRawTrialReply + + case object NotFoundError extends GetRawTrialReply with DomainError.NotFoundError { + override def userMessage: String = "Raw clinical trial not found" + } + } + + sealed trait GetRawTrialOptReply + object GetRawTrialOptReply { + case class TrialRawEntity(rawTrial: Option[ScrapedTrial]) extends GetRawTrialOptReply + } + + sealed trait GetAllRawTrialsExceptReply + object GetAllRawTrialsExceptReply { + case class MultipleRawTrials(rawTrials: Seq[ScrapedTrial]) + extends GetAllRawTrialsExceptReply + } + + sealed trait GetHtmlForReply + object GetHtmlForReply { + type TrialHtmlMap = Map[String, String] + + /** + * @param trialHtmlMap nctId -> html + */ + case class HtmlMap(trialHtmlMap: TrialHtmlMap) extends GetHtmlForReply + } +} + +trait ScrapedTrialsService { + + import ScrapedTrialsService._ + + def getRawTrial(nctId: String): Future[GetRawTrialReply] + + def getRawTrialOpt(nctId: String): Future[GetRawTrialOptReply] + + def getAllRawTrialsExcept(nctIds: Seq[String], limit: Int): Future[GetAllRawTrialsExceptReply] + + def getHtmlFor(nctIds: Set[String]): Future[GetHtmlForReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/StudyDesignService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/StudyDesignService.scala new file mode 100644 index 0000000..d086c5f --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/StudyDesignService.scala @@ -0,0 +1,28 @@ +package xyz.driver.pdsuidomain.services + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db.Sorting +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuidomain.entities.StudyDesign + +import scala.concurrent.Future + +object StudyDesignService { + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[StudyDesign], totalFound: Int) extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError { + def userMessage: String = "Access denied" + } + } +} + +trait StudyDesignService { + + import StudyDesignService._ + + def getAll(sorting: Option[Sorting] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] +} diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala new file mode 100644 index 0000000..4af4449 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala @@ -0,0 +1,132 @@ +package xyz.driver.pdsuidomain.services + + +import java.time.LocalDateTime + +import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext +import xyz.driver.pdsuicommon.db._ +import xyz.driver.pdsuicommon.domain.StringId +import xyz.driver.pdsuicommon.error.DomainError +import xyz.driver.pdsuicommon.logging._ +import xyz.driver.pdsuidomain.entities.Trial +import xyz.driver.pdsuidomain.entities.Trial.PdfSource + +import scala.concurrent.Future + +object TrialService { + + trait DefaultNotFoundError { + def userMessage: String = "Trial not found" + } + + trait DefaultAccessDeniedError { + def userMessage: String = "Access denied" + } + + sealed trait GetListReply + object GetListReply { + case class EntityList(xs: Seq[Trial], totalFound: Int, lastUpdate: Option[LocalDateTime]) + extends GetListReply + + case object AuthorizationError + extends GetListReply with DomainError.AuthorizationError with DefaultAccessDeniedError + } + + sealed trait GetByIdReply + object GetByIdReply { + case class Entity(x: Trial) extends GetByIdReply + + type Error = GetByIdReply with DomainError + + case object NotFoundError + extends GetByIdReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends GetByIdReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case class CommonError(userMessage: String)(implicit requestContext: AuthenticatedRequestContext) + extends GetByIdReply with DomainError + + implicit def toPhiString(reply: GetByIdReply): PhiString = reply match { + case x: DomainError => phi"GetByIdReply.Error($x)" + case Entity(x) => phi"GetByIdReply.Entity($x)" + } + } + + sealed trait GetPdfSourceReply + object GetPdfSourceReply { + type Error = GetPdfSourceReply with DomainError + + case class Entity(x: PdfSource) extends GetPdfSourceReply + + case object AuthorizationError + extends GetPdfSourceReply with DomainError.AuthorizationError with DefaultAccessDeniedError + + case object NotFoundError + extends GetPdfSourceReply with DomainError.NotFoundError { + def userMessage: String = "Trial's PDF hasn't been found" + } + + case object TrialNotFoundError + extends GetPdfSourceReply with DomainError.NotFoundError with DefaultNotFoundError + + case class CommonError(userMessage: String) + extends GetPdfSourceReply with DomainError + } + + sealed trait UpdateReply + object UpdateReply { + type Error = UpdateReply with DomainError + + case class Updated(updated: Trial) extends UpdateReply + + case object NotFoundError + extends UpdateReply with DefaultNotFoundError with DomainError.NotFoundError + + case object AuthorizationError + extends UpdateReply with DefaultAccessDeniedError with DomainError.AuthorizationError + + case class CommonError(userMessage: String) + extends UpdateReply with DomainError + + implicit def toPhiString(reply: UpdateReply): PhiString = reply match { + case Updated(x) => phi"Updated($x)" + case x: Error => DomainError.toPhiString(x) + } + } +} + +trait TrialService { + + import TrialService._ + + def getById(id: StringId[Trial]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetByIdReply] + + def getPdfSource(trialId: StringId[Trial]) + (implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply] + + def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty, + sorting: Option[Sorting] = None, + pagination: Option[Pagination] = None) + (implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] + + def update(origTrial: Trial, draftTrial: Trial) + (implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def start(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def submit(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def restart(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def flag(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def resolve(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def archive(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def unassign(origTrial: Trial)(implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply] + + def removeTrialDetails(trialId: StringId[Trial]): Unit +} -- cgit v1.2.3