In this example, we will show how to implement JSON-RPC client on Scala JS and JSON-RPC server on Scala JVM and how to let them communicate via JSON-RPC APIs.
You can see the complete code under this directory, but we have documented some highlights below.
We define the following 3 JSON-RPC APIs.
trait CalculatorAPI {
def add(lhs: Int, rhs: Int): Future[Int]
def subtract(lhs: Int, rhs: Int): Future[Int]
}
trait EchoAPI {
def echo(message: String): Future[String]
}
trait LoggerAPI {
def log(message: String): Unit
}
We implement the APIs on server side like below.
class CalculatorAPIImpl extends CalculatorAPI {
override def add(lhs: Int, rhs: Int): Future[Int] = {
Future(lhs + rhs)
}
override def subtract(lhs: Int, rhs: Int): Future[Int] = {
Future(lhs - rhs)
}
}
class EchoAPIImpl extends EchoAPI {
override def echo(message: String): Future[String] = {
Future(message) // It just returns the message as is
}
}
class LoggerAPIImpl extends LoggerAPI {
override def log(message: String): Unit = {
println(message) // It logs the message
}
}
We build JSON-RPC server using those API implementations.
object JSONRPCModule {
lazy val jsonRPCServer: JSONRPCServer[UpickleJSONSerializer] = {
val server = JSONRPCServer(UpickleJSONSerializer())
server.bindAPI[CalculatorAPI](new CalculatorAPIImpl)
server.bindAPI[EchoAPI](new EchoAPIImpl)
server.bindAPI[LoggerAPI](new LoggerAPIImpl)
server
}
}
To expose HTTP end point on the server, we are using Scalatra. We expose POST /jsonrpc end point to receive JSON-RPC request and notification.
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext): Unit = {
context.mount(new JSONRPCServlet, "/jsonrpc/*")
}
}
class JSONRPCServlet extends ScalatraServlet {
post("/") {
val server = JSONRPCModule.jsonRPCServer
val futureResult: Future[ActionResult] = server.receive(request.body).map {
case Some(responseJSON) => Ok(responseJSON) // For JSON-RPC request, we return response.
case None => NoContent() // For JSON-RPC notification, we do not return response.
}
Await.result(futureResult, 1.minutes)
}
}
On client side, we are using Ajax to send JSON-RPC request and notification. If the server responded 204 (no content), it is JSON-RPC notification.
val jsonSender: (String) => Future[Option[String]] =
(json: String) => {
val NoContentStatus = 204
dom.ext.Ajax
.post(url = "/jsonrpc", data = json)
.map(response => {
if (response.status == NoContentStatus) {
None
} else {
Option(response.responseText)
}
})
}
val client = JSONRPCClient(UpickleJSONSerializer(), jsonSender)
Once the client is built, we can use it to create and use the APIs like below.
val calculatorAPI = client.createAPI[CalculatorAPI]
val echoAPI = client.createAPI[EchoAPI]
val loggerAPI = client.createAPI[LoggerAPI]
loggerAPI.log("This is the beginning of my example.")
calculatorAPI.add(1, 2).onComplete {
case Success(result) => println(s"1 + 2 = $result")
case _ =>
}
calculatorAPI.subtract(1, 2).onComplete {
case Success(result) => println(s"1 - 2 = $result")
case _ =>
}
echoAPI.echo("Hello, World!").onComplete {
case Success(result) => println(s"""You said "$result"""")
case _ =>
}
loggerAPI.log("This is the end of my example.")
When you run this, you will see that:
- calculations via
calculatorAPI
is operated on server and returned to client. - messages sent via
echoAPI
reaches to server and returned to client as is. - messages sent via
loggerAPI
is logged on server.