jsonrpc4cats

Creating a Server

Dependencies

Add the following dependencies to your build.sbt:

libraryDependencies ++= Seq(
  "io.github.m3is0" %% "jsonrpc4cats-circe" % "0.7.0",
  "io.github.m3is0" %% "jsonrpc4cats-server" % "0.7.0"
)

A Basic Example

An annotated step-by-step example:

package jsonrpc4cats.example

import cats.Applicative
import cats.MonadError
import cats.data.OptionT
import io.circe.Json

import jsonrpc4cats.*
import jsonrpc4cats.circe.given
import jsonrpc4cats.server.*

object CalcServer {

  // 1. Define errors

  sealed trait Err
  case object DivByZero extends Err

  given errToRpcError[J]: ToRpcError[J, Err] =
    ToRpcError.instance {
      case DivByZero =>
        RpcError(RpcErrorCode(1000), "Division by zero")
    }

  // 2. Define methods

  def add[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "calc.add", (Int, Int), Err, Long] { (a, b) =>
      F.pure(Right(a.toLong + b.toLong))
    }

  def subtract[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "calc.sub", (Int, Int), Err, Long] { (a, b) =>
      F.pure(Right(a.toLong - b.toLong))
    }

  def multiply[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "calc.mul", (Int, Int), Err, Long] { (a, b) =>
      F.pure(Right(a.toLong * b.toLong))
    }

  def divide[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "calc.div", (Int, Int), Err, (Int, Int)] {
      case (_, 0) =>
        F.pure(Left(DivByZero))
      case (a, b) =>
        F.pure(Right((a / b, a % b)))
    }

  // 3. Define API (uniqueness of method names is checked at compile time)

  def api[F[_]: Applicative] =
    RpcServer
      .add(add[F])
      .add(subtract[F])
      .add(multiply[F])
      .add(divide[F])

  // 4. Handle a request (you can use any JSON library for which a module exists)

  def handle[F[_]](req: String)(using MonadError[F, Throwable]): OptionT[F, Json] =
    api[F].handle[Json](req)
}

Method Parameters

Method parameters must be of a product type:

package jsonrpc4cats.example

import cats.Applicative

import jsonrpc4cats.RpcErr
import jsonrpc4cats.server.*

object HelloServer {

  // a method without parameters
  def hello[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "hello.hello", EmptyTuple, RpcErr, String] { _ =>
      F.pure(Right("Hello!"))
    }

  // a method with a single non-product parameter
  def helloName[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "hello.name", Tuple1[String], RpcErr, String] { params =>
      F.pure(Right(s"Hello, ${params.head}!"))
    }

  // a method with a single non-product parameter, using the RpcMethod1 API
  def helloName1[F[_]](using F: Applicative[F]) =
    RpcMethod1.instance[F, "hello.name1", String, RpcErr, String] { name =>
      F.pure(Right(s"Hello, ${name}!"))
    }

  def api[F[_]: Applicative] =
    RpcServer
      .add(hello[F])
      .add(helloName[F])
      .add(helloName1[F])
}

Combining Servers

Servers can be combined as shown below:

package jsonrpc4cats.example

import cats.Applicative

object HelloCalc {

  // combining servers using the :+: operator, which is an alias for the 'extend' method
  def api[F[_]: Applicative] =
    HelloServer.api[F] :+: CalcServer.api[F]
}

Custom Types

To use custom types, provide codecs for the JSON library you are using:

package jsonrpc4cats.example

import cats.Applicative
import cats.MonadError
import cats.data.OptionT
import io.circe.Decoder
import io.circe.Encoder
import io.circe.Json
import io.circe.generic.semiauto.*
import io.circe.syntax.*

import jsonrpc4cats.*
import jsonrpc4cats.circe.given
import jsonrpc4cats.server.*

object CustomTypes {

  sealed trait DivError
  final case class DivByZero(divident: Int) extends DivError

  given toRpcError: ToRpcError[Json, DivError] =
    ToRpcError.instance {
      case DivByZero(a) =>
        RpcError(RpcErrorCode(1000), "Division by zero", Some(Map("divident" -> a).asJson))
    }

  final case class DivParams(divident: Int, divisor: Int)
  given paramsDecoder: Decoder[DivParams] = deriveDecoder[DivParams]

  final case class DivResult(quotient: Int, remainder: Int)
  given resultEncoder: Encoder[DivResult] = deriveEncoder[DivResult]

  def div[F[_]](using F: Applicative[F]) =
    RpcMethod.instance[F, "custom.div", DivParams, DivError, DivResult] {
      case DivParams(a, 0) =>
        F.pure(Left(DivByZero(a)))
      case DivParams(a, b) =>
        F.pure(Right(DivResult(a / b, a % b)))
    }

  def handle[F[_]](req: String)(using MonadError[F, Throwable]): OptionT[F, Json] =
    RpcServer.add(div[F]).handle[Json](req)
}