Skip to content

Commit 23b3eb7

Browse files
Create caches for document encoders (#91)
* Create caches for document encoders * Remove unused imports * Remove redundant classes * Apply review comments * add client side error handling * Mark impl details package private * minor simplification --------- Co-authored-by: ghostbuster91 <[email protected]> Co-authored-by: Jakub Kozłowski <[email protected]>
1 parent 9734e14 commit 23b3eb7

File tree

8 files changed

+185
-58
lines changed

8 files changed

+185
-58
lines changed

modules/core/src/main/scala/jsonrpclib/Monadic.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ object Monadic {
3131
implicit class MonadicOps[F[_], A](private val fa: F[A]) extends AnyVal {
3232
def flatMap[B](f: A => F[B])(implicit m: Monadic[F]): F[B] = m.doFlatMap(fa)(f)
3333
def map[B](f: A => B)(implicit m: Monadic[F]): F[B] = m.doMap(fa)(f)
34-
def attempt[B](implicit m: Monadic[F]): F[Either[Throwable, A]] = m.doAttempt(fa)
34+
def attempt(implicit m: Monadic[F]): F[Either[Throwable, A]] = m.doAttempt(fa)
3535
def void(implicit m: Monadic[F]): F[Unit] = m.doVoid(fa)
3636
}
3737
implicit class MonadicOpsPure[A](private val a: A) extends AnyVal {

modules/examples/server/src/main/scala/examples/server/ServerMain.scala

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import cats.effect._
44
import fs2.io._
55
import io.circe.generic.semiauto._
66
import io.circe.Codec
7-
import io.circe.Decoder
8-
import io.circe.Encoder
97
import jsonrpclib.fs2._
108
import jsonrpclib.CallId
119
import jsonrpclib.Endpoint

modules/smithy4s-tests/src/test/scala/jsonrpclib/smithy4sinterop/TestClientSpec.scala

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.circe.Encoder
88
import jsonrpclib._
99
import jsonrpclib.fs2._
1010
import test._
11+
import test.TestServerOperation.GreetError
1112
import weaver._
1213

1314
import scala.concurrent.duration._
@@ -78,4 +79,47 @@ object TestClientSpec extends SimpleIOSuite {
7879
expect.same(result.payload.message, "Hello Bob")
7980
}
8081
}
82+
83+
testRes("server returns known error") {
84+
implicit val greetInputDecoder: Decoder[GreetInput] = CirceJsonCodec.fromSchema
85+
implicit val greetOutputEncoder: Encoder[GreetOutput] = CirceJsonCodec.fromSchema
86+
implicit val greetErrorEncoder: Encoder[GreetError] = CirceJsonCodec.fromSchema
87+
implicit val errEncoder: ErrorEncoder[GreetError] =
88+
err => ErrorPayload(-1, "error", Some(Payload(greetErrorEncoder(err))))
89+
90+
val endpoint: Endpoint[IO] =
91+
Endpoint[IO]("greet").apply[GreetInput, GreetError, GreetOutput](in =>
92+
IO.pure(Left(GreetError.notWelcomeError(NotWelcomeError(s"${in.name} is not welcome"))))
93+
)
94+
95+
for {
96+
clientSideChannel <- setup(endpoint)
97+
clientStub = ClientStub(TestServer, clientSideChannel)
98+
result <- clientStub.greet("Bob").attempt.toStream
99+
} yield {
100+
matches(result) { case Left(t: NotWelcomeError) =>
101+
expect.same(t.msg, s"Bob is not welcome")
102+
}
103+
}
104+
}
105+
106+
testRes("server returns unknown error") {
107+
implicit val greetInputDecoder: Decoder[GreetInput] = CirceJsonCodec.fromSchema
108+
implicit val greetOutputEncoder: Encoder[GreetOutput] = CirceJsonCodec.fromSchema
109+
110+
val endpoint: Endpoint[IO] =
111+
Endpoint[IO]("greet").simple[GreetInput, GreetOutput](_ => IO.raiseError(new RuntimeException("boom!")))
112+
113+
for {
114+
clientSideChannel <- setup(endpoint)
115+
clientStub = ClientStub(TestServer, clientSideChannel)
116+
result <- clientStub.greet("Bob").attempt.toStream
117+
} yield {
118+
matches(result) { case Left(t: ErrorPayload) =>
119+
expect.same(t.code, 0) &&
120+
expect.same(t.message, "boom!") &&
121+
expect.same(t.data, None)
122+
}
123+
}
124+
}
81125
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package jsonrpclib.smithy4sinterop
2+
3+
import io.circe.{Decoder => CirceDecoder, _}
4+
import smithy4s.codecs.PayloadPath
5+
import smithy4s.schema.CachedSchemaCompiler
6+
import smithy4s.Document
7+
import smithy4s.Document.{Encoder => _, _}
8+
import smithy4s.Schema
9+
10+
private[smithy4sinterop] class CirceDecoderImpl extends CachedSchemaCompiler[CirceDecoder] {
11+
val decoder: CachedSchemaCompiler.DerivingImpl[Decoder] = Document.Decoder
12+
13+
type Cache = decoder.Cache
14+
def createCache(): Cache = decoder.createCache()
15+
16+
def fromSchema[A](schema: Schema[A], cache: Cache): CirceDecoder[A] =
17+
c => {
18+
c.as[Json]
19+
.map(fromJson(_))
20+
.flatMap { d =>
21+
decoder
22+
.fromSchema(schema, cache)
23+
.decode(d)
24+
.left
25+
.map(e =>
26+
DecodingFailure(DecodingFailure.Reason.CustomReason(e.getMessage), c.history ++ toCursorOps(e.path))
27+
)
28+
}
29+
}
30+
31+
def fromSchema[A](schema: Schema[A]): CirceDecoder[A] = fromSchema(schema, createCache())
32+
33+
private def toCursorOps(path: PayloadPath): List[CursorOp] =
34+
path.segments.map {
35+
case PayloadPath.Segment.Label(name) => CursorOp.DownField(name)
36+
case PayloadPath.Segment.Index(i) => CursorOp.DownN(i)
37+
}
38+
39+
private def fromJson(json: Json): Document = json.fold(
40+
jsonNull = DNull,
41+
jsonBoolean = DBoolean(_),
42+
jsonNumber = n => DNumber(n.toBigDecimal.get),
43+
jsonString = DString(_),
44+
jsonArray = arr => DArray(arr.map(fromJson)),
45+
jsonObject = obj => DObject(obj.toMap.view.mapValues(fromJson).toMap)
46+
)
47+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package jsonrpclib.smithy4sinterop
2+
3+
import io.circe.{Encoder => CirceEncoder, _}
4+
import smithy4s.schema.CachedSchemaCompiler
5+
import smithy4s.Document
6+
import smithy4s.Document._
7+
import smithy4s.Schema
8+
9+
private[smithy4sinterop] class CirceEncoderImpl extends CachedSchemaCompiler[CirceEncoder] {
10+
val encoder: CachedSchemaCompiler.DerivingImpl[Encoder] = Document.Encoder
11+
12+
type Cache = encoder.Cache
13+
def createCache(): Cache = encoder.createCache()
14+
15+
def fromSchema[A](schema: Schema[A], cache: Cache): CirceEncoder[A] =
16+
a => documentToJson(encoder.fromSchema(schema, cache).encode(a))
17+
18+
def fromSchema[A](schema: Schema[A]): CirceEncoder[A] = fromSchema(schema, createCache())
19+
20+
private val documentToJson: Document => Json = {
21+
case DNull => Json.Null
22+
case DString(value) => Json.fromString(value)
23+
case DBoolean(value) => Json.fromBoolean(value)
24+
case DNumber(value) => Json.fromBigDecimal(value)
25+
case DArray(values) => Json.fromValues(values.map(documentToJson))
26+
case DObject(entries) => Json.fromFields(entries.view.mapValues(documentToJson))
27+
}
28+
}
Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,33 @@
11
package jsonrpclib.smithy4sinterop
22

33
import io.circe._
4-
import smithy4s.codecs.PayloadPath
5-
import smithy4s.Document
6-
import smithy4s.Document.{Decoder => _, _}
4+
import smithy4s.schema.CachedSchemaCompiler
75
import smithy4s.Schema
86

97
object CirceJsonCodec {
108

9+
object Encoder extends CirceEncoderImpl
10+
object Decoder extends CirceDecoderImpl
11+
12+
object Codec extends CachedSchemaCompiler[Codec] {
13+
type Cache = (Encoder.Cache, Decoder.Cache)
14+
def createCache(): Cache = (Encoder.createCache(), Decoder.createCache())
15+
16+
def fromSchema[A](schema: Schema[A]): Codec[A] =
17+
io.circe.Codec.from(Decoder.fromSchema(schema), Encoder.fromSchema(schema))
18+
19+
def fromSchema[A](schema: Schema[A], cache: Cache): Codec[A] =
20+
io.circe.Codec.from(
21+
Decoder.fromSchema(schema, cache._2),
22+
Encoder.fromSchema(schema, cache._1)
23+
)
24+
}
25+
1126
/** Creates a Circe `Codec[A]` from a Smithy4s `Schema[A]`.
1227
*
1328
* This enables encoding values of type `A` to JSON and decoding JSON back into `A`, using the structure defined by
1429
* the Smithy schema.
1530
*/
16-
def fromSchema[A](implicit schema: Schema[A]): Codec[A] = Codec.from(
17-
c => {
18-
c.as[Json]
19-
.map(fromJson)
20-
.flatMap { d =>
21-
Document
22-
.decode[A](d)
23-
.left
24-
.map(e =>
25-
DecodingFailure(DecodingFailure.Reason.CustomReason(e.getMessage), c.history ++ toCursorOps(e.path))
26-
)
27-
}
28-
},
29-
a => documentToJson(Document.encode(a))
30-
)
31-
32-
private def toCursorOps(path: PayloadPath): List[CursorOp] =
33-
path.segments.map {
34-
case PayloadPath.Segment.Label(name) => CursorOp.DownField(name)
35-
case PayloadPath.Segment.Index(i) => CursorOp.DownN(i)
36-
}
37-
38-
private val documentToJson: Document => Json = {
39-
case DNull => Json.Null
40-
case DString(value) => Json.fromString(value)
41-
case DBoolean(value) => Json.fromBoolean(value)
42-
case DNumber(value) => Json.fromBigDecimal(value)
43-
case DArray(values) => Json.fromValues(values.map(documentToJson))
44-
case DObject(entries) => Json.fromFields(entries.view.mapValues(documentToJson))
45-
}
46-
47-
private def fromJson(json: Json): Document = json.fold(
48-
jsonNull = DNull,
49-
jsonBoolean = DBoolean(_),
50-
jsonNumber = n => DNumber(n.toBigDecimal.get),
51-
jsonString = DString(_),
52-
jsonArray = arr => DArray(arr.map(fromJson)),
53-
jsonObject = obj => DObject(obj.toMap.view.mapValues(fromJson).toMap)
54-
)
31+
def fromSchema[A](implicit schema: Schema[A]): Codec[A] =
32+
Codec.fromSchema(schema)
5533
}

modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package jsonrpclib.smithy4sinterop
22

33
import io.circe.Codec
4+
import io.circe.HCursor
45
import jsonrpclib.Channel
6+
import jsonrpclib.ErrorPayload
57
import jsonrpclib.Monadic
8+
import jsonrpclib.Monadic.syntax._
9+
import jsonrpclib.ProtocolError
610
import smithy4s.~>
711
import smithy4s.schema._
812
import smithy4s.Service
@@ -30,12 +34,13 @@ object ClientStub {
3034
private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Service[Alg], channel: Channel[F]) {
3135

3236
def compile: service.Impl[F] = {
37+
val codecCache = CirceJsonCodec.Codec.createCache()
3338
val interpreter = new service.FunctorEndpointCompiler[F] {
3439
def apply[I, E, O, SI, SO](e: service.Endpoint[I, E, O, SI, SO]): I => F[O] = {
3540
val shapeId = e.id
3641
val spec = EndpointSpec.fromHints(e.hints).toRight(NotJsonRPCEndpoint(shapeId)).toTry.get
3742

38-
jsonRPCStub(e, spec)
43+
jsonRPCStub(e, spec, codecCache)
3944
}
4045
}
4146

@@ -44,18 +49,42 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv
4449

4550
def jsonRPCStub[I, E, O, SI, SO](
4651
smithy4sEndpoint: service.Endpoint[I, E, O, SI, SO],
47-
endpointSpec: EndpointSpec
52+
endpointSpec: EndpointSpec,
53+
codecCache: CirceJsonCodec.Codec.Cache
4854
): I => F[O] = {
4955

50-
implicit val inputCodec: Codec[I] = CirceJsonCodec.fromSchema(smithy4sEndpoint.input)
51-
implicit val outputCodec: Codec[O] = CirceJsonCodec.fromSchema(smithy4sEndpoint.output)
56+
implicit val inputCodec: Codec[I] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.input, codecCache)
57+
implicit val outputCodec: Codec[O] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.output, codecCache)
58+
59+
def errorResponse(throwable: Throwable, errorCodec: Codec[E]): F[E] = {
60+
throwable match {
61+
case ErrorPayload(_, _, Some(payload)) =>
62+
errorCodec.decodeJson(payload.data) match {
63+
case Left(err) => ProtocolError.ParseError(err.getMessage).raiseError
64+
case Right(error) => error.pure
65+
}
66+
case e: Throwable => e.raiseError
67+
}
68+
}
5269

5370
endpointSpec match {
5471
case EndpointSpec.Notification(methodName) =>
5572
val coerce = coerceUnit[O](smithy4sEndpoint.output)
5673
channel.notificationStub[I](methodName).andThen(f => Monadic[F].doFlatMap(f)(_ => coerce))
5774
case EndpointSpec.Request(methodName) =>
58-
channel.simpleStub[I, O](methodName)
75+
smithy4sEndpoint.error match {
76+
case None => channel.simpleStub[I, O](methodName)
77+
case Some(errorSchema) =>
78+
val errorCodec = CirceJsonCodec.Codec.fromSchema(errorSchema.schema, codecCache)
79+
val stub = channel.simpleStub[I, O](methodName)
80+
(in: I) =>
81+
stub.apply(in).attempt.flatMap {
82+
case Right(success) => success.pure
83+
case Left(error) =>
84+
errorResponse(error, errorCodec)
85+
.flatMap(e => errorSchema.unliftError(e).raiseError)
86+
}
87+
}
5988
}
6089
}
6190

modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ object ServerEndpoints {
3434
)(implicit service: Service[Alg], F: Monadic[F]): List[Endpoint[F]] = {
3535
val transformedService = JsonRpcTransformations.apply(service)
3636
val interpreter: transformedService.FunctorInterpreter[F] = transformedService.toPolyFunction(impl)
37+
val codecCache = CirceJsonCodec.Codec.createCache()
3738
transformedService.endpoints.toList.flatMap { smithy4sEndpoint =>
3839
EndpointSpec
3940
.fromHints(smithy4sEndpoint.hints)
4041
.map { endpointSpec =>
41-
jsonRPCEndpoint(smithy4sEndpoint, endpointSpec, interpreter)
42+
jsonRPCEndpoint(smithy4sEndpoint, endpointSpec, interpreter, codecCache)
4243
}
4344
.toList
4445
}
@@ -55,17 +56,19 @@ object ServerEndpoints {
5556
* JSON-RPC method name and interaction hints
5657
* @param impl
5758
* Interpreter that executes the Smithy operation in `F`
59+
* @param codecCache
60+
* Coche for the schema to codec compilation results
5861
* @return
5962
* A JSON-RPC-compatible `Endpoint[F]`
6063
*/
6164
private def jsonRPCEndpoint[F[_]: Monadic, Op[_, _, _, _, _], I, E, O, SI, SO](
6265
smithy4sEndpoint: Smithy4sEndpoint[Op, I, E, O, SI, SO],
6366
endpointSpec: EndpointSpec,
64-
impl: FunctorInterpreter[Op, F]
67+
impl: FunctorInterpreter[Op, F],
68+
codecCache: CirceJsonCodec.Codec.Cache
6569
): Endpoint[F] = {
66-
67-
implicit val inputCodec: Codec[I] = CirceJsonCodec.fromSchema(smithy4sEndpoint.input)
68-
implicit val outputCodec: Codec[O] = CirceJsonCodec.fromSchema(smithy4sEndpoint.output)
70+
implicit val inputCodec: Codec[I] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.input, codecCache)
71+
implicit val outputCodec: Codec[O] = CirceJsonCodec.Codec.fromSchema(smithy4sEndpoint.output, codecCache)
6972

7073
def errorResponse(throwable: Throwable): F[E] = throwable match {
7174
case smithy4sEndpoint.Error((_, e)) => e.pure
@@ -86,7 +89,7 @@ object ServerEndpoints {
8689
impl(op)
8790
}
8891
case Some(errorSchema) =>
89-
implicit val errorCodec: ErrorEncoder[E] = errorCodecFromSchema(errorSchema)
92+
implicit val errorCodec: ErrorEncoder[E] = errorCodecFromSchema(errorSchema, codecCache)
9093
Endpoint[F](methodName).apply[I, E, O] { (input: I) =>
9194
val op = smithy4sEndpoint.wrap(input)
9295
impl(op).attempt.flatMap {
@@ -98,8 +101,8 @@ object ServerEndpoints {
98101
}
99102
}
100103

101-
private def errorCodecFromSchema[A](s: ErrorSchema[A]): ErrorEncoder[A] = {
102-
val circeCodec = CirceJsonCodec.fromSchema(s.schema)
104+
private def errorCodecFromSchema[A](s: ErrorSchema[A], cache: CirceJsonCodec.Codec.Cache): ErrorEncoder[A] = {
105+
val circeCodec = CirceJsonCodec.Codec.fromSchema(s.schema, cache)
103106
(a: A) =>
104107
ErrorPayload(
105108
0,

0 commit comments

Comments
 (0)