aboutsummaryrefslogblamecommitdiff
path: root/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
blob: 7d2838bf142ec61825ed611d39e9e8db78fbc8f7 (plain) (tree)










































































































































                                                                                                                      
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)
  }

}