Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 8 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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: 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: 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: 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

private[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
2 changes: 2 additions & 0 deletions modules/core/src/main/scala/jsonrpclib/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ package object jsonrpclib {
type ErrorCode = Int
type ErrorMessage = String

private[jsonrpclib] type ~>[F[_], G[_]] = jsonrpclib.PolyFunction[F, G]

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ 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

object ServerEndpoints {

Expand All @@ -24,7 +29,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 +38,38 @@ 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.schema)
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: Schema[A]): ErrorEncoder[A] = { (a: A) =>
ErrorPayload(-32000, "JSONRPC-smithy4s application error", Some(Payload(CirceJson.fromSchema(s).apply(a))))
}
}
Loading