diff options
author | Jakob Odersky <jakob@driver.xyz> | 2017-07-05 19:02:13 -0700 |
---|---|---|
committer | Jakob Odersky <jakob@driver.xyz> | 2017-07-12 21:04:25 -0700 |
commit | f9ac0adf5c3bcfcde03bd3ea2bc2471b0d0f99fe (patch) | |
tree | 9e26568fe6598074a6de8815b465cbfc7ff69b7c /src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala | |
parent | 3d902b5197db861c30325c159dc10cfb211ae209 (diff) | |
download | rest-query-f9ac0adf5c3bcfcde03bd3ea2bc2471b0d0f99fe.tar.gz rest-query-f9ac0adf5c3bcfcde03bd3ea2bc2471b0d0f99fe.tar.bz2 rest-query-f9ac0adf5c3bcfcde03bd3ea2bc2471b0d0f99fe.zip |
Implement REST services for trial curation
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.scala | 139 |
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) + } + +} |