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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"org.automorph" %% "automorph-zio" % "0.2.7",
"com.softwaremill.sttp.client3" %% "async-http-client-backend-zio" % "3.3.9",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"org.automorph" %% "automorph-upickle" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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)
Transport layer
Client transport
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "0.2.7",
"org.automorph" %% "automorph-sttp" % "0.2.7",
"com.softwaremill.sttp.client3" % "async-http-client-backend-future" % "@STTP_VERSION@",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
Source
import automorph.{Default, RpcClient}
import automorph.transport.http.HttpMethod
import automorph.transport.http.client.SttpClient
import java.net.URI
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
import sttp.client3.asynchttpclient.future.AsyncHttpClientFutureBackend
// 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 STTP HTTP client transport sending POST requests to 'http://localhost:9000/api'
val backend = AsyncHttpClientFutureBackend()
val clientTransport = SttpClient(
Default.effectSystem, backend, new URI("http://localhost:9000/api"), HttpMethod.Post
)
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(
"org.automorph" %% "automorph-default" % "0.2.7",
"org.automorph" %% "automorph-vertx" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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)
// Configure Undertow HTTP server listening on port 9000
val serverBuilder = 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)
// Create and start the Underdow server
val server = serverBuilder.setHandler(pathHandler).build()
server.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
server.stop()
WebSocket transport
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"org.automorph" %% "automorph-rabbitmq" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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}'.")
}
Arbitrary server
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-core" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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 arbitrary 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))
}
Customization
Data type serialization
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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()
Transport metadata
HTTP authentication
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
Source
import automorph.{Default, RpcCall}
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
def public(): String
}
// Create server implementation of the remote API
class Service {
def hello(message: String): String =
s"Hello world $message"
def public(): String =
"OK"
}
val service = new Service
// Customize RPC request filtering to reject unauthenticated calls to non-public functions
val filterCall = (call: RpcCall[ServerContext]) => call.context.authorization("Bearer") match {
case Some("valid") => None
case _ if call.function == "public" => None
case _ => Some(new IllegalAccessException("Authentication failed"))
}
// Initialize JSON-RPC HTTP & WebSocket server with API with custom request filter
val server = Default.rpcServerCustom(IdentitySystem(), 9000, "/api").bind(service).callFilter(filterCall).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 request context containing valid HTTP authentication used by the client transport plugin
implicit val validAuthentication: ClientContext = client.context
.authorization("Bearer", "valid")
// Call the authenticated remote API function via a local proxy using implicitly given valid authentication
println(
remoteApi.hello("test")
)
// Call the remote API function dynamically using valid authentication
println(
client.call[String]("hello")("message" -> "test")
)
}
{
// Call the public remote API function via a local proxy without authentication
println(
remoteApi.public()
)
// Call the authenticated remote API function via a local proxy without authentication and fail
println(Try(
remoteApi.hello("test")
).failed.get)
}
// Close the RPC client and server
client.close()
server.close()
HTTP request properties
Build
libraryDependencies ++= Seq(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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 request context containing HTTP metadata provided by the server 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 request context containing HTTP metadata used by the client transport plugin
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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 response context containing HTTP metadata provided by the client transport plugin
def hello(message: String): RpcResult[String, ClientContext]
}
// Create server implementation of the remote API
class Service {
// Return response context containing HTTP metadata used by the server 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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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 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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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 using built-in 'rpc.discover' function
result <- client.call[OpenRpc](JsonRpcProtocol.openRpcFunction)()
_ = println(result.methods.map(_.name))
// Retrieve the remote API schema in OpenAPI format using built-in 'api.discover' function
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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(
"org.automorph" %% "automorph-default" % "0.2.7",
"ch.qos.logback" % "logback-classic" % "1.5.12",
)
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)