aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala')
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala139
1 files changed, 139 insertions, 0 deletions
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
new file mode 100644
index 0000000..7d2838b
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
@@ -0,0 +1,139 @@
+package xyz.driver.pdsuidomain.services.rest
+
+import java.lang.RuntimeException
+
+import scala.concurrent.{ExecutionContext, Future}
+
+import akka.http.scaladsl.model.{HttpMethods, HttpRequest, HttpResponse, ResponseEntity, StatusCode, StatusCodes, Uri}
+import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
+import akka.stream.Materializer
+import xyz.driver.core.rest.ServiceRequestContext
+import xyz.driver.pdsuicommon.auth.{AnonymousRequestContext, AuthenticatedRequestContext}
+import xyz.driver.pdsuicommon.db.{
+ Pagination,
+ SearchFilterBinaryOperation,
+ SearchFilterExpr,
+ SearchFilterNAryOperation,
+ Sorting,
+ SortingOrder
+}
+import xyz.driver.pdsuicommon.error.DomainError
+
+trait RestHelper {
+
+ implicit protected val materializer: Materializer
+ implicit protected val exec: ExecutionContext
+
+ protected def endpointUri(baseUri: Uri, path: String) =
+ baseUri.withPath(Uri.Path(path))
+
+ protected def endpointUri(baseUri: Uri, path: String, query: Seq[(String, String)]) =
+ baseUri.withPath(Uri.Path(path)).withQuery(Uri.Query(query: _*))
+
+ def get(baseUri: Uri, path: String, query: Seq[(String, String)] = Seq.empty): HttpRequest =
+ HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query))
+
+ def sortingQuery(sorting: Option[Sorting]): Seq[(String, String)] = {
+ def dimensionQuery(dimension: Sorting.Dimension) = {
+ val ord = dimension.order match {
+ case SortingOrder.Ascending => ""
+ case SortingOrder.Descending => "-"
+ }
+ s"$ord${dimension.name}"
+ }
+
+ sorting match {
+ case None => Seq.empty
+ case Some(dimension: Sorting.Dimension) => Seq("sort" -> dimensionQuery(dimension))
+ case Some(Sorting.Sequential(dimensions)) => Seq("sort" -> dimensions.map(dimensionQuery).mkString(","))
+ }
+ }
+
+ def filterQuery(expr: SearchFilterExpr): Seq[(String, String)] = {
+ def opToString(op: SearchFilterBinaryOperation) = op match {
+ case SearchFilterBinaryOperation.Eq => "eq"
+ case SearchFilterBinaryOperation.NotEq => "ne"
+ case SearchFilterBinaryOperation.Like => "like"
+ case SearchFilterBinaryOperation.Gt => "gt"
+ case SearchFilterBinaryOperation.GtEq => "ge"
+ case SearchFilterBinaryOperation.Lt => "lt"
+ case SearchFilterBinaryOperation.LtEq => "le"
+ }
+
+ def exprToQuery(expr: SearchFilterExpr): Seq[(String, String)] = expr match {
+ case SearchFilterExpr.Empty => Seq.empty
+ case SearchFilterExpr.Atom.Binary(dimension, op, value) =>
+ Seq("filters" -> s"${dimension.name} ${opToString(op)} $value")
+ case SearchFilterExpr.Atom.NAry(dimension, SearchFilterNAryOperation.In, values) =>
+ Seq("filters" -> s"${dimension.name} in ${values.mkString(",")}")
+ case SearchFilterExpr.Intersection(ops) =>
+ ops.flatMap(op => exprToQuery(op))
+ case expr => sys.error(s"No parser available for filter expression $expr.")
+ }
+
+ exprToQuery(expr)
+ }
+
+ def paginationQuery(pagination: Option[Pagination]): Seq[(String, String)] = pagination match {
+ case None => Seq.empty
+ case Some(pp) =>
+ Seq(
+ "pageNumber" -> pp.pageNumber.toString,
+ "pageSize" -> pp.pageSize.toHexString
+ )
+ }
+
+ /** Utility method to parse responses that encode success and errors as subtypes
+ * of a common reply type.
+ *
+ * @tparam ApiReply The type of the serialized reply object, contained in the HTTP entity
+ * @tparam DomainReply The type of the domain object that will be created from a successful reply.
+ *
+ * @param response The HTTP response to parse.
+ * @param successMapper Transformation function from a deserialized api entity to a domain object.
+ * @param errorMapper Transformation function from general domain errors to
+ * specialized errors of the given DomainReply. Note that if a domain error
+ * is not explicitly handled, it will be encoded as a failure in the returned future.
+ * @param unmarshaller An unmarshaller that converts a successful response to an api reply.
+ */
+ def apiResponse[ApiReply, DomainReply](response: HttpResponse)(successMapper: ApiReply => DomainReply)(
+ errorMapper: PartialFunction[DomainError, DomainReply] = PartialFunction.empty)(
+ implicit unmarshaller: Unmarshaller[ResponseEntity, ApiReply]): Future[DomainReply] = {
+
+ val domainErrors: Map[StatusCode, DomainError] = Map(
+ StatusCodes.Unauthorized -> new DomainError.AuthenticationError {
+ override protected def userMessage: String = "unauthorized"
+ }, // 401
+ StatusCodes.Forbidden -> new DomainError.AuthorizationError {
+ override protected def userMessage: String = "forbidden"
+ }, // 403
+ StatusCodes.NotFound -> new DomainError.NotFoundError {
+ override protected def userMessage: String = "not found"
+ } // 404
+ )
+
+ if (response.status.isSuccess) {
+ val reply = Unmarshal(response.entity).to[ApiReply]
+ reply.map(successMapper)
+ } else {
+ val domainError = domainErrors.get(response.status)
+ domainError.flatMap(errorMapper.lift) match {
+ case Some(error) => Future.successful(error)
+ case None =>
+ Future.failed(
+ new RuntimeException(
+ s"Unhandled domain error for HTTP status ${response.status}. Message ${response.entity}")
+ )
+ }
+ }
+ }
+
+ implicit def toServiceRequestContext(requestContext: AnonymousRequestContext): ServiceRequestContext = {
+ val auth: Map[String, String] = requestContext match {
+ case ctx: AuthenticatedRequestContext => Map("Auth-token" -> ctx.authToken)
+ case _ => Map()
+ }
+ new ServiceRequestContext(contextHeaders = auth)
+ }
+
+}