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