From 0e1a6660bb0a948f6cfb48c78fb6d2209f982d28 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 00:05:29 +0200 Subject: [PATCH 1/7] Create caches for document encoders --- .../CachedDocumentCodecs.scala | 58 +++++++++++++++++++ .../smithy4sinterop/CirceDecoderImpl.scala | 47 +++++++++++++++ .../smithy4sinterop/CirceEncoderImpl.scala | 29 ++++++++++ .../smithy4sinterop/CirceJsonCodec.scala | 48 ++------------- .../smithy4sinterop/ClientStub.scala | 15 ++++- .../smithy4sinterop/ServerEndpoints.scala | 12 +++- 6 files changed, 163 insertions(+), 46 deletions(-) create mode 100644 modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala create mode 100644 modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala create mode 100644 modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala new file mode 100644 index 0000000..8b5143e --- /dev/null +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala @@ -0,0 +1,58 @@ +package jsonrpclib.smithy4sinterop + +import smithy4s.codecs.PayloadError +import smithy4s.internals.DocumentDecoderSchemaVisitor +import smithy4s.internals.DocumentEncoderSchemaVisitor +import smithy4s.schema.CachedSchemaCompiler +import smithy4s.schema.FieldFilter +import smithy4s.Document +import smithy4s.Document._ +import smithy4s.Schema + +private[smithy4sinterop] class CachedEncoderCompilerImpl(fieldFilter: FieldFilter) + extends CachedSchemaCompiler.DerivingImpl[Encoder] + with EncoderCompiler { + + protected type Aux[A] = smithy4s.internals.DocumentEncoder[A] + + def fromSchema[A]( + schema: Schema[A], + cache: Cache + ): Encoder[A] = { + val makeEncoder = + schema.compile( + new DocumentEncoderSchemaVisitor(cache, fieldFilter) + ) + new Encoder[A] { + def encode(a: A): Document = { + makeEncoder.apply(a) + } + } + } + + def withFieldFilter( + fieldFilter: FieldFilter + ): EncoderCompiler = new CachedEncoderCompilerImpl( + fieldFilter + ) +} + +private[smithy4sinterop] class CachedDecoderCompilerImpl extends CachedSchemaCompiler.Impl[Decoder] { + + protected type Aux[A] = smithy4s.internals.DocumentDecoder[A] + + def fromSchema[A]( + schema: Schema[A], + cache: Cache + ): Decoder[A] = { + val decodeFunction = + schema.compile(new DocumentDecoderSchemaVisitor(cache)) + new Decoder[A] { + def decode(a: Document): Either[PayloadError, A] = + try { Right(decodeFunction(Nil, a)) } + catch { + case e: PayloadError => Left(e) + } + } + } +} diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala new file mode 100644 index 0000000..b639ee4 --- /dev/null +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala @@ -0,0 +1,47 @@ +package jsonrpclib.smithy4sinterop + +import io.circe.{Decoder => CirceDecoder, _} +import smithy4s.codecs.PayloadPath +import smithy4s.schema.CachedSchemaCompiler +import smithy4s.Document +import smithy4s.Document.{Encoder => _, _} +import smithy4s.Schema + +class CirceDecoderImpl extends CachedSchemaCompiler[CirceDecoder] { + val decoder: CachedDecoderCompilerImpl = new CachedDecoderCompilerImpl() + + type Cache = decoder.Cache + def createCache(): Cache = decoder.createCache() + + def fromSchema[A](schema: Schema[A], cache: Cache): CirceDecoder[A] = + c => { + c.as[Json] + .map(fromJson(_)) + .flatMap { d => + decoder + .fromSchema(schema, cache) + .decode(d) + .left + .map(e => + DecodingFailure(DecodingFailure.Reason.CustomReason(e.getMessage), c.history ++ toCursorOps(e.path)) + ) + } + } + + def fromSchema[A](schema: Schema[A]): CirceDecoder[A] = fromSchema(schema, createCache()) + + private def toCursorOps(path: PayloadPath): List[CursorOp] = + path.segments.map { + case PayloadPath.Segment.Label(name) => CursorOp.DownField(name) + case PayloadPath.Segment.Index(i) => CursorOp.DownN(i) + } + + private def fromJson(json: Json): Document = json.fold( + jsonNull = DNull, + jsonBoolean = DBoolean(_), + jsonNumber = n => DNumber(n.toBigDecimal.get), + jsonString = DString(_), + jsonArray = arr => DArray(arr.map(fromJson)), + jsonObject = obj => DObject(obj.toMap.view.mapValues(fromJson).toMap) + ) +} diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala new file mode 100644 index 0000000..dc574b0 --- /dev/null +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala @@ -0,0 +1,29 @@ +package jsonrpclib.smithy4sinterop + +import io.circe._ +import smithy4s.schema.CachedSchemaCompiler +import smithy4s.schema.FieldFilter +import smithy4s.Document +import smithy4s.Document.{Encoder => _, _} +import smithy4s.Schema + +class CirceEncoderImpl(fieldFilter: FieldFilter) extends CachedSchemaCompiler[Encoder] { + val encoder: CachedEncoderCompilerImpl = new CachedEncoderCompilerImpl(fieldFilter) + + type Cache = encoder.Cache + def createCache(): Cache = encoder.createCache() + + def fromSchema[A](schema: Schema[A], cache: Cache): Encoder[A] = + a => documentToJson(encoder.fromSchema(schema, cache).encode(a)) + + def fromSchema[A](schema: Schema[A]): Encoder[A] = fromSchema(schema, createCache()) + + private val documentToJson: Document => Json = { + case DNull => Json.Null + case DString(value) => Json.fromString(value) + case DBoolean(value) => Json.fromBoolean(value) + case DNumber(value) => Json.fromBigDecimal(value) + case DArray(values) => Json.fromValues(values.map(documentToJson)) + case DObject(entries) => Json.fromFields(entries.view.mapValues(documentToJson)) + } +} diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala index 681c8e3..71f2006 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala @@ -1,55 +1,19 @@ package jsonrpclib.smithy4sinterop import io.circe._ -import smithy4s.codecs.PayloadPath -import smithy4s.Document -import smithy4s.Document.{Decoder => _, _} +import smithy4s.schema.FieldFilter import smithy4s.Schema object CirceJsonCodec { + object Encoder extends CirceEncoderImpl(FieldFilter.Default) + object Decoder extends CirceDecoderImpl + /** Creates a Circe `Codec[A]` from a Smithy4s `Schema[A]`. * * This enables encoding values of type `A` to JSON and decoding JSON back into `A`, using the structure defined by * the Smithy schema. */ - def fromSchema[A](implicit schema: Schema[A]): Codec[A] = Codec.from( - c => { - c.as[Json] - .map(fromJson) - .flatMap { d => - Document - .decode[A](d) - .left - .map(e => - DecodingFailure(DecodingFailure.Reason.CustomReason(e.getMessage), c.history ++ toCursorOps(e.path)) - ) - } - }, - a => documentToJson(Document.encode(a)) - ) - - private def toCursorOps(path: PayloadPath): List[CursorOp] = - path.segments.map { - case PayloadPath.Segment.Label(name) => CursorOp.DownField(name) - case PayloadPath.Segment.Index(i) => CursorOp.DownN(i) - } - - private val documentToJson: Document => Json = { - case DNull => Json.Null - case DString(value) => Json.fromString(value) - case DBoolean(value) => Json.fromBoolean(value) - case DNumber(value) => Json.fromBigDecimal(value) - case DArray(values) => Json.fromValues(values.map(documentToJson)) - case DObject(entries) => Json.fromFields(entries.view.mapValues(documentToJson)) - } - - private def fromJson(json: Json): Document = json.fold( - jsonNull = DNull, - jsonBoolean = DBoolean(_), - jsonNumber = n => DNumber(n.toBigDecimal.get), - jsonString = DString(_), - jsonArray = arr => DArray(arr.map(fromJson)), - jsonObject = obj => DObject(obj.toMap.view.mapValues(fromJson).toMap) - ) + def fromSchema[A](implicit schema: Schema[A]): Codec[A] = + Codec.from(Decoder.fromSchema(schema), Encoder.fromSchema(schema)) } diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index 4f2de95..821061e 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -4,7 +4,10 @@ import io.circe.Codec import jsonrpclib.Channel import jsonrpclib.Monadic import smithy4s.~> +import smithy4s.codecs.Decoder +import smithy4s.codecs.Encoder import smithy4s.schema._ +import smithy4s.Document import smithy4s.Service import smithy4s.ShapeId @@ -46,9 +49,17 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv smithy4sEndpoint: service.Endpoint[I, E, O, SI, SO], endpointSpec: EndpointSpec ): I => F[O] = { + val decoderCache = CirceJsonCodec.Decoder.createCache() + val encoderCache = CirceJsonCodec.Encoder.createCache() - implicit val inputCodec: Codec[I] = CirceJsonCodec.fromSchema(smithy4sEndpoint.input) - implicit val outputCodec: Codec[O] = CirceJsonCodec.fromSchema(smithy4sEndpoint.output) + implicit val inputCodec: Codec[I] = Codec.from( + CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.input, decoderCache), + CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.input, encoderCache) + ) + implicit val outputCodec: Codec[O] = Codec.from( + CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.output, decoderCache), + CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.output, encoderCache) + ) endpointSpec match { case EndpointSpec.Notification(methodName) => diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala index f4e375b..1117ddc 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala @@ -63,9 +63,17 @@ object ServerEndpoints { endpointSpec: EndpointSpec, impl: FunctorInterpreter[Op, F] ): Endpoint[F] = { + val decoderCache = CirceJsonCodec.Decoder.createCache() + val encoderCache = CirceJsonCodec.Encoder.createCache() - implicit val inputCodec: Codec[I] = CirceJsonCodec.fromSchema(smithy4sEndpoint.input) - implicit val outputCodec: Codec[O] = CirceJsonCodec.fromSchema(smithy4sEndpoint.output) + implicit val inputCodec: Codec[I] = Codec.from( + CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.input, decoderCache), + CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.input, encoderCache) + ) + implicit val outputCodec: Codec[O] = Codec.from( + CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.output, decoderCache), + CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.output, encoderCache) + ) def errorResponse(throwable: Throwable): F[E] = throwable match { case smithy4sEndpoint.Error((_, e)) => e.pure From fe98e4a20cac7a2861f39dbd381a7dceb61c3a8e Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 11:53:19 +0200 Subject: [PATCH 2/7] Remove unused imports --- .../src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index 821061e..606533c 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -4,10 +4,7 @@ import io.circe.Codec import jsonrpclib.Channel import jsonrpclib.Monadic import smithy4s.~> -import smithy4s.codecs.Decoder -import smithy4s.codecs.Encoder import smithy4s.schema._ -import smithy4s.Document import smithy4s.Service import smithy4s.ShapeId From 4732bbbb27605614a5195d898b9fc9c791b8576a Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 12:03:42 +0200 Subject: [PATCH 3/7] Remove redundant classes --- .../CachedDocumentCodecs.scala | 58 ------------------- .../smithy4sinterop/CirceDecoderImpl.scala | 2 +- .../smithy4sinterop/CirceEncoderImpl.scala | 14 ++--- .../smithy4sinterop/CirceJsonCodec.scala | 3 +- 4 files changed, 9 insertions(+), 68 deletions(-) delete mode 100644 modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala deleted file mode 100644 index 8b5143e..0000000 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CachedDocumentCodecs.scala +++ /dev/null @@ -1,58 +0,0 @@ -package jsonrpclib.smithy4sinterop - -import smithy4s.codecs.PayloadError -import smithy4s.internals.DocumentDecoderSchemaVisitor -import smithy4s.internals.DocumentEncoderSchemaVisitor -import smithy4s.schema.CachedSchemaCompiler -import smithy4s.schema.FieldFilter -import smithy4s.Document -import smithy4s.Document._ -import smithy4s.Schema - -private[smithy4sinterop] class CachedEncoderCompilerImpl(fieldFilter: FieldFilter) - extends CachedSchemaCompiler.DerivingImpl[Encoder] - with EncoderCompiler { - - protected type Aux[A] = smithy4s.internals.DocumentEncoder[A] - - def fromSchema[A]( - schema: Schema[A], - cache: Cache - ): Encoder[A] = { - val makeEncoder = - schema.compile( - new DocumentEncoderSchemaVisitor(cache, fieldFilter) - ) - new Encoder[A] { - def encode(a: A): Document = { - makeEncoder.apply(a) - } - } - } - - def withFieldFilter( - fieldFilter: FieldFilter - ): EncoderCompiler = new CachedEncoderCompilerImpl( - fieldFilter - ) -} - -private[smithy4sinterop] class CachedDecoderCompilerImpl extends CachedSchemaCompiler.Impl[Decoder] { - - protected type Aux[A] = smithy4s.internals.DocumentDecoder[A] - - def fromSchema[A]( - schema: Schema[A], - cache: Cache - ): Decoder[A] = { - val decodeFunction = - schema.compile(new DocumentDecoderSchemaVisitor(cache)) - new Decoder[A] { - def decode(a: Document): Either[PayloadError, A] = - try { Right(decodeFunction(Nil, a)) } - catch { - case e: PayloadError => Left(e) - } - } - } -} diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala index b639ee4..9d2bf75 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala @@ -8,7 +8,7 @@ import smithy4s.Document.{Encoder => _, _} import smithy4s.Schema class CirceDecoderImpl extends CachedSchemaCompiler[CirceDecoder] { - val decoder: CachedDecoderCompilerImpl = new CachedDecoderCompilerImpl() + val decoder: CachedSchemaCompiler.DerivingImpl[Decoder] = Document.Decoder type Cache = decoder.Cache def createCache(): Cache = decoder.createCache() diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala index dc574b0..c262c1c 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala @@ -1,22 +1,22 @@ package jsonrpclib.smithy4sinterop -import io.circe._ +import io.circe.{Encoder => CirceEncoder, _} import smithy4s.schema.CachedSchemaCompiler -import smithy4s.schema.FieldFilter import smithy4s.Document -import smithy4s.Document.{Encoder => _, _} +import smithy4s.Document._ +import smithy4s.Document.Encoder import smithy4s.Schema -class CirceEncoderImpl(fieldFilter: FieldFilter) extends CachedSchemaCompiler[Encoder] { - val encoder: CachedEncoderCompilerImpl = new CachedEncoderCompilerImpl(fieldFilter) +class CirceEncoderImpl extends CachedSchemaCompiler[CirceEncoder] { + val encoder: CachedSchemaCompiler.DerivingImpl[Encoder] = Document.Encoder type Cache = encoder.Cache def createCache(): Cache = encoder.createCache() - def fromSchema[A](schema: Schema[A], cache: Cache): Encoder[A] = + def fromSchema[A](schema: Schema[A], cache: Cache): CirceEncoder[A] = a => documentToJson(encoder.fromSchema(schema, cache).encode(a)) - def fromSchema[A](schema: Schema[A]): Encoder[A] = fromSchema(schema, createCache()) + def fromSchema[A](schema: Schema[A]): CirceEncoder[A] = fromSchema(schema, createCache()) private val documentToJson: Document => Json = { case DNull => Json.Null diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala index 71f2006..4b48510 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala @@ -1,12 +1,11 @@ package jsonrpclib.smithy4sinterop import io.circe._ -import smithy4s.schema.FieldFilter import smithy4s.Schema object CirceJsonCodec { - object Encoder extends CirceEncoderImpl(FieldFilter.Default) + object Encoder extends CirceEncoderImpl object Decoder extends CirceDecoderImpl /** Creates a Circe `Codec[A]` from a Smithy4s `Schema[A]`. From dea62f108e807026509bbadf15faf06fd90ee018 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 15:50:03 +0200 Subject: [PATCH 4/7] Apply review comments --- .../scala/examples/server/ServerMain.scala | 2 -- .../smithy4sinterop/CirceJsonCodec.scala | 17 +++++++++++- .../smithy4sinterop/ClientStub.scala | 18 +++++-------- .../smithy4sinterop/ServerEndpoints.scala | 27 ++++++++----------- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/modules/examples/server/src/main/scala/examples/server/ServerMain.scala b/modules/examples/server/src/main/scala/examples/server/ServerMain.scala index 786a278..2704b5b 100644 --- a/modules/examples/server/src/main/scala/examples/server/ServerMain.scala +++ b/modules/examples/server/src/main/scala/examples/server/ServerMain.scala @@ -4,8 +4,6 @@ import cats.effect._ import fs2.io._ import io.circe.generic.semiauto._ import io.circe.Codec -import io.circe.Decoder -import io.circe.Encoder import jsonrpclib.fs2._ import jsonrpclib.CallId import jsonrpclib.Endpoint diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala index 4b48510..8e37725 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJsonCodec.scala @@ -1,6 +1,7 @@ package jsonrpclib.smithy4sinterop import io.circe._ +import smithy4s.schema.CachedSchemaCompiler import smithy4s.Schema object CirceJsonCodec { @@ -8,11 +9,25 @@ object CirceJsonCodec { object Encoder extends CirceEncoderImpl object Decoder extends CirceDecoderImpl + object Codec extends CachedSchemaCompiler[Codec] { + type Cache = (Encoder.Cache, Decoder.Cache) + def createCache(): Cache = (Encoder.createCache(), Decoder.createCache()) + + def fromSchema[A](schema: Schema[A]): Codec[A] = + io.circe.Codec.from(Decoder.fromSchema(schema), Encoder.fromSchema(schema)) + + def fromSchema[A](schema: Schema[A], cache: Cache): Codec[A] = + io.circe.Codec.from( + Decoder.fromSchema(schema, cache._2), + Encoder.fromSchema(schema, cache._1) + ) + } + /** Creates a Circe `Codec[A]` from a Smithy4s `Schema[A]`. * * This enables encoding values of type `A` to JSON and decoding JSON back into `A`, using the structure defined by * the Smithy schema. */ def fromSchema[A](implicit schema: Schema[A]): Codec[A] = - Codec.from(Decoder.fromSchema(schema), Encoder.fromSchema(schema)) + Codec.fromSchema(schema) } diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index 606533c..c4baa0a 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -30,12 +30,13 @@ object ClientStub { private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Service[Alg], channel: Channel[F]) { def compile: service.Impl[F] = { + val codecCache = CirceJsonCodec.Codec.createCache() val interpreter = new service.FunctorEndpointCompiler[F] { def apply[I, E, O, SI, SO](e: service.Endpoint[I, E, O, SI, SO]): I => F[O] = { val shapeId = e.id val spec = EndpointSpec.fromHints(e.hints).toRight(NotJsonRPCEndpoint(shapeId)).toTry.get - jsonRPCStub(e, spec) + jsonRPCStub(e, spec, codecCache) } } @@ -44,19 +45,12 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv def jsonRPCStub[I, E, O, SI, SO]( smithy4sEndpoint: service.Endpoint[I, E, O, SI, SO], - endpointSpec: EndpointSpec + endpointSpec: EndpointSpec, + codecCache: CirceJsonCodec.Codec.Cache ): I => F[O] = { - val decoderCache = CirceJsonCodec.Decoder.createCache() - val encoderCache = CirceJsonCodec.Encoder.createCache() - implicit val inputCodec: Codec[I] = Codec.from( - CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.input, decoderCache), - CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.input, encoderCache) - ) - implicit val outputCodec: Codec[O] = Codec.from( - CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.output, decoderCache), - CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.output, encoderCache) - ) + implicit val inputCodec: Codec[I] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.input, codecCache) + implicit val outputCodec: Codec[O] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.output, codecCache) endpointSpec match { case EndpointSpec.Notification(methodName) => diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala index 1117ddc..92843f4 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala @@ -34,11 +34,12 @@ object ServerEndpoints { )(implicit service: Service[Alg], F: Monadic[F]): List[Endpoint[F]] = { val transformedService = JsonRpcTransformations.apply(service) val interpreter: transformedService.FunctorInterpreter[F] = transformedService.toPolyFunction(impl) + val codecCache = CirceJsonCodec.Codec.createCache() transformedService.endpoints.toList.flatMap { smithy4sEndpoint => EndpointSpec .fromHints(smithy4sEndpoint.hints) .map { endpointSpec => - jsonRPCEndpoint(smithy4sEndpoint, endpointSpec, interpreter) + jsonRPCEndpoint(smithy4sEndpoint, endpointSpec, interpreter, codecCache) } .toList } @@ -55,25 +56,19 @@ object ServerEndpoints { * JSON-RPC method name and interaction hints * @param impl * Interpreter that executes the Smithy operation in `F` + * @param codecCache + * Coche for the schema to codec compilation results * @return * A JSON-RPC-compatible `Endpoint[F]` */ private def jsonRPCEndpoint[F[_]: Monadic, Op[_, _, _, _, _], I, E, O, SI, SO]( smithy4sEndpoint: Smithy4sEndpoint[Op, I, E, O, SI, SO], endpointSpec: EndpointSpec, - impl: FunctorInterpreter[Op, F] + impl: FunctorInterpreter[Op, F], + codecCache: CirceJsonCodec.Codec.Cache ): Endpoint[F] = { - val decoderCache = CirceJsonCodec.Decoder.createCache() - val encoderCache = CirceJsonCodec.Encoder.createCache() - - implicit val inputCodec: Codec[I] = Codec.from( - CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.input, decoderCache), - CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.input, encoderCache) - ) - implicit val outputCodec: Codec[O] = Codec.from( - CirceJsonCodec.Decoder.fromSchema(smithy4sEndpoint.output, decoderCache), - CirceJsonCodec.Encoder.fromSchema(smithy4sEndpoint.output, encoderCache) - ) + implicit val inputCodec: Codec[I] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.input, codecCache) + implicit val outputCodec: Codec[O] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.output, codecCache) def errorResponse(throwable: Throwable): F[E] = throwable match { case smithy4sEndpoint.Error((_, e)) => e.pure @@ -94,7 +89,7 @@ object ServerEndpoints { impl(op) } case Some(errorSchema) => - implicit val errorCodec: ErrorEncoder[E] = errorCodecFromSchema(errorSchema) + implicit val errorCodec: ErrorEncoder[E] = errorCodecFromSchema(errorSchema, codecCache) Endpoint[F](methodName).apply[I, E, O] { (input: I) => val op = smithy4sEndpoint.wrap(input) impl(op).attempt.flatMap { @@ -106,8 +101,8 @@ object ServerEndpoints { } } - private def errorCodecFromSchema[A](s: ErrorSchema[A]): ErrorEncoder[A] = { - val circeCodec = CirceJsonCodec.fromSchema(s.schema) + private def errorCodecFromSchema[A](s: ErrorSchema[A], cache: CirceJsonCodec.Codec.Cache): ErrorEncoder[A] = { + val circeCodec = CirceJsonCodec.Codec.fromSchema(s.schema, cache) (a: A) => ErrorPayload( 0, From eb59f11762cdca3e032ed89c46105e7bf10ba2f0 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 19:37:32 +0200 Subject: [PATCH 5/7] add client side error handling --- .../src/main/scala/jsonrpclib/Monadic.scala | 2 +- .../smithy4sinterop/TestClientSpec.scala | 44 +++++++++++++++++++ .../smithy4sinterop/ClientStub.scala | 29 +++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/jsonrpclib/Monadic.scala b/modules/core/src/main/scala/jsonrpclib/Monadic.scala index 4bb6dc8..2acf020 100644 --- a/modules/core/src/main/scala/jsonrpclib/Monadic.scala +++ b/modules/core/src/main/scala/jsonrpclib/Monadic.scala @@ -31,7 +31,7 @@ object Monadic { implicit class MonadicOps[F[_], A](private val fa: F[A]) extends AnyVal { def flatMap[B](f: A => F[B])(implicit m: Monadic[F]): F[B] = m.doFlatMap(fa)(f) def map[B](f: A => B)(implicit m: Monadic[F]): F[B] = m.doMap(fa)(f) - def attempt[B](implicit m: Monadic[F]): F[Either[Throwable, A]] = m.doAttempt(fa) + def attempt(implicit m: Monadic[F]): F[Either[Throwable, A]] = m.doAttempt(fa) def void(implicit m: Monadic[F]): F[Unit] = m.doVoid(fa) } implicit class MonadicOpsPure[A](private val a: A) extends AnyVal { diff --git a/modules/smithy4s-tests/src/test/scala/jsonrpclib/smithy4sinterop/TestClientSpec.scala b/modules/smithy4s-tests/src/test/scala/jsonrpclib/smithy4sinterop/TestClientSpec.scala index 3592771..eddedb0 100644 --- a/modules/smithy4s-tests/src/test/scala/jsonrpclib/smithy4sinterop/TestClientSpec.scala +++ b/modules/smithy4s-tests/src/test/scala/jsonrpclib/smithy4sinterop/TestClientSpec.scala @@ -8,6 +8,7 @@ import io.circe.Encoder import jsonrpclib._ import jsonrpclib.fs2._ import test._ +import test.TestServerOperation.GreetError import weaver._ import scala.concurrent.duration._ @@ -78,4 +79,47 @@ object TestClientSpec extends SimpleIOSuite { expect.same(result.payload.message, "Hello Bob") } } + + testRes("server returns known error") { + implicit val greetInputDecoder: Decoder[GreetInput] = CirceJsonCodec.fromSchema + implicit val greetOutputEncoder: Encoder[GreetOutput] = CirceJsonCodec.fromSchema + implicit val greetErrorEncoder: Encoder[GreetError] = CirceJsonCodec.fromSchema + implicit val errEncoder: ErrorEncoder[GreetError] = + err => ErrorPayload(-1, "error", Some(Payload(greetErrorEncoder(err)))) + + val endpoint: Endpoint[IO] = + Endpoint[IO]("greet").apply[GreetInput, GreetError, GreetOutput](in => + IO.pure(Left(GreetError.notWelcomeError(NotWelcomeError(s"${in.name} is not welcome")))) + ) + + for { + clientSideChannel <- setup(endpoint) + clientStub = ClientStub(TestServer, clientSideChannel) + result <- clientStub.greet("Bob").attempt.toStream + } yield { + matches(result) { case Left(t: NotWelcomeError) => + expect.same(t.msg, s"Bob is not welcome") + } + } + } + + testRes("server returns unknown error") { + implicit val greetInputDecoder: Decoder[GreetInput] = CirceJsonCodec.fromSchema + implicit val greetOutputEncoder: Encoder[GreetOutput] = CirceJsonCodec.fromSchema + + val endpoint: Endpoint[IO] = + Endpoint[IO]("greet").simple[GreetInput, GreetOutput](_ => IO.raiseError(new RuntimeException("boom!"))) + + for { + clientSideChannel <- setup(endpoint) + clientStub = ClientStub(TestServer, clientSideChannel) + result <- clientStub.greet("Bob").attempt.toStream + } yield { + matches(result) { case Left(t: ErrorPayload) => + expect.same(t.code, 0) && + expect.same(t.message, "boom!") && + expect.same(t.data, None) + } + } + } } diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index c4baa0a..200d19a 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -1,8 +1,12 @@ package jsonrpclib.smithy4sinterop import io.circe.Codec +import io.circe.HCursor import jsonrpclib.Channel +import jsonrpclib.ErrorPayload import jsonrpclib.Monadic +import jsonrpclib.Monadic.syntax._ +import jsonrpclib.ProtocolError import smithy4s.~> import smithy4s.schema._ import smithy4s.Service @@ -52,12 +56,35 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv implicit val inputCodec: Codec[I] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.input, codecCache) implicit val outputCodec: Codec[O] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.output, codecCache) + def errorResponse(throwable: Throwable, errorCodec: Codec[E]): F[E] = { + throwable match { + case ErrorPayload(_, _, Some(payload)) => + errorCodec(HCursor.fromJson(payload.data)) match { + case Left(err) => ProtocolError.ParseError(err.getMessage).raiseError + case Right(error) => error.pure + } + case e: Throwable => e.raiseError + } + } + endpointSpec match { case EndpointSpec.Notification(methodName) => val coerce = coerceUnit[O](smithy4sEndpoint.output) channel.notificationStub[I](methodName).andThen(f => Monadic[F].doFlatMap(f)(_ => coerce)) case EndpointSpec.Request(methodName) => - channel.simpleStub[I, O](methodName) + smithy4sEndpoint.error match { + case None => channel.simpleStub[I, O](methodName) + case Some(errorSchema) => + val errorCodec = CirceJsonCodec.Codec.fromSchema(errorSchema.schema, codecCache) + val stub = channel.simpleStub[I, O](methodName) + (in: I) => + stub.apply(in).attempt.flatMap { + case Right(success) => success.pure + case Left(error) => + errorResponse(error, errorCodec) + .flatMap(e => errorSchema.unliftError(e).raiseError) + } + } } } From e8b3f2c7f3bb6c25ed5aa3148d5561ac24f0ff85 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sun, 8 Jun 2025 19:41:16 +0200 Subject: [PATCH 6/7] Mark impl details package private --- .../scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala | 2 +- .../scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala index 9d2bf75..ad6c55a 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceDecoderImpl.scala @@ -7,7 +7,7 @@ import smithy4s.Document import smithy4s.Document.{Encoder => _, _} import smithy4s.Schema -class CirceDecoderImpl extends CachedSchemaCompiler[CirceDecoder] { +private[smithy4sinterop] class CirceDecoderImpl extends CachedSchemaCompiler[CirceDecoder] { val decoder: CachedSchemaCompiler.DerivingImpl[Decoder] = Document.Decoder type Cache = decoder.Cache diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala index c262c1c..b03cf64 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceEncoderImpl.scala @@ -4,10 +4,9 @@ import io.circe.{Encoder => CirceEncoder, _} import smithy4s.schema.CachedSchemaCompiler import smithy4s.Document import smithy4s.Document._ -import smithy4s.Document.Encoder import smithy4s.Schema -class CirceEncoderImpl extends CachedSchemaCompiler[CirceEncoder] { +private[smithy4sinterop] class CirceEncoderImpl extends CachedSchemaCompiler[CirceEncoder] { val encoder: CachedSchemaCompiler.DerivingImpl[Encoder] = Document.Encoder type Cache = encoder.Cache From bb9c14b209f47ca89260be8ea947d621310cd211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:18:01 +0200 Subject: [PATCH 7/7] minor simplification --- .../src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index 200d19a..dbc8fde 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -59,7 +59,7 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv def errorResponse(throwable: Throwable, errorCodec: Codec[E]): F[E] = { throwable match { case ErrorPayload(_, _, Some(payload)) => - errorCodec(HCursor.fromJson(payload.data)) match { + errorCodec.decodeJson(payload.data) match { case Left(err) => ProtocolError.ParseError(err.getMessage).raiseError case Right(error) => error.pure }