Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ inThisBuild(
)

val scala213 = "2.13.16"
val scala3 = "3.3.5"
val scala3 = "3.3.6"
val jdkVersion = 11
val allScalaVersions = List(scala213, scala3)
val jvmScalaVersions = allScalaVersions
Expand All @@ -40,6 +40,14 @@ val commonSettings = Seq(
case Some((2, _)) => Seq(s"-target:jvm-$jdkVersion")
case _ => Seq(s"-java-output-version:$jdkVersion")
}
},
bspEnabled := {
val id = thisProjectRef.value.project
val isScala3 = scalaVersion.value.startsWith("3.")
val isJS = id.contains("JS")
val isNative = id.contains("Native")

isScala3 && !isJS && !isNative
}
)

Expand Down
3 changes: 2 additions & 1 deletion modules/core/src/main/scala/jsonrpclib/Channel.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package jsonrpclib

import io.circe.Codec
import jsonrpclib.ErrorCodec.errorPayloadCodec

trait Channel[F[_]] {
def mountEndpoint(endpoint: Endpoint[F]): F[Unit]
def unmountEndpoint(method: String): F[Unit]

def notificationStub[In: Codec](method: String): In => F[Unit]
def simpleStub[In: Codec, Out: Codec](method: String): In => F[Out]
def stub[In: Codec, Err: ErrorCodec, Out: Codec](method: String): In => F[Either[Err, Out]]
def stub[In: Codec, Err: ErrorDecoder, Out: Codec](method: String): In => F[Either[Err, Out]]
def stub[In, Err, Out](template: StubTemplate[In, Err, Out]): In => F[Either[Err, Out]]
}

Expand Down
30 changes: 20 additions & 10 deletions modules/core/src/main/scala/jsonrpclib/Endpoint.scala
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package jsonrpclib

import io.circe.Codec
import jsonrpclib.ErrorCodec.errorPayloadCodec

sealed trait Endpoint[F[_]] {
def method: String

def mapK[G[_]](f: PolyFunction[F, G]): Endpoint[G]
}

object Endpoint {
Expand All @@ -16,20 +19,20 @@ object Endpoint {
class PartiallyAppliedEndpoint[F[_]](method: MethodPattern) {
def apply[In, Err, Out](
run: In => F[Either[Err, Out]]
)(implicit inCodec: Codec[In], errCodec: ErrorCodec[Err], outCodec: Codec[Out]): Endpoint[F] =
RequestResponseEndpoint(method, (_: InputMessage, in: In) => run(in), inCodec, errCodec, outCodec)
)(implicit inCodec: Codec[In], errEncoder: ErrorEncoder[Err], outCodec: Codec[Out]): Endpoint[F] =
RequestResponseEndpoint(method, (_: InputMessage, in: In) => run(in), inCodec, errEncoder, outCodec)

def full[In, Err, Out](
run: (InputMessage, In) => F[Either[Err, Out]]
)(implicit inCodec: Codec[In], errCodec: ErrorCodec[Err], outCodec: Codec[Out]): Endpoint[F] =
RequestResponseEndpoint(method, run, inCodec, errCodec, outCodec)
)(implicit inCodec: Codec[In], errEncoder: ErrorEncoder[Err], outCodec: Codec[Out]): Endpoint[F] =
RequestResponseEndpoint(method, run, inCodec, errEncoder, outCodec)

def simple[In, Out](
run: In => F[Out]
)(implicit F: Monadic[F], inCodec: Codec[In], outCodec: Codec[Out]) =
apply[In, ErrorPayload, Out](in =>
F.doFlatMap(F.doAttempt(run(in))) {
case Left(error) => F.doPure(Left(ErrorPayload(0, error.getMessage(), None)))
case Left(error) => F.doPure(Left(ErrorPayload(-32000, error.getMessage(), None)))
case Right(value) => F.doPure(Right(value))
}
)
Expand All @@ -42,18 +45,25 @@ object Endpoint {

}

final case class NotificationEndpoint[F[_], In](
private[jsonrpclib] final case class NotificationEndpoint[F[_], In](
method: MethodPattern,
run: (InputMessage, In) => F[Unit],
inCodec: Codec[In]
) extends Endpoint[F]
) extends Endpoint[F] {

def mapK[G[_]](f: PolyFunction[F, G]): Endpoint[G] =
NotificationEndpoint[G, In](method, (msg, in) => f(run(msg, in)), inCodec)
}

final case class RequestResponseEndpoint[F[_], In, Err, Out](
private[jsonrpclib] final case class RequestResponseEndpoint[F[_], In, Err, Out](
method: Method,
run: (InputMessage, In) => F[Either[Err, Out]],
inCodec: Codec[In],
errCodec: ErrorCodec[Err],
errEncoder: ErrorEncoder[Err],
outCodec: Codec[Out]
) extends Endpoint[F]
) extends Endpoint[F] {

def mapK[G[_]](f: PolyFunction[F, G]): Endpoint[G] =
RequestResponseEndpoint[G, In, Err, Out](method, (msg, in) => f(run(msg, in)), inCodec, errEncoder, outCodec)
}
}
9 changes: 6 additions & 3 deletions modules/core/src/main/scala/jsonrpclib/ErrorCodec.scala
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package jsonrpclib

trait ErrorCodec[E] {

trait ErrorEncoder[E] {
def encode(a: E): ErrorPayload
def decode(error: ErrorPayload): Either[ProtocolError, E]
}

trait ErrorDecoder[E] {
def decode(error: ErrorPayload): Either[ProtocolError, E]
}

trait ErrorCodec[E] extends ErrorDecoder[E] with ErrorEncoder[E]

object ErrorCodec {
implicit val errorPayloadCodec: ErrorCodec[ErrorPayload] = new ErrorCodec[ErrorPayload] {
def encode(a: ErrorPayload): ErrorPayload = a
Expand Down
15 changes: 15 additions & 0 deletions modules/core/src/main/scala/jsonrpclib/Monadic.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,19 @@ object Monadic {

override def doMap[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
}

object syntax {
implicit class MonadicOps[F[_], A](fa: F[A]) {
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 void(implicit m: Monadic[F]): F[Unit] = m.doVoid(fa)
}
implicit class MonadicOpsPure[A](a: A) {
def pure[F[_]](implicit m: Monadic[F]): F[A] = m.doPure(a)
}
implicit class MonadicOpsThrowable(t: Throwable) {
def raiseError[F[_], A](implicit m: Monadic[F]): F[A] = m.doRaiseError(t)
}
}
}
5 changes: 5 additions & 0 deletions modules/core/src/main/scala/jsonrpclib/PolyFunction.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package jsonrpclib

trait PolyFunction[F[_], G[_]] { self =>
def apply[A0](fa: F[A0]): G[A0]
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F

def stub[In, Err, Out](
method: String
)(implicit inCodec: Codec[In], errCodec: ErrorCodec[Err], outCodec: Codec[Out]): In => F[Either[Err, Out]] = {
)(implicit inCodec: Codec[In], errDecoder: ErrorDecoder[Err], outCodec: Codec[Out]): In => F[Either[Err, Out]] = {
(input: In) =>
val encoded = inCodec(input)
doFlatMap(nextCallId()) { callId =>
val message = InputMessage.RequestMessage(method, callId, Some(Payload(encoded)))
doFlatMap(createPromise[Either[Err, Out]](callId)) { case (fulfill, future) =>
val pc = createPendingCall(errCodec, outCodec, fulfill)
val pc = createPendingCall(errDecoder, outCodec, fulfill)
doFlatMap(storePendingCall(callId, pc))(_ => doFlatMap(sendMessage(message))(_ => future()))
}
}
Expand Down Expand Up @@ -85,7 +85,7 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F
val responseData = ep.outCodec(data)
sendMessage(OutputMessage.ResponseMessage(callId, Payload(responseData)))
case Left(error) =>
val errorPayload = ep.errCodec.encode(error)
val errorPayload = ep.errEncoder.encode(error)
sendMessage(OutputMessage.ErrorMessage(callId, errorPayload))
}
case Left(pError) =>
Expand All @@ -111,13 +111,13 @@ private[jsonrpclib] abstract class MessageDispatcher[F[_]](implicit F: Monadic[F
}

private def createPendingCall[Err, Out](
errCodec: ErrorCodec[Err],
errDecoder: ErrorDecoder[Err],
outCodec: Codec[Out],
fulfill: Try[Either[Err, Out]] => F[Unit]
): OutputMessage => F[Unit] = { (message: OutputMessage) =>
message match {
case ErrorMessage(_, errorPayload) =>
errCodec.decode(errorPayload) match {
errDecoder.decode(errorPayload) match {
case Left(_) => fulfill(scala.util.Failure(errorPayload))
case Right(value) => fulfill(scala.util.Success(Left(value)))
}
Expand Down
1 change: 0 additions & 1 deletion modules/core/src/main/scala/jsonrpclib/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ package object jsonrpclib {

type ErrorCode = Int
type ErrorMessage = String

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import jsonrpclib.Endpoint
import smithy4s.Service
import smithy4s.kinds.FunctorAlgebra
import smithy4s.kinds.FunctorInterpreter
import smithy4s.schema.Schema
import jsonrpclib.Monadic
import jsonrpclib.Payload
import jsonrpclib.ErrorPayload
import io.circe.Codec
import jsonrpclib.Monadic.syntax._
import jsonrpclib.ErrorEncoder
import smithy4s.schema.ErrorSchema

object ServerEndpoints {

Expand All @@ -24,7 +30,6 @@ object ServerEndpoints {
}
}

// 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],
endpointSpec: EndpointSpec,
Expand All @@ -34,17 +39,44 @@ object ServerEndpoints {
implicit val inputCodec: Codec[I] = CirceJson.fromSchema(smithy4sEndpoint.input)
implicit val outputCodec: Codec[O] = CirceJson.fromSchema(smithy4sEndpoint.output)

def errorResponse(throwable: Throwable): F[E] = throwable match {
case smithy4sEndpoint.Error((_, e)) => e.pure
case e: Throwable => e.raiseError
}

endpointSpec match {
case EndpointSpec.Notification(methodName) =>
Endpoint[F](methodName).notification { (input: I) =>
val op = smithy4sEndpoint.wrap(input)
Monadic[F].doVoid(impl(op))
impl(op).void
}
case EndpointSpec.Request(methodName) =>
Endpoint[F](methodName).simple { (input: I) =>
val op = smithy4sEndpoint.wrap(input)
impl(op)
smithy4sEndpoint.error match {
case None =>
Endpoint[F](methodName).simple[I, O] { (input: I) =>
val op = smithy4sEndpoint.wrap(input)
impl(op)
}
case Some(errorSchema) =>
implicit val errorCodec: ErrorEncoder[E] = errorCodecFromSchema(errorSchema)
Endpoint[F](methodName).apply[I, E, O] { (input: I) =>
val op = smithy4sEndpoint.wrap(input)
impl(op).attempt.flatMap {
case Left(err) => errorResponse(err).map(r => Left(r): Either[E, O])
case Right(success) => (Right(success): Either[E, O]).pure
}
}
}
}
}

private def errorCodecFromSchema[A](s: ErrorSchema[A]): ErrorEncoder[A] = {
val circeCodec = CirceJson.fromSchema(s.schema)
(a: A) =>
ErrorPayload(
-32000,
Option(s.unliftError(a).getMessage()).getOrElse("JSONRPC-smithy4s application error"),
Some(Payload(circeCodec.apply(a)))
)
}
}