Skip to main content

Examples

The following examples illustrate how to address various remote call use-cases.

They mostly use default plugins and focus on specific features but will work with any other plugins or feature combinations.

Basic

Asynchronous call

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.transport.http.HttpMethod.{Post, Put}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for POST or PUT requests to '/api'
server <- Default.rpcServer(9000, "/api", Seq(Post, Put)).bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a proxy instance
result <- remoteApi.hello(1)
_ = println(result)

// Call the remote API function dynamically without an API trait
result <- client.call[String]("hello")("n" -> 1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Synchronous call

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.system.IdentitySystem
import java.net.URI

// Define a remote API
trait Api {
def hello(n: Int): String
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): String =
s"Hello world $n"
}

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for POST requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()

// Call the remote API function via a proxy instance
val remoteApi = client.bind[Api]
println(
remoteApi.hello(1)
)

// Call the remote API function dynamically without an API trait
println(
client.call[String]("hello")("n" -> 1)
)

// Close the RPC client and server
client.close()
server.close()

Optional parameters

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define client view of a remote API
trait Api {
def hello(who: String): Future[String]
}

// Create server implementation of the remote API
class Service {
def hello(who: String, n: Option[Int]): Future[String] =
Future(s"Hello $who ${n.getOrElse(0)}")

def hi(who: Option[String])(n: Int): Future[String] =
Future(s"Hi ${who.getOrElse("all")} $n")
}
val service = new Service

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a proxy instance
result <- remoteApi.hello("world")
_ = println(result)

// Call the remote API function dynamically without an API trait
result <- client.call[String]("hi")("n" -> 1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Multiple APIs

Build

libraryDependencies ++= Seq(
)

Source

import automorph.Default
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api1 {
def hello(n: Int): Future[String]
}

// Define another remote API
trait Api2 {
def hi(): Future[String]
}

// Create server implementation of the first remote API
val service1 = new Api1 {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create server implementation of the second remote API
val service2 = new Api2 {
def hi(): Future[String] =
Future("Hola!")
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for POST requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service1).bind(service2).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi1 = client.bind[Api1]
remoteApi2 = client.bind[Api2]

// Call the first remote API function
result <- remoteApi1.hello(1)
_ = println(result)

// Call the second remote API function
result <- remoteApi2.hi()
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Integration

Effect system

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"org.automorph" %% "automorph-zio" % "@PROJECT_VERSION@",
"com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % "3.3.9",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.system.ZioSystem
import java.net.URI
import zio.{Console, Task, Unsafe, ZIO}

// Define a remote API
trait Api {
def hello(n: Int): Task[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Task[String] =
ZIO.succeed(s"Hello world $n")
}

// Create ZIO effect system plugin
val effectSystem = ZioSystem.default

Unsafe.unsafe { implicit unsafe =>
ZioSystem.defaultRuntime.unsafe.run(
for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServerCustom(effectSystem, 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClientCustom(effectSystem, new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ <- Console.printLine(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield ()
)
}

Message codec

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"org.automorph" %% "automorph-upickle" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.codec.messagepack.{UpickleMessagePackCodec, UpickleMessagePackCustom}
import automorph.{RpcClient, Default, RpcServer}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Introduce custom data types
final case class Record(values: List[String])



// Create uPickle message codec for JSON format
val messageCodec = UpickleMessagePackCodec[UpickleMessagePackCustom]()

// Provide custom data type serialization and deserialization logic as needed
import messageCodec.custom.*
implicit def recordRw: messageCodec.custom.ReadWriter[Record] = messageCodec.custom.macroRW

// Define a remote API
trait Api {
def hello(n: Int): Future[Record]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[Record] =
Future(Record(List("Data", n.toString)))
}

// Create a server RPC protocol plugin
val serverRpcProtocol = Default.rpcProtocol[
UpickleMessagePackCodec.Node, messageCodec.type, Default.ServerContext
](messageCodec)

// Create HTTP & WebSocket server transport listening on port 9000 for requests to '/api'
val serverTransport = Default.serverTransport(9000, "/api")

// Create a client RPC protocol plugin
val clientRpcProtocol = Default.rpcProtocol[
UpickleMessagePackCodec.Node, messageCodec.type, Default.ClientContext
](messageCodec)

// Create HTTP client transport sending POST requests to 'http://localhost:9000/api'
val clientTransport = Default.clientTransport(new URI("http://localhost:9000/api"))

Await.ready(for {
// Initialize custom JSON-RPC HTTP & WebSocket server
server <- RpcServer.transport(serverTransport).rpcProtocol(serverRpcProtocol).bind(service).init()

// Initialize custom JSON-RPC HTTP client
client <- RpcClient.transport(clientTransport).rpcProtocol(clientRpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

RPC protocol

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.protocol.WebRpcProtocol
import automorph.{RpcClient, Default, RpcServer}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create a server Web-RPC protocol plugin with '/api' as URL path prefix
val serverRpcProtocol = WebRpcProtocol[Default.Node, Default.Codec, Default.ServerContext](
Default.messageCodec, "/api"
)

// Create HTTP & WebSocket server transport listening on port 9000 for requests to '/api'
val serverTransport = Default.serverTransport(9000, "/api")

// Create a client Web-RPC protocol plugin with '/api' path prefix
val clientRpcProtocol = WebRpcProtocol[Default.Node, Default.Codec, Default.ClientContext](
Default.messageCodec, "/api"
)

// Create HTTP & WebSocket client transport sending POST requests to 'http://localhost:9000/api'
val clientTransport = Default.clientTransport(new URI("http://localhost:9000/api"))

Await.ready(for {
// Initialize custom JSON-RPC HTTP & WebSocket server
server <- RpcServer.transport(serverTransport).rpcProtocol(serverRpcProtocol).bind(service).init()

// Initialize custom JSON-RPC HTTP client
client <- RpcClient.transport(clientTransport).rpcProtocol(clientRpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Unsupported server

Build

libraryDependencies ++= Seq(
)

Source

import automorph.transport.generic.endpoint.GenericEndpoint
import automorph.{Default, RpcEndpoint}
import java.nio.charset.StandardCharsets.UTF_8
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Random

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create generic endpoint transport plugin with Unit as RPC request context type
val endpointTransport = GenericEndpoint.context[Unit].effectSystem(Default.effectSystem)

// Setup JSON-RPC endpoint and bind the API implementation to it
val endpoint = RpcEndpoint.transport(endpointTransport).rpcProtocol(Default.rpcProtocol).bind(service)

// Define a function for processing JSON-RPC requests via the generic RPC endpoint.
// This function should be called from request handling logic of an unsupported server.
def processRpcRequest(requestBody: Array[Byte]): Future[Array[Byte]] = {
// Supply request context of type Unit as defined by the generic endpoint transport plugin
val requestContext: Unit = ()

// Supply request correlation identifier which will be included in logs associated with the request
val requestId = Random.nextInt(Int.MaxValue).toString

// Call the remote API function by passing the request body directly to the RPC endpoint request handler
val handlerResult = endpoint.handler.processRequest(requestBody, requestContext, requestId)

// Extract the response body containing a JSON-RPC response from the request handler result
handlerResult.map(_.map(_.responseBody).getOrElse(Array.emptyByteArray))
}

// Test the JSON-RPC request processing function
val requestBody =
"""
|{
| "jsonrpc" : "2.0",
| "id" : "1234",
| "method" : "hello",
| "params" : {
| "n" : 1
| }
|}
|""".getBytes(UTF_8)
val responseBody = processRpcRequest(requestBody)
responseBody.foreach { response =>
println(new String(response, UTF_8))
}

Test

// Test the JSON-RPC request processing function
val requestBody =
"""
|{
| "jsonrpc" : "2.0",
| "id" : "1234",
| "method" : "test",
| "params" : {
| "n" : 1
| }
|}
|""".getBytes(UTF_8)
val responseBody = processRpcRequest(requestBody)
responseBody.foreach { response =>
println(new String(response, UTF_8))
}

Transport

Client transport

Build

libraryDependencies ++= Seq(
)

Source

import automorph.{Default, RpcClient}
import automorph.transport.http.client.UrlClient
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
override def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create standard JRE HTTP client transport sending POST requests to 'http://localhost:9000/api'
val clientTransport = UrlClient(Default.effectSystem, new URI("http://localhost:9000/api"))

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 80 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize custom JSON-RPC HTTP client
client <- RpcClient.transport(clientTransport).rpcProtocol(Default.rpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Server transport

Build

libraryDependencies ++= Seq(
)

Source

import automorph.{Default, RpcServer}
import automorph.transport.http.server.VertxServer
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
override def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create Vert.x HTTP & WebSocket server transport plugin listening on port 9000 for requests to '/api'
val serverTransport = VertxServer(Default.effectSystem, 9000, "/api")

Await.ready(for {
// Initialize custom JSON-RPC HTTP & WebSocket server
server <- RpcServer.transport(serverTransport).rpcProtocol(Default.rpcProtocol).bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Endpoint transport

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import io.undertow.{Handlers, Undertow}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Setup JSON-RPC HTTP endpoint with Undertow adapter
val endpoint = Default.rpcEndpoint().bind(service)

// Create Undertow HTTP server listening on port 9000
val server = Undertow.builder().addHttpListener(9000, "0.0.0.0")

// Use the JSON-RPC HTTP endpoint adapter as an Undertow handler for requests to '/api'
val pathHandler = Handlers.path().addPrefixPath("/api", endpoint.adapter)
val apiServer = server.setHandler(pathHandler).build()

// Start the Undertow server
apiServer.start()

Await.ready(for {
// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client
_ <- client.close()
} yield (), Duration.Inf)

// Stop the Undertow server
apiServer.stop()

WebSocket transport

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP & WebSocket client for sending requests to 'ws://localhost:9000/api'
client <- Default.rpcClient(new URI("ws://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

AMQP transport

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"org.automorph" %% "automorph-rabbitmq" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.{RpcClient, Default, RpcServer}
import automorph.transport.amqp.client.RabbitMqClient
import automorph.transport.amqp.server.RabbitMqServer
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
override def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Check for the AMQP broker URL configuration
Option(System.getenv("AMQP_BROKER_URL")).map(new URI(_)).map { url =>

// Create RabbitMQ AMQP server transport consuming requests from the 'api' queue
val serverTransport = RabbitMqServer(Default.effectSystem, url, Seq("api"))

// Create RabbitMQ AMQP client transport publishing requests to the 'api' queue
val clientTransport = RabbitMqClient(Default.effectSystem, url, "api")

Await.ready(for {
// Initialize custom JSON-RPC AMQP server
server <- RpcServer.transport(serverTransport).rpcProtocol(Default.rpcProtocol).bind(service).init()

// Initialize custom JSON-RPC AMQP client
client <- RpcClient.transport(clientTransport).rpcProtocol(Default.rpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)
}.getOrElse {
println("Enable AMQP example by setting AMQP_BROKER_URL environment variable to 'amqp://{host}:{port}'.")
}

Customization

Data type serialization

Build

libraryDependencies ++= Seq(
)

Source

import automorph.Default
import io.circe.{Decoder, Encoder}
import io.circe.generic.auto.*
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Introduce custom data types
sealed abstract class State
object State {
case object On extends State
case object Off extends State
}

// Product data types will work automatically with most message codecs
final case class Record(
value: String,
state: State
)

// Provide custom data type serialization and deserialization logic as needed
implicit val enumEncoder: Encoder[State] = Encoder.encodeInt.contramap[State](Map(
State.Off -> 0,
State.On -> 1
))
implicit val enumDecoder: Decoder[State] = Decoder.decodeInt.map(Map(
0 -> State.Off,
1 -> State.On
))

// Define a remote API
trait Api {
def hello(record: Record): Future[Record]
}

// Create server implementation of the remote API
val service = new Api {
def hello(record: Record): Future[Record] =
Future(record.copy(value = s"Data ${record.value}"))
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy
result <- remoteApi.hello(Record("test", State.On))
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Client function names

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.system.IdentitySystem
import java.net.URI

// Define client view of a remote API
trait Api {
def hello(n: Int): String

def hi(n: Int): String
}

// Create server implementation of the remote API
class Service {
def hello(n: Int): String =
s"Hello world $n"
}
val service = new Service

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()

// Customize local proxy API to RPC function name mapping
val mapName = (name: String) => name match {
// Calling 'hi' translates to calling 'hello'
case "hi" => "hello"

// Other calls remain unchanged
case other => other
}

// Call the remote API function via a local proxy
val remoteApi = client.bind[Api](mapName)
println(
remoteApi.hello(1)
)
println(
remoteApi.hi(1)
)

// Close the RPC client and server
client.close()
server.close()

Server function names

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.system.IdentitySystem
import java.net.URI
import scala.util.Try

// Define client view of a remote API
trait Api {
def hello(n: Int): String

def hi(n: Int): String
}

// Create server implementation of the remote API
class ApiImpl {
def test(n: Int): String =
s"Hello world $n"

def sum(numbers: List[Double]): Double =
numbers.sum

def hidden(): String =
""
}
val service = new ApiImpl

// Customize served API to RPC function name mapping
val mapName = (name: String) => name match {
// 'test' is exposed both as 'hello' and 'hi'
case "test" => Seq("hello", "hi")

// 'hidden' is not exposed
case "hidden" => Seq.empty

// 'sum' is exposed as 'test.sum'
case other => Seq(s"test.$other")
}

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service, mapName).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()

// Call the remote API function via a local proxy
val remoteApi = client.bind[Api]
println(
remoteApi.hello(1)
)
println(
remoteApi.hi(1)
)

// Call the remote API function dynamically without an API trait
println(
client.call[Double]("test.sum")("numbers" -> List(1, 2, 3))
)

// Call the remote API function dynamically and fail with FunctionNotFoundException
println(Try(
client.call[String]("hidden")()
).failed.get)

// Close the RPC client and server
client.close()
server.close()

Metadata

HTTP authentication

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.Default.{ClientContext, ServerContext}
import automorph.system.IdentitySystem
import java.net.URI
import scala.util.Try

// Define client view of a remote API
trait Api {
// Accept HTTP request context consumed by the client transport plugin
def hello(message: String)(implicit http: ClientContext): String
}

// Create server implementation of the remote API
class Service {
// Accept HTTP request context provided by the server message transport plugin
def hello(message: String)(implicit httpRequest: ServerContext): String =
httpRequest.authorization("Bearer") match {
case Some("valid") => s"Note: $message!"
case _ => throw new IllegalAccessException("Authentication failed")
}
}
val service = new Service

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()
val remoteApi = client.bind[Api]

{
// Create client request context containing invalid HTTP authentication
implicit val validAuthentication: ClientContext = client.context
.authorization("Bearer", "valid")

// Call the remote API function via a local proxy using valid authentication
println(
remoteApi.hello("test")
)

// Call the remote API function dynamically using valid authentication
println(
client.call[String]("hello")("message" -> "test")
)
}

{
// Create client request context containing invalid HTTP authentication
implicit val invalidAuthentication: ClientContext = client.context
.headers("X-Authentication" -> "unsupported")

// Call the remote API function statically using invalid authentication
println(Try(
remoteApi.hello("test")
).failed.get)

// Call the remote API function dynamically using invalid authentication
println(Try(
client.call[String]("hello")("message" -> "test")
).failed.get)
}

// Close the RPC client and server
client.close()
server.close()

HTTP request properties

Build

libraryDependencies ++= Seq(
)

Source

import automorph.Default
import automorph.Default.{ClientContext, ServerContext}
import automorph.system.IdentitySystem
import java.net.URI

// Define client view of a remote API
trait Api {
// Accept HTTP request context consumed by the client transport plugin
def hello(message: String)(implicit http: ClientContext): String
}

// Create server implementation of the remote API
class Service {
// Accept HTTP request context provided by the server message transport plugin
def hello(message: String)(implicit httpRequest: ServerContext): String =
Seq(
Some(message),
httpRequest.path,
httpRequest.header("X-Test")
).flatten.mkString(",")
}
val service = new Service

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()

// Create client request context specifying HTTP request metadata
implicit val httpRequest: ClientContext = client.context
.parameters("test" -> "value")
.headers("X-Test" -> "value", "Cache-Control" -> "no-cache")
.cookies("Example" -> "value")
.authorization("Bearer", "value")

// Call the remote API function via a local proxy using implicitly given HTTP request metadata
val remoteApi = client.bind[Api]
println(
remoteApi.hello("test")
)

// Call the remote API function dynamically using implicitly given HTTP request metadata
println(
client.call[String]("hello")("message" -> "test")
)

// Close the RPC client and server
client.close()
server.close()

HTTP response properties

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default.{ClientContext, ServerContext}
import automorph.system.IdentitySystem
import automorph.transport.http.HttpContext
import automorph.{RpcResult, Default}
import java.net.URI

// Define client view of a remote API
trait Api {
// Return HTTP response context provided by the client transport plugin
def hello(message: String): RpcResult[String, ClientContext]
}

// Create server implementation of the remote API
class Service {

// Return HTTP response context consumed by the server message transport plugin
def hello(message: String): RpcResult[String, ServerContext] = RpcResult(
message,
HttpContext().headers("X-Test" -> "value", "Cache-Control" -> "no-cache").statusCode(200)
)
}
val service = new Service

// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
val client = Default.rpcClientCustom(IdentitySystem(), new URI("http://localhost:9000/api")).init()

// Call the remote API function via a local proxy retrieving a result with HTTP response metadata
val remoteApi = client.bind[Api]
val static = remoteApi.hello("test")
println(static.result)
println(static.context.header("X-Test"))

// Call the remote API function dynamically retrieving a result with HTTP response metadata
val dynamic = client.call[RpcResult[String, ClientContext]]("hello")("message" -> "test")
println(dynamic.result)
println(dynamic.context.header("X-Test"))

// Close the RPC client and server
client.close()
server.close()

Error handling

Client error mapping

Build

libraryDependencies ++= Seq(
)

Source

import automorph.{RpcClient, Default}
import java.net.URI
import java.sql.SQLException
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future.failed(new IllegalArgumentException("SQL error"))
}

// Customize remote API client RPC error to exception mapping
val rpcProtocol = Default.rpcProtocol[Default.ClientContext].mapError((message, code) =>
if (message.contains("SQL")) {
new SQLException(message)
} else {
Default.rpcProtocol.mapError(message, code)
}
)

// Create HTTP client transport sending POST requests to 'http://localhost:9000/api'
val clientTransport = Default.clientTransport(new URI("http://localhost:9000/api"))

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize custom JSON-RPC HTTP client
client <- RpcClient.transport(clientTransport).rpcProtocol(rpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy and fail with SQLException
error <- remoteApi.hello(1).failed
_ = println(error)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Server error mapping

Build

libraryDependencies ++= Seq(
)

Source

import automorph.protocol.jsonrpc.ErrorType.InvalidRequest
import automorph.protocol.jsonrpc.JsonRpcException
import automorph.{Default, RpcServer}
import java.net.URI
import java.sql.SQLException
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
if (n >= 0) {
Future.failed(new SQLException("Invalid request"))
} else {
Future.failed(JsonRpcException("Application error", 1))
}
}

// Customize remote API server exception to RPC error mapping
val rpcProtocol = Default.rpcProtocol[Default.ServerContext].mapException(_ match {
case _: SQLException => InvalidRequest
case error => Default.rpcProtocol.mapException(error)
})

// Create HTTP & WebSocket server transport listening on port 9000 for requests to '/api'
val serverTransport = Default.serverTransport(9000, "/api")

Await.ready(for {
// Initialize custom JSON-RPC HTTP & WebSocket server
server <- RpcServer.transport(serverTransport).rpcProtocol(rpcProtocol).bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy an fail with InvalidRequestException
error <- remoteApi.hello(1).failed
_ = println(error)

// Call the remote API function via a local proxy and fail with RuntimeException
error <- remoteApi.hello(-1).failed
_ = println(error)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

HTTP status code

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import automorph.transport.http.HttpContext
import java.net.URI
import java.sql.SQLException
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future.failed(new SQLException("Bad request"))
}

// Customize remote API server exception to HTTP status code mapping
val mapException = (error: Throwable) => error match {
case _: SQLException => 400
case e => HttpContext.defaultExceptionToStatusCode(e)
}

Await.ready(for {
// Initialize custom JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api", mapException = mapException).bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function via a local proxy and fail with InvalidRequestException
error <- remoteApi.hello(1).failed
_ = println(error)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Special

API discovery

Build

libraryDependencies ++= Seq(
)

Source

import automorph.Default
import automorph.protocol.JsonRpcProtocol
import automorph.schema.{OpenApi, OpenRpc}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server with API discovery enabled
server <- Default.rpcServer(9000, "/api").discovery(true).bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()

// Retrieve the remote API schema in OpenRPC format
result <- client.call[OpenRpc](JsonRpcProtocol.openRpcFunction)()
_ = println(result.methods.map(_.name))

// Retrieve the remote API schema in OpenAPI format
result <- client.call[OpenApi](JsonRpcProtocol.openApiFunction)()
_ = println(result.paths.get.keys.toList)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Dynamic payload

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import io.circe.Json
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define client view of a remote API
trait Api {
def hello(who: String, n: Json): Future[Json]
}

// Create server implementation of the remote API
class Service {
def hello(who: Json, n: Int): Future[Json] =
if (who.isString) {
val value = who.as[String].toTry.get
Future(Json.fromString(s"Hello $value $n!"))
} else {
Future(Json.fromValues(Seq(who, Json.fromInt(n))))
}
}
val service = new Service

Await.ready(for {
// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function a proxy instance
result <- remoteApi.hello("test", Json.fromInt(1))
_ = println(result)

// Call the remote API function dynamically without an API trait
result <- client.call[Seq[Int]]("hello")("who" -> Json.fromInt(0), "n" -> 1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

One-way message

Build

libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "@PROJECT_VERSION@",
"ch.qos.logback" % "logback-classic" % "@LOGGER_VERSION@",
)

Source

import automorph.Default
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize JSON-RPC HTTP client for sending POST requests to 'http://localhost:9000/api'
client <- Default.rpcClient(new URI("http://localhost:9000/api")).init()
remoteApi = client.bind[Api]

// Call the remote API function dynamically without expecting a response
_ <- client.tell("hello")("n" -> 1)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Positional arguments

Build

libraryDependencies ++= Seq(
)

Source

import automorph.{RpcClient, Default}
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Configure JSON-RPC to pass arguments by position instead of by name
val rpcProtocol = Default.rpcProtocol[Default.ClientContext].namedArguments(false)

// Create HTTP client transport sending POST requests to 'http://localhost:9000/api'
val clientTransport = Default.clientTransport(new URI("http://localhost:9000/api"))

Await.ready(for {
// Initialize JSON-RPC HTTP & WebSocket server listening on port 9000 for requests to '/api'
server <- Default.rpcServer(9000, "/api").bind(service).init()

// Initialize custom JSON-RPC HTTP client
client <- RpcClient.transport(clientTransport).rpcProtocol(rpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client and server
_ <- client.close()
_ <- server.close()
} yield (), Duration.Inf)

Local call

Build

libraryDependencies ++= Seq(
)

Source

import automorph.transport.http.HttpContext
import automorph.transport.local.client.LocalClient
import automorph.{Default, RpcClient}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}

// Define a remote API
trait Api {
def hello(n: Int): Future[String]
}

// Create server implementation of the remote API
val service = new Api {
def hello(n: Int): Future[String] =
Future(s"Hello world $n")
}

// Create passive JSON-RPC HTTP & WebSocket server on port 9000 for POST requests to '/api'
val server = Default.rpcServer(9000, "/api").bind(service)

// Create default value for request metadata of the type defined by the RPC server
val defaultRequestContext: Default.ServerContext = HttpContext()

// Create local client transport which passes requests directly to RPC server request handler
val clientTransport = LocalClient(Default.effectSystem, defaultRequestContext, server.handler)

Await.ready(for {
// Initialize local JSON-RPC client
client <- RpcClient.transport(clientTransport).rpcProtocol(Default.rpcProtocol).init()
remoteApi = client.bind[Api]

// Call the remote API function using the local client
result <- remoteApi.hello(1)
_ = println(result)

// Close the RPC client
_ <- client.close()
} yield (), Duration.Inf)