aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob Odersky <jakob@driver.xyz>2017-07-12 22:26:07 -0700
committerJakob Odersky <jakob@driver.xyz>2017-07-12 22:30:35 -0700
commit3e700be0b7df8022627b1f46890f3e3dad3fa54b (patch)
tree0066df95d2a2f033faf9e3e0f9eca92c9c1aad31
parentf9ac0adf5c3bcfcde03bd3ea2bc2471b0d0f99fe (diff)
downloadrest-query-3e700be0b7df8022627b1f46890f3e3dad3fa54b.tar.gz
rest-query-3e700be0b7df8022627b1f46890f3e3dad3fa54b.tar.bz2
rest-query-3e700be0b7df8022627b1f46890f3e3dad3fa54b.zip
Handle errors by failing futures in REST services
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala8
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala17
-rw-r--r--src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala89
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestArmService.scala10
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestCriterionService.scala10
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala47
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala2
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala6
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionTypeService.scala7
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala8
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestStudyDesignService.scala2
-rw-r--r--src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala8
12 files changed, 159 insertions, 55 deletions
diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala b/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala
index 4bf90d1..fc8e474 100644
--- a/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala
+++ b/src/main/scala/xyz/driver/pdsuicommon/error/DomainError.scala
@@ -28,3 +28,11 @@ object DomainError {
Unsafe(Utils.getClassSimpleName(x.getClass))
}
}
+
+/** Subclasses of this exception correspond to subclasses of DomainError. They
+ * are used in REST service implementations to fail futures rather than
+ * returning successful futures, completed with corresponding DomainErrors. */
+class DomainException(message: String) extends RuntimeException(message)
+class NotFoundException(message: String) extends DomainException(message) // 404
+class AuthenticationException(message: String) extends DomainException(message) // 401
+class AuthorizationException(message: String) extends DomainException(message) // 403
diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala
new file mode 100644
index 0000000..5574c01
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorCode.scala
@@ -0,0 +1,17 @@
+package xyz.driver.pdsuicommon.error
+
+import play.api.libs.functional.syntax._
+import play.api.libs.json.{Format, Reads, Writes}
+
+@SuppressWarnings(Array("org.wartremover.warts.Enumeration"))
+object ErrorCode extends Enumeration {
+
+ type ErrorCode = Value
+ val Unspecified = Value(1)
+
+ private val fromJsonReads: Reads[ErrorCode] = Reads.of[Int].map(ErrorCode.apply)
+ private val toJsonWrites: Writes[ErrorCode] = Writes.of[Int].contramap(_.id)
+
+ implicit val jsonFormat: Format[ErrorCode] = Format(fromJsonReads, toJsonWrites)
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala
new file mode 100644
index 0000000..6ceadb2
--- /dev/null
+++ b/src/main/scala/xyz/driver/pdsuicommon/error/ErrorsResponse.scala
@@ -0,0 +1,89 @@
+package xyz.driver.pdsuicommon.error
+
+import xyz.driver.pdsuicommon.json.Serialization.seqJsonFormat
+import ErrorCode.{ErrorCode, Unspecified}
+import ErrorsResponse.ResponseError
+import xyz.driver.pdsuicommon.auth.{AnonymousRequestContext, RequestId}
+import xyz.driver.pdsuicommon.utils.Utils
+import play.api.http.Writeable
+import play.api.libs.functional.syntax._
+import play.api.libs.json._
+import play.api.mvc.{Result, Results}
+import xyz.driver.pdsuicommon.validation.JsonValidationErrors
+
+class ErrorsResponse(val errors: Seq[ResponseError], private val httpStatus: Results#Status, val requestId: RequestId) {
+ def toResult(implicit writeable: Writeable[ErrorsResponse]): Result = httpStatus(this)
+}
+
+object ErrorsResponse {
+
+ /**
+ * @param data Any data that can be associated with particular error.Ex.: error field name
+ * @param message Error message
+ * @param code Unique error code
+ *
+ * @see https://driverinc.atlassian.net/wiki/display/RA/REST+API+Specification#RESTAPISpecification-HTTPStatuscodes
+ */
+ final case class ResponseError(data: Option[String], message: String, code: ErrorCode)
+
+ object ResponseError {
+
+ implicit val responseErrorJsonFormat: Format[ResponseError] = (
+ (JsPath \ "data").formatNullable[String] and
+ (JsPath \ "message").format[String] and
+ (JsPath \ "code").format[ErrorCode]
+ )(ResponseError.apply, unlift(ResponseError.unapply))
+
+ }
+
+ implicit val writes: Writes[ErrorsResponse] = Writes { errorsResponse =>
+ Json.obj(
+ "errors" -> Json.toJson(errorsResponse.errors),
+ "requestId" -> Json.toJson(errorsResponse.requestId.value)
+ )
+ }
+
+ // deprecated, will be removed in REP-436
+ def fromString(message: String, httpStatus: Results#Status)(
+ implicit context: AnonymousRequestContext): ErrorsResponse = {
+ new ErrorsResponse(
+ errors = Seq(
+ ResponseError(
+ data = None,
+ message = message,
+ code = Unspecified
+ )),
+ httpStatus = httpStatus,
+ requestId = context.requestId
+ )
+ }
+
+ // scalastyle:off null
+ def fromExceptionMessage(e: Throwable, httpStatus: Results#Status = Results.InternalServerError)(
+ implicit context: AnonymousRequestContext): ErrorsResponse = {
+ val message = if (e.getMessage == null || e.getMessage.isEmpty) {
+ Utils.getClassSimpleName(e.getClass)
+ } else {
+ e.getMessage
+ }
+
+ fromString(message, httpStatus)
+ }
+ // scalastyle:on null
+
+ // deprecated, will be removed in REP-436
+ def fromJsonValidationErrors(validationErrors: JsonValidationErrors)(
+ implicit context: AnonymousRequestContext): ErrorsResponse = {
+ val errors = validationErrors.map {
+ case (path, xs) =>
+ ResponseError(
+ data = Some(path.toString()),
+ message = xs.map(_.message).mkString("\n"),
+ code = Unspecified
+ )
+ }
+
+ new ErrorsResponse(errors, Results.BadRequest, context.requestId)
+ }
+
+}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestArmService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestArmService.scala
index 299e6f8..e7cd2db 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestArmService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestArmService.scala
@@ -40,7 +40,7 @@ class RestArmService(transport: ServiceTransport, baseUri: Uri)(implicit protect
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiArm], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount)
- }()
+ }
} yield {
reply
}
@@ -52,7 +52,7 @@ class RestArmService(transport: ServiceTransport, baseUri: Uri)(implicit protect
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiArm, GetByIdReply](response) { api =>
GetByIdReply.Entity(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -65,7 +65,7 @@ class RestArmService(transport: ServiceTransport, baseUri: Uri)(implicit protect
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiArm, CreateReply](response) { api =>
CreateReply.Created(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -78,7 +78,7 @@ class RestArmService(transport: ServiceTransport, baseUri: Uri)(implicit protect
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiArm, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -90,7 +90,7 @@ class RestArmService(transport: ServiceTransport, baseUri: Uri)(implicit protect
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiArm, DeleteReply](response) { _ =>
DeleteReply.Deleted
- }()
+ }
} yield {
reply
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestCriterionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestCriterionService.scala
index b27ce3d..f2cc9a8 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestCriterionService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestCriterionService.scala
@@ -36,7 +36,7 @@ class RestCriterionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiCriterion, CreateReply](response) { api =>
CreateReply.Created(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -48,7 +48,7 @@ class RestCriterionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiCriterion, GetByIdReply](response) { api =>
GetByIdReply.Entity(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -72,7 +72,7 @@ class RestCriterionService(transport: ServiceTransport, baseUri: Uri)(
api.meta.itemsCount,
api.meta.lastUpdate
)
- }()
+ }
} yield {
reply
}
@@ -87,7 +87,7 @@ class RestCriterionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiCriterion, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -99,7 +99,7 @@ class RestCriterionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiCriterion, DeleteReply](response) { _ =>
DeleteReply.Deleted
- }()
+ }
} yield {
reply
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
index 7d2838b..bd9580a 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHelper.scala
@@ -1,10 +1,8 @@
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.model.{HttpResponse, ResponseEntity, StatusCodes, Uri}
import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
import akka.stream.Materializer
import xyz.driver.core.rest.ServiceRequestContext
@@ -17,7 +15,8 @@ import xyz.driver.pdsuicommon.db.{
Sorting,
SortingOrder
}
-import xyz.driver.pdsuicommon.error.DomainError
+import xyz.driver.pdsuicommon.serialization.PlayJsonSupport
+import xyz.driver.pdsuicommon.error._
trait RestHelper {
@@ -30,9 +29,6 @@ trait RestHelper {
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 {
@@ -97,33 +93,30 @@ trait RestHelper {
* @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
- )
+ def extractErrorMessage(response: HttpResponse): Future[String] = {
+ import PlayJsonSupport._
+ Unmarshal(response.entity)
+ .to[ErrorsResponse.ResponseError]
+ .transform(
+ _.message,
+ ex => new DomainException(ex.getMessage)
+ )
+ }
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}")
- )
+ extractErrorMessage(response).flatMap { message =>
+ Future.failed(response.status match {
+ case StatusCodes.Unauthorized => new AuthenticationException(message)
+ case StatusCodes.Forbidden => new AuthorizationException(message)
+ case StatusCodes.NotFound => new NotFoundException(message)
+ case other =>
+ new DomainException(s"Unhandled domain error for HTTP status ${other.value}. Message ${message}")
+ })
}
}
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala
index ff9d490..085c347 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestHypothesisService.scala
@@ -30,7 +30,7 @@ class RestHypothesisService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiHypothesis], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount)
- }()
+ }
} yield {
reply
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
index 810a9d6..9b5c51c 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionService.scala
@@ -40,7 +40,7 @@ class RestInterventionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiIntervention], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount)
- }()
+ }
} yield {
reply
}
@@ -53,7 +53,7 @@ class RestInterventionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiIntervention, GetByIdReply](response) { api =>
GetByIdReply.Entity(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -69,7 +69,7 @@ class RestInterventionService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiIntervention, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionTypeService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionTypeService.scala
index 1243500..8a4694c 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionTypeService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestInterventionTypeService.scala
@@ -7,7 +7,6 @@ import akka.stream.ActorMaterializer
import xyz.driver.core.rest._
import xyz.driver.pdsuicommon.auth._
import xyz.driver.pdsuicommon.db._
-import xyz.driver.pdsuicommon.error.DomainError
import xyz.driver.pdsuidomain.formats.json.intervention.ApiInterventionType
import xyz.driver.pdsuidomain.services.InterventionTypeService
@@ -23,14 +22,12 @@ class RestInterventionTypeService(transport: ServiceTransport, baseUri: Uri)(
def getAll(sorting: Option[Sorting] = None)(
implicit requestContext: AuthenticatedRequestContext): Future[GetListReply] = {
+ val request = HttpRequest(HttpMethods.GET, endpointUri(baseUri, "/v1/intervention-type", sortingQuery(sorting)))
for {
- response <- transport.sendRequestGetResponse(requestContext)(
- get(baseUri, "/v1/intervention-type", query = sortingQuery(sorting)))
+ response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiInterventionType], GetListReply](response) { list =>
val domain = list.items.map(_.toDomain)
GetListReply.EntityList(domain.toList, list.meta.itemsCount)
- } {
- case _: DomainError.AuthorizationError => GetListReply.AuthorizationError
}
} yield {
reply
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala
index 61d2050..e0e0813 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestMessageService.scala
@@ -34,7 +34,7 @@ class RestMessageService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiMessage, CreateReply](response) { api =>
CreateReply.Created(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -66,7 +66,7 @@ class RestMessageService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiMessage], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount, api.meta.lastUpdate)
- }()
+ }
} yield {
reply
}
@@ -81,7 +81,7 @@ class RestMessageService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiMessage, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -94,7 +94,7 @@ class RestMessageService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiMessage, DeleteReply](response) { _ =>
DeleteReply.Deleted
- }()
+ }
} yield {
reply
}
diff --git a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestStudyDesignService.scala b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestStudyDesignService.scala
index 66f7a78..1945b0a 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestStudyDesignService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestStudyDesignService.scala
@@ -28,7 +28,7 @@ class RestStudyDesignService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiStudyDesign], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount)
- }()
+ }
} yield {
reply
}
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 9b4b576..db7f79e 100644
--- a/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
+++ b/src/main/scala/xyz/driver/pdsuidomain/services/rest/RestTrialService.scala
@@ -41,7 +41,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiTrial, GetByIdReply](response) { api =>
GetByIdReply.Entity(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -64,7 +64,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ListResponse[ApiTrial], GetListReply](response) { api =>
GetListReply.EntityList(api.items.map(_.toDomain), api.meta.itemsCount, api.meta.lastUpdate)
- }()
+ }
} yield {
reply
}
@@ -79,7 +79,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiTrial, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}
@@ -93,7 +93,7 @@ class RestTrialService(transport: ServiceTransport, baseUri: Uri)(
response <- transport.sendRequestGetResponse(requestContext)(request)
reply <- apiResponse[ApiTrial, UpdateReply](response) { api =>
UpdateReply.Updated(api.toDomain)
- }()
+ }
} yield {
reply
}