From 985ee69beed836b97f3476306736d3f15ce37e1c Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 2 Aug 2017 13:38:49 -0700 Subject: Add parsers to common --- .../pdsuicommon/parsers/DimensionsParser.scala | 33 +++++ .../pdsuicommon/parsers/ListRequestParser.scala | 20 +++ .../pdsuicommon/parsers/PaginationParser.scala | 60 +++++++++ .../parsers/ParseQueryArgException.scala | 3 + .../pdsuicommon/parsers/SearchFilterParser.scala | 145 +++++++++++++++++++++ .../driver/pdsuicommon/parsers/SortingParser.scala | 55 ++++++++ 6 files changed, 316 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala (limited to 'src/main/scala/xyz/driver') 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..147102b --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala @@ -0,0 +1,33 @@ +package xyz.driver.server.parsers + +import play.api.libs.json._ +import play.api.routing.sird._ +import xyz.driver.pdsuicommon.utils.WritesUtils + +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 { + + private class DimensionsWrapper[T](dimensions: Dimensions)(implicit orig: Writes[T]) extends Writes[T] { + private val filteredWrites = WritesUtils.filterKeys[T](dimensions.contains) + override def writes(o: T): JsValue = filteredWrites.writes(o) + } + + def tryParse(queryString: QueryString): Try[Dimensions] = { + val rawDimensions = queryString.getOrElse("dimensions", Seq.empty) + rawDimensions 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/ListRequestParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala new file mode 100644 index 0000000..617a77e --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala @@ -0,0 +1,20 @@ +package xyz.driver.server.parsers + +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import play.api.mvc._ + +import scala.util.Try + +final case class ListRequestParameters(filter: SearchFilterExpr, sorting: Sorting, pagination: Pagination) + +class ListRequestParser(validSortingFields: Set[String]) { + + def tryParse(request: Request[AnyContent]): Try[ListRequestParameters] = { + for { + queryFilters <- SearchFilterParser.parse(request.queryString) + sorting <- SortingParser.parse(validSortingFields, request.queryString) + pagination <- PaginationParser.parse(request.queryString) + } yield ListRequestParameters(queryFilters, sorting, pagination) + } + +} 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..a6a7fae --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala @@ -0,0 +1,60 @@ +package xyz.driver.server.parsers + +import xyz.driver.pdsuicommon.db.Pagination +import xyz.driver.server.parsers.errors.ParseQueryArgException +import play.api.data.validation._ +import play.api.routing.sird.QueryString +import xyz.driver.pdsuicommon.validation.AdditionalConstraints + +import scala.util.Try + +object PaginationParser { + + private val oneQueryArgConstraint: Constraint[Seq[String]] = { + Constraint("query.oneArg") { + case Nil => Valid + case x +: Nil => Valid + case xs => + Invalid(new ValidationError(Seq(s"must be one argument, but there are multiple: '${xs.mkString(", ")}'"))) + } + } + + private val pageSizeCheckConstraint: Constraint[Seq[String]] = { + Constraint("pagination.pageSize") { args => + oneQueryArgConstraint(args) match { + case x: Invalid => x + case Valid => AdditionalConstraints.positivePrintedNumber(args.head) + } + } + } + + private val pageNumberCheckConstraint: Constraint[Seq[String]] = { + Constraint("pagination.pageNumber") { args => + oneQueryArgConstraint(args) match { + case x: Invalid => x + case Valid => AdditionalConstraints.positivePrintedNumber(args.head) + } + } + } + + def parse(queryString: QueryString): Try[Pagination] = Try { + val rawPageSizes = queryString.getOrElse("pageSize", Seq(Pagination.Default.pageSize.toString)) + val rawPageNumbers = queryString.getOrElse("pageNumber", Seq(Pagination.Default.pageNumber.toString)) + + val validation = Seq( + "pageSize" -> pageSizeCheckConstraint(rawPageSizes), + "pageNumber" -> pageNumberCheckConstraint(rawPageNumbers) + ) + + val validationErrors = validation.collect { + case (fieldName, e: Invalid) => (fieldName, e.errors.mkString("; ")) + } + + if (validationErrors.isEmpty) { + Pagination(Integer.parseInt(rawPageSizes.head), Integer.parseInt(rawPageNumbers.head)) + } else { + throw new ParseQueryArgException(validationErrors: _*) + } + } + +} 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..c3ab020 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala @@ -0,0 +1,3 @@ +package xyz.driver.server.parsers.errors + +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..c6ea2e1 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -0,0 +1,145 @@ +package xyz.driver.server.parsers + +import xyz.driver.server.parsers.errors.ParseQueryArgException +import xyz.driver.pdsuicommon.utils.Implicits.{toCharOps, toStringOps} +import fastparse.all._ +import fastparse.core.Parsed +import fastparse.parsers.Intrinsics.CharPred +import play.api.routing.sird._ +import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation} + +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(SearchFilterExpr.Dimension(None, _)) + val pathParser = P(identParser.! ~ "." ~ identParser.!) map { + case (left, right) => SearchFilterExpr.Dimension(Some(left), 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) + + def parse(queryString: QueryString): Try[SearchFilterExpr] = Try { + queryString.getOrElse("filters", Seq.empty) 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..c042211 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala @@ -0,0 +1,55 @@ +package xyz.driver.server.parsers + +import xyz.driver.server.parsers.errors.ParseQueryArgException +import xyz.driver.pdsuicommon.db.{Sorting, SortingOrder} +import fastparse.all._ +import fastparse.core.Parsed +import play.api.routing.sird._ + +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, field, sortingOrder) + case 2 => Sorting.Dimension(Some(prefixedFields.head), 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) + } + } + + def parse(validDimensions: Set[String], queryString: QueryString): Try[Sorting] = Try { + queryString.getOrElse("sort", Seq.empty) 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 xs => 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) + } + +} -- cgit v1.2.3 From eb5e95e95714ce23705a0de0f4dd525a8efeafe2 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 3 Aug 2017 14:22:46 -0700 Subject: Remove play requirements from parsers --- .../pdsuicommon/parsers/DimensionsParser.scala | 14 ++--- .../pdsuicommon/parsers/ListRequestParser.scala | 20 -------- .../pdsuicommon/parsers/PaginationParser.scala | 60 ---------------------- .../parsers/ParseQueryArgException.scala | 2 +- .../pdsuicommon/parsers/SearchFilterParser.scala | 10 ++-- .../driver/pdsuicommon/parsers/SortingParser.scala | 12 ++--- 6 files changed, 17 insertions(+), 101 deletions(-) delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala index 147102b..f23b1b0 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala @@ -1,8 +1,7 @@ -package xyz.driver.server.parsers +package xyz.driver.pdsuicommon.parsers -import play.api.libs.json._ -import play.api.routing.sird._ -import xyz.driver.pdsuicommon.utils.WritesUtils +//import play.api.libs.json._ +//import xyz.driver.pdsuicommon.utils.WritesUtils import scala.util.{Failure, Success, Try} @@ -12,14 +11,15 @@ class Dimensions(private val xs: Set[String] = Set.empty) { object DimensionsParser { + /* private class DimensionsWrapper[T](dimensions: Dimensions)(implicit orig: Writes[T]) extends Writes[T] { private val filteredWrites = WritesUtils.filterKeys[T](dimensions.contains) override def writes(o: T): JsValue = filteredWrites.writes(o) } + */ - def tryParse(queryString: QueryString): Try[Dimensions] = { - val rawDimensions = queryString.getOrElse("dimensions", Seq.empty) - rawDimensions match { + def tryParse(query: Seq[(String, String)]): Try[Dimensions] = { + query.collect{ case ("dimensions", value) => value } match { case Nil => Success(new Dimensions()) case x +: Nil => diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala deleted file mode 100644 index 617a77e..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.driver.server.parsers - -import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} -import play.api.mvc._ - -import scala.util.Try - -final case class ListRequestParameters(filter: SearchFilterExpr, sorting: Sorting, pagination: Pagination) - -class ListRequestParser(validSortingFields: Set[String]) { - - def tryParse(request: Request[AnyContent]): Try[ListRequestParameters] = { - for { - queryFilters <- SearchFilterParser.parse(request.queryString) - sorting <- SortingParser.parse(validSortingFields, request.queryString) - pagination <- PaginationParser.parse(request.queryString) - } yield ListRequestParameters(queryFilters, sorting, pagination) - } - -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala deleted file mode 100644 index a6a7fae..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala +++ /dev/null @@ -1,60 +0,0 @@ -package xyz.driver.server.parsers - -import xyz.driver.pdsuicommon.db.Pagination -import xyz.driver.server.parsers.errors.ParseQueryArgException -import play.api.data.validation._ -import play.api.routing.sird.QueryString -import xyz.driver.pdsuicommon.validation.AdditionalConstraints - -import scala.util.Try - -object PaginationParser { - - private val oneQueryArgConstraint: Constraint[Seq[String]] = { - Constraint("query.oneArg") { - case Nil => Valid - case x +: Nil => Valid - case xs => - Invalid(new ValidationError(Seq(s"must be one argument, but there are multiple: '${xs.mkString(", ")}'"))) - } - } - - private val pageSizeCheckConstraint: Constraint[Seq[String]] = { - Constraint("pagination.pageSize") { args => - oneQueryArgConstraint(args) match { - case x: Invalid => x - case Valid => AdditionalConstraints.positivePrintedNumber(args.head) - } - } - } - - private val pageNumberCheckConstraint: Constraint[Seq[String]] = { - Constraint("pagination.pageNumber") { args => - oneQueryArgConstraint(args) match { - case x: Invalid => x - case Valid => AdditionalConstraints.positivePrintedNumber(args.head) - } - } - } - - def parse(queryString: QueryString): Try[Pagination] = Try { - val rawPageSizes = queryString.getOrElse("pageSize", Seq(Pagination.Default.pageSize.toString)) - val rawPageNumbers = queryString.getOrElse("pageNumber", Seq(Pagination.Default.pageNumber.toString)) - - val validation = Seq( - "pageSize" -> pageSizeCheckConstraint(rawPageSizes), - "pageNumber" -> pageNumberCheckConstraint(rawPageNumbers) - ) - - val validationErrors = validation.collect { - case (fieldName, e: Invalid) => (fieldName, e.errors.mkString("; ")) - } - - if (validationErrors.isEmpty) { - Pagination(Integer.parseInt(rawPageSizes.head), Integer.parseInt(rawPageNumbers.head)) - } else { - throw new ParseQueryArgException(validationErrors: _*) - } - } - -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala index c3ab020..64b3d2e 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ParseQueryArgException.scala @@ -1,3 +1,3 @@ -package xyz.driver.server.parsers.errors +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 index c6ea2e1..58b80ce 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -1,11 +1,9 @@ -package xyz.driver.server.parsers +package xyz.driver.pdsuicommon.parsers -import xyz.driver.server.parsers.errors.ParseQueryArgException import xyz.driver.pdsuicommon.utils.Implicits.{toCharOps, toStringOps} import fastparse.all._ import fastparse.core.Parsed import fastparse.parsers.Intrinsics.CharPred -import play.api.routing.sird._ import xyz.driver.pdsuicommon.db.{SearchFilterBinaryOperation, SearchFilterExpr, SearchFilterNAryOperation} import scala.util.Try @@ -110,11 +108,11 @@ object SearchFilterParser { private val atomParser: Parser[SearchFilterExpr.Atom] = P(binaryAtomParser | nAryAtomParser) - def parse(queryString: QueryString): Try[SearchFilterExpr] = Try { - queryString.getOrElse("filters", Seq.empty) match { + def parse(query: Seq[(String, String)]): Try[SearchFilterExpr] = Try { + query.toList.collect { case ("filters", value) => value } match { case Nil => SearchFilterExpr.Empty - case head +: Nil => + case head :: Nil => atomParser.parse(head) match { case Parsed.Success(x, _) => x case e: Parsed.Failure => throw new ParseQueryArgException("filters" -> formatFailure(1, e)) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala index c042211..375dee2 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala @@ -1,10 +1,8 @@ -package xyz.driver.server.parsers +package xyz.driver.pdsuicommon.parsers -import xyz.driver.server.parsers.errors.ParseQueryArgException import xyz.driver.pdsuicommon.db.{Sorting, SortingOrder} import fastparse.all._ import fastparse.core.Parsed -import play.api.routing.sird._ import scala.util.Try @@ -32,11 +30,11 @@ object SortingParser { } } - def parse(validDimensions: Set[String], queryString: QueryString): Try[Sorting] = Try { - queryString.getOrElse("sort", Seq.empty) match { + 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 => + case rawSorting :: Nil => val parser = sequentialSortingParser(validDimensions.toSeq) parser.parse(rawSorting) match { case Parsed.Success(x, _) => x @@ -44,7 +42,7 @@ object SortingParser { throw new ParseQueryArgException("sort" -> formatFailure(e)) } - case xs => throw new ParseQueryArgException("sort" -> "multiple sections are not allowed") + case _ => throw new ParseQueryArgException("sort" -> "multiple sections are not allowed") } } -- cgit v1.2.3 From 0783832d1c58f9463e85ed504fc0edbfaedc09c5 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 3 Aug 2017 14:23:12 -0700 Subject: Add parser directives --- .../xyz/driver/pdsuicommon/http/Directives.scala | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala (limited to 'src/main/scala/xyz/driver') 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..bb6a9e3 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -0,0 +1,31 @@ +package xyz.driver.pdsuicommon.http + +import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server.Directives._ +import xyz.driver.pdsuicommon.parsers._ +import xyz.driver.pdsuicommon.db.{Pagination, Sorting, SearchFilterExpr} +import scala.util._ + +trait Directives { + + val paginated: Directive1[Pagination] = parameters(('pageSize.as[Int], 'pageNumber.as[Int])).tmap { + case (size, number) => Pagination(size, number) + } + + def sorted(validDimensions: Set[String]): Directive1[Sorting] = parameterSeq.flatMap{ params => + SortingParser.parse(validDimensions, params) match { + case Success(sorting) => provide(sorting) + case Failure(ex) => failWith(ex) + } + } + + val searchFiltered: Directive1[SearchFilterExpr] = parameterSeq.flatMap{ params => + SearchFilterParser.parse(params) match { + case Success(sorting) => provide(sorting) + case Failure(ex) => failWith(ex) + } + } + +} + +object Directives extends Directives -- cgit v1.2.3 From dd9ae7da10818174541f415854e6a6f7b1791e10 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 3 Aug 2017 17:49:50 -0700 Subject: Add completion directives for service replies (also scalafmt) --- .../xyz/driver/pdsuicommon/http/Directives.scala | 56 ++++++++++++++++++++-- .../pdsuicommon/parsers/DimensionsParser.scala | 12 +---- 2 files changed, 53 insertions(+), 15 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index bb6a9e3..9b57b90 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -1,7 +1,13 @@ package xyz.driver.pdsuicommon.http +import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +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 scala.util._ @@ -12,17 +18,59 @@ trait Directives { case (size, number) => Pagination(size, number) } - def sorted(validDimensions: Set[String]): Directive1[Sorting] = parameterSeq.flatMap{ params => + def sorted(validDimensions: Set[String]): Directive1[Sorting] = parameterSeq.flatMap { params => SortingParser.parse(validDimensions, params) match { case Success(sorting) => provide(sorting) - case Failure(ex) => failWith(ex) + case Failure(ex) => failWith(ex) } } - val searchFiltered: Directive1[SearchFilterExpr] = parameterSeq.flatMap{ params => + val searchFiltered: Directive1[SearchFilterExpr] = parameterSeq.flatMap { params => SearchFilterParser.parse(params) match { case Success(sorting) => provide(sorting) - case Failure(ex) => failWith(ex) + case Failure(ex) => failWith(ex) + } + } + + @annotation.implicitNotFound("An ApiExtractor is required to complete service replies.") + trait ApiExtractor[Reply, Api] extends PartialFunction[Reply, Api] + object ApiExtractor { + // Note: make sure the Reply here is the most common response + // type. The specific entity type should be handled in the partial + // function. E.g. `apply[GetByIdReply, Api]{case + // GetByIdReply.Entity => Api}` + def apply[Reply, Api](pf: PartialFunction[Reply, Api]): ApiExtractor[Reply, Api] = new ApiExtractor[Reply, Api] { + override def isDefinedAt(x: Reply) = pf.isDefinedAt(x) + override def apply(x: Reply) = pf.apply(x) + } + } + + def completeService[Reply, Api](reply: => Reply)(implicit requestId: RequestId, + apiExtractor: ApiExtractor[Reply, Api], + apiMarshaller: ToEntityMarshaller[Api], + errorMarshaller: ToEntityMarshaller[ErrorsResponse]): Route = { + + def errorResponse(err: DomainError) = + ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), requestId) + + // TODO: rather than completing the bad requests here, we should + // consider throwing a corresponding exception and then handling + // it in an error handler + reply match { + case apiReply if apiExtractor.isDefinedAt(apiReply) => + complete(apiExtractor(reply)) + case err: NotFoundError => + complete(401 -> errorResponse(err)) + case err: AuthenticationError => + complete(401 -> errorResponse(err)) + case err: AuthorizationError => + complete(403 -> errorResponse(err)) + case err: DomainError => + complete(400 -> errorResponse(err)) + case other => + val msg = s"Got unexpected response type in completion directive: ${other.getClass.getSimpleName}" + val res = ErrorsResponse(Seq(ResponseError(None, msg, ErrorCode.Unspecified)), requestId) + complete(500 -> res) } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala index f23b1b0..fb5a6b9 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala @@ -1,8 +1,5 @@ package xyz.driver.pdsuicommon.parsers -//import play.api.libs.json._ -//import xyz.driver.pdsuicommon.utils.WritesUtils - import scala.util.{Failure, Success, Try} class Dimensions(private val xs: Set[String] = Set.empty) { @@ -11,15 +8,8 @@ class Dimensions(private val xs: Set[String] = Set.empty) { object DimensionsParser { - /* - private class DimensionsWrapper[T](dimensions: Dimensions)(implicit orig: Writes[T]) extends Writes[T] { - private val filteredWrites = WritesUtils.filterKeys[T](dimensions.contains) - override def writes(o: T): JsValue = filteredWrites.writes(o) - } - */ - def tryParse(query: Seq[(String, String)]): Try[Dimensions] = { - query.collect{ case ("dimensions", value) => value } match { + query.collect { case ("dimensions", value) => value } match { case Nil => Success(new Dimensions()) case x +: Nil => -- cgit v1.2.3 From 644f0b2ee2d8e69949337c816bb64b19f5787025 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 4 Aug 2017 13:06:22 -0700 Subject: Add auth context conversion --- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index 9b57b90..d5c6365 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -74,6 +74,17 @@ trait Directives { } } + import xyz.driver.core.rest.AuthorizedServiceRequestContext + import xyz.driver.core.rest.ContextHeaders + import xyz.driver.entities.users.UserInfo + + implicit def authContext(core: AuthorizedServiceRequestContext[UserInfo]): AuthenticatedRequestContext = + new AuthenticatedRequestContext( + core.authenticatedUser, + RequestId(), + core.contextHeaders(ContextHeaders.AuthenticationTokenHeader) + ) + } object Directives extends Directives -- cgit v1.2.3 From 6df9bf52db930ebd845ee7d35226b174d733e988 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 4 Aug 2017 13:07:24 -0700 Subject: Temporary back-port of parsers --- build.sbt | 1 + .../pdsuicommon/parsers/DimensionsParser.scala | 6 +++++ .../pdsuicommon/parsers/ListRequestParser.scala | 20 +++++++++++++++++ .../pdsuicommon/parsers/PagiationParser.scala | 26 ++++++++++++++++++++++ .../pdsuicommon/parsers/SearchFilterParser.scala | 6 +++++ .../driver/pdsuicommon/parsers/SortingParser.scala | 6 +++++ 6 files changed, 65 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala (limited to 'src/main/scala/xyz/driver') diff --git a/build.sbt b/build.sbt index 83602c8..b6e6d8c 100644 --- a/build.sbt +++ b/build.sbt @@ -4,6 +4,7 @@ import Keys._ lazy val core = (project in file(".")) .driverLibrary("pds-ui-common") .settings(scalastyleSettings ++ wartRemoverSettings ++ formatSettings) + .settings(scalacOptions -= "-Xfatal-warnings") // TODO re-enable after migration .settings(wartremoverErrors in (Compile, compile) --= Seq( Wart.ImplicitConversion, Wart.MutableDataStructures, Wart.TraversableOps, Wart.OptionPartial)) .settings(sources in (Compile, doc) := Seq.empty, publishArtifact in (Compile, packageDoc) := false) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala index fb5a6b9..29f2363 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala @@ -8,6 +8,12 @@ class Dimensions(private val xs: Set[String] = Set.empty) { 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()) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala new file mode 100644 index 0000000..0356784 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala @@ -0,0 +1,20 @@ +package xyz.driver.pdsuicommon.parsers + +import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} +import play.api.mvc._ + +import scala.util.Try + +final case class ListRequestParameters(filter: SearchFilterExpr, sorting: Sorting, pagination: Pagination) + +class ListRequestParser(validSortingFields: Set[String]) { + + def tryParse(request: Request[AnyContent]): Try[ListRequestParameters] = { + for { + queryFilters <- SearchFilterParser.parse(request.queryString) + sorting <- SortingParser.parse(validSortingFields, request.queryString) + pagination <- PaginationParser.parse(request.queryString) + } yield ListRequestParameters(queryFilters, sorting, pagination) + } + +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala new file mode 100644 index 0000000..3381542 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala @@ -0,0 +1,26 @@ +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) = query.collectFirst{case (`field`, size) => size} match { + case Some(IntString(x)) => x.toInt + case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)")) + case None => throw new ParseQueryArgException((field, "must be defined")) + } + + Try { + Pagination(validate("pageSize"), validate("pageNumber")) + } + } +} diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 58b80ce..061f2ef 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -108,6 +108,12 @@ object SearchFilterParser { 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 diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala index 375dee2..cc6ade3 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala @@ -30,6 +30,12 @@ object SortingParser { } } + @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) -- cgit v1.2.3 From 8c60e3f6b22e4ee94c5cf7a0cb1f36e1266269de Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Fri, 4 Aug 2017 16:32:40 -0700 Subject: Auth directives and formatting --- .../xyz/driver/pdsuicommon/http/Directives.scala | 63 +++++++++++----------- .../pdsuicommon/parsers/DimensionsParser.scala | 5 +- .../pdsuicommon/parsers/ListRequestParser.scala | 4 +- .../pdsuicommon/parsers/PagiationParser.scala | 11 ++-- .../pdsuicommon/parsers/SearchFilterParser.scala | 5 +- .../driver/pdsuicommon/parsers/SortingParser.scala | 5 +- 6 files changed, 50 insertions(+), 43 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index d5c6365..0fe1f73 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -4,13 +4,19 @@ import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route +import akka.http.scaladsl.model._ +import xyz.driver.core.rest.AuthorizedServiceRequestContext +import xyz.driver.core.rest.ContextHeaders +import xyz.driver.entities.users.UserInfo 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 scala.util._ +import scala.concurrent._ trait Directives { @@ -32,7 +38,7 @@ trait Directives { } } - @annotation.implicitNotFound("An ApiExtractor is required to complete service replies.") + @annotation.implicitNotFound("An ApiExtractor of ${Reply} to ${Api} is required to complete service replies.") trait ApiExtractor[Reply, Api] extends PartialFunction[Reply, Api] object ApiExtractor { // Note: make sure the Reply here is the most common response @@ -45,45 +51,42 @@ trait Directives { } } - def completeService[Reply, Api](reply: => Reply)(implicit requestId: RequestId, - apiExtractor: ApiExtractor[Reply, Api], - apiMarshaller: ToEntityMarshaller[Api], - errorMarshaller: ToEntityMarshaller[ErrorsResponse]): Route = { + implicit def replyMarshaller[Reply, Api]( + implicit ctx: AuthenticatedRequestContext, + apiExtractor: ApiExtractor[Reply, Api], + apiMarshaller: ToEntityMarshaller[Api], + errorMarshaller: ToEntityMarshaller[ErrorsResponse] + ): ToResponseMarshaller[Reply] = { def errorResponse(err: DomainError) = - ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), requestId) + ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), ctx.requestId) - // TODO: rather than completing the bad requests here, we should - // consider throwing a corresponding exception and then handling - // it in an error handler - reply match { - case apiReply if apiExtractor.isDefinedAt(apiReply) => - complete(apiExtractor(reply)) - case err: NotFoundError => - complete(401 -> errorResponse(err)) - case err: AuthenticationError => - complete(401 -> errorResponse(err)) - case err: AuthorizationError => - complete(403 -> errorResponse(err)) - case err: DomainError => - complete(400 -> errorResponse(err)) - case other => - val msg = s"Got unexpected response type in completion directive: ${other.getClass.getSimpleName}" - val res = ErrorsResponse(Seq(ResponseError(None, msg, ErrorCode.Unspecified)), requestId) - complete(500 -> res) + Marshaller[Reply, HttpResponse] { (executionContext: ExecutionContext) => (reply: Reply) => + implicit val ec = executionContext + reply match { + case apiReply if apiExtractor.isDefinedAt(apiReply) => + Marshaller.fromToEntityMarshaller[Api](StatusCodes.OK).apply(apiExtractor(apiReply)) + case err: NotFoundError => + Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.Unauthorized).apply(errorResponse(err)) + case err: AuthorizationError => + Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.Forbidden).apply(errorResponse(err)) + case err: DomainError => + Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.BadRequest).apply(errorResponse(err)) + case other => + val msg = s"Got unexpected response type in completion directive: ${other.getClass.getSimpleName}" + val res = ErrorsResponse(Seq(ResponseError(None, msg, ErrorCode.Unspecified)), ctx.requestId) + Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.InternalServerError).apply(res) + } } } - import xyz.driver.core.rest.AuthorizedServiceRequestContext - import xyz.driver.core.rest.ContextHeaders - import xyz.driver.entities.users.UserInfo - - implicit def authContext(core: AuthorizedServiceRequestContext[UserInfo]): AuthenticatedRequestContext = - new AuthenticatedRequestContext( + implicit class PdsContext(core: AuthorizedServiceRequestContext[UserInfo]) { + def authenticated = new AuthenticatedRequestContext( core.authenticatedUser, RequestId(), core.contextHeaders(ContextHeaders.AuthenticationTokenHeader) ) + } } diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala index 29f2363..17c09ed 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/DimensionsParser.scala @@ -10,8 +10,9 @@ 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) + tryParse(query.toSeq.flatMap { + case (key, values) => + values.map(value => key -> value) }) def tryParse(query: Seq[(String, String)]): Try[Dimensions] = { diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala index 0356784..c3146ce 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala @@ -12,8 +12,8 @@ class ListRequestParser(validSortingFields: Set[String]) { def tryParse(request: Request[AnyContent]): Try[ListRequestParameters] = { for { queryFilters <- SearchFilterParser.parse(request.queryString) - sorting <- SortingParser.parse(validSortingFields, request.queryString) - pagination <- PaginationParser.parse(request.queryString) + sorting <- SortingParser.parse(validSortingFields, request.queryString) + pagination <- PaginationParser.parse(request.queryString) } yield ListRequestParameters(queryFilters, sorting, pagination) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala index 3381542..3988668 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala @@ -7,16 +7,17 @@ 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) + 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) = query.collectFirst{case (`field`, size) => size} match { + def validate(field: String) = query.collectFirst { case (`field`, size) => size } match { case Some(IntString(x)) => x.toInt - case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)")) - case None => throw new ParseQueryArgException((field, "must be defined")) + case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)")) + case None => throw new ParseQueryArgException((field, "must be defined")) } Try { diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 061f2ef..768e5f5 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -110,8 +110,9 @@ object SearchFilterParser { @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) + parse(query.toSeq.flatMap { + case (key, values) => + values.map(value => key -> value) }) def parse(query: Seq[(String, String)]): Try[SearchFilterExpr] = Try { diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala index cc6ade3..c1c332f 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala @@ -32,8 +32,9 @@ object SortingParser { @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) + 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 { -- cgit v1.2.3 From 8657a5cfa8695607feb9025ebdbfbd13a5b4a8a8 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Aug 2017 10:40:30 -0700 Subject: Fix pagination parser --- src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala index 3988668..dd492e4 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala @@ -13,7 +13,7 @@ object PaginationParser { }) def parse(query: Seq[(String, String)]): Try[Pagination] = { - val IntString = """\d+""".r + val IntString = """(\d+)""".r def validate(field: String) = query.collectFirst { case (`field`, size) => size } match { case Some(IntString(x)) => x.toInt case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)")) -- cgit v1.2.3 From 7f3e788642b1d3946fccc0e13d3c3121034d9d7d Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Aug 2017 15:47:02 -0700 Subject: Add path matchers for custom id types --- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index 0fe1f73..7a6266f 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -3,7 +3,7 @@ package xyz.driver.pdsuicommon.http import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.server.Directive1 import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.{PathMatcher1, PathMatchers} import akka.http.scaladsl.model._ import xyz.driver.core.rest.AuthorizedServiceRequestContext import xyz.driver.core.rest.ContextHeaders @@ -14,6 +14,7 @@ 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 scala.util._ import scala.concurrent._ @@ -38,6 +39,15 @@ trait Directives { } } + def StringIdInPath[T]: PathMatcher1[StringId[T]] = + PathMatchers.Segment.map((id) => StringId(id.toString.toLowerCase)) + + def LongIdInPath[T]: PathMatcher1[LongId[T]] = + PathMatchers.LongNumber.map((id) => LongId(id)) + + def UuidIdInPath[T]: PathMatcher1[UuidId[T]] = + PathMatchers.JavaUUID.map((id) => UuidId(id)) + @annotation.implicitNotFound("An ApiExtractor of ${Reply} to ${Api} is required to complete service replies.") trait ApiExtractor[Reply, Api] extends PartialFunction[Reply, Api] object ApiExtractor { -- cgit v1.2.3 From 5ad077c8ae500f6154043dc0c8ffd457ddaba204 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Aug 2017 15:52:34 -0700 Subject: Use akka streams in trial pdfs --- .../xyz/driver/pdsuidomain/entities/Trial.scala | 3 --- .../driver/pdsuidomain/services/TrialService.scala | 24 ++++------------------ .../services/fake/FakeTrialService.scala | 5 ++++- .../services/rest/RestTrialService.scala | 14 +++++++++++-- 4 files changed, 20 insertions(+), 26 deletions(-) (limited to 'src/main/scala/xyz/driver') 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/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..a68cb52 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, -- cgit v1.2.3 From 14c6ae3bcdc1560e91d0443ede592bf0ae876674 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Aug 2017 19:26:54 -0700 Subject: Exception-based error handling --- .../xyz/driver/pdsuicommon/http/Directives.scala | 92 +++++++++++----------- .../services/rest/RestTrialService.scala | 2 +- 2 files changed, 45 insertions(+), 49 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index 7a6266f..a9d7b38 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -1,11 +1,8 @@ package xyz.driver.pdsuicommon.http -import akka.http.scaladsl.marshalling._ -import akka.http.scaladsl.server.Directive1 +import akka.http.scaladsl.server._ import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{PathMatcher1, PathMatchers} import akka.http.scaladsl.model._ -import xyz.driver.core.rest.AuthorizedServiceRequestContext import xyz.driver.core.rest.ContextHeaders import xyz.driver.entities.users.UserInfo import xyz.driver.pdsuicommon.auth._ @@ -15,9 +12,10 @@ 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._ -import scala.concurrent._ trait Directives { @@ -25,13 +23,20 @@ trait Directives { case (size, number) => Pagination(size, number) } - def sorted(validDimensions: Set[String]): Directive1[Sorting] = parameterSeq.flatMap { params => + 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) => failWith(ex) } } + val dimensioned: Directive1[Dimensions] = parameterSeq.flatMap { params => + DimensionsParser.tryParse(params) match { + case Success(dims) => provide(dims) + case Failure(ex) => failWith(ex) + } + } + val searchFiltered: Directive1[SearchFilterExpr] = parameterSeq.flatMap { params => SearchFilterParser.parse(params) match { case Success(sorting) => provide(sorting) @@ -48,54 +53,45 @@ trait Directives { def UuidIdInPath[T]: PathMatcher1[UuidId[T]] = PathMatchers.JavaUUID.map((id) => UuidId(id)) - @annotation.implicitNotFound("An ApiExtractor of ${Reply} to ${Api} is required to complete service replies.") - trait ApiExtractor[Reply, Api] extends PartialFunction[Reply, Api] - object ApiExtractor { - // Note: make sure the Reply here is the most common response - // type. The specific entity type should be handled in the partial - // function. E.g. `apply[GetByIdReply, Api]{case - // GetByIdReply.Entity => Api}` - def apply[Reply, Api](pf: PartialFunction[Reply, Api]): ApiExtractor[Reply, Api] = new ApiExtractor[Reply, Api] { - override def isDefinedAt(x: Reply) = pf.isDefinedAt(x) - override def apply(x: Reply) = pf.apply(x) - } + 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 } - implicit def replyMarshaller[Reply, Api]( - implicit ctx: AuthenticatedRequestContext, - apiExtractor: ApiExtractor[Reply, Api], - apiMarshaller: ToEntityMarshaller[Api], - errorMarshaller: ToEntityMarshaller[ErrorsResponse] - ): ToResponseMarshaller[Reply] = { - + def domainExceptionHandler(req: RequestId) = { def errorResponse(err: DomainError) = - ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), ctx.requestId) - - Marshaller[Reply, HttpResponse] { (executionContext: ExecutionContext) => (reply: Reply) => - implicit val ec = executionContext - reply match { - case apiReply if apiExtractor.isDefinedAt(apiReply) => - Marshaller.fromToEntityMarshaller[Api](StatusCodes.OK).apply(apiExtractor(apiReply)) - case err: NotFoundError => - Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.Unauthorized).apply(errorResponse(err)) - case err: AuthorizationError => - Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.Forbidden).apply(errorResponse(err)) - case err: DomainError => - Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.BadRequest).apply(errorResponse(err)) - case other => - val msg = s"Got unexpected response type in completion directive: ${other.getClass.getSimpleName}" - val res = ErrorsResponse(Seq(ResponseError(None, msg, ErrorCode.Unspecified)), ctx.requestId) - Marshaller.fromToEntityMarshaller[ErrorsResponse](StatusCodes.InternalServerError).apply(res) - } + ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), req) + ExceptionHandler { + case err: AuthenticationError => complete(StatusCodes.Unauthorized -> errorResponse(err)) + case err: AuthorizationError => complete(StatusCodes.Forbidden -> errorResponse(err)) + case err: NotFoundError => complete(StatusCodes.NotFound -> errorResponse(err)) + case err: DomainError => complete(StatusCodes.BadRequest -> errorResponse(err)) } } - implicit class PdsContext(core: AuthorizedServiceRequestContext[UserInfo]) { - def authenticated = new AuthenticatedRequestContext( - core.authenticatedUser, - RequestId(), - core.contextHeaders(ContextHeaders.AuthenticationTokenHeader) - ) + val tracked: Directive1[RequestId] = optionalHeaderValueByName(ContextHeaders.TrackingIdHeader) flatMap { + case Some(id) => provide(RequestId(id)) + case None => provide(RequestId()) + } + + val handleDomainExceptions: Directive0 = tracked.flatMap { + case id => + handleExceptions(domainExceptionHandler(id)) + } + + implicit class AuthProviderWrapper(provider: AuthProvider[UserInfo]) { + val authenticate: Directive1[AuthenticatedRequestContext] = (provider.authorize() & tracked) tflatMap { + case (core, requestId) => + provide( + new AuthenticatedRequestContext( + core.authenticatedUser, + requestId, + core.contextHeaders(ContextHeaders.AuthenticationTokenHeader) + )) + } } } 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 a68cb52..f826b98 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala @@ -46,7 +46,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(implicit prote } def getPdfSource(trialId: StringId[Trial])( - implicit requestContext: AuthenticatedRequestContext): Future[Source[ByteString, NotUsed]] = { + 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) -- cgit v1.2.3 From 75fcedad9270c70c014af819b7d46dcfe00c1282 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Mon, 7 Aug 2017 22:34:33 -0700 Subject: App toDomain converion to ApiTrialIssue --- .../formats/json/trialissue/ApiTrialIssue.scala | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') 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, -- cgit v1.2.3 From a59d6910eaccf9b5540fb41648a7d4e67cc241b5 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 8 Aug 2017 00:11:13 -0700 Subject: Fix parsers and renable fatal warnings --- build.sbt | 4 +- .../pdsuicommon/parsers/ListRequestParser.scala | 20 --- .../pdsuicommon/parsers/PagiationParser.scala | 27 --- .../pdsuicommon/parsers/PaginationParser.scala | 29 ++++ .../parsers/PaginationParserSuite.scala | 95 +++++++++++ .../parsers/SearchFilterParserSuite.scala | 185 +++++++++++++++++++++ .../pdsuicommon/parsers/SortingParserSuite.scala | 91 ++++++++++ .../xyz/driver/pdsuicommon/parsers/TestUtils.scala | 52 ++++++ 8 files changed, 454 insertions(+), 49 deletions(-) delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala delete mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/parsers/PaginationParser.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/parsers/PaginationParserSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/parsers/SortingParserSuite.scala create mode 100644 src/test/scala/xyz/driver/pdsuicommon/parsers/TestUtils.scala (limited to 'src/main/scala/xyz/driver') diff --git a/build.sbt b/build.sbt index b6e6d8c..c281a85 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,6 @@ import Keys._ lazy val core = (project in file(".")) .driverLibrary("pds-ui-common") .settings(scalastyleSettings ++ wartRemoverSettings ++ formatSettings) - .settings(scalacOptions -= "-Xfatal-warnings") // TODO re-enable after migration .settings(wartremoverErrors in (Compile, compile) --= Seq( Wart.ImplicitConversion, Wart.MutableDataStructures, Wart.TraversableOps, Wart.OptionPartial)) .settings(sources in (Compile, doc) := Seq.empty, publishArtifact in (Compile, packageDoc) := false) @@ -30,5 +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" + "org.scalacheck" %% "scalacheck" % "1.13.4" % "test", + "org.scalatest" %% "scalatest" % "3.0.1" % "test" )) diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala deleted file mode 100644 index c3146ce..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/ListRequestParser.scala +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.driver.pdsuicommon.parsers - -import xyz.driver.pdsuicommon.db.{Pagination, SearchFilterExpr, Sorting} -import play.api.mvc._ - -import scala.util.Try - -final case class ListRequestParameters(filter: SearchFilterExpr, sorting: Sorting, pagination: Pagination) - -class ListRequestParser(validSortingFields: Set[String]) { - - def tryParse(request: Request[AnyContent]): Try[ListRequestParameters] = { - for { - queryFilters <- SearchFilterParser.parse(request.queryString) - sorting <- SortingParser.parse(validSortingFields, request.queryString) - pagination <- PaginationParser.parse(request.queryString) - } yield ListRequestParameters(queryFilters, sorting, pagination) - } - -} diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala deleted file mode 100644 index dd492e4..0000000 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/PagiationParser.scala +++ /dev/null @@ -1,27 +0,0 @@ -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) = query.collectFirst { case (`field`, size) => size } match { - case Some(IntString(x)) => x.toInt - case Some(str) => throw new ParseQueryArgException((field, s"must be an integer (found $str)")) - case None => throw new ParseQueryArgException((field, "must be defined")) - } - - Try { - Pagination(validate("pageSize"), validate("pageNumber")) - } - } -} 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/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..5cd2dc9 --- /dev/null +++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala @@ -0,0 +1,185 @@ +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 + +object SearchFilterParserSuite { + + class UnexpectedSearchFilterExprException(x: SearchFilterExpr) extends Exception(s"unexpected $x") + +} + +class SearchFilterParserSuite extends FreeSpecLike with Checkers { + + import SearchFilterParserSuite._ + + "parse" - { + "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" + SearchFilterParser.dimensionParser.parse(raw) match { + case Parsed.Success(Dimension(Some(`left`), `right`), _) => true + case res => false + } + } + } + "just with field name" in check { + Prop.forAllNoShrink(Gen.identifier) { s => + SearchFilterParser.dimensionParser.parse(s) match { + case Parsed.Success(Dimension(None, `s`), _) => 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 + } + + } + +} -- cgit v1.2.3 From 5e6f97aab7c05a7f38b0c1abee2ccdc2f6bfc83a Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 10 Aug 2017 14:16:13 -0700 Subject: Use AuthUserInfo --- project/build.properties | 2 +- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src/main/scala/xyz/driver') 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/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index a9d7b38..ec89649 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -4,7 +4,7 @@ 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.UserInfo +import xyz.driver.entities.users.AuthUserInfo import xyz.driver.pdsuicommon.auth._ import xyz.driver.pdsuicommon.error._ import xyz.driver.pdsuicommon.error.DomainError._ @@ -82,7 +82,7 @@ trait Directives { handleExceptions(domainExceptionHandler(id)) } - implicit class AuthProviderWrapper(provider: AuthProvider[UserInfo]) { + implicit class AuthProviderWrapper(provider: AuthProvider[AuthUserInfo]) { val authenticate: Directive1[AuthenticatedRequestContext] = (provider.authorize() & tracked) tflatMap { case (core, requestId) => provide( -- cgit v1.2.3 From 9e6c7099285280dfecd939e7c374c0ef99fe1e35 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 15 Aug 2017 20:01:50 -0700 Subject: Remove unused parameter --- src/main/scala/xyz/driver/pdsuicommon/db/MySqlContext.scala | 5 +---- src/test/scala/xyz/driver/pdsuicommon/Mocks.scala | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) (limited to 'src/main/scala/xyz/driver') 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/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 ) } -- cgit v1.2.3 From fc08317f7a8eed466f6b52a888387e96b6a85532 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 16 Aug 2017 13:22:45 -0700 Subject: Don't require parser query string --- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index ec89649..feb224a 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -19,8 +19,11 @@ import scala.util._ trait Directives { - val paginated: Directive1[Pagination] = parameters(('pageSize.as[Int], 'pageNumber.as[Int])).tmap { - case (size, number) => Pagination(size, number) + val paginated: Directive1[Pagination] = parameterSeq.flatMap { params => + PaginationParser.parse(params) match { + case Success(pagination) => provide(pagination) + case Failure(ex) => failWith(ex) + } } def sorted(validDimensions: Set[String] = Set.empty): Directive1[Sorting] = parameterSeq.flatMap { params => -- cgit v1.2.3 From 48cb3aa145a883cb7a3bb6d6c8edd23af7dda486 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 16 Aug 2017 19:02:59 -0700 Subject: Add rejection handling --- .../xyz/driver/pdsuicommon/http/Directives.scala | 49 +++++++++++++++------- 1 file changed, 34 insertions(+), 15 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index feb224a..3f81b8d 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -14,6 +14,7 @@ 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._ @@ -22,28 +23,32 @@ trait Directives { val paginated: Directive1[Pagination] = parameterSeq.flatMap { params => PaginationParser.parse(params) match { case Success(pagination) => provide(pagination) - case Failure(ex) => failWith(ex) + case Failure(ex) => + reject(new 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) => failWith(ex) + case Failure(ex) => + reject(new 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) => failWith(ex) + case Failure(ex) => + reject(new 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) => failWith(ex) + case Failure(ex) => + reject(new ValidationRejection("invalid filter parameter", Some(ex))) } } @@ -64,14 +69,29 @@ trait Directives { case other => other } - def domainExceptionHandler(req: RequestId) = { - def errorResponse(err: DomainError) = - ErrorsResponse(Seq(ResponseError(None, err.getMessage, ErrorCode.Unspecified)), req) + def domainExceptionHandler(req: RequestId): ExceptionHandler = { + def errorResponse(ex: Throwable) = + ErrorsResponse(Seq(ResponseError(None, ex.getMessage, ErrorCode.Unspecified)), req) ExceptionHandler { - case err: AuthenticationError => complete(StatusCodes.Unauthorized -> errorResponse(err)) - case err: AuthorizationError => complete(StatusCodes.Forbidden -> errorResponse(err)) - case err: NotFoundError => complete(StatusCodes.NotFound -> errorResponse(err)) - case err: DomainError => complete(StatusCodes.BadRequest -> errorResponse(err)) + 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 } } @@ -80,13 +100,12 @@ trait Directives { case None => provide(RequestId()) } - val handleDomainExceptions: Directive0 = tracked.flatMap { - case id => - handleExceptions(domainExceptionHandler(id)) + val domainResponse: Directive0 = tracked.flatMap { id => + handleExceptions(domainExceptionHandler(id)) & handleRejections(domainRejectionHandler(id)) } implicit class AuthProviderWrapper(provider: AuthProvider[AuthUserInfo]) { - val authenticate: Directive1[AuthenticatedRequestContext] = (provider.authorize() & tracked) tflatMap { + val authenticated: Directive1[AuthenticatedRequestContext] = (provider.authorize() & tracked) tflatMap { case (core, requestId) => provide( new AuthenticatedRequestContext( -- cgit v1.2.3 From 8acbc7e0490f81c22d13319237f54c45e9885e26 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 17 Aug 2017 14:15:20 -0700 Subject: Moving PostgresContext to the common code --- .../driver/pdsuicommon/db/PostgresContext.scala | 92 ++++++++++++++++++++ .../pdsuicommon/db/PostgresQueryBuilder.scala | 98 ++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala create mode 100644 src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala (limited to 'src/main/scala/xyz/driver') 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..1b7e2fb --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala @@ -0,0 +1,92 @@ +package xyz.driver.pdsuicommon.db + +import java.io.Closeable +import java.sql.Types +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") + } + } + +} + +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) + } + + // Override localDateTime encoder and decoder cause + // clinicaltrials.gov uses bigint to store timestamps + + override implicit val localDateTimeEncoder: Encoder[LocalDateTime] = + encoder(Types.BIGINT, + (index, value, row) => row.setLong(index, value.atZone(ZoneOffset.UTC).toInstant.toEpochMilli)) + + override implicit val localDateTimeDecoder: Decoder[LocalDateTime] = + decoder( + Types.BIGINT, + (index, row) => { + row.getLong(index) match { + case 0 => throw new NullPointerException("0 is decoded as null") + case x => LocalDateTime.ofInstant(Instant.ofEpochMilli(x), ZoneId.of("Z")) + } + } + ) + + 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..8ef1829 --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala @@ -0,0 +1,98 @@ +package xyz.driver.pdsuicommon.db + +import java.sql.ResultSet + +import io.getquill.{PostgresDialect, PostgresEscape} + +import scala.collection.breakOut + +object PostgresQueryBuilder { + + import xyz.driver.pdsuicommon.db.QueryBuilder._ + + type Escape = PostgresEscape + val Escape = PostgresEscape + + 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 = PostgresEscape) + sqlContext.executeQuery[T](sql, binder, { resultSet => + extractor(resultSet) + }) + } + + val countRunner: CountRunner = { parameters => + val (sql, binder) = parameters.toSql(countQuery = true, namingStrategy = PostgresEscape) + 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, PostgresEscape] = { + new PostgresQueryBuilder[T](parameters.copy(filter = newFilter)) + } + + def withSorting(newSorting: Sorting): QueryBuilder[T, PostgresDialect, PostgresEscape] = { + new PostgresQueryBuilder[T](parameters.copy(sorting = newSorting)) + } + + def withPagination(newPagination: Pagination): QueryBuilder[T, PostgresDialect, PostgresEscape] = { + new PostgresQueryBuilder[T](parameters.copy(pagination = Some(newPagination))) + } + + def resetPagination: QueryBuilder[T, PostgresDialect, PostgresEscape] = { + new PostgresQueryBuilder[T](parameters.copy(pagination = None)) + } +} -- cgit v1.2.3 From bc60e6aef2a22bdf2167f56417477da121cdeed1 Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 26 Aug 2017 18:41:47 -0700 Subject: Making Query builder to escape table names with schemas properly --- .../pdsuicommon/db/PostgresQueryBuilder.scala | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala index 8ef1829..0ddf811 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresQueryBuilder.scala @@ -3,6 +3,7 @@ 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 @@ -10,8 +11,17 @@ object PostgresQueryBuilder { import xyz.driver.pdsuicommon.db.QueryBuilder._ - type Escape = PostgresEscape - val Escape = PostgresEscape + 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], @@ -42,14 +52,14 @@ object PostgresQueryBuilder { extractor: ResultSet => T)(implicit sqlContext: PostgresContext): PostgresQueryBuilder[T] = { val runner: Runner[T] = { parameters => - val (sql, binder) = parameters.toSql(countQuery = false, fields = fields, namingStrategy = PostgresEscape) + 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 = PostgresEscape) + val (sql, binder) = parameters.toSql(countQuery = true, namingStrategy = SmartPostgresEscape) sqlContext .executeQuery[CountResult]( sql, @@ -80,19 +90,19 @@ class PostgresQueryBuilder[T](parameters: PostgresQueryBuilderParameters)(implic countRunner: QueryBuilder.CountRunner) extends QueryBuilder[T, PostgresDialect, PostgresQueryBuilder.Escape](parameters) { - def withFilter(newFilter: SearchFilterExpr): QueryBuilder[T, PostgresDialect, PostgresEscape] = { + def withFilter(newFilter: SearchFilterExpr): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = { new PostgresQueryBuilder[T](parameters.copy(filter = newFilter)) } - def withSorting(newSorting: Sorting): QueryBuilder[T, PostgresDialect, PostgresEscape] = { + def withSorting(newSorting: Sorting): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = { new PostgresQueryBuilder[T](parameters.copy(sorting = newSorting)) } - def withPagination(newPagination: Pagination): QueryBuilder[T, PostgresDialect, PostgresEscape] = { + def withPagination(newPagination: Pagination): QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = { new PostgresQueryBuilder[T](parameters.copy(pagination = Some(newPagination))) } - def resetPagination: QueryBuilder[T, PostgresDialect, PostgresEscape] = { + def resetPagination: QueryBuilder[T, PostgresDialect, SmartPostgresEscape] = { new PostgresQueryBuilder[T](parameters.copy(pagination = None)) } } -- cgit v1.2.3 From 4512539b01e6e598d494246426b9eac261d10acf Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 26 Aug 2017 19:33:50 -0700 Subject: localDateTimeEncoder/Decoder to ctgov repo --- .../xyz/driver/pdsuicommon/db/PostgresContext.scala | 19 ------------------- 1 file changed, 19 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala index 1b7e2fb..cbb23d4 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala @@ -1,7 +1,6 @@ package xyz.driver.pdsuicommon.db import java.io.Closeable -import java.sql.Types import java.time._ import java.util.UUID import java.util.concurrent.Executors @@ -58,24 +57,6 @@ class PostgresContext(val dataSource: DataSource with Closeable, settings: Setti LocalDateTime.ofInstant(timestamp.toInstant, ZoneOffset.UTC) } - // Override localDateTime encoder and decoder cause - // clinicaltrials.gov uses bigint to store timestamps - - override implicit val localDateTimeEncoder: Encoder[LocalDateTime] = - encoder(Types.BIGINT, - (index, value, row) => row.setLong(index, value.atZone(ZoneOffset.UTC).toInstant.toEpochMilli)) - - override implicit val localDateTimeDecoder: Decoder[LocalDateTime] = - decoder( - Types.BIGINT, - (index, row) => { - row.getLong(index) match { - case 0 => throw new NullPointerException("0 is decoded as null") - case x => LocalDateTime.ofInstant(Instant.ofEpochMilli(x), ZoneId.of("Z")) - } - } - ) - implicit def encodeUuidId[T] = MappedEncoding[UuidId[T], String](_.toString) implicit def decodeUuidId[T] = MappedEncoding[String, UuidId[T]] { uuid => UuidId[T](UUID.fromString(uuid)) -- cgit v1.2.3 From b249515292bfefbb70d1b5743ca46ced6da22a00 Mon Sep 17 00:00:00 2001 From: Kseniya Tomskikh Date: Mon, 28 Aug 2017 17:11:27 +0700 Subject: Added converting camel case to snake case for filters and sorting AST --- .../pdsuicommon/parsers/SearchFilterParser.scala | 4 +++- .../driver/pdsuicommon/parsers/SortingParser.scala | 8 ++++++-- .../scala/xyz/driver/pdsuicommon/utils/Utils.scala | 20 ++++++++++++++++++++ .../parsers/SearchFilterParserSuite.scala | 7 +++++-- 4 files changed, 34 insertions(+), 5 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 768e5f5..7bbcd10 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -5,6 +5,7 @@ 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 @@ -56,7 +57,8 @@ object SearchFilterParser { CharPred(c => c.isLetterOrDigit) .rep(min = 1)).!.map(SearchFilterExpr.Dimension(None, _)) val pathParser = P(identParser.! ~ "." ~ identParser.!) map { - case (left, right) => SearchFilterExpr.Dimension(Some(left), right) + case (left, right) => + SearchFilterExpr.Dimension(Some(toSnakeCase(left)), toSnakeCase(right)) } P(pathParser | identParser) } diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala index c1c332f..4bfc669 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SortingParser.scala @@ -3,6 +3,7 @@ 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 @@ -18,8 +19,11 @@ object SortingParser { case (sortingOrder, field) => val prefixedFields = field.split("\\.", 2) prefixedFields.size match { - case 1 => Sorting.Dimension(None, field, sortingOrder) - case 2 => Sorting.Dimension(Some(prefixedFields.head), prefixedFields.last, sortingOrder) + case 1 => Sorting.Dimension(None, toSnakeCase(field), sortingOrder) + case 2 => + Sorting.Dimension(Some(prefixedFields.head).map(toSnakeCase), + toSnakeCase(prefixedFields.last), + sortingOrder) } } } 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/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala index 5cd2dc9..9ca2343 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.utils.Utils._ object SearchFilterParserSuite { @@ -30,8 +31,10 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers { Prop.forAllNoShrink(dimensionGen) { case (left, right) => val raw = s"$left.$right" - SearchFilterParser.dimensionParser.parse(raw) match { - case Parsed.Success(Dimension(Some(`left`), `right`), _) => true + val l = toSnakeCase(left) + val r = toSnakeCase(right) + SearchFilterParser.dimensionParser.parse(raw) match { + case Parsed.Success(Dimension(Some(`l`), `r`), _) => true case res => false } } -- cgit v1.2.3 From b6f8b09dcc9b39f1ae9c94959c82e7eb21e37da6 Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 28 Aug 2017 09:57:41 -0700 Subject: Small clean-up --- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index 3f81b8d..54f7102 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -24,7 +24,7 @@ trait Directives { PaginationParser.parse(params) match { case Success(pagination) => provide(pagination) case Failure(ex) => - reject(new ValidationRejection("invalid pagination parameter", Some(ex))) + reject(ValidationRejection("invalid pagination parameter", Some(ex))) } } @@ -32,7 +32,7 @@ trait Directives { SortingParser.parse(validDimensions, params) match { case Success(sorting) => provide(sorting) case Failure(ex) => - reject(new ValidationRejection("invalid sorting parameter", Some(ex))) + reject(ValidationRejection("invalid sorting parameter", Some(ex))) } } @@ -40,7 +40,7 @@ trait Directives { DimensionsParser.tryParse(params) match { case Success(dims) => provide(dims) case Failure(ex) => - reject(new ValidationRejection("invalid dimension parameter", Some(ex))) + reject(ValidationRejection("invalid dimension parameter", Some(ex))) } } @@ -48,7 +48,7 @@ trait Directives { SearchFilterParser.parse(params) match { case Success(sorting) => provide(sorting) case Failure(ex) => - reject(new ValidationRejection("invalid filter parameter", Some(ex))) + reject(ValidationRejection("invalid filter parameter", Some(ex))) } } -- cgit v1.2.3 From 618d30aa8b2c80043525546ffe7a301b0398b30f Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 28 Aug 2017 10:54:39 -0700 Subject: Fixing filter parser + test --- .../pdsuicommon/parsers/SearchFilterParser.scala | 2 +- .../pdsuicommon/parsers/SearchFilterParserSuite.scala | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala index 7bbcd10..8aff397 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParser.scala @@ -55,7 +55,7 @@ object SearchFilterParser { val dimensionParser: Parser[SearchFilterExpr.Dimension] = { val identParser = P( CharPred(c => c.isLetterOrDigit) - .rep(min = 1)).!.map(SearchFilterExpr.Dimension(None, _)) + .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)) diff --git a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala index 9ca2343..36f1c35 100644 --- a/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala +++ b/src/test/scala/xyz/driver/pdsuicommon/parsers/SearchFilterParserSuite.scala @@ -9,8 +9,11 @@ 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 scala.util.Success + object SearchFilterParserSuite { class UnexpectedSearchFilterExprException(x: SearchFilterExpr) extends Exception(s"unexpected $x") @@ -22,6 +25,20 @@ 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 = { @@ -35,7 +52,7 @@ class SearchFilterParserSuite extends FreeSpecLike with Checkers { val r = toSnakeCase(right) SearchFilterParser.dimensionParser.parse(raw) match { case Parsed.Success(Dimension(Some(`l`), `r`), _) => true - case res => false + case _ => false } } } -- cgit v1.2.3 From 116c78627fd22c4a6b70d5343d6e89fa9ab9e7ad Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 28 Aug 2017 17:47:43 -0700 Subject: Making StringId parser to preserve case --- src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala index 54f7102..e9a4132 100644 --- a/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala +++ b/src/main/scala/xyz/driver/pdsuicommon/http/Directives.scala @@ -53,7 +53,7 @@ trait Directives { } def StringIdInPath[T]: PathMatcher1[StringId[T]] = - PathMatchers.Segment.map((id) => StringId(id.toString.toLowerCase)) + PathMatchers.Segment.map((id) => StringId(id.toString)) def LongIdInPath[T]: PathMatcher1[LongId[T]] = PathMatchers.LongNumber.map((id) => LongId(id)) -- cgit v1.2.3 From 7bf2da3afbde1b4cec0d68cb4899e51a63e8a9b1 Mon Sep 17 00:00:00 2001 From: Kseniya Tomskikh Date: Mon, 28 Aug 2017 15:27:40 +0700 Subject: Created custom formats of TriC for swagger --- .../utils/CustomSwaggerJsonFormats.scala | 55 +++++++++ .../driver/pdsuidomain/fakes/entities/common.scala | 34 +++++ .../pdsuidomain/fakes/entities/trialcuration.scala | 137 +++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuicommon/utils/CustomSwaggerJsonFormats.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala create mode 100644 src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala (limited to 'src/main/scala/xyz/driver') 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/pdsuidomain/fakes/entities/common.scala b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala new file mode 100644 index 0000000..fbab2ce --- /dev/null +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala @@ -0,0 +1,34 @@ +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} + +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 = { + val date = generators.nextDate() + LocalDate.of(date.year, date.month, date.day) + } + + 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..caa6bca --- /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()), + description = generators.nextString(), + originalDescription = 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() + ) + +} -- cgit v1.2.3 From d58b6242a11a0c97c8c9a12e8b19cd3217f18ade Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 6 Sep 2017 11:52:53 -0700 Subject: Add spray-json formats for document types --- .../formats/json/sprayformats/documenttype.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/documenttype.scala (limited to 'src/main/scala/xyz/driver') 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) + +} -- cgit v1.2.3 From ea0884b02daa719a6936abe6b15bc80d9a80a71f Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 6 Sep 2017 14:30:16 -0700 Subject: Add missing formats --- .../formats/json/sprayformats/documentissue.scala | 13 +------------ .../formats/json/sprayformats/providerttype.scala | 12 ++++++++++++ .../pdsuidomain/formats/json/sprayformats/recordissue.scala | 13 +------------ 3 files changed, 14 insertions(+), 24 deletions(-) create mode 100644 src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/providerttype.scala (limited to 'src/main/scala/xyz/driver') 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/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) } -- cgit v1.2.3 From 2e2f9213a16bf5728f4615f927ba29536b3c20be Mon Sep 17 00:00:00 2001 From: tomskikh_ka Date: Thu, 14 Sep 2017 16:26:18 +0700 Subject: PDSUI-2273 Renamed intervention.description to dosage --- src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala | 2 +- .../xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala | 2 +- .../pdsuidomain/formats/json/intervention/ApiIntervention.scala | 8 ++++---- .../formats/json/intervention/ApiPartialIntervention.scala | 8 ++++---- .../pdsuidomain/formats/json/sprayformats/intervention.scala | 8 ++++---- .../formats/json/sprayformats/InterventionFormatSuite.scala | 8 ++++---- 6 files changed, 18 insertions(+), 18 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala index e691547..3911437 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala @@ -27,7 +27,7 @@ final case class Intervention(id: LongId[Intervention], originalName: String, typeId: Option[LongId[InterventionType]], originalType: Option[String], - description: String, + dosage: String, originalDescription: String, isActive: Boolean) 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 caa6bca..35ca3fd 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala @@ -74,7 +74,7 @@ object trialcuration { originalName = generators.nextString(), typeId = Option(nextLongId[InterventionType]), originalType = Option(generators.nextString()), - description = generators.nextString(), + dosage = generators.nextString(), originalDescription = generators.nextString(), isActive = generators.nextBoolean() ) 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..ba383b6 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,7 +8,7 @@ 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, @@ -24,7 +24,7 @@ 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, + dosage = this.dosage, originalDescription = this.originalDescription, isActive = this.isActive ) @@ -43,7 +43,7 @@ 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 @@ -60,7 +60,7 @@ 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, 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/intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala index a8ce950..41afe6a 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 @@ -14,7 +14,7 @@ object intervention { "id" -> obj.intervention.id.toJson, "name" -> obj.intervention.name.toJson, "typeId" -> obj.intervention.typeId.toJson, - "description" -> obj.intervention.description.toJson, + "dosage" -> obj.intervention.dosage.toJson, "isActive" -> obj.intervention.isActive.toJson, "arms" -> obj.arms.map(_.armId).toJson, "trialId" -> obj.intervention.trialId.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/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala b/src/test/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/InterventionFormatSuite.scala index 784a655..18c790a 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,7 +16,7 @@ class InterventionFormatSuite extends FlatSpec with Matchers { originalName = "orig name", typeId = Some(LongId(10)), originalType = Some("orig type"), - description = "", + dosage = "", originalDescription = "", isActive = true ) @@ -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], + """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22], "trialId":"NCT000001","originalName":"orig name","originalDescription":"","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)) -- cgit v1.2.3 From f1f9740b7d301c4e7cc7c5c2e38f7661ea0cfbb5 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Thu, 14 Sep 2017 13:32:50 -0700 Subject: Fix random date generation --- src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') 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 fbab2ce..52d7b98 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/common.scala @@ -22,7 +22,7 @@ object common { def nextLocalDate = { val date = generators.nextDate() - LocalDate.of(date.year, date.month, date.day) + LocalDate.of(date.year, date.month + 1, date.day + 1) } def nextCondition = generators.oneOf[Trial.Condition](Trial.Condition.All) -- cgit v1.2.3 From 44c94efb464245544191524925d6d328386543e1 Mon Sep 17 00:00:00 2001 From: tomskikh_ka Date: Fri, 15 Sep 2017 15:44:35 +0700 Subject: PDSUI-2273 Renamed intervention.originalDescription to originalDosage --- .../driver/pdsuidomain/entities/Intervention.scala | 2 +- .../pdsuidomain/fakes/entities/trialcuration.scala | 2 +- .../formats/json/intervention/ApiIntervention.scala | 8 ++++---- .../formats/json/sprayformats/intervention.scala | 20 ++++++++++---------- .../json/sprayformats/InterventionFormatSuite.scala | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) (limited to 'src/main/scala/xyz/driver') diff --git a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala index 3911437..cb677cf 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/entities/Intervention.scala @@ -28,7 +28,7 @@ final case class Intervention(id: LongId[Intervention], typeId: Option[LongId[InterventionType]], originalType: Option[String], dosage: String, - originalDescription: String, + originalDosage: String, isActive: Boolean) object Intervention { 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 35ca3fd..ecb6e0a 100644 --- a/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala +++ b/src/main/scala/xyz/driver/pdsuidomain/fakes/entities/trialcuration.scala @@ -75,7 +75,7 @@ object trialcuration { typeId = Option(nextLongId[InterventionType]), originalType = Option(generators.nextString()), dosage = generators.nextString(), - originalDescription = generators.nextString(), + originalDosage = generators.nextString(), isActive = generators.nextBoolean() ) 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 ba383b6..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 @@ -13,7 +13,7 @@ final case class ApiIntervention(id: Long, arms: List[Long], trialId: String, originalName: String, - originalDescription: String, + originalDosage: String, originalType: Option[String]) { def toDomain = { @@ -25,7 +25,7 @@ final case class ApiIntervention(id: Long, typeId = this.typeId.map(id => LongId(id)), originalType = this.originalType.map(id => id.toString), dosage = this.dosage, - originalDescription = this.originalDescription, + originalDosage = this.originalDosage, isActive = this.isActive ) @@ -48,7 +48,7 @@ object ApiIntervention { (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)) @@ -65,7 +65,7 @@ object ApiIntervention { 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/sprayformats/intervention.scala b/src/main/scala/xyz/driver/pdsuidomain/formats/json/sprayformats/intervention.scala index 41afe6a..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, - "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, - "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 ) } 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 18c790a..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 @@ -17,7 +17,7 @@ class InterventionFormatSuite extends FlatSpec with Matchers { typeId = Some(LongId(10)), originalType = Some("orig type"), dosage = "", - originalDescription = "", + originalDosage = "", isActive = true ) val arms = List( @@ -33,7 +33,7 @@ class InterventionFormatSuite extends FlatSpec with Matchers { writtenJson should be( """{"id":1,"name":"intervention name","typeId":10,"dosage":"","isActive":true,"arms":[20,21,22], - "trialId":"NCT000001","originalName":"orig name","originalDescription":"","originalType":"orig type"}""".parseJson) + "trialId":"NCT000001","originalName":"orig name","originalDosage":"","originalType":"orig type"}""".parseJson) val updateInterventionJson = """{"dosage":"descr","arms":[21,22]}""".parseJson val expectedUpdatedIntervention = orig.copy( -- cgit v1.2.3 From df2d159dc7392e824013846b55cf1bf4b5502c3c Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 19 Sep 2017 20:07:58 -0700 Subject: Add cause to postgres connection errors --- src/main/scala/xyz/driver/pdsuicommon/db/PostgresContext.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver') 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) } } -- cgit v1.2.3 From d4b18efda238f506103dddbf3b400ae17c797276 Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Tue, 19 Sep 2017 22:02:44 -0700 Subject: Fix date generation --- .../scala/xyz/driver/pdsuidomain/fakes/entities/common.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'src/main/scala/xyz/driver') 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) -- cgit v1.2.3