Extract ErrorOps, use liftErr instead of leftMap (#4730)

* Extract ErrorOps, use liftErr instead of leftMap
JSON error formatting cleanup

CHANGELOG_BEGIN
CHANGELOG_END

* Good we have tests for this stuff

* Apply https://github.com/scala/bug/issues/3664 work-around,

so JsonError can be used instead of JsonError.apply

* error formatting
This commit is contained in:
Leonid Shlyapnikov 2020-02-26 15:06:26 -05:00 committed by GitHub
parent 9c414f3fed
commit d93e4382f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 105 additions and 96 deletions

View File

@ -25,7 +25,6 @@ import com.digitalasset.util.ExceptionOps._
import com.typesafe.scalalogging.StrictLogging
import scalaz.std.scalaFuture._
import scalaz.syntax.bitraverse._
import scalaz.syntax.show._
import scalaz.syntax.std.option._
import scalaz.syntax.traverse._
import scalaz.{-\/, Bitraverse, EitherT, NonEmptyList, Show, \/, \/-}
@ -51,6 +50,7 @@ class Endpoints(
extends StrictLogging {
import Endpoints._
import util.ErrorOps._
import json.JsonProtocol._
lazy val all: PartialFunction[HttpRequest, Future[HttpResponse]] = {
@ -70,16 +70,14 @@ class Endpoints(
(jwt, jwtPayload, reqBody) = t3
cmd <- either(
decoder
.decodeR[domain.CreateCommand](reqBody)
.leftMap(e => InvalidUserInput(e.shows))
decoder.decodeR[domain.CreateCommand](reqBody).liftErr(InvalidUserInput)
): ET[domain.CreateCommand[lav1.value.Record]]
ac <- eitherT(
handleFutureFailure(commandService.create(jwt, jwtPayload, cmd))
): ET[domain.ActiveContract[lav1.value.Value]]
jsVal <- either(encoder.encodeV(ac).leftMap(e => ServerError(e.shows))): ET[JsValue]
jsVal <- either(encoder.encodeV(ac).liftErr(ServerError)): ET[JsValue]
} yield domain.OkResponse(jsVal)
@ -90,9 +88,7 @@ class Endpoints(
(jwt, jwtPayload, reqBody) = t3
cmd <- either(
decoder
.decodeExerciseCommand(reqBody)
.leftMap(e => InvalidUserInput(e.shows))
decoder.decodeExerciseCommand(reqBody).liftErr(InvalidUserInput)
): ET[domain.ExerciseCommand[LfValue, domain.ContractLocator[LfValue]]]
resolvedRef <- eitherT(
@ -124,9 +120,7 @@ class Endpoints(
_ = logger.debug(s"/v1/fetch reqBody: $reqBody")
cl <- either(
decoder
.decodeContractLocator(reqBody)
.leftMap(e => InvalidUserInput(e.shows))
decoder.decodeContractLocator(reqBody).liftErr(InvalidUserInput)
): ET[domain.ContractLocator[LfValue]]
_ = logger.debug(s"/v1/fetch cl: $cl")
@ -136,7 +130,7 @@ class Endpoints(
): ET[Option[domain.ActiveContract[LfValue]]]
jsVal <- either(
ac.cata(x => lfAcToJsValue(x).leftMap(e => ServerError(e.shows)), \/-(JsNull))
ac.cata(x => lfAcToJsValue(x), \/-(JsNull))
): ET[JsValue]
} yield domain.OkResponse(jsVal)
@ -163,7 +157,7 @@ class Endpoints(
case (jwt, jwtPayload, reqBody) =>
SprayJson
.decode[domain.GetActiveContractsRequest](reqBody)
.leftMap(e => InvalidUserInput(e.shows))
.liftErr(InvalidUserInput)
.map { cmd =>
val result: SearchResult[ContractsService.Error \/ domain.ActiveContract[JsValue]] =
contractsService
@ -193,9 +187,7 @@ class Endpoints(
(jwt, _, reqBody) = t3
cmd <- either(
SprayJson
.decode[NonEmptyList[domain.Party]](reqBody)
.leftMap(e => InvalidUserInput(e.shows))
SprayJson.decode[NonEmptyList[domain.Party]](reqBody).liftErr(InvalidUserInput)
): ET[NonEmptyList[domain.Party]]
ps <- eitherT(
@ -212,7 +204,7 @@ class Endpoints(
} yield result
private def handleFutureFailure[A: Show, B](fa: Future[A \/ B]): Future[ServerError \/ B] =
fa.map(a => a.leftMap(e => ServerError(e.shows))).recover {
fa.map(_.liftErr(ServerError)).recover {
case NonFatal(e) =>
logger.error("Future failed", e)
-\/(ServerError(e.description))
@ -227,7 +219,7 @@ class Endpoints(
private def handleSourceFailure[E: Show, A]: Flow[E \/ A, ServerError \/ A, NotUsed] =
Flow
.fromFunction((_: E \/ A).leftMap(e => ServerError(e.shows)))
.fromFunction((_: E \/ A).liftErr(ServerError))
.recover {
case NonFatal(e) =>
logger.error("Source failed", e)
@ -316,6 +308,7 @@ class Endpoints(
}
object Endpoints {
import util.ErrorOps._
import json.JsonProtocol._
private type ET[A] = EitherT[Future, Error, A]
@ -325,14 +318,13 @@ object Endpoints {
private type LfValue = lf.value.Value[lf.value.Value.AbsoluteContractId]
private def apiValueToLfValue(a: ApiValue): Error \/ LfValue =
ApiValueToLfValueConverter.apiValueToLfValue(a).leftMap(e => ServerError(e.shows))
ApiValueToLfValueConverter.apiValueToLfValue(a).liftErr(ServerError)
private def lfValueToJsValue(a: LfValue): Error \/ JsValue =
\/.fromTryCatchNonFatal(LfValueCodec.apiValueToJsValue(a)).leftMap(e =>
ServerError(e.description))
\/.fromTryCatchNonFatal(LfValueCodec.apiValueToJsValue(a)).liftErr(ServerError)
private def lfValueToApiValue(a: LfValue): Error \/ ApiValue =
JsValueToApiValueConverter.lfValueToApiValue(a).leftMap(e => ServerError(e.shows))
JsValueToApiValueConverter.lfValueToApiValue(a).liftErr(ServerError)
@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def lfAcToJsValue(a: domain.ActiveContract[LfValue]): Error \/ JsValue = {
@ -348,8 +340,9 @@ object Endpoints {
else domain.OkResponse(parties, Some(domain.UnknownParties(unknownParties)))
}
private def toJsValue[A: JsonWriter](a: A): Error \/ JsValue =
SprayJson.encode(a).leftMap(e => ServerError(e.shows))
private def toJsValue[A: JsonWriter](a: A): Error \/ JsValue = {
SprayJson.encode(a).liftErr(ServerError)
}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
private def toJsValueWithBitraverse[F[_, _], A, B](fab: F[A, B])(
@ -357,11 +350,6 @@ object Endpoints {
ev2: JsonWriter[F[JsValue, JsValue]],
ev3: JsonWriter[A],
ev4: JsonWriter[B]): Error \/ JsValue =
for {
fjj <- fab.bitraverse(
a => toJsValue(a),
b => toJsValue(b)
): Error \/ F[JsValue, JsValue]
jsVal <- toJsValue(fjj)
} yield jsVal
SprayJson.encode2(fab).liftErr(ServerError)
}

View File

@ -3,27 +3,16 @@
package com.digitalasset.http
import akka.http.scaladsl.model.{
ContentTypes,
HttpEntity,
HttpMethod,
HttpRequest,
HttpResponse,
StatusCode,
StatusCodes,
Uri
}
import akka.http.scaladsl.model._
import akka.util.ByteString
import com.digitalasset.http.domain.JwtPayload
import com.digitalasset.http.json.{ResponseFormats, SprayJson}
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import com.digitalasset.http.json.ResponseFormats
import com.digitalasset.jwt.domain.{DecodedJwt, Jwt}
import spray.json.{JsObject, JsValue}
import scalaz.{-\/, Show, \/}
import scalaz.syntax.std.option._
import scalaz.syntax.show._
import com.digitalasset.http.json.JsonProtocol._
import com.digitalasset.ledger.api.auth.AuthServiceJWTCodec
import com.digitalasset.ledger.api.refinements.{ApiTypes => lar}
import scalaz.syntax.std.option._
import scalaz.{-\/, Show, \/}
import spray.json.{JsArray, JsString, JsValue}
import scala.concurrent.Future
@ -59,18 +48,19 @@ object EndpointsCompanion {
httpResponse(StatusCodes.OK, ResponseFormats.resultJsObject(data))
private[http] def httpResponseError(error: Error): HttpResponse = {
val (status, jsObject) = errorsJsObject(error)
httpResponse(status, jsObject)
import com.digitalasset.http.json.JsonProtocol._
val resp = errorResponse(error)
httpResponse(resp.status, resp.toJson)
}
private[http] def errorsJsObject(error: Error): (StatusCode, JsObject) = {
private[http] def errorResponse(error: Error): domain.ErrorResponse[JsValue] = {
val (status, errorMsg): (StatusCode, String) = error match {
case InvalidUserInput(e) => StatusCodes.BadRequest -> e
case ServerError(e) => StatusCodes.InternalServerError -> e
case Unauthorized(e) => StatusCodes.Unauthorized -> e
case NotFound(e) => StatusCodes.NotFound -> e
}
(status, ResponseFormats.errorsJsObject(status, errorMsg))
domain.ErrorResponse(errors = JsArray(JsString(errorMsg)), status = status)
}
private[http] def httpResponse(status: StatusCode, data: JsValue): HttpResponse = {
@ -112,8 +102,4 @@ object EndpointsCompanion {
)
)
}
private[http] def encodeList(as: Seq[JsValue]): ServerError \/ JsValue =
SprayJson.encode(as).leftMap(e => ServerError(e.shows))
}

View File

@ -17,7 +17,7 @@ import json.JsonProtocol.LfValueCodec.{apiValueToJsValue => lfValueToJsValue}
import query.ValuePredicate.{LfV, TypeLookup}
import com.digitalasset.jwt.domain.Jwt
import com.typesafe.scalalogging.LazyLogging
import scalaz.{-\/, Liskov, NonEmptyList, Show, \/, \/-}
import scalaz.{Liskov, NonEmptyList}
import Liskov.<~<
import com.digitalasset.http.query.ValuePredicate
import scalaz.syntax.bifunctor._
@ -28,13 +28,15 @@ import scalaz.syntax.traverse._
import scalaz.std.map._
import scalaz.std.set._
import scalaz.std.tuple._
import scalaz.{-\/, Show, \/, \/-}
import scalaz.{-\/, \/, \/-}
import spray.json.{JsObject, JsString, JsTrue, JsValue}
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
object WebSocketService {
import util.ErrorOps._
private type CompiledQueries = Map[domain.TemplateId.RequiredPkg, LfV => Boolean]
private type StreamPredicate[+Positive] = (
@ -46,11 +48,6 @@ object WebSocketService {
val heartBeat: String = JsObject("heartbeat" -> JsString("ping")).compactPrint
private val liveMarker = JsObject("live" -> JsTrue)
private implicit final class `\\/ WSS extras`[L, R](private val self: L \/ R) extends AnyVal {
def liftErr[M](f: String => M)(implicit L: Show[L]): M \/ R =
self leftMap (e => f(e.shows))
}
private final case class StepAndErrors[+Pos, +LfV](
errors: Seq[ServerError],
step: ContractStreamStep[domain.ArchivedContract, (domain.ActiveContract[LfV], Pos)]) {
@ -242,6 +239,7 @@ class WebSocketService(
extends LazyLogging {
import WebSocketService._
import util.ErrorOps._
import com.digitalasset.http.json.JsonProtocol._
private val numConns = new java.util.concurrent.atomic.AtomicInteger(0)

View File

@ -8,7 +8,6 @@ import com.digitalasset.http.domain.HasTemplateId
import com.digitalasset.http.{PackageService, domain}
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.syntax.bitraverse._
import scalaz.syntax.show._
import scalaz.syntax.std.option._
import scalaz.syntax.traverse._
import scalaz.{Traverse, \/, \/-}
@ -25,12 +24,14 @@ class DomainJsonDecoder(
jsValueToApiValue: (domain.LfType, JsValue) => JsonError \/ lav1.value.Value,
jsValueToLfValue: (domain.LfType, JsValue) => JsonError \/ domain.LfValue) {
import com.digitalasset.http.util.ErrorOps._
def decodeR[F[_]](a: String)(
implicit ev1: JsonReader[F[JsObject]],
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Record] =
for {
b <- SprayJson.parse(a).leftMap(e => JsonError(e.shows))
b <- SprayJson.parse(a).liftErr(JsonError)
c <- SprayJson.mustBeJsObject(b)
d <- decodeR(c)
} yield d
@ -40,7 +41,7 @@ class DomainJsonDecoder(
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Record] =
for {
b <- SprayJson.decode[F[JsObject]](a)(ev1).leftMap(e => JsonError(e.shows))
b <- SprayJson.decode[F[JsObject]](a)(ev1).liftErr(JsonError)
c <- decodeUnderlyingRecords(b)
} yield c
@ -58,7 +59,7 @@ class DomainJsonDecoder(
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Value] =
for {
b <- SprayJson.parse(a).leftMap(e => JsonError(e.shows))
b <- SprayJson.parse(a).liftErr(JsonError)
d <- decodeV(b)
} yield d
@ -67,7 +68,7 @@ class DomainJsonDecoder(
ev2: Traverse[F],
ev3: domain.HasTemplateId[F]): JsonError \/ F[lav1.value.Value] =
for {
b <- SprayJson.decode[F[JsValue]](a)(ev1).leftMap(e => JsonError(e.shows))
b <- SprayJson.decode[F[JsValue]](a)(ev1).liftErr(JsonError)
c <- decodeUnderlyingValues(b)
} yield c
@ -94,19 +95,17 @@ class DomainJsonDecoder(
val templateId: domain.TemplateId.OptionalPkg = H.templateId(fa)
for {
tId <- resolveTemplateId(templateId).toRightDisjunction(
JsonError(s"DomainJsonDecoder_lookupLfType: ${cannotResolveTemplateId(templateId)}"))
JsonError(s"DomainJsonDecoder_lookupLfType ${cannotResolveTemplateId(templateId)}"))
lfType <- H
.lfType(fa, tId, resolveTemplateRecordType, resolveRecordType, resolveKey)
.leftMap(e => JsonError("DomainJsonDecoder_lookupLfType " + e.shows))
.liftErrS("DomainJsonDecoder_lookupLfType")(JsonError)
} yield lfType
}
def decodeContractLocator(a: String)(implicit ev: JsonReader[domain.ContractLocator[JsValue]])
: JsonError \/ domain.ContractLocator[domain.LfValue] =
for {
b <- SprayJson
.parse(a)
.leftMap(e => JsonError("DomainJsonDecoder_decodeContractLocator " + e.shows))
b <- SprayJson.parse(a).liftErrS("DomainJsonDecoder_decodeContractLocator")(JsonError)
c <- decodeContractLocator(b)
} yield c
@ -114,7 +113,7 @@ class DomainJsonDecoder(
: JsonError \/ domain.ContractLocator[domain.LfValue] =
SprayJson
.decode[domain.ContractLocator[JsValue]](a)
.leftMap(e => JsonError("DomainJsonDecoder_decodeContractLocator " + e.shows))
.liftErrS("DomainJsonDecoder_decodeContractLocator")(JsonError)
.flatMap(decodeContractLocatorUnderlyingValue)
private def decodeContractLocatorUnderlyingValue(
@ -130,9 +129,7 @@ class DomainJsonDecoder(
implicit ev1: JsonReader[domain.ExerciseCommand[JsValue, domain.ContractLocator[JsValue]]])
: JsonError \/ domain.ExerciseCommand[domain.LfValue, domain.ContractLocator[domain.LfValue]] =
for {
b <- SprayJson
.parse(a)
.leftMap(e => JsonError("DomainJsonDecoder_decodeExerciseCommand " + e.shows))
b <- SprayJson.parse(a).liftErrS("DomainJsonDecoder_decodeExerciseCommand")(JsonError)
c <- decodeExerciseCommand(b)
} yield c
@ -143,7 +140,7 @@ class DomainJsonDecoder(
for {
cmd0 <- SprayJson
.decode[domain.ExerciseCommand[JsValue, domain.ContractLocator[JsValue]]](a)
.leftMap(e => JsonError("DomainJsonDecoder_decodeExerciseCommand " + e.shows))
.liftErrS("DomainJsonDecoder_decodeExerciseCommand")(JsonError)
lfType <- lookupLfType[domain.ExerciseCommand[+?, domain.ContractLocator[_]]](cmd0)(
domain.ExerciseCommand.hasTemplateId)

View File

@ -5,7 +5,6 @@ package com.digitalasset.http.json
import com.digitalasset.http.domain
import com.digitalasset.ledger.api.{v1 => lav1}
import scalaz.syntax.show._
import scalaz.syntax.traverse._
import scalaz.syntax.bitraverse._
import scalaz.{Traverse, \/}
@ -17,12 +16,14 @@ class DomainJsonEncoder(
apiRecordToJsObject: lav1.value.Record => JsonError \/ JsObject,
apiValueToJsValue: lav1.value.Value => JsonError \/ JsValue) {
import com.digitalasset.http.util.ErrorOps._
def encodeR[F[_]](fa: F[lav1.value.Record])(
implicit ev1: Traverse[F],
ev2: JsonWriter[F[JsObject]]): JsonError \/ JsObject =
for {
a <- encodeUnderlyingRecord(fa)
b <- SprayJson.encode[F[JsObject]](a)(ev2).leftMap(e => JsonError(e.shows))
b <- SprayJson.encode[F[JsObject]](a)(ev2).liftErr(JsonError)
c <- SprayJson.mustBeJsObject(b)
} yield c
@ -36,7 +37,7 @@ class DomainJsonEncoder(
ev2: JsonWriter[F[JsValue]]): JsonError \/ JsValue =
for {
a <- encodeUnderlyingValue(fa)
b <- SprayJson.encode[F[JsValue]](a)(ev2).leftMap(e => JsonError(e.shows))
b <- SprayJson.encode[F[JsValue]](a)(ev2).liftErr(JsonError)
} yield b
// encode underlying values
@ -55,7 +56,7 @@ class DomainJsonEncoder(
ref => encodeContractLocatorUnderlyingValue(ref)
)
y <- SprayJson.encode(x).leftMap(e => JsonError(e.shows))
y <- SprayJson.encode(x).liftErr(JsonError)
} yield y

View File

@ -3,31 +3,33 @@
package com.digitalasset.http.json
import JsonProtocol.LfValueCodec
import com.digitalasset.daml.lf
import com.digitalasset.daml.lf.iface
import com.digitalasset.http.domain
import com.digitalasset.http.json.JsValueToApiValueConverter.LfTypeLookup
import com.digitalasset.http.json.JsonProtocol.LfValueCodec
import com.digitalasset.ledger.api.{v1 => lav1}
import com.digitalasset.platform.participant.util.LfEngineToApi
import scalaz.std.string._
import scalaz.{-\/, \/, \/-}
import spray.json.{JsObject, JsValue}
class JsValueToApiValueConverter(lfTypeLookup: LfTypeLookup) {
import com.digitalasset.http.util.ErrorOps._
def jsValueToLfValue(
lfId: lf.data.Ref.Identifier,
jsValue: JsValue): JsonError \/ lf.value.Value[lf.value.Value.AbsoluteContractId] =
\/.fromTryCatchNonFatal(
LfValueCodec.jsValueToApiValue(jsValue, lfId, lfTypeLookup)
).leftMap(JsonError.toJsonError)
).liftErr(JsonError)
def jsValueToLfValue(
lfType: iface.Type,
jsValue: JsValue): JsonError \/ lf.value.Value[lf.value.Value.AbsoluteContractId] =
\/.fromTryCatchNonFatal(
LfValueCodec.jsValueToApiValue(jsValue, lfType, lfTypeLookup)
).leftMap(JsonError.toJsonError)
).liftErr(JsonError)
def jsValueToApiValue(lfType: domain.LfType, jsValue: JsValue): JsonError \/ lav1.value.Value =
for {
@ -50,9 +52,10 @@ class JsValueToApiValueConverter(lfTypeLookup: LfTypeLookup) {
}
object JsValueToApiValueConverter {
import com.digitalasset.http.util.ErrorOps._
type LfTypeLookup = lf.data.Ref.Identifier => Option[lf.iface.DefDataType.FWT]
def lfValueToApiValue(lfValue: domain.LfValue): JsonError \/ lav1.value.Value =
\/.fromEither(LfEngineToApi.lfValueToApiValue(verbose = true, lfValue))
.leftMap(JsonError.toJsonError)
\/.fromEither(LfEngineToApi.lfValueToApiValue(verbose = true, lfValue)).liftErr(JsonError)
}

View File

@ -3,19 +3,11 @@
package com.digitalasset.http.json
import com.digitalasset.util.ExceptionOps._
import scalaz.Show
import scalaz.syntax.show._
final case class JsonError(message: String)
object JsonError {
def toJsonError(e: String) = JsonError(e)
def toJsonError[E: Show](e: E) = JsonError(e.shows)
def toJsonError(e: Throwable) = JsonError(e.description)
object JsonError extends (String => JsonError) {
implicit val ShowInstance: Show[JsonError] = Show shows { f =>
s"JsonError: ${f.message}"
}

View File

@ -72,6 +72,25 @@ object SprayJson {
\/.fromTryCatchNonFatal(a.toJson).leftMap(e => JsonWriterError(a, e.description))
}
def encode1[F[_], A](fa: F[A])(
implicit ev1: JsonWriter[F[JsValue]],
ev2: Traverse[F],
ev3: JsonWriter[A]): JsonWriterError \/ JsValue =
for {
fj <- fa.traverse(encode[A](_))
jsVal <- encode[F[JsValue]](fj)
} yield jsVal
def encode2[F[_, _], A, B](fab: F[A, B])(
implicit ev1: JsonWriter[F[JsValue, JsValue]],
ev2: Bitraverse[F],
ev3: JsonWriter[A],
ev4: JsonWriter[B]): JsonWriterError \/ JsValue =
for {
fjj <- fab.bitraverse(encode[A](_), encode[B](_))
jsVal <- encode[F[JsValue, JsValue]](fjj)
} yield jsVal
def mustBeJsObject(a: JsValue): JsonError \/ JsObject = a match {
case b: JsObject => \/-(b)
case _ => -\/(JsonError(s"Expected JsObject, got: ${a: JsValue}"))

View File

@ -0,0 +1,25 @@
// Copyright (c) 2020 The DAML Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.digitalasset.http.util
import com.digitalasset.util.ExceptionOps
import scalaz.syntax.show._
import scalaz.{Show, \/}
object ErrorOps {
implicit final class `\\/ WSS extras throwable`[R](private val self: Throwable \/ R)
extends AnyVal {
def liftErr[M](f: String => M): M \/ R =
self leftMap (e => f(ExceptionOps.getDescription(e)))
}
implicit final class `\\/ WSS extras`[L, R](private val self: L \/ R) extends AnyVal {
def liftErr[M](f: String => M)(implicit L: Show[L]): M \/ R =
self leftMap (e => f(e.shows))
def liftErrS[M](msg: String)(f: String => M)(implicit L: Show[L]): M \/ R =
liftErr(x => f(msg + " " + x))
}
}