diff --git a/build.sbt b/build.sbt index e8d995d..a67087c 100644 --- a/build.sbt +++ b/build.sbt @@ -33,7 +33,7 @@ val commonSettings = Seq( "com.disneystreaming" %%% "weaver-cats" % "0.8.4" % Test ), mimaPreviousArtifacts := Set( - organization.value %%% name.value % "0.0.7" + // organization.value %%% name.value % "0.0.7" ), scalacOptions ++= { CrossVersion.partialVersion(scalaVersion.value) match { @@ -69,7 +69,7 @@ val core = projectMatrix name := "jsonrpclib-core", commonSettings, libraryDependencies ++= Seq( - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.30.2" + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-circe" % "2.30.2" ) ) @@ -84,7 +84,8 @@ val fs2 = projectMatrix name := "jsonrpclib-fs2", commonSettings, libraryDependencies ++= Seq( - "co.fs2" %%% "fs2-core" % fs2Version + "co.fs2" %%% "fs2-core" % fs2Version, + "io.circe" %%% "circe-generic" % "0.14.7" % Test ) ) @@ -127,7 +128,6 @@ val smithy4s = projectMatrix commonSettings, mimaPreviousArtifacts := Set.empty, libraryDependencies ++= Seq( - "co.fs2" %%% "fs2-core" % fs2Version, "com.disneystreaming.smithy4s" %%% "smithy4s-json" % smithy4sVersion.value ), buildTimeProtocolDependency @@ -141,7 +141,8 @@ val exampleServer = projectMatrix commonSettings, publish / skip := true, libraryDependencies ++= Seq( - "co.fs2" %%% "fs2-io" % fs2Version + "co.fs2" %%% "fs2-io" % fs2Version, + "io.circe" %%% "circe-generic" % "0.14.7" ) ) .disablePlugins(MimaPlugin) @@ -161,7 +162,8 @@ val exampleClient = projectMatrix commonSettings, publish / skip := true, libraryDependencies ++= Seq( - "co.fs2" %%% "fs2-io" % fs2Version + "co.fs2" %%% "fs2-io" % fs2Version, + "io.circe" %%% "circe-generic" % "0.14.7" ) ) .disablePlugins(MimaPlugin) @@ -236,17 +238,7 @@ val root = project ).flatMap(_.projectRefs): _* ) -// The core compiles are a workaround for https://github.com/plokhotnyuk/jsoniter-scala/issues/564 -// when we switch to SN 0.5, we can use `makeWithSkipNestedOptionValues` instead: https://github.com/plokhotnyuk/jsoniter-scala/issues/564#issuecomment-2787096068 -val compileCoreModules = { - for { - scalaVersionSuffix <- List("", "3") - platformSuffix <- List("", "JS", "Native") - task <- List("compile", "package") - } yield s"core$platformSuffix$scalaVersionSuffix/$task" -}.mkString(";") - addCommandAlias( "ci", - s"$compileCoreModules;test;scalafmtCheckAll;mimaReportBinaryIssues" + s"compile;test;scalafmtCheckAll;mimaReportBinaryIssues" ) diff --git a/modules/core/src/main/scala/jsonrpclib/CallId.scala b/modules/core/src/main/scala/jsonrpclib/CallId.scala index c70fd9f..dcf7dd6 100644 --- a/modules/core/src/main/scala/jsonrpclib/CallId.scala +++ b/modules/core/src/main/scala/jsonrpclib/CallId.scala @@ -1,7 +1,6 @@ package jsonrpclib -import com.github.plokhotnyuk.jsoniter_scala.core._ -import scala.annotation.switch +import io.circe.{Decoder, Encoder, Json, Codec} sealed trait CallId object CallId { @@ -9,24 +8,17 @@ object CallId { final case class StringId(string: String) extends CallId case object NullId extends CallId - implicit val callIdRW: JsonValueCodec[CallId] = new JsonValueCodec[CallId] { - def decodeValue(in: JsonReader, default: CallId): CallId = { - val nt = in.nextToken() - - (nt: @switch) match { - case 'n' => in.readNullOrError(default, "expected null") - case '"' => in.rollbackToken(); StringId(in.readString(null)) - case _ => in.rollbackToken(); NumberId(in.readLong()) - - } + implicit val codec: Codec[CallId] = Codec.from( + Decoder + .decodeOption(Decoder.decodeString.map(StringId(_): CallId).or(Decoder.decodeLong.map(NumberId(_): CallId))) + .map { + case None => NullId + case Some(v) => v + }, + { + case NumberId(n) => Json.fromLong(n) + case StringId(str) => Json.fromString(str) + case NullId => Json.Null } - - def encodeValue(x: CallId, out: JsonWriter): Unit = x match { - case NumberId(long) => out.writeVal(long) - case StringId(string) => out.writeVal(string) - case NullId => out.writeNull() - } - - def nullValue: CallId = CallId.NullId - } + ) } diff --git a/modules/core/src/main/scala/jsonrpclib/Channel.scala b/modules/core/src/main/scala/jsonrpclib/Channel.scala index 6efcda6..ba533e4 100644 --- a/modules/core/src/main/scala/jsonrpclib/Channel.scala +++ b/modules/core/src/main/scala/jsonrpclib/Channel.scala @@ -1,5 +1,7 @@ package jsonrpclib +import io.circe.Codec + trait Channel[F[_]] { def mountEndpoint(endpoint: Endpoint[F]): F[Unit] def unmountEndpoint(method: String): F[Unit] diff --git a/modules/core/src/main/scala/jsonrpclib/Codec.scala b/modules/core/src/main/scala/jsonrpclib/Codec.scala deleted file mode 100644 index 1cc3059..0000000 --- a/modules/core/src/main/scala/jsonrpclib/Codec.scala +++ /dev/null @@ -1,35 +0,0 @@ -package jsonrpclib - -import com.github.plokhotnyuk.jsoniter_scala.core._ - -trait Codec[A] { - - def encode(a: A): Payload - def decode(payload: Option[Payload]): Either[ProtocolError, A] - -} - -object Codec { - - def encode[A](a: A)(implicit codec: Codec[A]): Payload = codec.encode(a) - def decode[A](payload: Option[Payload])(implicit codec: Codec[A]): Either[ProtocolError, A] = codec.decode(payload) - - implicit def fromJsonCodec[A](implicit jsonCodec: JsonValueCodec[A]): Codec[A] = new Codec[A] { - def encode(a: A): Payload = { - Payload(writeToArray(a)) - } - - def decode(payload: Option[Payload]): Either[ProtocolError, A] = { - try { - payload match { - case Some(Payload.Data(payload)) => Right(readFromArray(payload)) - case Some(Payload.NullPayload) => Right(readFromArray(nullArray)) - case None => Left(ProtocolError.ParseError("Expected to decode a payload")) - } - } catch { case e: JsonReaderException => Left(ProtocolError.ParseError(e.getMessage())) } - } - } - - private val nullArray = "null".getBytes() - -} diff --git a/modules/core/src/main/scala/jsonrpclib/Endpoint.scala b/modules/core/src/main/scala/jsonrpclib/Endpoint.scala index f46267c..1d7197a 100644 --- a/modules/core/src/main/scala/jsonrpclib/Endpoint.scala +++ b/modules/core/src/main/scala/jsonrpclib/Endpoint.scala @@ -1,5 +1,7 @@ package jsonrpclib +import io.circe.Codec + sealed trait Endpoint[F[_]] { def method: String } diff --git a/modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala b/modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala index b0a2cc3..fddb5a2 100644 --- a/modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala +++ b/modules/core/src/main/scala/jsonrpclib/ErrorPayload.scala @@ -1,7 +1,6 @@ package jsonrpclib -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker +import io.circe.{Decoder, Encoder} case class ErrorPayload(code: Int, message: String, data: Option[Payload]) extends Throwable { override def getMessage(): String = s"JsonRPC Error $code: $message" @@ -9,7 +8,9 @@ case class ErrorPayload(code: Int, message: String, data: Option[Payload]) exten object ErrorPayload { - implicit val rawMessageStubJsonValueCodecs: JsonValueCodec[ErrorPayload] = - JsonCodecMaker.make + implicit val errorPayloadEncoder: Encoder[ErrorPayload] = + Encoder.forProduct3("code", "message", "data")(e => (e.code, e.message, e.data)) + implicit val errorPayloadDecoder: Decoder[ErrorPayload] = + Decoder.forProduct3("code", "message", "data")(ErrorPayload.apply) } diff --git a/modules/core/src/main/scala/jsonrpclib/Message.scala b/modules/core/src/main/scala/jsonrpclib/Message.scala index 10d50fa..ee7d643 100644 --- a/modules/core/src/main/scala/jsonrpclib/Message.scala +++ b/modules/core/src/main/scala/jsonrpclib/Message.scala @@ -1,42 +1,43 @@ package jsonrpclib -import com.github.plokhotnyuk.jsoniter_scala.core.JsonReader -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter +import io.circe.{Decoder, Encoder} +import io.circe.syntax._ +import io.circe.Codec sealed trait Message { def maybeCallId: Option[CallId] } sealed trait InputMessage extends Message { def method: String } sealed trait OutputMessage extends Message { - def callId: CallId; final override def maybeCallId: Option[CallId] = Some(callId) + def callId: CallId + final override def maybeCallId: Option[CallId] = Some(callId) } object InputMessage { case class RequestMessage(method: String, callId: CallId, params: Option[Payload]) extends InputMessage { def maybeCallId: Option[CallId] = Some(callId) } + case class NotificationMessage(method: String, params: Option[Payload]) extends InputMessage { def maybeCallId: Option[CallId] = None } + } + object OutputMessage { def errorFrom(callId: CallId, protocolError: ProtocolError): OutputMessage = ErrorMessage(callId, ErrorPayload(protocolError.code, protocolError.getMessage(), None)) case class ErrorMessage(callId: CallId, payload: ErrorPayload) extends OutputMessage case class ResponseMessage(callId: CallId, data: Payload) extends OutputMessage + } object Message { + import jsonrpclib.internals.RawMessage - implicit val messageJsonValueCodecs: JsonValueCodec[Message] = new JsonValueCodec[Message] { - val rawMessageCodec = implicitly[JsonValueCodec[internals.RawMessage]] - def decodeValue(in: JsonReader, default: Message): Message = - rawMessageCodec.decodeValue(in, null).toMessage match { - case Left(error) => throw error - case Right(value) => value - } - def encodeValue(x: Message, out: JsonWriter): Unit = - rawMessageCodec.encodeValue(internals.RawMessage.from(x), out) - def nullValue: Message = null - } + implicit val codec: Codec[Message] = Codec.from( + { c => + c.as[RawMessage].flatMap(_.toMessage.left.map(e => io.circe.DecodingFailure(e.getMessage, c.history))) + }, + RawMessage.from(_).asJson + ) } diff --git a/modules/core/src/main/scala/jsonrpclib/Payload.scala b/modules/core/src/main/scala/jsonrpclib/Payload.scala index a423c2b..cc77767 100644 --- a/modules/core/src/main/scala/jsonrpclib/Payload.scala +++ b/modules/core/src/main/scala/jsonrpclib/Payload.scala @@ -1,50 +1,15 @@ package jsonrpclib -import com.github.plokhotnyuk.jsoniter_scala.core.JsonReader -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter +import io.circe.{Decoder, Encoder, Json} -import java.util.Base64 -import jsonrpclib.Payload.Data -import jsonrpclib.Payload.NullPayload - -sealed trait Payload extends Product with Serializable { - def stripNull: Option[Payload.Data] = this match { - case d @ Data(_) => Some(d) - case NullPayload => None - } +case class Payload(data: Json) { + def stripNull: Option[Payload] = Option(Payload(data)).filter(p => !p.data.isNull) } object Payload { - def apply(value: Array[Byte]) = { - if (value == null) NullPayload - else Data(value) - } - final case class Data(array: Array[Byte]) extends Payload { - override def equals(other: Any) = other match { - case bytes: Data => java.util.Arrays.equals(array, bytes.array) - case _ => false - } - - override lazy val hashCode: Int = java.util.Arrays.hashCode(array) - - override def toString = Base64.getEncoder.encodeToString(array) - } - - case object NullPayload extends Payload - - implicit val payloadJsonValueCodec: JsonValueCodec[Payload] = new JsonValueCodec[Payload] { - def decodeValue(in: JsonReader, default: Payload): Payload = { - Data(in.readRawValAsBytes()) - } - - def encodeValue(bytes: Payload, out: JsonWriter): Unit = - bytes match { - case Data(array) => out.writeRawVal(array) - case NullPayload => out.writeNull() - } + val NullPayload: Payload = Payload(Json.Null) - def nullValue: Payload = null - } + implicit val payloadEncoder: Encoder[Payload] = Encoder[Json].contramap(_.data) + implicit val payloadDecoder: Decoder[Payload] = Decoder[Json].map(Payload(_)) } diff --git a/modules/core/src/main/scala/jsonrpclib/StubTemplate.scala b/modules/core/src/main/scala/jsonrpclib/StubTemplate.scala index 36f0a17..17491e3 100644 --- a/modules/core/src/main/scala/jsonrpclib/StubTemplate.scala +++ b/modules/core/src/main/scala/jsonrpclib/StubTemplate.scala @@ -1,5 +1,7 @@ package jsonrpclib +import io.circe.Codec + sealed trait StubTemplate[In, Err, Out] object StubTemplate { def notification[In](method: String)(implicit inCodec: Codec[In]): StubTemplate[In, Nothing, Unit] = diff --git a/modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala b/modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala index cb73e08..0dd6c15 100644 --- a/modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala +++ b/modules/core/src/main/scala/jsonrpclib/internals/FutureBaseChannel.scala @@ -7,6 +7,8 @@ import scala.concurrent.ExecutionContext import scala.concurrent.Future import scala.concurrent.Promise import scala.util.Try +import io.circe.Codec +import io.circe.Encoder abstract class FutureBasedChannel(endpoints: List[Endpoint[Future]])(implicit ec: ExecutionContext) extends MessageDispatcher[Future] { @@ -25,7 +27,7 @@ abstract class FutureBasedChannel(endpoints: List[Endpoint[Future]])(implicit ec protected def getEndpoint(method: String): Future[Option[Endpoint[Future]]] = Future.successful(endpointsMap.get(method)) protected def sendMessage(message: Message): Future[Unit] = { - sendPayload(Codec.encode(message)).map(_ => ()) + sendPayload(Payload(Encoder[Message].apply(message))).map(_ => ()) } protected def nextCallId(): Future[CallId] = Future.successful(CallId.NumberId(nextID.incrementAndGet())) diff --git a/modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala b/modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala index 6042597..f64a12d 100644 --- a/modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala +++ b/modules/core/src/main/scala/jsonrpclib/internals/MessageDispatcher.scala @@ -6,6 +6,8 @@ import jsonrpclib.Endpoint.RequestResponseEndpoint import jsonrpclib.OutputMessage.ErrorMessage import jsonrpclib.OutputMessage.ResponseMessage import scala.util.Try +import io.circe.Codec +import io.circe.HCursor private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F]) extends Channel.MonadicChannel[F] { @@ -21,8 +23,8 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F protected def removePendingCall(callId: CallId): F[Option[OutputMessage => F[Unit]]] def notificationStub[In](method: String)(implicit inCodec: Codec[In]): In => F[Unit] = { (input: In) => - val encoded = inCodec.encode(input) - val message = InputMessage.NotificationMessage(method, Some(encoded)) + val encoded = inCodec(input) + val message = InputMessage.NotificationMessage(method, Some(Payload(encoded))) sendMessage(message) } @@ -30,9 +32,9 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F method: String )(implicit inCodec: Codec[In], errCodec: ErrorCodec[Err], outCodec: Codec[Out]): In => F[Either[Err, Out]] = { (input: In) => - val encoded = inCodec.encode(input) + val encoded = inCodec(input) doFlatMap(nextCallId()) { callId => - val message = InputMessage.RequestMessage(method, callId, Some(encoded)) + val message = InputMessage.RequestMessage(method, callId, Some(Payload(encoded))) doFlatMap(createPromise[Either[Err, Out]](callId)) { case (fulfill, future) => val pc = createPendingCall(errCodec, outCodec, fulfill) doFlatMap(storePendingCall(callId, pc))(_ => doFlatMap(sendMessage(message))(_ => future())) @@ -70,25 +72,33 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F private def executeInputMessage(input: InputMessage, endpoint: Endpoint[F]): F[Unit] = { (input, endpoint) match { - case (InputMessage.NotificationMessage(_, params), ep: NotificationEndpoint[F, in]) => - ep.inCodec.decode(params) match { + case (InputMessage.NotificationMessage(_, Some(params)), ep: NotificationEndpoint[F, in]) => + ep.inCodec(HCursor.fromJson(params.data)) match { case Right(value) => ep.run(input, value) - case Left(value) => reportError(params, value, ep.method) + case Left(value) => reportError(Some(params), ProtocolError.ParseError(value.getMessage), ep.method) } - case (InputMessage.RequestMessage(_, callId, params), ep: RequestResponseEndpoint[F, in, err, out]) => - ep.inCodec.decode(params) match { + case (InputMessage.RequestMessage(_, callId, Some(params)), ep: RequestResponseEndpoint[F, in, err, out]) => + ep.inCodec(HCursor.fromJson(params.data)) match { case Right(value) => doFlatMap(ep.run(input, value)) { case Right(data) => - val responseData = ep.outCodec.encode(data) - sendMessage(OutputMessage.ResponseMessage(callId, responseData)) + val responseData = ep.outCodec(data) + sendMessage(OutputMessage.ResponseMessage(callId, Payload(responseData))) case Left(error) => val errorPayload = ep.errCodec.encode(error) sendMessage(OutputMessage.ErrorMessage(callId, errorPayload)) } case Left(pError) => - sendProtocolError(callId, pError) + sendProtocolError(callId, ProtocolError.ParseError(pError.getMessage)) } + case (InputMessage.NotificationMessage(_, None), _: NotificationEndpoint[F, in]) => + val message = "Missing payload" + val pError = ProtocolError.InvalidRequest(message) + sendProtocolError(pError) + case (InputMessage.RequestMessage(_, _, None), _: RequestResponseEndpoint[F, in, err, out]) => + val message = "Missing payload" + val pError = ProtocolError.InvalidRequest(message) + sendProtocolError(pError) case (InputMessage.NotificationMessage(_, _), ep: RequestResponseEndpoint[F, in, err, out]) => val message = s"This ${ep.method} endpoint cannot process notifications, request is missing callId" val pError = ProtocolError.InvalidRequest(message) @@ -111,8 +121,8 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F case Left(_) => fulfill(scala.util.Failure(errorPayload)) case Right(value) => fulfill(scala.util.Success(Left(value))) } - case ResponseMessage(_, data) => - outCodec.decode(Some(data)) match { + case ResponseMessage(_, payload) => + outCodec(HCursor.fromJson(payload.data)) match { case Left(decodeError) => fulfill(scala.util.Failure(decodeError)) case Right(value) => fulfill(scala.util.Success(Right(value))) } diff --git a/modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala b/modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala index 7738dd3..62eaf0d 100644 --- a/modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala +++ b/modules/core/src/main/scala/jsonrpclib/internals/RawMessage.scala @@ -1,9 +1,8 @@ package jsonrpclib package internals -import com.github.plokhotnyuk.jsoniter_scala.core._ -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker -import com.github.plokhotnyuk.jsoniter_scala.macros.CodecMakerConfig +import io.circe.{Decoder, Encoder, Json} +import io.circe.syntax._ private[jsonrpclib] case class RawMessage( jsonrpc: String, @@ -44,7 +43,8 @@ private[jsonrpclib] object RawMessage { val `2.0` = "2.0" def from(message: Message): RawMessage = message match { - case InputMessage.NotificationMessage(method, params) => RawMessage(`2.0`, method = Some(method), params = params) + case InputMessage.NotificationMessage(method, params) => + RawMessage(`2.0`, method = Some(method), params = params) case InputMessage.RequestMessage(method, callId, params) => RawMessage(`2.0`, method = Some(method), params = params, id = Some(callId)) case OutputMessage.ErrorMessage(callId, errorPayload) => @@ -53,7 +53,38 @@ private[jsonrpclib] object RawMessage { RawMessage(`2.0`, result = Some(data.stripNull), id = Some(callId)) } - implicit val rawMessageJsonValueCodecs: JsonValueCodec[RawMessage] = - JsonCodecMaker.make(CodecMakerConfig.withSkipNestedOptionValues(true)) + // Custom encoder to flatten nested Option[Option[Payload]] + implicit val rawMessageEncoder: Encoder[RawMessage] = { msg => + Json + .obj( + List( + "jsonrpc" -> msg.jsonrpc.asJson, + "method" -> msg.method.asJson, + "params" -> msg.params.asJson, + "error" -> msg.error.asJson, + "id" -> msg.id.asJson + ) ++ { + msg.result match { + case Some(Some(payload)) => List("result" -> payload.asJson) + case Some(None) => List("result" -> Json.Null) + case None => Nil + } + }: _* + ) + } + // Custom decoder to wrap result into Option[Option[Payload]] + implicit val rawMessageDecoder: Decoder[RawMessage] = Decoder.instance { c => + for { + jsonrpc <- c.downField("jsonrpc").as[String] + method <- c.downField("method").as[Option[String]] + params <- c.downField("params").as[Option[Payload]] + error <- c.downField("error").as[Option[ErrorPayload]] + id <- c.downField("id").as[Option[CallId]] + resultOpt <- + if (c.downField("result").succeeded) + c.downField("result").as[Option[Payload]].map(res => Some(res)) + else Right(None) + } yield RawMessage(jsonrpc, method, resultOpt, error, params, id) + } } diff --git a/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala b/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala index b227173..e56ec41 100644 --- a/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala +++ b/modules/core/src/test/scala/jsonrpclib/CallIdSpec.scala @@ -2,6 +2,9 @@ package jsonrpclib import weaver._ import com.github.plokhotnyuk.jsoniter_scala.core._ +import io.circe.Json +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec._ +import cats.syntax.all._ object CallIdSpec extends FunSuite { test("json parsing") { @@ -12,9 +15,9 @@ object CallIdSpec extends FunSuite { val longJson = Long.MaxValue.toString val nullJson = "null" - assert.same(readFromString[CallId](strJson), CallId.StringId("25")) && - assert.same(readFromString[CallId](intJson), CallId.NumberId(25)) && - assert.same(readFromString[CallId](longJson), CallId.NumberId(Long.MaxValue)) && - assert.same(readFromString[CallId](nullJson), CallId.NullId) + assert.same(readFromString[Json](strJson).as[CallId], CallId.StringId("25").asRight) && + assert.same(readFromString[Json](intJson).as[CallId], CallId.NumberId(25).asRight) && + assert.same(readFromString[Json](longJson).as[CallId], CallId.NumberId(Long.MaxValue).asRight) && + assert.same(readFromString[Json](nullJson).as[CallId], CallId.NullId.asRight) } } diff --git a/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala b/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala index be5e41a..11b2e5d 100644 --- a/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala +++ b/modules/core/src/test/scala/jsonrpclib/RawMessageSpec.scala @@ -5,15 +5,20 @@ import jsonrpclib.internals._ import com.github.plokhotnyuk.jsoniter_scala.core._ import jsonrpclib.CallId.NumberId import jsonrpclib.OutputMessage.ResponseMessage +import io.circe.Json +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec._ object RawMessageSpec extends FunSuite { test("json parsing with null result") { // This is a perfectly valid response object, as result field has to be present, // but can be null: https://www.jsonrpc.org/specification#response_object - val rawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","result":null,"id":3} """.trim) + val rawMessage = readFromString[Json](""" {"jsonrpc":"2.0","result":null,"id":3} """.trim) + .as[RawMessage] + .fold(throw _, identity) // This, on the other hand, is an invalid response message, as result field is missing - val invalidRawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","id":3} """.trim) + val invalidRawMessage = + readFromString[Json](""" {"jsonrpc":"2.0","id":3} """.trim).as[RawMessage].fold(throw _, identity) assert.same( rawMessage, diff --git a/modules/examples/client/src/main/scala/examples/client/ClientMain.scala b/modules/examples/client/src/main/scala/examples/client/ClientMain.scala index 1173094..21b432d 100644 --- a/modules/examples/client/src/main/scala/examples/client/ClientMain.scala +++ b/modules/examples/client/src/main/scala/examples/client/ClientMain.scala @@ -2,8 +2,8 @@ package examples.client import cats.effect._ import cats.syntax.all._ -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker +import io.circe.Codec +import io.circe.generic.semiauto._ import fs2.Stream import fs2.io._ import fs2.io.process.Processes @@ -18,7 +18,7 @@ object ClientMain extends IOApp.Simple { // Creating a datatype that'll serve as a request (and response) of an endpoint case class IntWrapper(value: Int) object IntWrapper { - implicit val jcodec: JsonValueCodec[IntWrapper] = JsonCodecMaker.make + implicit val codec: Codec[IntWrapper] = deriveCodec } type IOStream[A] = fs2.Stream[IO, A] 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 445274e..8372ea0 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,8 @@ import jsonrpclib.CallId import jsonrpclib.fs2._ import cats.effect._ import fs2.io._ -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker +import io.circe.{Decoder, Encoder, Codec} +import io.circe.generic.semiauto._ import jsonrpclib.Endpoint object ServerMain extends IOApp.Simple { @@ -16,7 +16,7 @@ object ServerMain extends IOApp.Simple { // Creating a datatype that'll serve as a request (and response) of an endpoint case class IntWrapper(value: Int) object IntWrapper { - implicit val jcodec: JsonValueCodec[IntWrapper] = JsonCodecMaker.make + implicit val codec: Codec[IntWrapper] = deriveCodec } // Implementing an incrementation endpoint diff --git a/modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala index ed0c426..cf904e1 100644 --- a/modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala +++ b/modules/fs2/src/main/scala/jsonrpclib/fs2/CancelTemplate.scala @@ -1,6 +1,6 @@ package jsonrpclib.fs2 -import jsonrpclib.Codec +import io.circe.Codec import jsonrpclib.CallId /** A cancelation template that represents the RPC method by which cancelation diff --git a/modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala index c2cfa78..a00f090 100644 --- a/modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala +++ b/modules/fs2/src/main/scala/jsonrpclib/fs2/FS2Channel.scala @@ -13,6 +13,7 @@ import cats.effect.std.Supervisor import cats.syntax.all._ import cats.effect.syntax.all._ import jsonrpclib.internals.MessageDispatcher +import io.circe.Codec import scala.util.Try import java.util.regex.Pattern diff --git a/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala b/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala index 5ef82c4..1084f17 100644 --- a/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala +++ b/modules/fs2/src/main/scala/jsonrpclib/fs2/lsp.scala @@ -5,27 +5,28 @@ import fs2.Chunk import fs2.Stream import fs2.Pipe import jsonrpclib.Payload -import jsonrpclib.Codec - +import io.circe.{Encoder, Decoder, HCursor} import java.nio.charset.Charset import java.nio.charset.StandardCharsets import jsonrpclib.Message import jsonrpclib.ProtocolError -import jsonrpclib.Payload.Data -import jsonrpclib.Payload.NullPayload import scala.annotation.tailrec +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec._ +import io.circe.Json + object lsp { def encodeMessages[F[_]]: Pipe[F, Message, Byte] = - (_: Stream[F, Message]).map(Codec.encode(_)).through(encodePayloads) + (_: Stream[F, Message]).map(Encoder[Message].apply(_)).map(Payload(_)).through(encodePayloads) def encodePayloads[F[_]]: Pipe[F, Payload, Byte] = (_: Stream[F, Payload]).map(writeChunk).flatMap(Stream.chunk(_)) def decodeMessages[F[_]: MonadThrow]: Pipe[F, Byte, Either[ProtocolError, Message]] = (_: Stream[F, Byte]).through(decodePayloads).map { payload => - Codec.decode[Message](Some(payload)) + Decoder[Message].apply(HCursor.fromJson(payload.data)).left.map(e => ProtocolError.ParseError(e.getMessage)) } /** Split a stream of bytes into payloads by extracting each frame based on information contained in the headers. @@ -39,20 +40,16 @@ object lsp { (ns, Chunk(maybeResult)) } .flatMap { - case Right(acc) => Stream.iterable(acc).map(c => Payload(c.toArray)) + case Right(acc) => Stream.iterable(acc).map(c => Payload(readFromArray[Json](c.toArray))) case Left(error) => Stream.raiseError[F](error) } private def writeChunk(payload: Payload): Chunk[Byte] = { - val bytes = payload match { - case Data(array) => array - case NullPayload => nullArray - } + val bytes = writeToArray(payload.data) val header = s"Content-Length: ${bytes.size}" + "\r\n" * 2 Chunk.array(header.getBytes()) ++ Chunk.array(bytes) } - private val nullArray = "null".getBytes() private val returnByte = '\r'.toByte private val newlineByte = '\n'.toByte diff --git a/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala b/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala index e6c94dc..2bc3695 100644 --- a/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala +++ b/modules/fs2/src/test/scala/jsonrpclib/fs2/FS2ChannelSpec.scala @@ -2,11 +2,11 @@ package jsonrpclib.fs2 import cats.effect.IO import cats.syntax.all._ -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import fs2.Stream import jsonrpclib._ import weaver._ +import io.circe.Codec +import io.circe.generic.semiauto._ import scala.concurrent.duration._ @@ -14,12 +14,12 @@ object FS2ChannelSpec extends SimpleIOSuite { case class IntWrapper(int: Int) object IntWrapper { - implicit val jcodec: JsonValueCodec[IntWrapper] = JsonCodecMaker.make + implicit val codec: Codec[IntWrapper] = deriveCodec } case class CancelRequest(callId: CallId) object CancelRequest { - implicit val jcodec: JsonValueCodec[CancelRequest] = JsonCodecMaker.make + implicit val codec: Codec[CancelRequest] = deriveCodec } def testRes(name: TestName)(run: Stream[IO, Expectations]): Unit = diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJson.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJson.scala new file mode 100644 index 0000000..38dd09a --- /dev/null +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/CirceJson.scala @@ -0,0 +1,51 @@ +package jsonrpclib.smithy4sinterop + +import smithy4s.Document +import smithy4s.Schema +import smithy4s.codecs.PayloadPath + +import smithy4s.Document.{Decoder => _, _} +import io.circe._ + +private[jsonrpclib] object CirceJson { + + 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) + ) +} diff --git a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala index ee8c71e..f947323 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ClientStub.scala @@ -4,9 +4,7 @@ import smithy4s.~> import smithy4s.Service import smithy4s.schema._ import smithy4s.ShapeId -import smithy4s.json.Json -import jsonrpclib.Codec._ -import com.github.plokhotnyuk.jsoniter_scala.core._ +import io.circe.Codec import jsonrpclib.Channel import jsonrpclib.Monadic @@ -31,18 +29,13 @@ private class ClientStub[Alg[_[_, _, _, _, _]], F[_]: Monadic](val service: Serv service.impl(interpreter) } - private val jsoniterCodecGlobalCache = Json.jsoniter.createCache() - - private def deriveJsonCodec[A](schema: Schema[A]): JsonCodec[A] = - Json.jsoniter.fromSchema(schema, jsoniterCodecGlobalCache) - def jsonRPCStub[I, E, O, SI, SO]( smithy4sEndpoint: service.Endpoint[I, E, O, SI, SO], endpointSpec: EndpointSpec ): I => F[O] = { - implicit val inputCodec: JsonCodec[I] = deriveJsonCodec(smithy4sEndpoint.input) - implicit val outputCodec: JsonCodec[O] = deriveJsonCodec(smithy4sEndpoint.output) + implicit val inputCodec: Codec[I] = CirceJson.fromSchema(smithy4sEndpoint.input) + implicit val outputCodec: Codec[O] = CirceJson.fromSchema(smithy4sEndpoint.output) 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 a6593f7..9e8971d 100644 --- a/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala +++ b/modules/smithy4s/src/main/scala/jsonrpclib/smithy4sinterop/ServerEndpoints.scala @@ -5,11 +5,8 @@ import jsonrpclib.Endpoint import smithy4s.Service import smithy4s.kinds.FunctorAlgebra import smithy4s.kinds.FunctorInterpreter -import smithy4s.json.Json -import smithy4s.schema.Schema -import jsonrpclib.Codec._ -import com.github.plokhotnyuk.jsoniter_scala.core._ import jsonrpclib.Monadic +import io.circe.Codec object ServerEndpoints { @@ -27,11 +24,6 @@ object ServerEndpoints { } } - private val jsoniterCodecGlobalCache = Json.jsoniter.createCache() - - private def deriveJsonCodec[A](schema: Schema[A]): JsonCodec[A] = - Json.jsoniter.fromSchema(schema, jsoniterCodecGlobalCache) - // TODO : codify errors at smithy level and handle them. def jsonRPCEndpoint[F[_]: Monadic, Op[_, _, _, _, _], I, E, O, SI, SO]( smithy4sEndpoint: Smithy4sEndpoint[Op, I, E, O, SI, SO], @@ -39,8 +31,8 @@ object ServerEndpoints { impl: FunctorInterpreter[Op, F] ): Endpoint[F] = { - implicit val inputCodec: JsonCodec[I] = deriveJsonCodec(smithy4sEndpoint.input) - implicit val outputCodec: JsonCodec[O] = deriveJsonCodec(smithy4sEndpoint.output) + implicit val inputCodec: Codec[I] = CirceJson.fromSchema(smithy4sEndpoint.input) + implicit val outputCodec: Codec[O] = CirceJson.fromSchema(smithy4sEndpoint.output) endpointSpec match { case EndpointSpec.Notification(methodName) =>