type-safe json-rpc api

Armadillo

London, 2022

We are hiring :)

  • fully decentralized remote working organisation 
  • software development based on research papers
  • creators of cardano, first PoS blockchain built using evidence-based methods 
  • established collaboration with recognized universities such as Edinburgh and Stanford

scala

haskell

typescript

agda

How it started?

  • a requirement to provide json-rpc api
  • how do we document that api?
  • how to keep that documentation up-to-date?

JSON-RPC protocol

  • created in 2005 as a JSON based alternative to XML-RPC protocol
  • last revision - 2.0 from 2013
  • transport layer agnostic
  • mainly used for inter-process communication
  • generally quite simple, but there as some caveats

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Request with named parameters:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":{
      "subtrahend":23,
      "minuend":42
   },
   "id":3
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

A notification:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":{
      "subtrahend":23,
      "minuend":42
   }
}

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Batch request:

--> [
        {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
        {"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
    ]
<-- [
        {"jsonrpc": "2.0", "result": 7, "id": "1"},
        {"jsonrpc": "2.0", "result": 19, "id": "2"},
    ]

https://www.jsonrpc.org/specification

JSON-RPC protocol

--> data sent to Server
<-- data sent to Client

Batch request with a notification:

--> [
	{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
        {"jsonrpc": "2.0", "method": "notify_hello", "params": ["hello"]},
    ]
<-- [
        {"jsonrpc": "2.0", "result": 7, "id": "1"},
    ]

https://www.jsonrpc.org/specification

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}
val jsonRpc = endpoint

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}
val jsonRpc = endpoint.post

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}
val jsonRpc = endpoint.post.in(jsonBody[JsonRpcRequest])

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}
val jsonRpc = endpoint.post.in(jsonBody[JsonRpcRequest])
		.out(jsonBody[Option[JsonRpcResponse]])

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}

HTTP POST    /
{
   "jsonrpc":"2.0",
   "method":"sum",
   "params":[
      42,
      23
   ],
   "id":1
}
Endpoint[JsonRpcRequest, JsonRpcError, Option[JsonRpcResponse]]

Our goals

  • always up-to-date, automatically generated documentation
  • fully support JSON-RPC 2.0 specification
  • express endpoint as values
  • ability to run it side-by-side with legacy code
  • integration with multiple json libraries
  • integration with multiple http servers

Why just not use tapir?

https://github.com/softwaremill/tapir/issues/621

Meet armadillo

Declarative, type-safe json-rpc endpoints library

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
 

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract = 
  jsonRpcEndpoint(m"subtract")
  

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
  )

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract: JsonRpcEndpoint[(Int, Int), Unit, Int] = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")

Meet armadillo

Basic request:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":[
      42,
      23
   ],
   "id":1
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract: JsonRpcServerEndpoint[Id] = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")
  .serverLogic[Id] { case (subtrahend, minuend) =>
    Right(subtrahend - minuend)
  }

Meet armadillo

Request with named parameters:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":{
      "subtrahend":23,
      "minuend":42
   },
   "id":3
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract: JsonRpcServerEndpoint[Id] = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")
  .serverLogic[Id] { case (subtrahend, minuend) =>
    Right(subtrahend - minuend)
  }

Meet armadillo

Request with named parameters:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":{
      "subtrahend":23,
      "minuend":42
   },
   "id":3
}
<-- {
   "jsonrpc":"2.0",
   "result":19,
   "id":1
}
val subtract: JsonRpcServerEndpoint[Id] = 
  jsonRpcEndpoint(m"subtract", ParamStructure.ByName)
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")
  .serverLogic[Id] { case (subtrahend, minuend) =>
    Right(subtrahend - minuend)
  }

Meet armadillo

Notification:

--> {
   "jsonrpc":"2.0",
   "method":"subtract",
   "params":{
      "subtrahend":23,
      "minuend":42
   }
   # no request id
}
val subtract: JsonRpcServerEndpoint[Id] = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")
  .serverLogic[Id] { case (subtrahend, minuend) =>
    Right(subtrahend - minuend)
  }

Meet armadillo

Batch request:

-->[
   {
      "jsonrpc":"2.0",
      "method":"subtract",
      "params":[
         42,
         23
      ],
      "id":1
   },
   {
      "jsonrpc":"2.0",
      "method":"subtract",
      "params":[
         42,
         1
      ],
      "id":2
   }
]
val subtract: JsonRpcServerEndpoint[Id] = 
  jsonRpcEndpoint(m"subtract")
  .in(
    param[Int]("subtrahend")
      .and(param[Int]("minuend"))
  )
  .out[Int]("result of the subtraction")
  .serverLogic[Id] { case (subtrahend, minuend) =>
    Right(subtrahend - minuend)
  }
<-- [
   {
      "jsonrpc":"2.0",
      "result":19,
      "id":"1"
   },
   {
      "jsonrpc":"2.0",
      "result":41,
      "id":"2"
   }
]

Interpreters - http

subtract: JsonRpcServerEndpoint[F[_]]
sum: JsonRpcServerEndpoint[F[_]]
greeting: JsonRpcServerEndpoint[F[_]]

tapir

interperter

tapirEndpoint: ServerEndpoint[-R, F[_]]

http4s

zio-http

netty

...

 Endpoint[String, (Json, StatusCode), (Option[Json], StatusCode)]

Interpreters - docs

subtract: JsonRpcServerEndpoint[F[_]]
sum: JsonRpcServerEndpoint[F[_]]
greeting: JsonRpcServerEndpoint[F[_]]

OpenRpc

interperter

openRpcSpecification: Either[Json, Yaml]

Interpreters

subtract: JsonRpcServerEndpoint[F[_]]
sum: JsonRpcServerEndpoint[F[_]]
greeting: JsonRpcServerEndpoint[F[_]]

Fs2

interperter

jsonRpcServer: fs2.Pipe[IO, String, Json]

unix-domain-socket

websocket

Extension points

Interceptor desing based on tapir's interceptors

Allow for customization on three different layers:

  • request layer 
  • method layer 
  • endpoint 

Armadillo

jsonRpcEndpoint

jsonRpcEndpoint

jsonRpcEndpoint

Custom Interceptor

legacyEndpoint

Bonus

https://www.flaticon.com/free-icon/armadillo_4215064

Demo

Thank you!

https://github.com/input-output-hk/armadillo

https://github.com/ghostbuster91

https://github.com/softwaremill/tapir

kkondzielski

Made with Slides.com