aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkseniya <ktomskih@datamonsters.co>2017-09-20 18:01:15 +0700
committerkseniya <ktomskih@datamonsters.co>2017-09-20 18:01:15 +0700
commit9968eaefa2a97ebe495fa51b640e31c78db61ac6 (patch)
tree4eed12a4ebb2829e336a3da673c7c8462e7ab845
parentd5ecec043a3d70dd09bda8a79fcd188f411b47df (diff)
parentd4b18efda238f506103dddbf3b400ae17c797276 (diff)
downloadrest-query-9968eaefa2a97ebe495fa51b640e31c78db61ac6.tar.gz
rest-query-9968eaefa2a97ebe495fa51b640e31c78db61ac6.tar.bz2
rest-query-9968eaefa2a97ebe495fa51b640e31c78db61ac6.zip
Merge branch 'master' into slick-query-builder
-rw-r--r--build.sbt9
-rw-r--r--project/build.properties2
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala5
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala73
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala108
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala121
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala30
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala29
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala3
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala152
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala64
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala55
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/utils/Utils.scala20
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala4
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala3
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala36
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala137
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala16
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala8
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documentissue.scala13
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documenttype.scala13
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala26
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/providerttype.scala12
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/recordissue.scala13
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/trialissue/ApiTrialIssue.scala20
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala24
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala5
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala14
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/Mocks.scala1
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/parsers/PaginationParserSuite.scala95
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala207
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/parsers/SortingParserSuite.scala91
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/parsers/TestUtils.scala52
-rw-r--r--src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/DocumentIssueFormatSuite.scala4
-rw-r--r--src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala12
-rw-r--r--src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordIssueFormatSuite.scala4
36 files changed, 1383 insertions, 98 deletions
diff --git a/build.sbt b/build.sbt
index 7778e02..837a1f0 100644
--- a/build.sbt
+++ b/build.sbt
@@ -10,6 +10,7 @@ lazy val core = (project in file("."))
.settings(libraryDependencies ++= Seq(
"com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.8.3",
"com.github.pureconfig" %% "pureconfig" % "0.7.2",
+ "com.lihaoyi" %% "fastparse" % "0.3.7",
"com.typesafe.akka" %% "akka-http" % "10.0.9",
"com.typesafe.play" %% "play" % "2.5.15",
"com.typesafe.scala-logging" %% "scala-logging" % "3.5.0",
@@ -17,8 +18,8 @@ lazy val core = (project in file("."))
"io.github.cloudify" %% "spdf" % "1.4.0",
"org.davidbild" %% "tristate-core" % "0.2.0",
"org.davidbild" %% "tristate-play" % "0.2.0" exclude ("com.typesafe.play", "play-json"),
- "xyz.driver" %% "core" % "0.16.3",
- "xyz.driver" %% "domain-model" % "0.12.5",
+ "xyz.driver" %% "core" % "0.16.7",
+ "xyz.driver" %% "domain-model" % "0.12.26",
"ch.qos.logback" % "logback-classic" % "1.1.7",
"com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.8.4",
"com.github.spullara.mustache.java" % "scala-extensions-2.11" % "0.9.4",
@@ -28,6 +29,6 @@ lazy val core = (project in file("."))
"org.asynchttpclient" % "async-http-client" % "2.0.24",
"org.slf4j" % "slf4j-api" % "1.7.21",
"ai.x" %% "diff" % "1.2.0-get-simple-name-fix" % "test",
- "org.scalatest" %% "scalatest" % "3.0.0" % "test",
- "xyz.driver" %% "core" % "0.16.3" excludeAll (ExclusionRule(organization = "io.netty"))
+ "org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
+ "org.scalatest" %% "scalatest" % "3.0.1" % "test"
))
diff --git a/project/build.properties b/project/build.properties
index 6561361..3507256 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1,4 +1,4 @@
#Activator-generated Properties
#Wed Jul 06 16:08:49 PDT 2016
template.uuid=a675a7df-bee3-48df-9eaa-688d99e5814e
-sbt.version=0.13.15
+sbt.version=0.13.16
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala
index c547bf4..9d2664d 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala
@@ -28,10 +28,7 @@ object MySqlContext extends PhiLogging {
connectionParams: String,
url: String)
- final case class Settings(credentials: DbCredentials,
- connection: Config,
- connectionAttemptsOnStartup: Int,
- threadPoolSize: Int)
+ final case class Settings(credentials: DbCredentials, connection: Config, threadPoolSize: Int)
def apply(settings: Settings): MySqlContext = {
// Prevent leaking credentials to a log
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala
new file mode 100644
index 0000000..7bdfd1b
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala
@@ -0,0 +1,73 @@
+package xyz.driver.pdsuicommon.db
+
+import java.io.Closeable
+import java.time._
+import java.util.UUID
+import java.util.concurrent.Executors
+import javax.sql.DataSource
+
+import io.getquill._
+import xyz.driver.pdsuicommon.concurrent.MdcExecutionContext
+import xyz.driver.pdsuicommon.db.PostgresContext.Settings
+import xyz.driver.pdsuicommon.domain.UuidId
+import xyz.driver.pdsuicommon.logging._
+
+import scala.concurrent.ExecutionContext
+import scala.util.control.NonFatal
+import scala.util.{Failure, Success, Try}
+
+object PostgresContext extends PhiLogging {
+
+ final case class Settings(connection: com.typesafe.config.Config,
+ connectionAttemptsOnStartup: Int,
+ threadPoolSize: Int)
+
+ def apply(settings: Settings): PostgresContext = {
+ // Prevent leaking credentials to a log
+ Try(JdbcContextConfig(settings.connection).dataSource) match {
+ case Success(dataSource) => new PostgresContext(dataSource, settings)
+ case Failure(NonFatal(e)) =>
+ logger.error(phi"Can not load dataSource, error: ${Unsafe(e.getClass.getName)}")
+ throw new IllegalArgumentException("Can not load dataSource from config. Check your database and config", e)
+ }
+ }
+
+}
+
+class PostgresContext(val dataSource: DataSource with Closeable, settings: Settings)
+ extends PostgresJdbcContext[SnakeCase](dataSource) with TransactionalContext
+ with EntityExtractorDerivation[SnakeCase] {
+
+ private val tpe = Executors.newFixedThreadPool(settings.threadPoolSize)
+
+ implicit val executionContext: ExecutionContext = {
+ val orig = ExecutionContext.fromExecutor(tpe)
+ MdcExecutionContext.from(orig)
+ }
+
+ override def close(): Unit = {
+ super.close()
+ tpe.shutdownNow()
+ }
+
+ /**
+ * Usable for QueryBuilder's extractors
+ */
+ def timestampToLocalDateTime(timestamp: java.sql.Timestamp): LocalDateTime = {
+ LocalDateTime.ofInstant(timestamp.toInstant, ZoneOffset.UTC)
+ }
+
+ implicit def encodeUuidId[T] = MappedEncoding[UuidId[T], String](_.toString)
+ implicit def decodeUuidId[T] = MappedEncoding[String, UuidId[T]] { uuid =>
+ UuidId[T](UUID.fromString(uuid))
+ }
+
+ def decodeOptUuidId[T] = MappedEncoding[Option[String], Option[UuidId[T]]] {
+ case Some(x) => Option(x).map(y => UuidId[T](UUID.fromString(y)))
+ case None => None
+ }
+
+ implicit def decodeUuid[T] = MappedEncoding[String, UUID] { uuid =>
+ UUID.fromString(uuid)
+ }
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala
new file mode 100644
index 0000000..0ddf811
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala
@@ -0,0 +1,108 @@
+package xyz.driver.pdsuicommon.db
+
+import java.sql.ResultSet
+
+import io.getquill.{PostgresDialect, PostgresEscape}
+import xyz.driver.pdsuicommon.db.PostgresQueryBuilder.SmartPostgresEscape
+
+import scala.collection.breakOut
+
+object PostgresQueryBuilder {
+
+ import xyz.driver.pdsuicommon.db.QueryBuilder._
+
+ trait SmartPostgresEscape extends PostgresEscape {
+ override def column(s: String): String =
+ if (s.startsWith("$")) s else super.column(s)
+ override def default(s: String): String =
+ s.split("\\.").map(ss => s""""$ss"""").mkString(".")
+ }
+
+ object SmartPostgresEscape extends SmartPostgresEscape
+
+ type Escape = SmartPostgresEscape
+ val Escape = SmartPostgresEscape
+
+ def apply[T](tableName: String,
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[TableLink],
+ runner: Runner[T],
+ countRunner: CountRunner): PostgresQueryBuilder[T] = {
+ val parameters = PostgresQueryBuilderParameters(
+ tableData = TableData(tableName, lastUpdateFieldName, nullableFields),
+ links = links.map(x => x.foreignTableName -> x)(breakOut)
+ )
+ new PostgresQueryBuilder[T](parameters)(runner, countRunner)
+ }
+
+ def apply[T](tableName: String,
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[TableLink],
+ extractor: ResultSet => T)(implicit sqlContext: PostgresContext): PostgresQueryBuilder[T] = {
+ apply(tableName, QueryBuilderParameters.AllFields, lastUpdateFieldName, nullableFields, links, extractor)
+ }
+
+ def apply[T](tableName: String,
+ fields: Set[String],
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[TableLink],
+ extractor: ResultSet => T)(implicit sqlContext: PostgresContext): PostgresQueryBuilder[T] = {
+
+ val runner: Runner[T] = { parameters =>
+ val (sql, binder) = parameters.toSql(countQuery = false, fields = fields, namingStrategy = SmartPostgresEscape)
+ sqlContext.executeQuery[T](sql, binder, { resultSet =>
+ extractor(resultSet)
+ })
+ }
+
+ val countRunner: CountRunner = { parameters =>
+ val (sql, binder) = parameters.toSql(countQuery = true, namingStrategy = SmartPostgresEscape)
+ sqlContext
+ .executeQuery[CountResult](
+ sql,
+ binder, { resultSet =>
+ val count = resultSet.getInt(1)
+ val lastUpdate = if (parameters.tableData.lastUpdateFieldName.isDefined) {
+ Option(resultSet.getTimestamp(2)).map(sqlContext.timestampToLocalDateTime)
+ } else None
+
+ (count, lastUpdate)
+ }
+ )
+ .head
+ }
+
+ apply[T](
+ tableName = tableName,
+ lastUpdateFieldName = lastUpdateFieldName,
+ nullableFields = nullableFields,
+ links = links,
+ runner = runner,
+ countRunner = countRunner
+ )
+ }
+}
+
+class PostgresQueryBuilder[T](parameters: PostgresQueryBuilderParameters)(implicit runner: QueryBuilder.Runner[T],
+ countRunner: QueryBuilder.CountRunner)
+ extends QueryBuilder[T, PostgresDialect, PostgresQueryBuilder.Escape](parameters) {
+
+ def withFilter(newFilter: SearchFilterExpr): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = {
+ new PostgresQueryBuilder[T](parameters.copy(filter = newFilter))
+ }
+
+ def withSorting(newSorting: Sorting): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = {
+ new PostgresQueryBuilder[T](parameters.copy(sorting = newSorting))
+ }
+
+ def withPagination(newPagination: Pagination): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = {
+ new PostgresQueryBuilder[T](parameters.copy(pagination = Some(newPagination)))
+ }
+
+ def resetPagination: QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = {
+ new PostgresQueryBuilder[T](parameters.copy(pagination = None))
+ }
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala
new file mode 100644
index 0000000..e9a4132
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala
@@ -0,0 +1,121 @@
+package xyz.driver.pdsuicommon.http
+
+import akka.http.scaladsl.server._
+import akka.http.scaladsl.server.Directives._
+import akka.http.scaladsl.model._
+import xyz.driver.core.rest.ContextHeaders
+import xyz.driver.entities.users.AuthUserInfo
+import xyz.driver.pdsuicommon.auth._
+import xyz.driver.pdsuicommon.error._
+import xyz.driver.pdsuicommon.error.DomainError._
+import xyz.driver.pdsuicommon.error.ErrorsResponse.ResponseError
+import xyz.driver.pdsuicommon.parsers._
+import xyz.driver.pdsuicommon.db.{Pagination, Sorting, SearchFilterExpr}
+import xyz.driver.pdsuicommon.domain._
+import xyz.driver.pdsuicommon.serialization.PlayJsonSupport._
+import xyz.driver.core.rest.AuthProvider
+import scala.util.control._
+
+import scala.util._
+
+trait Directives {
+
+ val paginated: Directive1[Pagination] = parameterSeq.flatMap { params =>
+ PaginationParser.parse(params) match {
+ case Success(pagination) => provide(pagination)
+ case Failure(ex) =>
+ reject(ValidationRejection("invalid pagination parameter", Some(ex)))
+ }
+ }
+
+ def sorted(validDimensions: Set[String] = Set.empty): Directive1[Sorting] = parameterSeq.flatMap { params =>
+ SortingParser.parse(validDimensions, params) match {
+ case Success(sorting) => provide(sorting)
+ case Failure(ex) =>
+ reject(ValidationRejection("invalid sorting parameter", Some(ex)))
+ }
+ }
+
+ val dimensioned: Directive1[Dimensions] = parameterSeq.flatMap { params =>
+ DimensionsParser.tryParse(params) match {
+ case Success(dims) => provide(dims)
+ case Failure(ex) =>
+ reject(ValidationRejection("invalid dimension parameter", Some(ex)))
+ }
+ }
+
+ val searchFiltered: Directive1[SearchFilterExpr] = parameterSeq.flatMap { params =>
+ SearchFilterParser.parse(params) match {
+ case Success(sorting) => provide(sorting)
+ case Failure(ex) =>
+ reject(ValidationRejection("invalid filter parameter", Some(ex)))
+ }
+ }
+
+ def StringIdInPath[T]: PathMatcher1[StringId[T]] =
+ PathMatchers.Segment.map((id) => StringId(id.toString))
+
+ def LongIdInPath[T]: PathMatcher1[LongId[T]] =
+ PathMatchers.LongNumber.map((id) => LongId(id))
+
+ def UuidIdInPath[T]: PathMatcher1[UuidId[T]] =
+ PathMatchers.JavaUUID.map((id) => UuidId(id))
+
+ def failFast[A](reply: A): A = reply match {
+ case err: NotFoundError => throw new NotFoundException(err.getMessage)
+ case err: AuthenticationError => throw new AuthenticationException(err.getMessage)
+ case err: AuthorizationError => throw new AuthorizationException(err.getMessage)
+ case err: DomainError => throw new DomainException(err.getMessage)
+ case other => other
+ }
+
+ def domainExceptionHandler(req: RequestId): ExceptionHandler = {
+ def errorResponse(ex: Throwable) =
+ ErrorsResponse(Seq(ResponseError(None, ex.getMessage, ErrorCode.Unspecified)), req)
+ ExceptionHandler {
+ case ex: AuthenticationException => complete(StatusCodes.Unauthorized -> errorResponse(ex))
+ case ex: AuthorizationException => complete(StatusCodes.Forbidden -> errorResponse(ex))
+ case ex: NotFoundException => complete(StatusCodes.NotFound -> errorResponse(ex))
+ case ex: DomainException => complete(StatusCodes.BadRequest -> errorResponse(ex))
+ case NonFatal(ex) => complete(StatusCodes.InternalServerError -> errorResponse(ex))
+ }
+ }
+
+ def domainRejectionHandler(req: RequestId): RejectionHandler = {
+ def wrapContent(message: String) = {
+ import play.api.libs.json._
+ val err = ErrorsResponse(Seq(ResponseError(None, message, ErrorCode.Unspecified)), req)
+ val text = Json.stringify(implicitly[Writes[ErrorsResponse]].writes(err))
+ HttpEntity(ContentTypes.`application/json`, text)
+ }
+ RejectionHandler.default.mapRejectionResponse {
+ case res @ HttpResponse(_, _, ent: HttpEntity.Strict, _) =>
+ res.copy(entity = wrapContent(ent.data.utf8String))
+ case x => x // pass through all other types of responses
+ }
+ }
+
+ val tracked: Directive1[RequestId] = optionalHeaderValueByName(ContextHeaders.TrackingIdHeader) flatMap {
+ case Some(id) => provide(RequestId(id))
+ case None => provide(RequestId())
+ }
+
+ val domainResponse: Directive0 = tracked.flatMap { id =>
+ handleExceptions(domainExceptionHandler(id)) & handleRejections(domainRejectionHandler(id))
+ }
+
+ implicit class AuthProviderWrapper(provider: AuthProvider[AuthUserInfo]) {
+ val authenticated: Directive1[AuthenticatedRequestContext] = (provider.authorize() & tracked) tflatMap {
+ case (core, requestId) =>
+ provide(
+ new AuthenticatedRequestContext(
+ core.authenticatedUser,
+ requestId,
+ core.contextHeaders(ContextHeaders.AuthenticationTokenHeader)
+ ))
+ }
+ }
+
+}
+
+object Directives extends Directives
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala
new file mode 100644
index 0000000..17c09ed
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala
@@ -0,0 +1,30 @@
+package xyz.driver.pdsuicommon.parsers
+
+import scala.util.{Failure, Success, Try}
+
+class Dimensions(private val xs: Set[String] = Set.empty) {
+ def contains(x: String): Boolean = xs.isEmpty || xs.contains(x)
+}
+
+object DimensionsParser {
+
+ @deprecated("play-akka transition", "0")
+ def tryParse(query: Map[String, Seq[String]]): Try[Dimensions] =
+ tryParse(query.toSeq.flatMap {
+ case (key, values) =>
+ values.map(value => key -> value)
+ })
+
+ def tryParse(query: Seq[(String, String)]): Try[Dimensions] = {
+ query.collect { case ("dimensions", value) => value } match {
+ case Nil => Success(new Dimensions())
+
+ case x +: Nil =>
+ val raw: Set[String] = x.split(",").view.map(_.trim).filter(_.nonEmpty).to[Set]
+ Success(new Dimensions(raw))
+
+ case xs =>
+ Failure(new IllegalArgumentException(s"Dimensions are specified ${xs.size} times"))
+ }
+ }
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala
new file mode 100644
index 0000000..b59b1a5
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala
@@ -0,0 +1,29 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.db._
+import scala.util._
+
+object PaginationParser {
+
+ @deprecated("play-akka transition", "0")
+ def parse(query: Map[String, Seq[String]]): Try[Pagination] =
+ parse(query.toSeq.flatMap {
+ case (key, values) =>
+ values.map(value => key -> value)
+ })
+
+ def parse(query: Seq[(String, String)]): Try[Pagination] = {
+ val IntString = """(\d+)""".r
+ def validate(field: String, default: Int) = query.collectFirst { case (`field`, size) => size } match {
+ case Some(IntString(x)) if x.toInt > 0 => x.toInt
+ case Some(IntString(x)) => throw new ParseQueryArgException((field, s"must greater than zero (found $x)"))
+ case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)"))
+ case None => default
+ }
+
+ Try {
+ Pagination(validate("pageSize", Pagination.Default.pageSize),
+ validate("pageNumber", Pagination.Default.pageNumber))
+ }
+ }
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala
new file mode 100644
index 0000000..64b3d2e
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala
@@ -0,0 +1,3 @@
+package xyz.driver.pdsuicommon.parsers
+
+class ParseQueryArgException(val errors: (String, String)*) extends Exception(errors.mkString(","))
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
new file mode 100644
index 0000000..8aff397
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
@@ -0,0 +1,152 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.utils.Implicits.{toCharOps, toStringOps}
+import fastparse.all._
+import fastparse.core.Parsed
+import fastparse.parsers.Intrinsics.CharPred
+import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation}
+import xyz.driver.pdsuicommon.utils.Utils._
+
+import scala.util.Try
+
+@SuppressWarnings(Array("org.wartremover.warts.Product", "org.wartremover.warts.Serializable"))
+object SearchFilterParser {
+
+ private object BinaryAtomFromTuple {
+ def unapply(input: (SearchFilterExpr.Dimension, (String, String))): Option[SearchFilterExpr.Atom.Binary] = {
+ val (dimensionName, (strOperation, value)) = input
+ parseOperation(strOperation.toLowerCase).map { op =>
+ SearchFilterExpr.Atom.Binary(dimensionName, op, value.safeTrim)
+ }
+ }
+ }
+
+ private object NAryAtomFromTuple {
+ // Compiler warning: unchecked since it is eliminated by erasure, if we user Seq[String]
+ def unapply(input: (SearchFilterExpr.Dimension, (String, Seq[_]))): Option[SearchFilterExpr.Atom.NAry] = {
+ val (dimensionName, (strOperation, xs)) = input
+ if (strOperation.toLowerCase == "in") {
+ val values = xs.asInstanceOf[Seq[String]].map(_.safeTrim)
+ Some(SearchFilterExpr.Atom.NAry(dimensionName, SearchFilterNAryOperation.In, values))
+ } else {
+ None
+ }
+ }
+ }
+
+ private val operationsMapping = {
+ import xyz.driver.pdsuicommon.db.SearchFilterBinaryOperation._
+
+ Map[String, SearchFilterBinaryOperation](
+ "eq" -> Eq,
+ "noteq" -> NotEq,
+ "like" -> Like,
+ "gt" -> Gt,
+ "gteq" -> GtEq,
+ "lt" -> Lt,
+ "lteq" -> LtEq
+ )
+ }
+
+ private def parseOperation(x: String): Option[SearchFilterBinaryOperation] = operationsMapping.get(x)
+
+ private val whitespaceParser = P(CharPred(_.isSafeWhitespace))
+
+ val dimensionParser: Parser[SearchFilterExpr.Dimension] = {
+ val identParser = P(
+ CharPred(c => c.isLetterOrDigit)
+ .rep(min = 1)).!.map(s => SearchFilterExpr.Dimension(None, toSnakeCase(s)))
+ val pathParser = P(identParser.! ~ "." ~ identParser.!) map {
+ case (left, right) =>
+ SearchFilterExpr.Dimension(Some(toSnakeCase(left)), toSnakeCase(right))
+ }
+ P(pathParser | identParser)
+ }
+
+ private val commonOperatorParser: Parser[String] = {
+ P(IgnoreCase("eq") | IgnoreCase("like") | IgnoreCase("noteq")).!
+ }
+
+ private val numericOperatorParser: Parser[String] = {
+ P((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?).!
+ }
+
+ private val naryOperatorParser: Parser[String] = P(IgnoreCase("in")).!
+
+ private val isPositiveParser: Parser[Boolean] = P(CharIn("-+").!.?).map {
+ case Some("-") => false
+ case _ => true
+ }
+
+ // Exclude Unicode "digits"
+ private val digitsParser: Parser[String] = P(CharIn('0' to '9').rep(min = 1).!)
+
+ // @TODO Make complex checking here
+ private val numberParser: Parser[String] = P(isPositiveParser ~ digitsParser.! ~ ("." ~ digitsParser).!.?).map {
+ case (false, intPart, Some(fracPart)) => s"-$intPart.${fracPart.tail}"
+ case (false, intPart, None) => s"-$intPart"
+ case (_, intPart, Some(fracPart)) => s"$intPart.${fracPart.tail}"
+ case (_, intPart, None) => s"$intPart"
+ }
+
+ private val nAryValueParser: Parser[String] = P(CharPred(_ != ',').rep(min = 1).!)
+
+ private val binaryAtomParser: Parser[SearchFilterExpr.Atom.Binary] = P(
+ dimensionParser ~ whitespaceParser ~ (
+ (commonOperatorParser.! ~/ whitespaceParser ~/ AnyChar.rep(min = 1).!)
+ | (numericOperatorParser.! ~/ whitespaceParser ~/ numberParser.!)
+ ) ~ End
+ ).map {
+ case BinaryAtomFromTuple(atom) => atom
+ }
+
+ private val nAryAtomParser: Parser[SearchFilterExpr.Atom.NAry] = P(
+ dimensionParser ~ whitespaceParser ~ (
+ naryOperatorParser ~/ whitespaceParser ~/ nAryValueParser.!.rep(min = 1, sep = ",")
+ ) ~ End
+ ).map {
+ case NAryAtomFromTuple(atom) => atom
+ }
+
+ private val atomParser: Parser[SearchFilterExpr.Atom] = P(binaryAtomParser | nAryAtomParser)
+
+ @deprecated("play-akka transition", "0")
+ def parse(query: Map[String, Seq[String]]): Try[SearchFilterExpr] =
+ parse(query.toSeq.flatMap {
+ case (key, values) =>
+ values.map(value => key -> value)
+ })
+
+ def parse(query: Seq[(String, String)]): Try[SearchFilterExpr] = Try {
+ query.toList.collect { case ("filters", value) => value } match {
+ case Nil => SearchFilterExpr.Empty
+
+ case head :: Nil =>
+ atomParser.parse(head) match {
+ case Parsed.Success(x, _) => x
+ case e: Parsed.Failure => throw new ParseQueryArgException("filters" -> formatFailure(1, e))
+ }
+
+ case xs =>
+ val parsed = xs.map(x => atomParser.parse(x))
+ val failures: Seq[String] = parsed.zipWithIndex.collect {
+ case (e: Parsed.Failure, index) => formatFailure(index, e)
+ }
+
+ if (failures.isEmpty) {
+ val filters = parsed.collect {
+ case Parsed.Success(x, _) => x
+ }
+
+ SearchFilterExpr.Intersection.create(filters: _*)
+ } else {
+ throw new ParseQueryArgException("filters" -> failures.mkString("; "))
+ }
+ }
+ }
+
+ private def formatFailure(sectionIndex: Int, e: Parsed.Failure): String = {
+ s"section $sectionIndex: ${ParseError.msg(e.extra.input, e.extra.traced.expected, e.index)}"
+ }
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala
new file mode 100644
index 0000000..4bfc669
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala
@@ -0,0 +1,64 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.db.{Sorting, SortingOrder}
+import fastparse.all._
+import fastparse.core.Parsed
+import xyz.driver.pdsuicommon.utils.Utils._
+
+import scala.util.Try
+
+object SortingParser {
+
+ private val sortingOrderParser: Parser[SortingOrder] = P("-".!.?).map {
+ case Some(_) => SortingOrder.Descending
+ case None => SortingOrder.Ascending
+ }
+
+ private def dimensionSortingParser(validDimensions: Seq[String]): Parser[Sorting.Dimension] = {
+ P(sortingOrderParser ~ StringIn(validDimensions: _*).!).map {
+ case (sortingOrder, field) =>
+ val prefixedFields = field.split("\\.", 2)
+ prefixedFields.size match {
+ case 1 => Sorting.Dimension(None, toSnakeCase(field), sortingOrder)
+ case 2 =>
+ Sorting.Dimension(Some(prefixedFields.head).map(toSnakeCase),
+ toSnakeCase(prefixedFields.last),
+ sortingOrder)
+ }
+ }
+ }
+
+ private def sequentialSortingParser(validDimensions: Seq[String]): Parser[Sorting.Sequential] = {
+ P(dimensionSortingParser(validDimensions).rep(min = 1, sep = ",") ~ End).map { dimensions =>
+ Sorting.Sequential(dimensions)
+ }
+ }
+
+ @deprecated("play-akka transition", "0")
+ def parse(validDimensions: Set[String], query: Map[String, Seq[String]]): Try[Sorting] =
+ parse(validDimensions, query.toSeq.flatMap {
+ case (key, values) =>
+ values.map(value => key -> value)
+ })
+
+ def parse(validDimensions: Set[String], query: Seq[(String, String)]): Try[Sorting] = Try {
+ query.toList.collect { case ("sort", value) => value } match {
+ case Nil => Sorting.Sequential(Seq.empty)
+
+ case rawSorting :: Nil =>
+ val parser = sequentialSortingParser(validDimensions.toSeq)
+ parser.parse(rawSorting) match {
+ case Parsed.Success(x, _) => x
+ case e: Parsed.Failure =>
+ throw new ParseQueryArgException("sort" -> formatFailure(e))
+ }
+
+ case _ => throw new ParseQueryArgException("sort" -> "multiple sections are not allowed")
+ }
+ }
+
+ private def formatFailure(e: Parsed.Failure): String = {
+ ParseError.msg(e.extra.input, e.extra.traced.expected, e.index)
+ }
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala
new file mode 100644
index 0000000..c1a2c7c
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala
@@ -0,0 +1,55 @@
+package xyz.driver.pdsuicommon.utils
+
+import java.time.{LocalDate, LocalDateTime}
+
+import io.swagger.models.properties.Property
+import spray.json.JsValue
+import xyz.driver.pdsuicommon.domain.{LongId, StringId, UuidId}
+import xyz.driver.pdsuidomain.entities._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.arm._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.criterion._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.intervention._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.hypothesis._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.studydesign._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.trial._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.trialhistory._
+import xyz.driver.pdsuidomain.formats.json.sprayformats.trialissue._
+import xyz.driver.core.swagger.CustomSwaggerJsonConverter._
+import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion
+
+object CustomSwaggerJsonFormats {
+
+ val customCommonProperties = Map[Class[_], Property](
+ classOf[LocalDateTime] -> stringProperty(example = Some("2010-12-31'T'18:59:59Z")),
+ classOf[LocalDate] -> stringProperty(example = Some("2010-12-31")),
+ classOf[UuidId[_]] -> stringProperty(example = Some("370b0450-35cb-4aab-ba74-0145be75add5")),
+ classOf[StringId[_]] -> stringProperty(),
+ classOf[LongId[_]] -> stringProperty()
+ )
+ val customTrialCurationProperties = Map[Class[_], Property](
+ classOf[Trial.Status] -> stringProperty(),
+ classOf[Trial.Condition] -> stringProperty(),
+ classOf[TrialHistory.Action] -> stringProperty(),
+ classOf[TrialHistory.State] -> stringProperty()
+ ) ++ customCommonProperties
+
+ val customTrialCurationObjectsExamples = Map[Class[_], JsValue](
+ classOf[Trial] -> trialWriter.write(xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextTrial()),
+ classOf[Arm] -> armFormat.write(xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextArm()),
+ classOf[TrialHistory] -> trialHistoryFormat.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextTrialHistory()),
+ classOf[TrialIssue] -> trialIssueWriter.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextTrialIssue()),
+ classOf[RichCriterion] -> richCriterionFormat.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextRichCriterion()),
+ classOf[InterventionWithArms] -> interventionWriter.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextInterventionWithArms()),
+ classOf[InterventionType] -> interventionTypeFormat.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextInterventionType()),
+ classOf[Hypothesis] -> hypothesisFormat.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextHypothesis()),
+ classOf[StudyDesign] -> studyDesignFormat.write(
+ xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextStudyDesign())
+ )
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/Utils.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/Utils.scala
index 02c9e28..63b0572 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/utils/Utils.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/utils/Utils.scala
@@ -1,6 +1,7 @@
package xyz.driver.pdsuicommon.utils
import java.time.LocalDateTime
+import java.util.regex.{Matcher, Pattern}
object Utils {
@@ -20,4 +21,23 @@ object Utils {
fullClassName.substring(fullClassName.lastIndexOf("$") + 1)
}
}
+
+ def toSnakeCase(str: String): String =
+ str
+ .replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2")
+ .replaceAll("([a-z\\d])([A-Z])", "$1_$2")
+ .toLowerCase
+
+ def toCamelCase(str: String): String = {
+ val sb = new StringBuffer()
+ def loop(m: Matcher): Unit = if (m.find()) {
+ m.appendReplacement(sb, m.group(1).toUpperCase())
+ loop(m)
+ }
+ val m: Matcher = Pattern.compile("_(.)").matcher(str)
+ loop(m)
+ m.appendTail(sb)
+ sb.toString
+ }
+
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
index e691547..cb677cf 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
@@ -27,8 +27,8 @@ final case class Intervention(id: LongId[Intervention],
originalName: String,
typeId: Option[LongId[InterventionType]],
originalType: Option[String],
- description: String,
- originalDescription: String,
+ dosage: String,
+ originalDosage: String,
isActive: Boolean)
object Intervention {
diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala
index 2f90820..db4def2 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Trial.scala
@@ -1,6 +1,5 @@
package xyz.driver.pdsuidomain.entities
-import java.nio.file.Path
import java.time.LocalDateTime
import xyz.driver.pdsuicommon.domain.{LongId, StringId, User, UuidId}
@@ -52,8 +51,6 @@ object Trial {
implicit def toPhiString(x: Status): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass))
}
- final case class PdfSource(path: Path) extends AnyVal
-
implicit def toPhiString(x: Trial): PhiString = {
import x._
phi"Trial(id=$id, externalId=$externalId, status=$status, previousStatus=$previousStatus, " +
diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala
new file mode 100644
index 0000000..b259b07
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala
@@ -0,0 +1,36 @@
+package xyz.driver.pdsuidomain.fakes.entities
+
+import java.time.{LocalDate, LocalDateTime, LocalTime}
+
+import xyz.driver.pdsuicommon.domain.{LongId, StringId, UuidId}
+import xyz.driver.pdsuidomain.entities.{Trial, TrialHistory}
+import scala.util.Random
+
+object common {
+ import xyz.driver.core.generators
+
+ def nextUuidId[T] = UuidId[T](generators.nextUuid())
+
+ def nextLongId[T] = LongId[T](generators.nextInt(Int.MaxValue).toLong)
+
+ def nextStringId[T] = StringId[T](generators.nextString(maxLength = 20))
+
+ def nextTrialStatus = generators.oneOf[Trial.Status](Trial.Status.All)
+
+ def nextPreviousTrialStatus = generators.oneOf[Trial.Status](Trial.Status.AllPrevious)
+
+ def nextLocalDateTime = LocalDateTime.of(nextLocalDate, LocalTime.MIDNIGHT)
+
+ def nextLocalDate = LocalDate.of(
+ 1970 + Random.nextInt(68),
+ 1 + Random.nextInt(12),
+ 1 + Random.nextInt(28) // all months have at least 28 days
+ )
+
+ def nextCondition = generators.oneOf[Trial.Condition](Trial.Condition.All)
+
+ def nextTrialAction = generators.oneOf[TrialHistory.Action](TrialHistory.Action.All)
+
+ def nextTrialState = generators.oneOf[TrialHistory.State](TrialHistory.State.All)
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala
new file mode 100644
index 0000000..ecb6e0a
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala
@@ -0,0 +1,137 @@
+package xyz.driver.pdsuidomain.fakes.entities
+
+import xyz.driver.pdsuicommon.domain.{LongId, User}
+import xyz.driver.pdsuidomain.entities._
+import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion
+
+object trialcuration {
+ import xyz.driver.core.generators
+ import common._
+
+ def nextTrial(): Trial = Trial(
+ id = nextStringId[Trial],
+ externalId = nextUuidId[Trial],
+ status = nextTrialStatus,
+ assignee = Option(nextStringId[User]),
+ previousStatus = Option(nextPreviousTrialStatus),
+ previousAssignee = Option(nextStringId[User]),
+ lastActiveUserId = Option(nextStringId[User]),
+ lastUpdate = nextLocalDateTime,
+ condition = nextCondition,
+ phase = generators.nextString(),
+ hypothesisId = Option(nextUuidId[Hypothesis]),
+ studyDesignId = Option(nextLongId[StudyDesign]),
+ originalStudyDesign = Option(generators.nextString()),
+ isPartner = generators.nextBoolean(),
+ overview = Option(generators.nextString()),
+ overviewTemplate = generators.nextString(),
+ isUpdated = generators.nextBoolean(),
+ title = generators.nextString(),
+ originalTitle = generators.nextString()
+ )
+
+ def nextArm(): Arm = Arm(
+ id = nextLongId[Arm],
+ name = generators.nextString(),
+ originalName = generators.nextString(),
+ trialId = nextStringId[Trial],
+ deleted = Option(nextLocalDateTime)
+ )
+
+ def nextCriterion(): Criterion = Criterion(
+ id = nextLongId[Criterion],
+ trialId = nextStringId[Trial],
+ text = Option(generators.nextString()),
+ isCompound = generators.nextBoolean(),
+ meta = generators.nextString()
+ )
+
+ def nextCriterionLabel(criterionId: LongId[Criterion]): CriterionLabel = CriterionLabel(
+ id = nextLongId[CriterionLabel],
+ labelId = Option(nextLongId[Label]),
+ criterionId = criterionId,
+ categoryId = Option(nextLongId[Category]),
+ value = Option(generators.nextBoolean()),
+ isDefining = generators.nextBoolean()
+ )
+
+ def nextRichCriterion(): RichCriterion = {
+ val criterion = nextCriterion()
+ RichCriterion(
+ criterion = criterion,
+ armIds = Seq(nextLongId[Arm], nextLongId[Arm]),
+ labels = Seq(
+ nextCriterionLabel(criterion.id),
+ nextCriterionLabel(criterion.id)
+ )
+ )
+ }
+
+ def nextIntervention(): Intervention = Intervention(
+ id = nextLongId[Intervention],
+ trialId = nextStringId[Trial],
+ name = generators.nextString(),
+ originalName = generators.nextString(),
+ typeId = Option(nextLongId[InterventionType]),
+ originalType = Option(generators.nextString()),
+ dosage = generators.nextString(),
+ originalDosage = generators.nextString(),
+ isActive = generators.nextBoolean()
+ )
+
+ def nextInterventionArm(interventionId: LongId[Intervention]): InterventionArm = InterventionArm(
+ interventionId = interventionId,
+ armId = nextLongId[Arm]
+ )
+
+ def nextInterventionWithArms(): InterventionWithArms = {
+ val intervention = nextIntervention()
+ InterventionWithArms(
+ intervention = intervention,
+ arms = List(
+ nextInterventionArm(intervention.id),
+ nextInterventionArm(intervention.id),
+ nextInterventionArm(intervention.id)
+ )
+ )
+ }
+
+ def nextTrialIssue(): TrialIssue = TrialIssue(
+ id = nextLongId[TrialIssue],
+ userId = nextStringId[User],
+ trialId = nextStringId[Trial],
+ lastUpdate = nextLocalDateTime,
+ isDraft = generators.nextBoolean(),
+ text = generators.nextString(),
+ evidence = generators.nextString(),
+ archiveRequired = generators.nextBoolean(),
+ meta = generators.nextString()
+ )
+
+ def nextTrialHistory(): TrialHistory = TrialHistory(
+ id = nextLongId[TrialHistory],
+ executor = nextStringId[User],
+ trialId = nextStringId[Trial],
+ state = nextTrialState,
+ action = nextTrialAction,
+ created = nextLocalDateTime
+ )
+
+ def nextHypothesis(): Hypothesis = Hypothesis(
+ id = nextUuidId[Hypothesis],
+ name = generators.nextString(),
+ treatmentType = generators.nextString(),
+ description = generators.nextString()
+ )
+
+ def nextStudyDesign(): StudyDesign = StudyDesign(
+ id = nextLongId[StudyDesign],
+ name = generators.nextString()
+ )
+
+ def nextInterventionType(): InterventionType = InterventionType(
+ id = nextLongId[InterventionType],
+ name = generators.nextString()
+ )
+
+}
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
index 39acbde..f306a71 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala
@@ -8,12 +8,12 @@ import play.api.libs.json.{Format, JsPath}
final case class ApiIntervention(id: Long,
name: String,
typeId: Option[Long],
- description: String,
+ dosage: String,
isActive: Boolean,
arms: List[Long],
trialId: String,
originalName: String,
- originalDescription: String,
+ originalDosage: String,
originalType: Option[String]) {
def toDomain = {
@@ -24,8 +24,8 @@ final case class ApiIntervention(id: Long,
originalName = this.originalName,
typeId = this.typeId.map(id => LongId(id)),
originalType = this.originalType.map(id => id.toString),
- description = this.description,
- originalDescription = this.originalDescription,
+ dosage = this.dosage,
+ originalDosage = this.originalDosage,
isActive = this.isActive
)
@@ -43,12 +43,12 @@ object ApiIntervention {
(JsPath \ "id").format[Long] and
(JsPath \ "name").format[String] and
(JsPath \ "typeId").formatNullable[Long] and
- (JsPath \ "description").format[String] and
+ (JsPath \ "dosage").format[String] and
(JsPath \ "isActive").format[Boolean] and
(JsPath \ "arms").format[List[Long]] and
(JsPath \ "trialId").format[String] and
(JsPath \ "originalName").format[String] and
- (JsPath \ "originalDescription").format[String] and
+ (JsPath \ "originalDosage").format[String] and
(JsPath \ "originalType").formatNullable[String]
)(ApiIntervention.apply, unlift(ApiIntervention.unapply))
@@ -60,12 +60,12 @@ object ApiIntervention {
id = intervention.id.id,
name = intervention.name,
typeId = intervention.typeId.map(_.id),
- description = intervention.description,
+ dosage = intervention.dosage,
isActive = intervention.isActive,
arms = arms.map(_.armId.id),
trialId = intervention.trialId.id,
originalName = intervention.originalName,
- originalDescription = intervention.originalDescription,
+ originalDosage = intervention.originalDosage,
originalType = intervention.originalType
)
}
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
index f67ba6b..aa55506 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala
@@ -6,7 +6,7 @@ import play.api.libs.functional.syntax._
import play.api.libs.json._
final case class ApiPartialIntervention(typeId: Option[Long],
- description: Option[String],
+ dosage: Option[String],
isActive: Option[Boolean],
arms: Option[List[Long]]) {
@@ -16,7 +16,7 @@ final case class ApiPartialIntervention(typeId: Option[Long],
orig.copy(
intervention = origIntervention.copy(
typeId = typeId.map(LongId(_)).orElse(origIntervention.typeId),
- description = description.getOrElse(origIntervention.description),
+ dosage = dosage.getOrElse(origIntervention.dosage),
isActive = isActive.getOrElse(origIntervention.isActive)
),
arms = draftArmList.getOrElse(orig.arms)
@@ -28,14 +28,14 @@ object ApiPartialIntervention {
private val reads: Reads[ApiPartialIntervention] = (
(JsPath \ "typeId").readNullable[Long] and
- (JsPath \ "description").readNullable[String] and
+ (JsPath \ "dosage").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 \ "dosage").writeNullable[String] and
(JsPath \ "isActive").writeNullable[Boolean] and
(JsPath \ "arms").writeNullable[List[Long]]
)(unlift(ApiPartialIntervention.unapply))
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documentissue.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documentissue.scala
index 28b2a5e..a658cfa 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documentissue.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documentissue.scala
@@ -61,17 +61,6 @@ object documentissue {
case _ => deserializationError(s"Expected Json Object as DocumentIssue, but got $json")
}
- implicit val documentIssueWriter = new JsonWriter[DocumentIssue] {
- override def write(obj: DocumentIssue) = JsObject(
- "id" -> obj.id.toJson,
- "startPage" -> obj.startPage.toJson,
- "endPage" -> obj.endPage.toJson,
- "text" -> obj.text.toJson,
- "lastUpdate" -> obj.lastUpdate.toJson,
- "userId" -> obj.userId.toJson,
- "isDraft" -> obj.isDraft.toJson,
- "archiveRequired" -> obj.archiveRequired.toJson
- )
- }
+ implicit val documentIssueFormat: RootJsonFormat[DocumentIssue] = jsonFormat9(DocumentIssue.apply)
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documenttype.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documenttype.scala
new file mode 100644
index 0000000..8119d35
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documenttype.scala
@@ -0,0 +1,13 @@
+package xyz.driver.pdsuidomain.formats.json.sprayformats
+
+import spray.json._
+import xyz.driver.pdsuidomain.entities._
+import xyz.driver.pdsuidomain.entities.DocumentType
+
+object documenttype {
+ import DefaultJsonProtocol._
+ import common._
+
+ implicit val format: RootJsonFormat[DocumentType] = jsonFormat2(DocumentType.apply)
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala
index a8ce950..9314391 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala
@@ -11,16 +11,16 @@ object intervention {
implicit val interventionWriter: JsonWriter[InterventionWithArms] = new JsonWriter[InterventionWithArms] {
override def write(obj: InterventionWithArms) =
JsObject(
- "id" -> obj.intervention.id.toJson,
- "name" -> obj.intervention.name.toJson,
- "typeId" -> obj.intervention.typeId.toJson,
- "description" -> obj.intervention.description.toJson,
- "isActive" -> obj.intervention.isActive.toJson,
- "arms" -> obj.arms.map(_.armId).toJson,
- "trialId" -> obj.intervention.trialId.toJson,
- "originalName" -> obj.intervention.originalName.toJson,
- "originalDescription" -> obj.intervention.originalDescription.toJson,
- "originalType" -> obj.intervention.originalType.toJson
+ "id" -> obj.intervention.id.toJson,
+ "name" -> obj.intervention.name.toJson,
+ "typeId" -> obj.intervention.typeId.toJson,
+ "dosage" -> obj.intervention.dosage.toJson,
+ "isActive" -> obj.intervention.isActive.toJson,
+ "arms" -> obj.arms.map(_.armId).toJson,
+ "trialId" -> obj.intervention.trialId.toJson,
+ "originalName" -> obj.intervention.originalName.toJson,
+ "originalDosage" -> obj.intervention.originalDosage.toJson,
+ "originalType" -> obj.intervention.originalType.toJson
)
}
@@ -30,8 +30,8 @@ object intervention {
.get("typeId")
.map(_.convertTo[LongId[InterventionType]])
- val description = fields
- .get("description")
+ val dosage = fields
+ .get("dosage")
.map(_.convertTo[String])
val isActive = fields
@@ -46,7 +46,7 @@ object intervention {
orig.copy(
intervention = origIntervention.copy(
typeId = typeId.orElse(origIntervention.typeId),
- description = description.getOrElse(origIntervention.description),
+ dosage = dosage.getOrElse(origIntervention.dosage),
isActive = isActive.getOrElse(origIntervention.isActive)
),
arms = arms.getOrElse(orig.arms)
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/providerttype.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/providerttype.scala
new file mode 100644
index 0000000..385feb2
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/providerttype.scala
@@ -0,0 +1,12 @@
+package xyz.driver.pdsuidomain.formats.json.sprayformats
+
+import spray.json._
+import xyz.driver.pdsuidomain.entities.ProviderType
+
+object providertype {
+ import DefaultJsonProtocol._
+ import common._
+
+ implicit val providerTypeFormat: RootJsonFormat[ProviderType] = jsonFormat2(ProviderType.apply)
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/recordissue.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/recordissue.scala
index 4ae04d0..4ac5f6d 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/recordissue.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/recordissue.scala
@@ -63,17 +63,6 @@ object recordissue {
case _ => deserializationError(s"Expected Json Object as MedicalRecordIssue, but got $json")
}
- implicit val recordIssueWriter = new JsonWriter[MedicalRecordIssue] {
- override def write(obj: MedicalRecordIssue) = JsObject(
- "id" -> obj.id.toJson,
- "startPage" -> obj.startPage.toJson,
- "endPage" -> obj.endPage.toJson,
- "text" -> obj.text.toJson,
- "lastUpdate" -> obj.lastUpdate.toJson,
- "userId" -> obj.userId.toJson,
- "isDraft" -> obj.isDraft.toJson,
- "archiveRequired" -> obj.archiveRequired.toJson
- )
- }
+ implicit val recordIssueFormat = jsonFormat9(MedicalRecordIssue.apply)
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trialissue/ApiTrialIssue.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trialissue/ApiTrialIssue.scala
index 852c4f6..c9475c6 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/trialissue/ApiTrialIssue.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/trialissue/ApiTrialIssue.scala
@@ -4,20 +4,37 @@ import java.time.{ZoneId, ZonedDateTime}
import play.api.libs.functional.syntax._
import play.api.libs.json._
+import xyz.driver.pdsuicommon.domain._
import xyz.driver.pdsuidomain.entities.TrialIssue
final case class ApiTrialIssue(id: Long,
+ trialId: String,
text: String,
lastUpdate: ZonedDateTime,
userId: String,
isDraft: Boolean,
evidence: String,
archiveRequired: Boolean,
- meta: String)
+ meta: String) {
+
+ def toDomain = TrialIssue(
+ id = LongId(this.id),
+ trialId = StringId(this.trialId),
+ text = this.text,
+ userId = StringId(this.userId),
+ lastUpdate = this.lastUpdate.toLocalDateTime,
+ isDraft = this.isDraft,
+ evidence = this.evidence,
+ archiveRequired = this.archiveRequired,
+ meta = this.meta
+ )
+
+}
object ApiTrialIssue {
implicit val format: Format[ApiTrialIssue] = (
(JsPath \ "id").format[Long] and
+ (JsPath \ "trialId").format[String] and
(JsPath \ "text").format[String] and
(JsPath \ "lastUpdate").format[ZonedDateTime] and
(JsPath \ "userId").format[String] and
@@ -31,6 +48,7 @@ object ApiTrialIssue {
def fromDomain(x: TrialIssue) = ApiTrialIssue(
id = x.id.id,
+ trialId = x.trialId.id,
text = x.text,
lastUpdate = ZonedDateTime.of(x.lastUpdate, ZoneId.of("Z")),
userId = x.userId.id,
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala
index d140d27..5bd99a8 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/TrialService.scala
@@ -2,13 +2,15 @@ package xyz.driver.pdsuidomain.services
import java.time.LocalDateTime
+import akka.NotUsed
+import akka.stream.scaladsl.Source
+import akka.util.ByteString
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 xyz.driver.pdsuidomain.entities.export.trial.ExportTrialWithLabels
import scala.concurrent.Future
@@ -66,24 +68,6 @@ object TrialService {
extends GetTrialWithLabelsReply with DomainError.AuthorizationError with DefaultAccessDeniedError
}
- sealed trait GetPdfSourceReply
- object GetPdfSourceReply {
- type Error = GetPdfSourceReply with DomainError
-
- final 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
-
- final case class CommonError(userMessage: String) extends GetPdfSourceReply with DomainError
- }
-
sealed trait UpdateReply
object UpdateReply {
type Error = UpdateReply with DomainError
@@ -114,7 +98,7 @@ trait TrialService {
implicit requestContext: AuthenticatedRequestContext): Future[GetTrialWithLabelsReply]
def getPdfSource(trialId: StringId[Trial])(
- implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply]
+ implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]]
def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty,
sorting: Option[Sorting] = None,
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala
index 7c0e313..3793c1f 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/fake/FakeTrialService.scala
@@ -2,6 +2,9 @@ package xyz.driver.pdsuidomain.services.fake
import java.time.LocalDateTime
+import akka.NotUsed
+import akka.stream.scaladsl.Source
+import akka.util.ByteString
import xyz.driver.core.generators
import xyz.driver.pdsuicommon.auth.AuthenticatedRequestContext
import xyz.driver.pdsuicommon.db._
@@ -44,7 +47,7 @@ class FakeTrialService extends TrialService {
)
def getPdfSource(trialId: StringId[Trial])(
- implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply] =
+ implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] =
Future.failed(new NotImplementedError("fake pdf download is not implemented"))
def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty,
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
index 9f72760..f826b98 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
@@ -1,6 +1,9 @@
package xyz.driver.pdsuidomain.services.rest
import scala.concurrent.{ExecutionContext, Future}
+import akka.NotUsed
+import akka.stream.scaladsl.Source
+import akka.util.ByteString
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.stream.Materializer
@@ -43,8 +46,15 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(implicit prote
}
def getPdfSource(trialId: StringId[Trial])(
- implicit requestContext: AuthenticatedRequestContext): Future[GetPdfSourceReply] =
- Future.failed(new NotImplementedError("Streaming PDF over network is not supported."))
+ implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] = {
+ val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/trial/${trialId}/source"))
+ for {
+ response <- transport.sendRequestGetResponse(requestContext)(request)
+ reply <- apiResponse[HttpEntity](response)
+ } yield {
+ reply.dataBytes.mapMaterializedValue(_ => NotUsed)
+ }
+ }
def getAll(filter: SearchFilterExpr = SearchFilterExpr.Empty,
sorting: Option[Sorting] = None,
diff --git a/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala b/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala
index 1c01483..51d39e5 100644
--- a/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala
+++ b/src/test/scala/xyz/driver/pdsuicommon/Mocks.scala
@@ -42,7 +42,6 @@ object MockMySqlContext {
url = ""
),
connection = ConfigFactory.empty(),
- connectionAttemptsOnStartup = 1,
threadPoolSize = 10
)
}
diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/PaginationParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/PaginationParserSuite.scala
new file mode 100644
index 0000000..48fc99b
--- /dev/null
+++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/PaginationParserSuite.scala
@@ -0,0 +1,95 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.db.Pagination
+import xyz.driver.pdsuicommon.parsers.TestUtils._
+import org.scalatest.{FreeSpecLike, MustMatchers}
+
+import scala.util.{Failure, Try}
+
+class PaginationParserSuite extends FreeSpecLike with MustMatchers {
+
+ "parse" - {
+ "pageSize" - {
+ "should parse positive value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "10",
+ "pageNumber" -> "1"
+ ))
+ pagination must success
+ pagination.get.pageSize mustBe 10
+ }
+
+ "should return a default value if there is no one" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageNumber" -> "1"
+ ))
+ pagination must success
+ pagination.get.pageSize mustBe 100
+ }
+
+ "should return a error for zero value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "0",
+ "pageNumber" -> "1"
+ ))
+
+ checkFailedValidationOnlyOn(pagination, "pageSize")
+ }
+
+ "should return a error for negative value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "-10",
+ "pageNumber" -> "1"
+ ))
+
+ checkFailedValidationOnlyOn(pagination, "pageSize")
+ }
+ }
+
+ "pageNumber" - {
+ "should parse positive value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "1",
+ "pageNumber" -> "1"
+ ))
+ pagination must success
+ pagination.get.pageSize mustBe 1
+ }
+
+ "should return a default value if there is no one" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "1"
+ ))
+ pagination must success
+ pagination.get.pageNumber mustBe 1
+ }
+
+ "should return a error for zero value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "1",
+ "pageNumber" -> "0"
+ ))
+
+ checkFailedValidationOnlyOn(pagination, "pageNumber")
+ }
+
+ "should return a error for negative value" in {
+ val pagination = PaginationParser.parse(Seq(
+ "pageSize" -> "1",
+ "pageNumber" -> "-1"
+ ))
+
+ checkFailedValidationOnlyOn(pagination, "pageNumber")
+ }
+ }
+ }
+
+ private def checkFailedValidationOnlyOn(pagination: Try[Pagination], key: String): Unit = {
+ pagination must failWith[ParseQueryArgException]
+
+ val Failure(e: ParseQueryArgException) = pagination
+ e.errors.size mustBe 1
+ e.errors.head._1 mustBe key
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala
new file mode 100644
index 0000000..f47f4c2
--- /dev/null
+++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala
@@ -0,0 +1,207 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.db.SearchFilterExpr.Dimension
+import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation}
+import xyz.driver.pdsuicommon.utils.Implicits.toStringOps
+import xyz.driver.pdsuicommon.parsers.TestUtils._
+import fastparse.core.Parsed
+import org.scalacheck.Arbitrary.arbitrary
+import org.scalacheck.{Gen, Prop}
+import org.scalatest.FreeSpecLike
+import org.scalatest.prop.Checkers
+import xyz.driver.pdsuicommon.db.SearchFilterNAryOperation.In
+import xyz.driver.pdsuicommon.utils.Utils
+import xyz.driver.pdsuicommon.utils.Utils._
+
+import scala.util.Success
+
+object SearchFilterParserSuite {
+
+ class UnexpectedSearchFilterExprException(x: SearchFilterExpr) extends Exception(s"unexpected $x")
+
+}
+
+class SearchFilterParserSuite extends FreeSpecLike with Checkers {
+
+ import SearchFilterParserSuite._
+
+ "parse" - {
+ "should convert column names to snake case" in {
+ import SearchFilterBinaryOperation._
+
+ val filter = SearchFilterParser.parse(Seq(
+ "filters" -> "status IN Summarized,ReviewCriteria,Flagged,Done",
+ "filters" -> "previousStatus NOTEQ New",
+ "filters" -> "previousStatus NOTEQ ReviewSummary"
+ ))
+
+ assert(filter === Success(SearchFilterExpr.Intersection(List(
+ SearchFilterExpr.Atom.NAry(Dimension(None, "status"), In, Seq("Summarized", "ReviewCriteria", "Flagged", "Done")),
+ SearchFilterExpr.Atom.Binary(Dimension(None, "previous_status"), NotEq, "New"),
+ SearchFilterExpr.Atom.Binary(Dimension(None, "previous_status"), NotEq, "ReviewSummary")))))
+ }
+ "dimensions" - {
+ "with table name" in check {
+ val dimensionGen = {
+ for (left <- Gen.identifier; right <- Gen.identifier)
+ yield left -> right
+ }
+ Prop.forAllNoShrink(dimensionGen) {
+ case (left, right) =>
+ val raw = s"$left.$right"
+ val l = toSnakeCase(left)
+ val r = toSnakeCase(right)
+ SearchFilterParser.dimensionParser.parse(raw) match {
+ case Parsed.Success(Dimension(Some(`l`), `r`), _) => true
+ case _ => false
+ }
+ }
+ }
+ "just with field name" in check {
+ Prop.forAllNoShrink(Gen.identifier) { s =>
+ val databaseS = Utils.toSnakeCase(s)
+ SearchFilterParser.dimensionParser.parse(s) match {
+ case Parsed.Success(Dimension(None, `databaseS`), _) => true
+ case _ => false
+ }
+ }
+ }
+ }
+ "atoms" - {
+ "binary" - {
+ "common operators" - {
+ "should be parsed with text values" in check {
+ import SearchFilterBinaryOperation._
+
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = commonBinaryOpsGen,
+ valueGen = nonEmptyString
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser.parse(Seq("filters" -> query))
+ .map {
+ case SearchFilterExpr.Atom.Binary(_, Eq | NotEq | Like, _) => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+
+ "numeric operators" - {
+ "should not be parsed with text values" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = numericBinaryOpsGen,
+ valueGen = nonEmptyString.filter { s => !s.matches("^\\d+$") }
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser.parse(Seq("filters" -> query)).failureProp
+ }
+ }
+ }
+
+ "all operators" - {
+ "should be parsed with numeric values" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = allBinaryOpsGen,
+ valueGen = numericBinaryAtomValuesGen
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser.parse(Seq("filters" -> query))
+ .map {
+ case _: SearchFilterExpr.Atom.Binary => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+ }
+
+ "n-ary" - {
+ "in" in check {
+ val testQueryGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = Gen.const("in"),
+ valueGen = inValuesGen
+ )
+
+ Prop.forAllNoShrink(testQueryGen) { query =>
+ SearchFilterParser.parse(Seq("filters" -> query))
+ .map {
+ case SearchFilterExpr.Atom.NAry(_, SearchFilterNAryOperation.In, _) => true
+ case x => throw new UnexpectedSearchFilterExprException(x)
+ }
+ .successProp
+ }
+ }
+ }
+ }
+
+ "intersections" - {
+ "should be parsed" in check {
+ val commonAtomsGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = commonBinaryOpsGen,
+ valueGen = nonEmptyString
+ )
+
+ val numericAtomsGen = queryGen(
+ dimensionGen = Gen.identifier,
+ opGen = numericBinaryOpsGen,
+ valueGen = numericBinaryAtomValuesGen
+ )
+
+ val allAtomsGen = Gen.oneOf(commonAtomsGen, numericAtomsGen)
+ val intersectionsGen = Gen.choose(1, 3).flatMap { size =>
+ Gen.containerOfN[Seq, String](size, allAtomsGen)
+ }
+
+ Prop.forAllNoShrink(intersectionsGen) { queries =>
+ SearchFilterParser.parse(queries.map(query => "filters" -> query)).successProp
+ }
+ }
+ }
+ }
+
+ private val CommonBinaryOps = Seq("eq", "noteq", "like")
+ private val NumericBinaryOps = Seq("gt", "gteq", "lt", "lteq")
+
+ private val allBinaryOpsGen: Gen[String] = Gen.oneOf(CommonBinaryOps ++ NumericBinaryOps).flatMap(randomCapitalization)
+ private val commonBinaryOpsGen: Gen[String] = Gen.oneOf(CommonBinaryOps).flatMap(randomCapitalization)
+ private val numericBinaryOpsGen: Gen[String] = Gen.oneOf(NumericBinaryOps).flatMap(randomCapitalization)
+
+ private val inValueCharsGen: Gen[Char] = arbitrary[Char].filter(_ != ',')
+
+ private val nonEmptyString = arbitrary[String].filter { s => !s.safeTrim.isEmpty }
+
+ private val numericBinaryAtomValuesGen: Gen[String] = arbitrary[BigInt].map(_.toString)
+ private val inValueGen: Gen[String] = {
+ Gen.nonEmptyContainerOf[Seq, Char](inValueCharsGen).map(_.mkString).filter(_.safeTrim.nonEmpty)
+ }
+ private val inValuesGen: Gen[String] = Gen.choose(1, 5).flatMap { size =>
+ Gen.containerOfN[Seq, String](size, inValueGen).map(_.mkString(","))
+ }
+
+ private def queryGen(dimensionGen: Gen[String], opGen: Gen[String], valueGen: Gen[String]): Gen[String] = for {
+ dimension <- dimensionGen
+ op <- opGen
+ value <- valueGen
+ } yield s"$dimension $op $value"
+
+ private def randomCapitalization(input: String): Gen[String] = {
+ Gen.containerOfN[Seq, Boolean](input.length, arbitrary[Boolean]).map { capitalize =>
+ input.view.zip(capitalize).map {
+ case (currChar, true) => currChar.toUpper
+ case (currChar, false) => currChar
+ }.mkString
+ }
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SortingParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SortingParserSuite.scala
new file mode 100644
index 0000000..e46015c
--- /dev/null
+++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SortingParserSuite.scala
@@ -0,0 +1,91 @@
+package xyz.driver.pdsuicommon.parsers
+
+import xyz.driver.pdsuicommon.parsers.TestUtils._
+import org.scalacheck.Arbitrary.arbitrary
+import org.scalacheck.{Gen, Prop}
+import org.scalatest.prop.Checkers
+import org.scalatest.{FreeSpecLike, MustMatchers}
+
+class SortingParserSuite extends FreeSpecLike with MustMatchers with Checkers {
+
+ "parse" - {
+ "single dimension" - commonTests(singleSortingQueryGen)
+ "multiple dimensions in one query" - commonTests(multipleSortingQueryGen)
+ "multiple queries" in {
+ val r = SortingParser.parse(Set("foo", "bar"), Seq("sort" -> "foo", "sort" ->"bar"))
+ r must failWith[ParseQueryArgException]
+ }
+ }
+
+ private def commonTests(queryGen: Set[String] => Gen[String]): Unit = {
+ "valid" in check {
+ val inputGen: Gen[(Set[String], String)] = for {
+ validDimensions <- dimensionsGen
+ sorting <- queryGen(validDimensions)
+ } yield (validDimensions, sorting)
+
+ Prop.forAllNoShrink(inputGen) {
+ case (validDimensions, query) =>
+ SortingParser.parse(validDimensions, Seq("sort" -> query)).successProp
+ }
+ }
+
+ "invalid" in check {
+ val inputGen: Gen[(Set[String], String)] = for {
+ validDimensions <- dimensionsGen
+ invalidDimensions <- dimensionsGen.filter { xs => xs.intersect(validDimensions).isEmpty }
+ sorting <- queryGen(invalidDimensions)
+ } yield (validDimensions, sorting)
+
+ Prop.forAllNoShrink(inputGen) {
+ case (validDimensions, query) =>
+ SortingParser.parse(validDimensions, Seq("sort" -> query)).failureProp
+ }
+ }
+ }
+
+ private val dimensionsGen: Gen[Set[String]] = for {
+ unPrefixedSize <- Gen.choose(0, 3)
+ prefixedSize <- Gen.choose(0, 3)
+ if (unPrefixedSize + prefixedSize) > 0
+
+ unPrefixedDimensions <- Gen.containerOfN[Set, String](unPrefixedSize, Gen.identifier)
+
+ prefixes <- Gen.containerOfN[Set, String](prefixedSize, Gen.identifier)
+ dimensions <- Gen.containerOfN[Set, String](prefixedSize, Gen.identifier)
+ } yield {
+ val prefixedDimensions = prefixes.zip(dimensions).map {
+ case (prefix, dimension) => s"$prefix.$dimension"
+ }
+ unPrefixedDimensions ++ prefixedDimensions
+ }
+
+ private def multipleSortingQueryGen(validDimensions: Set[String]): Gen[String] = {
+ val validDimensionsSeq = validDimensions.toSeq
+ val indexGen = Gen.oneOf(validDimensionsSeq.indices)
+ val multipleDimensionsGen = Gen.nonEmptyContainerOf[Set, Int](indexGen).filter(_.size >= 2).map { indices =>
+ indices.map(validDimensionsSeq.apply)
+ }
+
+ for {
+ dimensions <- multipleDimensionsGen
+ isAscending <- Gen.containerOfN[Seq, Boolean](dimensions.size, arbitrary[Boolean])
+ } yield {
+ isAscending.zip(dimensions)
+ .map {
+ case (true, dimension) => dimension
+ case (false, dimension) => "-" + dimension
+ }
+ .mkString(",")
+ }
+ }
+
+ private def singleSortingQueryGen(validDimensions: Set[String]): Gen[String] = for {
+ isAscending <- arbitrary[Boolean]
+ dimensions <- Gen.oneOf(validDimensions.toSeq)
+ } yield isAscending match {
+ case true => dimensions
+ case false => "-" + dimensions
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/TestUtils.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/TestUtils.scala
new file mode 100644
index 0000000..4892b95
--- /dev/null
+++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/TestUtils.scala
@@ -0,0 +1,52 @@
+package xyz.driver.pdsuicommon.parsers
+
+import org.scalacheck.Prop
+import org.scalacheck.Prop.BooleanOperators
+import org.scalatest.matchers.{MatchResult, Matcher}
+import xyz.driver.pdsuicommon.utils.Utils
+
+import scala.reflect.ClassTag
+import scala.util.{Failure, Success, Try}
+
+object TestUtils {
+
+ object success extends Matcher[Try[Any]] {
+ override def apply(left: Try[Any]) = {
+ MatchResult(left.isSuccess, s"$left did not fail", s"did fail with $left")
+ }
+ }
+
+ class FailWith[ThrowableT <: Throwable](implicit ct: ClassTag[ThrowableT]) extends Matcher[Try[Any]] {
+ override def apply(left: Try[Any]): MatchResult = {
+ MatchResult(
+ left.isFailure && left.failed.get.getClass == ct.runtimeClass,
+ left match {
+ case Success(x) => s"$left did not fail"
+ case Failure(e) => s"$left did fail with ${Utils.getClassSimpleName(e.getClass)}, " +
+ s"not ${Utils.getClassSimpleName(ct.runtimeClass)}"
+ },
+ left match {
+ case Success(_) => s"$left failed with ${Utils.getClassSimpleName(ct.runtimeClass)}"
+ case Failure(e) => s"$left failed with ${Utils.getClassSimpleName(e.getClass)}"
+ }
+ )
+ }
+ }
+
+ def failWith[ThrowableT <:Throwable](implicit ct: ClassTag[ThrowableT]) = new FailWith[ThrowableT]
+
+ final implicit class TryPropOps(val self: Try[Any]) extends AnyVal {
+
+ def successProp: Prop = self match {
+ case Success(_) => true :| "ok"
+ case Failure(e) => false :| e.getMessage
+ }
+
+ def failureProp: Prop = self match {
+ case Success(x) => false :| s"invalid: $x"
+ case Failure(e) => true
+ }
+
+ }
+
+}
diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/DocumentIssueFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/DocumentIssueFormatSuite.scala
index 1a8e3f0..c4c9f7c 100644
--- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/DocumentIssueFormatSuite.scala
+++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/DocumentIssueFormatSuite.scala
@@ -22,10 +22,10 @@ class DocumentIssueFormatSuite extends FlatSpec with Matchers {
startPage = Some(1.0),
endPage = Some(2.0)
)
- val writtenJson = documentIssueWriter.write(documentIssue)
+ val writtenJson = documentIssueFormat.write(documentIssue)
writtenJson should be(
- """{"id":10,"userId":"userId-001","lastUpdate":"2017-08-10T18:00Z","isDraft":false,
+ """{"id":10,"userId":"userId-001","documentId":1,"lastUpdate":"2017-08-10T18:00Z","isDraft":false,
"text":"message text","archiveRequired":false,"startPage":1.0,"endPage":2.0}""".parseJson)
val createDocumentIssueJson = """{"text":"message text","startPage":1.0,"endPage":2.0}""".parseJson
diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala
index 784a655..0f01d4a 100644
--- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala
+++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala
@@ -16,8 +16,8 @@ class InterventionFormatSuite extends FlatSpec with Matchers {
originalName = "orig name",
typeId = Some(LongId(10)),
originalType = Some("orig type"),
- description = "",
- originalDescription = "",
+ dosage = "",
+ originalDosage = "",
isActive = true
)
val arms = List(
@@ -32,12 +32,12 @@ class InterventionFormatSuite extends FlatSpec with Matchers {
val writtenJson = interventionWriter.write(orig)
writtenJson should be(
- """{"id":1,"name":"intervention name","typeId":10,"description":"","isActive":true,"arms":[20,21,22],
- "trialId":"NCT000001","originalName":"orig name","originalDescription":"","originalType":"orig type"}""".parseJson)
+ """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22],
+ "trialId":"NCT000001","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson)
- val updateInterventionJson = """{"description":"descr","arms":[21,22]}""".parseJson
+ val updateInterventionJson = """{"dosage":"descr","arms":[21,22]}""".parseJson
val expectedUpdatedIntervention = orig.copy(
- intervention = intervention.copy(description = "descr"),
+ intervention = intervention.copy(dosage = "descr"),
arms = List(
InterventionArm(interventionId = intervention.id, armId = LongId(21)),
InterventionArm(interventionId = intervention.id, armId = LongId(22))
diff --git a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordIssueFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordIssueFormatSuite.scala
index 9b89c97..c23ca37 100644
--- a/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordIssueFormatSuite.scala
+++ b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/MedicalRecordIssueFormatSuite.scala
@@ -22,10 +22,10 @@ class MedicalRecordIssueFormatSuite extends FlatSpec with Matchers {
startPage = Some(1.0),
endPage = Some(2.0)
)
- val writtenJson = recordIssueWriter.write(recordIssue)
+ val writtenJson = recordIssueFormat.write(recordIssue)
writtenJson should be(
- """{"id":10,"userId":"userId-001","lastUpdate":"2017-08-10T18:00Z","isDraft":false,
+ """{"id":10,"recordId":1,"userId":"userId-001","lastUpdate":"2017-08-10T18:00Z","isDraft":false,
"text":"message text","archiveRequired":false,"startPage":1.0,"endPage":2.0}""".parseJson)
val createRecordIssueJson = """{"text":"message text","startPage":1.0,"endPage":2.0}""".parseJson