aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKseniya Tomskikh <ktomskih@datamonsters.co>2017-09-28 17:52:21 +0700
committerKseniya Tomskikh <ktomskih@datamonsters.co>2017-09-28 17:52:21 +0700
commit283ca02360949143ffe7ee8ad87d51902426b450 (patch)
treec098efe0628fc80a0a6b8f05fa465a99ff494fb8
parentd82c93fef0fc0bb937220334f73c264fbb1082f2 (diff)
parentc24679f1ae7d7ccc4e6693535b0aa3ac0e1ca225 (diff)
downloadrest-query-283ca02360949143ffe7ee8ad87d51902426b450.tar.gz
rest-query-283ca02360949143ffe7ee8ad87d51902426b450.tar.bz2
rest-query-283ca02360949143ffe7ee8ad87d51902426b450.zip
Merge branch 'master' into synch-refactor
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala4
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala2
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala4
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/SlickPostgresQueryBuilder.scala116
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala358
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala28
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala17
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala2
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala4
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala193
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala7
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala10
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala17
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiIntervention.scala6
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala12
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiPartialIntervention.scala60
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala92
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala2
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala92
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala36
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala23
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala6
-rw-r--r--src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala10
-rw-r--r--src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala26
24 files changed, 1019 insertions, 108 deletions
diff --git a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala
index f2a0ef0..180ebf9 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/acl/ACL.scala
@@ -168,8 +168,10 @@ object ACL extends PhiLogging {
object Intervention
extends BaseACL(
label = "intervention",
+ create = Set(TrialSummarizer, TrialAdmin),
read = Set(TrialSummarizer, TrialAdmin),
- update = Set(TrialSummarizer, TrialAdmin)
+ update = Set(TrialSummarizer, TrialAdmin),
+ delete = Set(TrialSummarizer, TrialAdmin)
)
object InterventionType
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala
index cbb23d4..7bdfd1b 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala
@@ -28,7 +28,7 @@ object PostgresContext extends PhiLogging {
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")
+ throw new IllegalArgumentException("Can not load dataSource from config. Check your database and config", e)
}
}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala
index aa32166..0bf1ed6 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/QueryBuilder.scala
@@ -57,10 +57,10 @@ sealed trait QueryBuilderParameters {
def toSql(countQuery: Boolean, fields: Set[String], namingStrategy: NamingStrategy): (String, QueryBuilder.Binder) = {
val escapedTableName = namingStrategy.table(tableData.tableName)
val fieldsSql: String = if (countQuery) {
- val suffix: String = (tableData.lastUpdateFieldName match {
+ val suffix: String = tableData.lastUpdateFieldName match {
case Some(lastUpdateField) => s", max($escapedTableName.${namingStrategy.column(lastUpdateField)})"
case None => ""
- })
+ }
"count(*)" + suffix
} else {
if (fields == QueryBuilderParameters.AllFields) {
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SlickPostgresQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SlickPostgresQueryBuilder.scala
new file mode 100644
index 0000000..f882441
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/SlickPostgresQueryBuilder.scala
@@ -0,0 +1,116 @@
+package xyz.driver.pdsuicommon.db
+
+import java.time.{LocalDateTime, ZoneOffset}
+
+import slick.driver.JdbcProfile
+import slick.jdbc.GetResult
+import xyz.driver.core.database.SlickDal
+import xyz.driver.pdsuicommon.logging._
+
+import scala.collection.breakOut
+import scala.concurrent.ExecutionContext
+
+object SlickPostgresQueryBuilder extends PhiLogging {
+
+ import xyz.driver.pdsuicommon.db.SlickQueryBuilder._
+
+ def apply[T](databaseName: String,
+ tableName: String,
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[SlickTableLink],
+ runner: Runner[T],
+ countRunner: CountRunner)(implicit sqlContext: SlickDal,
+ profile: JdbcProfile,
+ getResult: GetResult[T],
+ ec: ExecutionContext): SlickPostgresQueryBuilder[T] = {
+ val parameters = SlickPostgresQueryBuilderParameters(
+ databaseName = databaseName,
+ tableData = TableData(tableName, lastUpdateFieldName, nullableFields),
+ links = links.map(x => x.foreignTableName -> x)(breakOut)
+ )
+ new SlickPostgresQueryBuilder[T](parameters)(runner, countRunner)
+ }
+
+ def apply[T](databaseName: String,
+ tableName: String,
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[SlickTableLink])(implicit sqlContext: SlickDal,
+ profile: JdbcProfile,
+ getResult: GetResult[T],
+ ec: ExecutionContext): SlickPostgresQueryBuilder[T] = {
+ apply[T](databaseName,
+ tableName,
+ SlickQueryBuilderParameters.AllFields,
+ lastUpdateFieldName,
+ nullableFields,
+ links)
+ }
+
+ def apply[T](databaseName: String,
+ tableName: String,
+ fields: Set[String],
+ lastUpdateFieldName: Option[String],
+ nullableFields: Set[String],
+ links: Set[SlickTableLink])(implicit sqlContext: SlickDal,
+ profile: JdbcProfile,
+ getResult: GetResult[T],
+ ec: ExecutionContext): SlickPostgresQueryBuilder[T] = {
+
+ val runner: Runner[T] = { parameters =>
+ val sql = parameters.toSql(countQuery = false, fields = fields).as[T]
+ logger.debug(phi"${Unsafe(sql)}")
+ sqlContext.execute(sql)
+ }
+
+ val countRunner: CountRunner = { parameters =>
+ implicit val getCountResult: GetResult[(Int, Option[LocalDateTime])] = GetResult({ r =>
+ val count = r.rs.getInt(1)
+ val lastUpdate = if (parameters.tableData.lastUpdateFieldName.isDefined) {
+ Option(r.rs.getTimestamp(2)).map(timestampToLocalDateTime)
+ } else None
+ (count, lastUpdate)
+ })
+ val sql = parameters.toSql(countQuery = true).as[(Int, Option[LocalDateTime])]
+ logger.debug(phi"${Unsafe(sql)}")
+ sqlContext.execute(sql).map(_.head)
+ }
+
+ apply[T](
+ databaseName = databaseName,
+ tableName = tableName,
+ lastUpdateFieldName = lastUpdateFieldName,
+ nullableFields = nullableFields,
+ links = links,
+ runner = runner,
+ countRunner = countRunner
+ )
+ }
+
+ def timestampToLocalDateTime(timestamp: java.sql.Timestamp): LocalDateTime = {
+ LocalDateTime.ofInstant(timestamp.toInstant, ZoneOffset.UTC)
+ }
+}
+
+class SlickPostgresQueryBuilder[T](parameters: SlickPostgresQueryBuilderParameters)(
+ implicit runner: SlickQueryBuilder.Runner[T],
+ countRunner: SlickQueryBuilder.CountRunner)
+ extends SlickQueryBuilder[T](parameters) {
+
+ def withFilter(newFilter: SearchFilterExpr): SlickQueryBuilder[T] = {
+ new SlickPostgresQueryBuilder[T](parameters.copy(filter = newFilter))
+ }
+
+ def withSorting(newSorting: Sorting): SlickQueryBuilder[T] = {
+ new SlickPostgresQueryBuilder[T](parameters.copy(sorting = newSorting))
+ }
+
+ def withPagination(newPagination: Pagination): SlickQueryBuilder[T] = {
+ new SlickPostgresQueryBuilder[T](parameters.copy(pagination = Some(newPagination)))
+ }
+
+ def resetPagination: SlickQueryBuilder[T] = {
+ new SlickPostgresQueryBuilder[T](parameters.copy(pagination = None))
+ }
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala
new file mode 100644
index 0000000..ab2757b
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/db/SlickQueryBuilder.scala
@@ -0,0 +1,358 @@
+package xyz.driver.pdsuicommon.db
+
+import java.sql.PreparedStatement
+import java.time.LocalDateTime
+
+import slick.driver.JdbcProfile
+import slick.jdbc.{PositionedParameters, SQLActionBuilder, SetParameter}
+import xyz.driver.pdsuicommon.db.Sorting.{Dimension, Sequential}
+import xyz.driver.pdsuicommon.db.SortingOrder.{Ascending, Descending}
+
+import scala.concurrent.{ExecutionContext, Future}
+
+object SlickQueryBuilder {
+
+ type Runner[T] = SlickQueryBuilderParameters => Future[Seq[T]]
+
+ type CountResult = Future[(Int, Option[LocalDateTime])]
+
+ type CountRunner = SlickQueryBuilderParameters => CountResult
+
+ /**
+ * Binder for PreparedStatement
+ */
+ type Binder = PreparedStatement => PreparedStatement
+
+ final case class TableData(tableName: String,
+ lastUpdateFieldName: Option[String] = None,
+ nullableFields: Set[String] = Set.empty)
+
+ val AllFields = Set("*")
+
+ implicit class SQLActionBuilderConcat(a: SQLActionBuilder) {
+ def concat(b: SQLActionBuilder): SQLActionBuilder = {
+ SQLActionBuilder(a.queryParts ++ b.queryParts, new SetParameter[Unit] {
+ def apply(p: Unit, pp: PositionedParameters): Unit = {
+ a.unitPConv.apply(p, pp)
+ b.unitPConv.apply(p, pp)
+ }
+ })
+ }
+ }
+}
+
+final case class SlickTableLink(keyColumnName: String, foreignTableName: String, foreignKeyColumnName: String)
+
+object SlickQueryBuilderParameters {
+ val AllFields = Set("*")
+}
+
+sealed trait SlickQueryBuilderParameters {
+ import SlickQueryBuilder._
+
+ def databaseName: String
+ def tableData: SlickQueryBuilder.TableData
+ def links: Map[String, SlickTableLink]
+ def filter: SearchFilterExpr
+ def sorting: Sorting
+ def pagination: Option[Pagination]
+
+ def qs: String
+
+ def findLink(tableName: String): SlickTableLink = links.get(tableName) match {
+ case None => throw new IllegalArgumentException(s"Cannot find a link for `$tableName`")
+ case Some(link) => link
+ }
+
+ def toSql(countQuery: Boolean = false)(implicit profile: JdbcProfile): SQLActionBuilder = {
+ toSql(countQuery, QueryBuilderParameters.AllFields)
+ }
+
+ def toSql(countQuery: Boolean, fields: Set[String])(implicit profile: JdbcProfile): SQLActionBuilder = {
+ import profile.api._
+ val escapedTableName = s"""$qs$databaseName$qs.$qs${tableData.tableName}$qs"""
+ val fieldsSql: String = if (countQuery) {
+ val suffix: String = tableData.lastUpdateFieldName match {
+ case Some(lastUpdateField) => s", max($escapedTableName.$qs$lastUpdateField$qs)"
+ case None => ""
+ }
+ s"count(*) $suffix"
+ } else {
+ if (fields == SlickQueryBuilderParameters.AllFields) {
+ s"$escapedTableName.*"
+ } else {
+ fields
+ .map { field =>
+ s"$escapedTableName.$qs$field$qs"
+ }
+ .mkString(", ")
+ }
+ }
+ val where = filterToSql(escapedTableName, filter)
+ val orderBy = sortingToSql(escapedTableName, sorting)
+
+ val limitSql = limitToSql()
+
+ val sql = sql"""select #$fieldsSql from #$escapedTableName"""
+
+ val filtersTableLinks: Seq[SlickTableLink] = {
+ import SearchFilterExpr._
+ def aux(expr: SearchFilterExpr): Seq[SlickTableLink] = expr match {
+ case Atom.TableName(tableName) => List(findLink(tableName))
+ case Intersection(xs) => xs.flatMap(aux)
+ case Union(xs) => xs.flatMap(aux)
+ case _ => Nil
+ }
+ aux(filter)
+ }
+
+ val sortingTableLinks: Seq[SlickTableLink] = Sorting.collect(sorting) {
+ case Dimension(Some(foreignTableName), _, _) => findLink(foreignTableName)
+ }
+
+ // Combine links from sorting and filter without duplicates
+ val foreignTableLinks = (filtersTableLinks ++ sortingTableLinks).distinct
+
+ def fkSql(fkLinksSql: SQLActionBuilder, tableLinks: Seq[SlickTableLink]): SQLActionBuilder = {
+ if (tableLinks.nonEmpty) {
+ tableLinks.head match {
+ case SlickTableLink(keyColumnName, foreignTableName, foreignKeyColumnName) =>
+ val escapedForeignTableName = s"$qs$databaseName$qs.$qs$foreignTableName$qs"
+ val join = sql""" inner join #$escapedForeignTableName
+ on #$escapedTableName.#$qs#$keyColumnName#$qs=#$escapedForeignTableName.#$qs#$foreignKeyColumnName#$qs"""
+ fkSql(fkLinksSql concat join, tableLinks.tail)
+ }
+ } else fkLinksSql
+ }
+ val foreignTableLinksSql = fkSql(sql"", foreignTableLinks)
+
+ val whereSql = if (where.queryParts.size > 1) {
+ sql" where " concat where
+ } else sql""
+
+ val orderSql = if (orderBy.nonEmpty && !countQuery) {
+ sql" order by #$orderBy"
+ } else sql""
+
+ val limSql = if (limitSql.queryParts.size > 1 && !countQuery) {
+ sql" " concat limitSql
+ } else sql""
+
+ sql concat foreignTableLinksSql concat whereSql concat orderSql concat limSql
+ }
+
+ /**
+ * Converts filter expression to SQL expression.
+ *
+ * @return Returns SQL string and list of values for binding in prepared statement.
+ */
+ protected def filterToSql(escapedTableName: String, filter: SearchFilterExpr)(
+ implicit profile: JdbcProfile): SQLActionBuilder = {
+ import SearchFilterBinaryOperation._
+ import SearchFilterExpr._
+ import profile.api._
+
+ def isNull(string: AnyRef) = Option(string).isEmpty || string.toString.toLowerCase == "null"
+
+ def escapeDimension(dimension: SearchFilterExpr.Dimension) = {
+ s"$escapedTableName.$qs${dimension.name}$qs"
+ }
+
+ def filterToSqlMultiple(operands: Seq[SearchFilterExpr]) = operands.collect {
+ case x if !SearchFilterExpr.isEmpty(x) => filterToSql(escapedTableName, x)
+ }
+
+ def multipleSqlToAction(first: Boolean,
+ op: String,
+ conditions: Seq[SQLActionBuilder],
+ sql: SQLActionBuilder): SQLActionBuilder = {
+ if (conditions.nonEmpty) {
+ val condition = conditions.head
+ if (first) {
+ multipleSqlToAction(false, op, conditions.tail, condition)
+ } else {
+ multipleSqlToAction(false, op, conditions.tail, sql concat sql" #${op} " concat condition)
+ }
+ } else sql
+ }
+
+ filter match {
+ case x if isEmpty(x) =>
+ sql""
+
+ case AllowAll =>
+ sql"1"
+
+ case DenyAll =>
+ sql"0"
+
+ case Atom.Binary(dimension, Eq, value) if isNull(value) =>
+ sql"#${escapeDimension(dimension)} is NULL"
+
+ case Atom.Binary(dimension, NotEq, value) if isNull(value) =>
+ sql"#${escapeDimension(dimension)} is not NULL"
+
+ case Atom.Binary(dimension, NotEq, value) if tableData.nullableFields.contains(dimension.name) =>
+ // In MySQL NULL <> Any === NULL
+ // So, to handle NotEq for nullable fields we need to use more complex SQL expression.
+ // http://dev.mysql.com/doc/refman/5.7/en/working-with-null.html
+ val escapedColumn = escapeDimension(dimension)
+ sql"(#${escapedColumn} is null or #${escapedColumn} != ${value.toString})"
+
+ case Atom.Binary(dimension, op, value) =>
+ val operator = op match {
+ case Eq => sql"="
+ case NotEq => sql"!="
+ case Like => sql" like "
+ case Gt => sql">"
+ case GtEq => sql">="
+ case Lt => sql"<"
+ case LtEq => sql"<="
+ }
+ sql"#${escapeDimension(dimension)}" concat operator concat sql"""${value.toString}"""
+
+ case Atom.NAry(dimension, op, values) =>
+ val sqlOp = op match {
+ case SearchFilterNAryOperation.In => sql" in "
+ case SearchFilterNAryOperation.NotIn => sql" not in "
+ }
+
+ val formattedValues = if (values.nonEmpty) {
+ val condition = s"(${values.map(v => "'" + v.toString + "'").mkString(",")})"
+ sql"#${condition}"
+ } else sql"NULL"
+ sql"#${escapeDimension(dimension)}" concat sqlOp concat formattedValues
+
+ case Intersection(operands) =>
+ val filter = multipleSqlToAction(true, "and", filterToSqlMultiple(operands), sql"")
+ sql"(" concat filter concat sql")"
+
+ case Union(operands) =>
+ val filter = multipleSqlToAction(true, "or", filterToSqlMultiple(operands), sql"")
+ sql"(" concat filter concat sql")"
+ }
+ }
+
+ protected def limitToSql()(implicit profile: JdbcProfile): SQLActionBuilder
+
+ /**
+ * @param escapedMainTableName Should be escaped
+ */
+ protected def sortingToSql(escapedMainTableName: String, sorting: Sorting)(implicit profile: JdbcProfile): String = {
+ sorting match {
+ case Dimension(optSortingTableName, field, order) =>
+ val sortingTableName =
+ optSortingTableName.map(x => s"$qs$databaseName$qs.$qs$x$qs").getOrElse(escapedMainTableName)
+ val fullName = s"$sortingTableName.$qs$field$qs"
+
+ s"$fullName ${orderToSql(order)}"
+
+ case Sequential(xs) =>
+ xs.map(sortingToSql(escapedMainTableName, _)).mkString(", ")
+ }
+ }
+
+ protected def orderToSql(x: SortingOrder): String = x match {
+ case Ascending => "asc"
+ case Descending => "desc"
+ }
+
+ protected def binder(bindings: List[AnyRef])(bind: PreparedStatement): PreparedStatement = {
+ bindings.zipWithIndex.foreach {
+ case (binding, index) =>
+ bind.setObject(index + 1, binding)
+ }
+
+ bind
+ }
+
+}
+
+final case class SlickPostgresQueryBuilderParameters(databaseName: String,
+ tableData: SlickQueryBuilder.TableData,
+ links: Map[String, SlickTableLink] = Map.empty,
+ filter: SearchFilterExpr = SearchFilterExpr.Empty,
+ sorting: Sorting = Sorting.Empty,
+ pagination: Option[Pagination] = None)
+ extends SlickQueryBuilderParameters {
+
+ def limitToSql()(implicit profile: JdbcProfile): SQLActionBuilder = {
+ import profile.api._
+ pagination.map { pagination =>
+ val startFrom = (pagination.pageNumber - 1) * pagination.pageSize
+ sql"limit #${pagination.pageSize} OFFSET #$startFrom"
+ } getOrElse (sql"")
+ }
+
+ val qs = """""""
+
+}
+
+/**
+ * @param links Links to another tables grouped by foreignTableName
+ */
+final case class SlickMysqlQueryBuilderParameters(databaseName: String,
+ tableData: SlickQueryBuilder.TableData,
+ links: Map[String, SlickTableLink] = Map.empty,
+ filter: SearchFilterExpr = SearchFilterExpr.Empty,
+ sorting: Sorting = Sorting.Empty,
+ pagination: Option[Pagination] = None)
+ extends SlickQueryBuilderParameters {
+
+ def limitToSql()(implicit profile: JdbcProfile): SQLActionBuilder = {
+ import profile.api._
+ pagination
+ .map { pagination =>
+ val startFrom = (pagination.pageNumber - 1) * pagination.pageSize
+ sql"limit #$startFrom, #${pagination.pageSize}"
+ }
+ .getOrElse(sql"")
+ }
+
+ val qs = """`"""
+
+}
+
+abstract class SlickQueryBuilder[T](val parameters: SlickQueryBuilderParameters)(
+ implicit runner: SlickQueryBuilder.Runner[T],
+ countRunner: SlickQueryBuilder.CountRunner) {
+
+ def run()(implicit ec: ExecutionContext): Future[Seq[T]] = runner(parameters)
+
+ def runCount()(implicit ec: ExecutionContext): SlickQueryBuilder.CountResult = countRunner(parameters)
+
+ /**
+ * Runs the query and returns total found rows without considering of pagination.
+ */
+ def runWithCount()(implicit ec: ExecutionContext): Future[(Seq[T], Int, Option[LocalDateTime])] = {
+ for {
+ all <- run
+ (total, lastUpdate) <- runCount
+ } yield (all, total, lastUpdate)
+ }
+
+ def withFilter(newFilter: SearchFilterExpr): SlickQueryBuilder[T]
+
+ def withFilter(filter: Option[SearchFilterExpr]): SlickQueryBuilder[T] = {
+ filter.fold(this)(withFilter)
+ }
+
+ def resetFilter: SlickQueryBuilder[T] = withFilter(SearchFilterExpr.Empty)
+
+ def withSorting(newSorting: Sorting): SlickQueryBuilder[T]
+
+ def withSorting(sorting: Option[Sorting]): SlickQueryBuilder[T] = {
+ sorting.fold(this)(withSorting)
+ }
+
+ def resetSorting: SlickQueryBuilder[T] = withSorting(Sorting.Empty)
+
+ def withPagination(newPagination: Pagination): SlickQueryBuilder[T]
+
+ def withPagination(pagination: Option[Pagination]): SlickQueryBuilder[T] = {
+ pagination.fold(this)(withPagination)
+ }
+
+ def resetPagination: SlickQueryBuilder[T]
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala b/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala
index 4e98f40..36c3de7 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/domain/FuzzyValue.scala
@@ -10,21 +10,31 @@ object FuzzyValue {
case object No extends FuzzyValue
case object Maybe extends FuzzyValue
- val All: Set[FuzzyValue] = Set(Yes, No, Maybe)
+ private val yes = "Yes"
+ private val no = "No"
+ private val maybe = "Maybe"
- def fromBoolean(x: Boolean): FuzzyValue = if (x) Yes else No
+ val All: Set[FuzzyValue] =
+ Set(Yes, No, Maybe)
- implicit def toPhiString(x: FuzzyValue): PhiString = Unsafe(Utils.getClassSimpleName(x.getClass))
+ 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
+ case fuzzy =>
+ fuzzy.toLowerCase.capitalize match {
+ 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"
+ case Yes => `yes`
+ case No => `no`
+ case Maybe => `maybe`
}
}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
index 8aff397..e0adeb8 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala
@@ -13,10 +13,15 @@ import scala.util.Try
object SearchFilterParser {
private object BinaryAtomFromTuple {
- def unapply(input: (SearchFilterExpr.Dimension, (String, String))): Option[SearchFilterExpr.Atom.Binary] = {
+ def unapply(input: (SearchFilterExpr.Dimension, (String, Any))): Option[SearchFilterExpr.Atom.Binary] = {
val (dimensionName, (strOperation, value)) = input
+ val updatedValue = value match {
+ case s: String => s.safeTrim
+ case a => a
+ }
+
parseOperation(strOperation.toLowerCase).map { op =>
- SearchFilterExpr.Atom.Binary(dimensionName, op, value.safeTrim)
+ SearchFilterExpr.Atom.Binary(dimensionName, op, updatedValue.asInstanceOf[AnyRef])
}
}
}
@@ -68,7 +73,7 @@ object SearchFilterParser {
}
private val numericOperatorParser: Parser[String] = {
- P((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?).!
+ P(IgnoreCase("eq") | ((IgnoreCase("gt") | IgnoreCase("lt")) ~ IgnoreCase("eq").?)).!
}
private val naryOperatorParser: Parser[String] = P(IgnoreCase("in")).!
@@ -91,10 +96,12 @@ object SearchFilterParser {
private val nAryValueParser: Parser[String] = P(CharPred(_ != ',').rep(min = 1).!)
+ private val longParser: Parser[Long] = P(CharIn('0' to '9').rep(1).!.map(_.toLong))
+
private val binaryAtomParser: Parser[SearchFilterExpr.Atom.Binary] = P(
dimensionParser ~ whitespaceParser ~ (
- (commonOperatorParser.! ~/ whitespaceParser ~/ AnyChar.rep(min = 1).!)
- | (numericOperatorParser.! ~/ whitespaceParser ~/ numberParser.!)
+ (numericOperatorParser.! ~ whitespaceParser ~ (longParser | numberParser.!)) |
+ (commonOperatorParser.! ~ whitespaceParser ~ AnyChar.rep(min = 1).!)
) ~ End
).map {
case BinaryAtomFromTuple(atom) => atom
diff --git a/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala b/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala
index c1a2c7c..6c87858 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala
@@ -42,7 +42,7 @@ object CustomSwaggerJsonFormats {
xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextTrialIssue()),
classOf[RichCriterion] -> richCriterionFormat.write(
xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextRichCriterion()),
- classOf[InterventionWithArms] -> interventionWriter.write(
+ classOf[InterventionWithArms] -> interventionFormat.write(
xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextInterventionWithArms()),
classOf[InterventionType] -> interventionTypeFormat.write(
xyz.driver.pdsuidomain.fakes.entities.trialcuration.nextInterventionType()),
diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala
index 1f73184..839fead 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Document.scala
@@ -14,6 +14,8 @@ import xyz.driver.pdsuicommon.validation.Validators
import xyz.driver.pdsuicommon.validation.Validators.Validator
import xyz.driver.pdsuidomain.entities.Document.Meta
+import scalaz.Equal
+
final case class ProviderType(id: LongId[ProviderType], name: String)
object ProviderType {
@@ -124,6 +126,8 @@ object DocumentType {
}
}
+ implicit def equal: Equal[DocumentType] = Equal.equal[DocumentType](_ == _)
+
implicit def toPhiString(x: DocumentType): PhiString = {
import x._
phi"DocumentType(id=$id, name=${Unsafe(name)})"
diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
index cb677cf..9ccc267 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala
@@ -2,10 +2,186 @@ package xyz.driver.pdsuidomain.entities
import xyz.driver.pdsuicommon.domain.{LongId, StringId}
import xyz.driver.pdsuicommon.logging._
+import xyz.driver.pdsuidomain.entities.InterventionType.DeliveryMethod
+import xyz.driver.pdsuidomain.entities.InterventionType.DeliveryMethod._
-final case class InterventionType(id: LongId[InterventionType], name: String)
+sealed trait InterventionType {
+ val id: LongId[InterventionType]
+ val name: String
+ val deliveryMethods: Set[DeliveryMethod]
+}
object InterventionType {
+
+ final case object RadiationTherapy extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](1)
+ val name: String = "Radiation therapy"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object Chemotherapy extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](2)
+ val name: String = "Chemotherapy"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object TargetedTherapy extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](3)
+ val name: String = "Targeted therapy"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object Immunotherapy extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](4)
+ val name: String = "Immunotherapy"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object Surgery extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](5)
+ val name: String = "Surgery"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object HormoneTherapy extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](6)
+ val name: String = "Hormone therapy"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object Other extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](7)
+ val name: String = "Other"
+ val deliveryMethods: Set[DeliveryMethod] = commonMethods
+ }
+
+ final case object Radiation extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](8)
+ val name: String = "Radiation"
+ val deliveryMethods: Set[DeliveryMethod] = Set(
+ ExternalRadiationTherapy,
+ Brachytherapy,
+ SystemicRadiationTherapyIV,
+ SystemicRadiationTherapyOral,
+ ProtonBeamTherapy
+ )
+ }
+
+ final case object SurgeryProcedure extends InterventionType {
+ val id: LongId[InterventionType] = LongId[InterventionType](9)
+ val name: String = "Surgery/Procedure"
+ val deliveryMethods: Set[DeliveryMethod] = Set(
+ RadioFrequencyAblationRFA,
+ Cryoablation,
+ TherapeuticConventionalSurgery,
+ RoboticAssistedLaparoscopicSurgery
+ )
+ }
+
+ def typeFromString: PartialFunction[String, InterventionType] = {
+ case "Radiation therapy" => RadiationTherapy
+ case "Chemotherapy" => Chemotherapy
+ case "Targeted therapy" => TargetedTherapy
+ case "Immunotherapy" => Immunotherapy
+ case "Surgery" => Surgery
+ case "Hormone therapy" => HormoneTherapy
+ case "Other" => Other
+ case "Radiation" => Radiation
+ case "Surgery/Procedure" => SurgeryProcedure
+ }
+
+ sealed trait DeliveryMethod
+ object DeliveryMethod {
+ case object IntravenousInfusionIV extends DeliveryMethod
+ case object IntramuscularInjection extends DeliveryMethod
+ case object SubcutaneousInjection extends DeliveryMethod
+ case object IntradermalInjection extends DeliveryMethod
+ case object SpinalInjection extends DeliveryMethod
+ case object Oral extends DeliveryMethod
+ case object Topical extends DeliveryMethod
+ case object TransdermalPatch extends DeliveryMethod
+ case object Inhalation extends DeliveryMethod
+ case object Rectal extends DeliveryMethod
+ case object ExternalRadiationTherapy extends DeliveryMethod
+ case object Brachytherapy extends DeliveryMethod
+ case object SystemicRadiationTherapyIV extends DeliveryMethod
+ case object SystemicRadiationTherapyOral extends DeliveryMethod
+ case object ProtonBeamTherapy extends DeliveryMethod
+ case object RadioFrequencyAblationRFA extends DeliveryMethod
+ case object Cryoablation extends DeliveryMethod
+ case object TherapeuticConventionalSurgery extends DeliveryMethod
+ case object RoboticAssistedLaparoscopicSurgery extends DeliveryMethod
+
+ def fromString: PartialFunction[String, DeliveryMethod] = {
+ case "Intravenous Infusion (IV)" => IntravenousInfusionIV
+ case "Intramuscular Injection" => IntramuscularInjection
+ case "Subcutaneous Injection" => SubcutaneousInjection
+ case "Intradermal Injection" => IntradermalInjection
+ case "Spinal Injection" => SpinalInjection
+ case "Oral" => Oral
+ case "Topical" => Topical
+ case "Transdermal Patch" => TransdermalPatch
+ case "Inhalation" => Inhalation
+ case "Rectal" => Rectal
+ case "External Radiation Therapy" => ExternalRadiationTherapy
+ case "Brachytherapy" => Brachytherapy
+ case "Systemic Radiation Therapy (IV)" => SystemicRadiationTherapyIV
+ case "Systemic Radiation Therapy (Oral)" => SystemicRadiationTherapyOral
+ case "Proton Beam Therapy" => ProtonBeamTherapy
+ case "Radio-Frequency Ablation (RFA)" => RadioFrequencyAblationRFA
+ case "Cryoablation" => Cryoablation
+ case "Therapeutic Conventional Surgery" => TherapeuticConventionalSurgery
+ case "Robotic Assisted Laparoscopic Surgery" => RoboticAssistedLaparoscopicSurgery
+ }
+
+ def methodToString(x: DeliveryMethod): String = x match {
+ case IntravenousInfusionIV => "Intravenous Infusion (IV)"
+ case IntramuscularInjection => "Intramuscular Injection"
+ case SubcutaneousInjection => "Subcutaneous Injection"
+ case IntradermalInjection => "Intradermal Injection"
+ case SpinalInjection => "Spinal Injection"
+ case Oral => "Oral"
+ case Topical => "Topical"
+ case TransdermalPatch => "Transdermal Patch"
+ case Inhalation => "Inhalation"
+ case Rectal => "Rectal"
+ case ExternalRadiationTherapy => "External Radiation Therapy"
+ case Brachytherapy => "Brachytherapy"
+ case SystemicRadiationTherapyIV => "Systemic Radiation Therapy (IV)"
+ case SystemicRadiationTherapyOral => "Systemic Radiation Therapy (Oral)"
+ case ProtonBeamTherapy => "Proton Beam Therapy"
+ case RadioFrequencyAblationRFA => "Radio-Frequency Ablation (RFA)"
+ case Cryoablation => "Cryoablation"
+ case TherapeuticConventionalSurgery => "Therapeutic Conventional Surgery"
+ case RoboticAssistedLaparoscopicSurgery => "Robotic Assisted Laparoscopic Surgery"
+ }
+ }
+
+ val commonMethods = Set[DeliveryMethod](
+ IntravenousInfusionIV,
+ IntramuscularInjection,
+ SubcutaneousInjection,
+ IntradermalInjection,
+ SpinalInjection,
+ Oral,
+ Topical,
+ TransdermalPatch,
+ Inhalation,
+ Rectal
+ )
+
+ val All: Map[LongId[InterventionType], InterventionType] = Map[LongId[InterventionType], InterventionType](
+ LongId[InterventionType](1) -> RadiationTherapy,
+ LongId[InterventionType](2) -> Chemotherapy,
+ LongId[InterventionType](3) -> TargetedTherapy,
+ LongId[InterventionType](4) -> Immunotherapy,
+ LongId[InterventionType](5) -> Surgery,
+ LongId[InterventionType](6) -> HormoneTherapy,
+ LongId[InterventionType](7) -> Other,
+ LongId[InterventionType](8) -> Radiation,
+ LongId[InterventionType](9) -> SurgeryProcedure
+ )
+
implicit def toPhiString(x: InterventionType): PhiString = {
import x._
phi"InterventionType(id=$id, name=${Unsafe(name)})"
@@ -29,12 +205,23 @@ final case class Intervention(id: LongId[Intervention],
originalType: Option[String],
dosage: String,
originalDosage: String,
- isActive: Boolean)
+ isActive: Boolean,
+ deliveryMethod: Option[String]) {
+ def deliveryMethodIsCorrect: Boolean = {
+ if (this.typeId.nonEmpty) {
+ this.deliveryMethod.nonEmpty &&
+ InterventionType.All
+ .getOrElse(this.typeId.get, throw new IllegalArgumentException(s"Not found Intervention type ${this.typeId}"))
+ .deliveryMethods
+ .contains(DeliveryMethod.fromString(this.deliveryMethod.get))
+ } else true
+ }
+}
object Intervention {
implicit def toPhiString(x: Intervention): PhiString = {
import x._
- phi"Intervention(id=$id, trialId=$trialId, name=${Unsafe(name)}, typeId=$typeId, isActive=$isActive)"
+ phi"Intervention(id=$id, trialId=$trialId, name=${Unsafe(name)}, typeId=$typeId, isActive=$isActive, deliveryMethod=${Unsafe(deliveryMethod)})"
}
}
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
index 60b74ff..cf55694 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/entities/export/trial/ExportTrialWithLabels.scala
@@ -27,7 +27,7 @@ object ExportTrialWithLabels {
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")
+ assert(trials.size == 1, "There are more than one trial in the rawData")
val trial = rawData.head
ExportTrialWithLabels(
@@ -43,9 +43,7 @@ object ExportTrialWithLabels {
ExportTrialArm(armId, rawTrials.head.armName)
}(breakOut),
criteria = rawData
- .groupBy { x =>
- (x.criterionId, x.labelId)
- }
+ .groupBy(x => (x.criterionId, x.labelId))
.map {
case (_, rawTrialLabels) =>
val armIds = rawTrialLabels.map(_.criterionArmId).toSet
@@ -53,5 +51,4 @@ object ExportTrialWithLabels {
}(breakOut)
)
}
-
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala
index 52d7b98..b259b07 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala
@@ -4,6 +4,7 @@ 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
@@ -20,10 +21,11 @@ object common {
def nextLocalDateTime = LocalDateTime.of(nextLocalDate, LocalTime.MIDNIGHT)
- def nextLocalDate = {
- val date = generators.nextDate()
- LocalDate.of(date.year, date.month + 1, date.day + 1)
- }
+ 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)
diff --git a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala
index ecb6e0a..fe5bf09 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala
@@ -7,6 +7,7 @@ import xyz.driver.pdsuidomain.services.CriterionService.RichCriterion
object trialcuration {
import xyz.driver.core.generators
import common._
+ import xyz.driver.pdsuidomain.entities.InterventionType._
def nextTrial(): Trial = Trial(
id = nextStringId[Trial],
@@ -76,7 +77,8 @@ object trialcuration {
originalType = Option(generators.nextString()),
dosage = generators.nextString(),
originalDosage = generators.nextString(),
- isActive = generators.nextBoolean()
+ isActive = generators.nextBoolean(),
+ deliveryMethod = Option(generators.nextString())
)
def nextInterventionArm(interventionId: LongId[Intervention]): InterventionArm = InterventionArm(
@@ -129,9 +131,16 @@ object trialcuration {
name = generators.nextString()
)
- def nextInterventionType(): InterventionType = InterventionType(
- id = nextLongId[InterventionType],
- name = generators.nextString()
+ def nextInterventionType(): InterventionType = generators.oneOf[InterventionType](
+ RadiationTherapy,
+ Chemotherapy,
+ TargetedTherapy,
+ Immunotherapy,
+ Surgery,
+ HormoneTherapy,
+ Other,
+ Radiation,
+ SurgeryProcedure
)
}
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 f306a71..072ed25 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
@@ -12,6 +12,7 @@ final case class ApiIntervention(id: Long,
isActive: Boolean,
arms: List[Long],
trialId: String,
+ deliveryMethod: Option[String],
originalName: String,
originalDosage: String,
originalType: Option[String]) {
@@ -26,7 +27,8 @@ final case class ApiIntervention(id: Long,
originalType = this.originalType.map(id => id.toString),
dosage = this.dosage,
originalDosage = this.originalDosage,
- isActive = this.isActive
+ isActive = this.isActive,
+ deliveryMethod = this.deliveryMethod
)
InterventionWithArms(intervention, this.arms.map { armId =>
@@ -47,6 +49,7 @@ object ApiIntervention {
(JsPath \ "isActive").format[Boolean] and
(JsPath \ "arms").format[List[Long]] and
(JsPath \ "trialId").format[String] and
+ (JsPath \ "deliveryMethod").formatNullable[String] and
(JsPath \ "originalName").format[String] and
(JsPath \ "originalDosage").format[String] and
(JsPath \ "originalType").formatNullable[String]
@@ -64,6 +67,7 @@ object ApiIntervention {
isActive = intervention.isActive,
arms = arms.map(_.armId.id),
trialId = intervention.trialId.id,
+ deliveryMethod = intervention.deliveryMethod,
originalName = intervention.originalName,
originalDosage = intervention.originalDosage,
originalType = intervention.originalType
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
index ebef225..3db8bfa 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/intervention/ApiInterventionType.scala
@@ -2,23 +2,25 @@ package xyz.driver.pdsuidomain.formats.json.intervention
import play.api.libs.functional.syntax._
import play.api.libs.json.{Format, JsPath}
-import xyz.driver.pdsuicommon.domain.LongId
import xyz.driver.pdsuidomain.entities.InterventionType
+import xyz.driver.pdsuidomain.entities.InterventionType.DeliveryMethod
-final case class ApiInterventionType(id: Long, name: String) {
+final case class ApiInterventionType(id: Long, name: String, deliveryMethods: List[String]) {
- def toDomain = InterventionType(id = LongId[InterventionType](id), name = name)
+ def toDomain = InterventionType.typeFromString(name)
}
object ApiInterventionType {
implicit val format: Format[ApiInterventionType] = (
(JsPath \ "id").format[Long] and
- (JsPath \ "name").format[String]
+ (JsPath \ "name").format[String] and
+ (JsPath \ "deliveryMethods").format[List[String]]
)(ApiInterventionType.apply, unlift(ApiInterventionType.unapply))
def fromDomain(interventionType: InterventionType) = ApiInterventionType(
id = interventionType.id.id,
- name = interventionType.name
+ name = interventionType.name,
+ deliveryMethods = interventionType.deliveryMethods.map(DeliveryMethod.methodToString).toList
)
}
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 aa55506..28a8555 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
@@ -1,42 +1,88 @@
package xyz.driver.pdsuidomain.formats.json.intervention
-import xyz.driver.pdsuicommon.domain.LongId
-import xyz.driver.pdsuidomain.entities.{InterventionArm, InterventionWithArms}
+import play.api.data.validation.Invalid
+import xyz.driver.pdsuicommon.domain.{LongId, StringId}
+import xyz.driver.pdsuidomain.entities.{Intervention, InterventionArm, InterventionWithArms, Trial}
import play.api.libs.functional.syntax._
import play.api.libs.json._
+import xyz.driver.pdsuicommon.json.JsonValidationException
+import xyz.driver.pdsuicommon.validation.{AdditionalConstraints, JsonValidationErrors}
-final case class ApiPartialIntervention(typeId: Option[Long],
+import scala.collection.breakOut
+import scala.util.Try
+
+final case class ApiPartialIntervention(name: Option[String],
+ trialId: Option[String],
+ typeId: Option[Long],
dosage: Option[String],
isActive: Option[Boolean],
+ deliveryMethod: Option[String],
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)))
+ val draftArmList = arms.map(_.map(x => InterventionArm(armId = LongId(x), interventionId = orig.intervention.id)))
orig.copy(
intervention = origIntervention.copy(
+ name = name.getOrElse(origIntervention.name),
typeId = typeId.map(LongId(_)).orElse(origIntervention.typeId),
dosage = dosage.getOrElse(origIntervention.dosage),
- isActive = isActive.getOrElse(origIntervention.isActive)
+ isActive = isActive.getOrElse(origIntervention.isActive),
+ deliveryMethod = deliveryMethod.orElse(origIntervention.deliveryMethod)
),
arms = draftArmList.getOrElse(orig.arms)
)
}
+
+ def toDomain: Try[InterventionWithArms] = Try {
+ val validation = Map(JsPath \ "trialId" -> AdditionalConstraints.optionNonEmptyConstraint(trialId))
+
+ val validationErrors: JsonValidationErrors = validation.collect({
+ case (fieldName, e: Invalid) => (fieldName, e.errors)
+ })(breakOut)
+
+ if (validationErrors.isEmpty) {
+ InterventionWithArms(
+ intervention = Intervention(
+ id = LongId(0),
+ trialId = trialId.map(StringId[Trial]).get,
+ name = name.getOrElse(""),
+ originalName = name.getOrElse(""),
+ typeId = typeId.map(LongId(_)),
+ originalType = Option(""),
+ dosage = dosage.getOrElse(""),
+ originalDosage = dosage.getOrElse(""),
+ isActive = isActive.getOrElse(false),
+ deliveryMethod = deliveryMethod
+ ),
+ arms =
+ arms.map(_.map(x => InterventionArm(armId = LongId(x), interventionId = LongId(0)))).getOrElse(List.empty)
+ )
+ } else {
+ throw new JsonValidationException(validationErrors)
+ }
+ }
}
object ApiPartialIntervention {
private val reads: Reads[ApiPartialIntervention] = (
- (JsPath \ "typeId").readNullable[Long] and
+ (JsPath \ "name").readNullable[String] and
+ (JsPath \ "trialId").readNullable[String] and
+ (JsPath \ "typeId").readNullable[Long] and
(JsPath \ "dosage").readNullable[String] and
(JsPath \ "isActive").readNullable[Boolean] and
+ (JsPath \ "deliveryMethod").readNullable[String] and
(JsPath \ "arms").readNullable[List[Long]]
)(ApiPartialIntervention.apply _)
private val writes: Writes[ApiPartialIntervention] = (
- (JsPath \ "typeId").writeNullable[Long] and
+ (JsPath \ "name").writeNullable[String] and
+ (JsPath \ "trialId").writeNullable[String] and
+ (JsPath \ "typeId").writeNullable[Long] and
(JsPath \ "dosage").writeNullable[String] and
(JsPath \ "isActive").writeNullable[Boolean] and
+ (JsPath \ "deliveryMethod").writeNullable[String] and
(JsPath \ "arms").writeNullable[List[Long]]
)(unlift(ApiPartialIntervention.unapply))
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
index b255892..e7b58cd 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiRecord.scala
@@ -13,49 +13,11 @@ import play.api.libs.json._
import xyz.driver.pdsuicommon.json.JsonSerializer
import xyz.driver.pdsuicommon.domain.{LongId, StringId, TextJson, UuidId}
-final case class ApiRecord(id: Long,
- patientId: String,
- caseId: Option[String],
- disease: String,
- physician: Option[String],
- lastUpdate: ZonedDateTime,
- status: String,
- previousStatus: Option[String],
- assignee: Option[String],
- previousAssignee: Option[String],
- lastActiveUser: Option[String],
- requestId: UUID,
- meta: String) {
-
- private def extractStatus(status: String): Status =
- Status
- .fromString(status)
- .getOrElse(
- throw new NoSuchElementException(s"Status $status not found")
- )
-
- def toDomain = MedicalRecord(
- id = LongId(this.id),
- status = extractStatus(this.status),
- previousStatus = this.previousStatus.map(extractStatus),
- assignee = this.assignee.map(StringId(_)),
- previousAssignee = this.previousAssignee.map(StringId(_)),
- lastActiveUserId = this.lastActiveUser.map(StringId(_)),
- patientId = UuidId(patientId),
- requestId = RecordRequestId(this.requestId),
- disease = this.disease,
- caseId = caseId.map(CaseId(_)),
- physician = this.physician,
- meta = Some(TextJson(JsonSerializer.deserialize[List[MedicalRecord.Meta]](this.meta))),
- predictedMeta = None,
- predictedDocuments = None,
- lastUpdate = this.lastUpdate.toLocalDateTime()
- )
-
-}
-
object ApiRecord {
+ private val emptyMeta: String =
+ "[]"
+
private val statusFormat = Format(
Reads.StringReads.filter(ValidationError("unknown status")) {
case x if MedicalRecordStatus.statusFromString.isDefinedAt(x) => true
@@ -95,6 +57,52 @@ object ApiRecord {
previousAssignee = record.previousAssignee.map(_.id),
lastActiveUser = record.lastActiveUserId.map(_.id),
requestId = record.requestId.id,
- meta = record.meta.map(x => JsonSerializer.serialize(x.content)).getOrElse("[]")
+ meta = record.meta.map(x => JsonSerializer.serialize(x.content)).getOrElse(emptyMeta)
+ )
+}
+
+final case class ApiRecord(id: Long,
+ patientId: String,
+ caseId: Option[String],
+ disease: String,
+ physician: Option[String],
+ lastUpdate: ZonedDateTime,
+ status: String,
+ previousStatus: Option[String],
+ assignee: Option[String],
+ previousAssignee: Option[String],
+ lastActiveUser: Option[String],
+ requestId: UUID,
+ meta: String) {
+
+ private def extractStatus(status: String): Status =
+ Status
+ .fromString(status)
+ .getOrElse(
+ throw new NoSuchElementException(s"Status $status not found")
+ )
+
+ def toDomain = MedicalRecord(
+ id = LongId(this.id),
+ status = extractStatus(this.status),
+ previousStatus = this.previousStatus.map(extractStatus),
+ assignee = this.assignee.map(StringId(_)),
+ previousAssignee = this.previousAssignee.map(StringId(_)),
+ lastActiveUserId = this.lastActiveUser.map(StringId(_)),
+ patientId = UuidId(patientId),
+ requestId = RecordRequestId(this.requestId),
+ disease = this.disease,
+ caseId = caseId.map(CaseId(_)),
+ physician = this.physician,
+ meta = {
+ if (this.meta == ApiRecord.emptyMeta) {
+ None
+ } else {
+ Some(TextJson(JsonSerializer.deserialize[List[MedicalRecord.Meta]](this.meta)))
+ }
+ },
+ predictedMeta = None,
+ predictedDocuments = None,
+ lastUpdate = this.lastUpdate.toLocalDateTime()
)
}
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
index 47bc493..05d5a60 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/formats/json/record/ApiUpdateRecord.scala
@@ -37,7 +37,7 @@ object ApiUpdateRecord {
JsSuccess(Json.stringify(x))
})
.map {
- case Tristate.Present("{}") => Tristate.Absent
+ case Tristate.Present("[]") => Tristate.Absent
case x => x
}
)(ApiUpdateRecord.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 9314391..62cb9fa 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
@@ -1,14 +1,15 @@
package xyz.driver.pdsuidomain.formats.json.sprayformats
import spray.json._
-import xyz.driver.pdsuicommon.domain.LongId
+import xyz.driver.pdsuicommon.domain.{LongId, StringId}
+import xyz.driver.pdsuidomain.entities.InterventionType.DeliveryMethod
import xyz.driver.pdsuidomain.entities._
object intervention {
import DefaultJsonProtocol._
import common._
- implicit val interventionWriter: JsonWriter[InterventionWithArms] = new JsonWriter[InterventionWithArms] {
+ implicit val interventionFormat: JsonFormat[InterventionWithArms] = new RootJsonFormat[InterventionWithArms] {
override def write(obj: InterventionWithArms) =
JsObject(
"id" -> obj.intervention.id.toJson,
@@ -18,14 +19,72 @@ object intervention {
"isActive" -> obj.intervention.isActive.toJson,
"arms" -> obj.arms.map(_.armId).toJson,
"trialId" -> obj.intervention.trialId.toJson,
+ "deliveryMethod" -> obj.intervention.deliveryMethod.toJson,
"originalName" -> obj.intervention.originalName.toJson,
"originalDosage" -> obj.intervention.originalDosage.toJson,
"originalType" -> obj.intervention.originalType.toJson
)
+
+ override def read(json: JsValue): InterventionWithArms = json match {
+ case JsObject(fields) =>
+ val trialId = fields
+ .get("trialId")
+ .map(_.convertTo[StringId[Trial]])
+ .getOrElse(deserializationError(s"Intervention json object does not contain `trialId` field: $json"))
+
+ val typeId = fields
+ .get("typeId")
+ .map(_.convertTo[LongId[InterventionType]])
+
+ val name = fields
+ .get("name")
+ .map(_.convertTo[String])
+ .getOrElse("")
+
+ val dosage = fields
+ .get("dosage")
+ .map(_.convertTo[String])
+ .getOrElse("")
+
+ val isActive = fields
+ .get("isActive")
+ .exists(_.convertTo[Boolean])
+
+ val deliveryMethod = fields
+ .get("deliveryMethod")
+ .map(_.convertTo[String])
+
+ val arms = fields
+ .get("arms")
+ .map(_.convertTo[List[LongId[Arm]]].map(x => InterventionArm(armId = x, interventionId = LongId(0))))
+ .getOrElse(List.empty[InterventionArm])
+
+ InterventionWithArms(
+ intervention = Intervention(
+ id = LongId(0),
+ trialId = trialId,
+ name = name,
+ originalName = name,
+ typeId = typeId,
+ originalType = None,
+ dosage = dosage,
+ originalDosage = dosage,
+ isActive = isActive,
+ deliveryMethod = deliveryMethod
+ ),
+ arms = arms
+ )
+
+ case _ => deserializationError(s"Expected Json Object as create Intervention json, but got $json")
+ }
}
def applyUpdateToInterventionWithArms(json: JsValue, orig: InterventionWithArms): InterventionWithArms = json match {
case JsObject(fields) =>
+ val name = fields
+ .get("name")
+ .map(_.convertTo[String])
+
val typeId = fields
.get("typeId")
.map(_.convertTo[LongId[InterventionType]])
@@ -38,6 +97,10 @@ object intervention {
.get("isActive")
.map(_.convertTo[Boolean])
+ val deliveryMethod = fields
+ .get("deliveryMethod")
+ .map(_.convertTo[String])
+
val origIntervention = orig.intervention
val arms = fields
.get("arms")
@@ -45,9 +108,11 @@ object intervention {
orig.copy(
intervention = origIntervention.copy(
+ name = name.getOrElse(origIntervention.name),
typeId = typeId.orElse(origIntervention.typeId),
dosage = dosage.getOrElse(origIntervention.dosage),
- isActive = isActive.getOrElse(origIntervention.isActive)
+ isActive = isActive.getOrElse(origIntervention.isActive),
+ deliveryMethod = deliveryMethod.orElse(origIntervention.deliveryMethod)
),
arms = arms.getOrElse(orig.arms)
)
@@ -55,6 +120,25 @@ object intervention {
case _ => deserializationError(s"Expected Json Object as partial Intervention, but got $json")
}
- implicit val interventionTypeFormat: RootJsonFormat[InterventionType] = jsonFormat2(InterventionType.apply)
+ implicit val interventionTypeFormat: JsonFormat[InterventionType] = new RootJsonFormat[InterventionType] {
+ override def read(json: JsValue) = json match {
+ case JsObject(fields) =>
+ val name = fields
+ .get("name")
+ .map(_.convertTo[String])
+ .getOrElse(deserializationError(s"Intervention type json object does not contain `name` field: $json"))
+
+ InterventionType.typeFromString(name)
+
+ case _ => deserializationError(s"Expected Json Object as Intervention type, but got $json")
+ }
+
+ override def write(obj: InterventionType) =
+ JsObject(
+ "id" -> obj.id.toJson,
+ "name" -> obj.name.toJson,
+ "deliveryMethods" -> obj.deliveryMethods.map(DeliveryMethod.methodToString).toJson
+ )
+ }
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala
index 439e456..1e7c7f1 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/InterventionService.scala
@@ -61,6 +61,37 @@ object InterventionService {
final case class CommonError(userMessage: String) extends UpdateReply with DomainError
}
+ sealed trait CreateReply
+ object CreateReply {
+ final case class Created(x: InterventionWithArms) extends CreateReply
+
+ type Error = CreateReply with DomainError
+
+ case object AuthorizationError
+ extends CreateReply with DefaultAccessDeniedError with DomainError.AuthorizationError
+
+ final case class CommonError(userMessage: String) extends CreateReply with DomainError
+
+ 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
+
+ final case class CommonError(userMessage: String) extends DeleteReply with DomainError
+ }
+
}
trait InterventionService {
@@ -76,4 +107,9 @@ trait InterventionService {
def update(origIntervention: InterventionWithArms, draftIntervention: InterventionWithArms)(
implicit requestContext: AuthenticatedRequestContext): Future[UpdateReply]
+
+ def create(draftIntervention: InterventionWithArms)(
+ implicit requestContext: AuthenticatedRequestContext): Future[CreateReply]
+
+ def delete(id: LongId[Intervention])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply]
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
index e593c3b..025a48a 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
@@ -1,7 +1,6 @@
package xyz.driver.pdsuidomain.services.rest
import scala.concurrent.{ExecutionContext, Future}
-
import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model._
import akka.stream.Materializer
@@ -61,4 +60,26 @@ class RestInterventionService(transport: ServiceTransport, baseUri: Uri)(
}
}
+ def create(draftIntervention: InterventionWithArms)(
+ implicit requestContext: AuthenticatedRequestContext): Future[CreateReply] = {
+ for {
+ entity <- Marshal(ApiIntervention.fromDomain(draftIntervention)).to[RequestEntity]
+ request = HttpRequest(HttpMethods.POST, endpointUri(baseUri, "/v1/intervention")).withEntity(entity)
+ response <- transport.sendRequestGetResponse(requestContext)(request)
+ reply <- apiResponse[ApiIntervention](response)
+ } yield {
+ CreateReply.Created(reply.toDomain)
+ }
+ }
+
+ def delete(id: LongId[Intervention])(implicit requestContext: AuthenticatedRequestContext): Future[DeleteReply] = {
+ val request = HttpRequest(HttpMethods.DELETE, endpointUri(baseUri, s"/v1/intervention/$id"))
+ for {
+ response <- transport.sendRequestGetResponse(requestContext)(request)
+ _ <- apiResponse[HttpEntity](response)
+ } yield {
+ DeleteReply.Deleted
+ }
+ }
+
}
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 f826b98..b77e6df 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
@@ -34,9 +34,9 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(implicit prote
}
}
- def getTrialWithLabels(trialId: StringId[Trial], condition: String)(
+ def getTrialWithLabels(trialId: StringId[Trial], disease: String)(
implicit requestContext: AuthenticatedRequestContext): Future[GetTrialWithLabelsReply] = {
- val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/export/trial/$trialId"))
+ val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/export/trial/$disease/$trialId"))
for {
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiExportTrialWithLabels](response)
@@ -47,7 +47,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(implicit prote
def getPdfSource(trialId: StringId[Trial])(
implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] = {
- val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/trial/${trialId}/source"))
+ val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, s"/v1/trial/$trialId/source"))
for {
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[HttpEntity](response)
diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala
index f47f4c2..ba67d13 100644
--- a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala
+++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala
@@ -9,6 +9,7 @@ import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.{Gen, Prop}
import org.scalatest.FreeSpecLike
import org.scalatest.prop.Checkers
+import xyz.driver.pdsuicommon.db.SearchFilterBinaryOperation.Eq
import xyz.driver.pdsuicommon.db.SearchFilterNAryOperation.In
import xyz.driver.pdsuicommon.utils.Utils
import xyz.driver.pdsuicommon.utils.Utils._
@@ -104,6 +105,13 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers {
}
}
+ "actual recordId" - {
+ "should not be parsed with text values" in {
+ val filter = SearchFilterParser.parse(Seq("filters" -> "recordId EQ 1"))
+ assert(filter === Success(SearchFilterExpr.Atom.Binary(Dimension(None, "record_id"), Eq, Long.box(1))))
+ }
+ }
+
"all operators" - {
"should be parsed with numeric values" in check {
val testQueryGen = queryGen(
@@ -181,7 +189,7 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers {
private val nonEmptyString = arbitrary[String].filter { s => !s.safeTrim.isEmpty }
- private val numericBinaryAtomValuesGen: Gen[String] = arbitrary[BigInt].map(_.toString)
+ private val numericBinaryAtomValuesGen: Gen[String] = arbitrary[Long].map(_.toString)
private val inValueGen: Gen[String] = {
Gen.nonEmptyContainerOf[Seq, Char](inValueCharsGen).map(_.mkString).filter(_.safeTrim.nonEmpty)
}
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 0f01d4a..dad39c8 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
@@ -18,7 +18,8 @@ class InterventionFormatSuite extends FlatSpec with Matchers {
originalType = Some("orig type"),
dosage = "",
originalDosage = "",
- isActive = true
+ isActive = true,
+ deliveryMethod = Some("pill")
)
val arms = List(
InterventionArm(interventionId = intervention.id, armId = LongId(20)),
@@ -29,11 +30,21 @@ class InterventionFormatSuite extends FlatSpec with Matchers {
intervention = intervention,
arms = arms
)
- val writtenJson = interventionWriter.write(orig)
+ val writtenJson = interventionFormat.write(orig)
writtenJson should be(
"""{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22],
- "trialId":"NCT000001","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson)
+ "trialId":"NCT000001","deliveryMethod":"pill","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson)
+
+ val createInterventionJson =
+ """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22],
+ "trialId":"NCT000001","deliveryMethod":"pill"}""".parseJson
+ val parsedCreateIntervention = interventionFormat.read(createInterventionJson)
+ val expectedCreateIntervention = parsedCreateIntervention.copy(
+ intervention = intervention.copy(id = LongId(0), originalType = None, originalName = "intervention name"),
+ arms = arms.map(_.copy(interventionId = LongId(0)))
+ )
+ parsedCreateIntervention should be(expectedCreateIntervention)
val updateInterventionJson = """{"dosage":"descr","arms":[21,22]}""".parseJson
val expectedUpdatedIntervention = orig.copy(
@@ -48,13 +59,12 @@ class InterventionFormatSuite extends FlatSpec with Matchers {
}
"Json format for InterventionType" should "read and write correct JSON" in {
- val interventionType = InterventionType(
- id = LongId(10),
- name = "type name"
- )
+ val interventionType = InterventionType.typeFromString("Surgery/Procedure")
val writtenJson = interventionTypeFormat.write(interventionType)
- writtenJson should be("""{"id":10,"name":"type name"}""".parseJson)
+ writtenJson should be(
+ """{"id":9,"name":"Surgery/Procedure","deliveryMethods":["Radio-Frequency Ablation (RFA)",
+ "Cryoablation","Therapeutic Conventional Surgery","Robotic Assisted Laparoscopic Surgery"]}""".parseJson)
val parsedInterventionType = interventionTypeFormat.read(writtenJson)
parsedInterventionType should be(interventionType)