Skip to content

Commit e074535

Browse files
committed
Add docs and examples
1 parent ccec187 commit e074535

13 files changed

+276
-8
lines changed

README.md

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
# JSONRPC2Plug
1+
# JSON-RPC 2.0 plug
22

3-
JSON-RPC 2 plug
3+
`JSONRPC2Plug` is an Elixir library for a JSON-RPC 2.0 server. Can be used as the plug middleware or as a standalone transport-agnostic server handler.
44

55
## Installation
66

7-
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8-
by adding `jsonrpc2_plug` to your list of dependencies in `mix.exs`:
7+
The package can be installed by adding `jsonrpc2_plug` to your list of dependencies in `mix.exs`:
98

109
```elixir
1110
def deps do
@@ -15,6 +14,88 @@ def deps do
1514
end
1615
```
1716

17+
## Usage
18+
19+
### Defining Services
20+
21+
Services should use `JSONRPC2Plug.Service`, which allows describing methods of service. Each method is a module which use `JSONRPC2Plug.Method`.
22+
23+
Examples:
24+
25+
26+
```elixir
27+
defmodule CalculatorService do
28+
use JSONRPC2Plug.Service
29+
30+
method "add", AddMethod
31+
method "subtract", SubtractMethod
32+
method "multiply", MultiplyMethod
33+
method "divide", DivideMethod
34+
end
35+
36+
defmodule AddMethod do
37+
use JSONRPC2Plug.Method
38+
end
39+
# and so on...
40+
```
41+
42+
### Defining Methods
43+
44+
There are two possible ways to execute a request: `call` and `cast`. The first assumes the response which the service will return, the second does not. The module should implement at least one `handle_call` or `handle_cast` callback function to handle requests.
45+
46+
```elixir
47+
defmodule AddMethod do
48+
use JSONRPC2Plug.Method
49+
50+
# It handles requests like this:
51+
# {"id": "123", "method": "add", "params": {"x": 10, "y": 20}, "jsonrpc": "2.0"}
52+
def handle_call(%{"x" = > x, "y" => y}, _conn) do
53+
{:ok, x + y}
54+
end
55+
end
56+
```
57+
58+
The first argument is the `"params"` data comes from request JSON. According to [JSONRPC2 spec](https://www.jsonrpc.org/specification), it must be either object or an array of arguments.
59+
60+
The second argument is the `Plug.Conn` struct. Sometimes it could be useful to access the `Plug` connection.
61+
62+
The module implements behaviour `JSONRPC2Plug.Method` which consists of five callbacks: `handle_call`, `handle_cast`, `validate`, `handle_error` and, `handle_exception`.
63+
64+
#### `handle_call` and `handle_cast`
65+
66+
_TODO: Add description_
67+
68+
#### `validate`
69+
70+
This function is for the validation of the input dataset.
71+
72+
```elixir
73+
import JSONRPC2Plug.Validator, only: [type: 1, required: 0]
74+
75+
def validate(params) do
76+
params
77+
|> Validator.validate("x", [type(:integer), required()])
78+
|> Validator.validate("y", [type(:integer), required()])
79+
|> Validator.unwrap()
80+
end
81+
```
82+
83+
The library has its own validator. It has 8 built-in validations: `type`, `required`, `not_empty`, `exclude`, `include`, `len`, `number` and `format`. However, you can write custom validations and extend existing ones.
84+
85+
Moreover, you can use any preferred validator (eg. [`valdi`](https://github.com/bluzky/valdi)), but you should respect the following requirements: the `validate` function should return either `{:ok, params}` or `{:invalid, errors}`. Where `errors` could be any type that can be safely encoded to JSON and `params` is params to pass into `handle_call` or `handle_cast` functions.
86+
87+
#### `handle_error` and `handle_exception`
88+
89+
_TODO: Add description_
90+
91+
### Add as a plug to the router
92+
93+
_TODO: Add description_
94+
95+
### Using as standalone module
96+
97+
_TODO: Add description_
98+
1899
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19100
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20101
be found at [https://hexdocs.pm/jsonrpc2_plug](https://hexdocs.pm/jsonrpc2_plug).

examples/add_method.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule AddMethod do
2+
use JSONRPC2Plug.Method
3+
4+
alias JSONRPC2Plug.Validator
5+
require JSONRPC2Plug.Validator, [type: 1, required: 0]
6+
7+
def handle_call(%{"x" => x, "y" => y}),
8+
do: {:ok, x + y}
9+
10+
def validate(params) do
11+
params
12+
|> Validator.validate("x", [type(:integer), required()])
13+
|> Validator.validate("y", [type(:integer), required()])
14+
|> Validator.unwrap()
15+
end
16+
end

examples/calculator_service.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
defmodule CalculatorService do
2+
use JSONRPC2Plug.Service
3+
4+
method "add", AddMethod
5+
method "subtract", SubtractMethod
6+
method "multiply", MultiplyMethod
7+
method "divide", DivideMethod
8+
end

examples/divide_method.ex

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule DivideMethod do
2+
use JSONRPC2Plug.Method
3+
4+
alias JSONRPC2Plug.Validator
5+
require JSONRPC2Plug.Validator, [type: 1, required: 0]
6+
7+
def handle_call(%{"x" => x, "y" => y}),
8+
do: {:ok, round(x / y)}
9+
10+
def validate(params) do
11+
params
12+
|> Validator.validate("x", [type(:integer), required()])
13+
|> Validator.validate("y", [type(:integer), required()])
14+
|> Validator.unwrap()
15+
end
16+
17+
def handle_exception(request, %ArithmeticError{message: message} = ex, stacktrace) do
18+
log_error(request, {:error, ex}, stacktrace)
19+
error(12345, message)
20+
end
21+
end

examples/multiply_method.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule MultiplyMethod do
2+
use JSONRPC2Plug.Method
3+
4+
alias JSONRPC2Plug.Validator
5+
require JSONRPC2Plug.Validator, [type: 1, required: 0]
6+
7+
def handle_call(%{"x" => x, "y" => y}),
8+
do: {:ok, x * y}
9+
10+
def validate(params) do
11+
params
12+
|> Validator.validate("x", [type(:integer), required()])
13+
|> Validator.validate("y", [type(:integer), required()])
14+
|> Validator.unwrap()
15+
end
16+
end

examples/subtract_method.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule SubtractMethod do
2+
use JSONRPC2Plug.Method
3+
4+
alias JSONRPC2Plug.Validator
5+
require JSONRPC2Plug.Validator, [type: 1, required: 0]
6+
7+
def handle_call(%{"x" => x, "y" => y}),
8+
do: {:ok, x - y}
9+
10+
def validate(params) do
11+
params
12+
|> Validator.validate("x", [type(:integer), required()])
13+
|> Validator.validate("y", [type(:integer), required()])
14+
|> Validator.unwrap()
15+
end
16+
end

lib/jsonrpc2_plug.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
defmodule JSONRPC2Plug do
2+
@moduledoc "README.md" |> File.read!()
3+
24
@behaviour Plug
35
require Logger
46

@@ -7,6 +9,14 @@ defmodule JSONRPC2Plug do
79
def init(handler),
810
do: handler
911

12+
@doc """
13+
HTTP entry point to JSONRPC 2.0 services. It's usual plug and accepts service handler module as a param.
14+
15+
Example:
16+
use Plug.Router
17+
18+
forward "/jsonrpc", to: JSONRPC2Plug, init_opts: CalculatorService
19+
"""
1020
@impl true
1121
@spec call(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
1222
def call(%{method: "POST", body_params: %Plug.Conn.Unfetched{}}, _handler),
@@ -27,6 +37,25 @@ defmodule JSONRPC2Plug do
2737
def call(conn, _),
2838
do: Plug.Conn.resp(conn, 404, "")
2939

40+
@doc """
41+
Send an error encoded according to JSONRPC 2.0 spec. It can be useful for global error handler in the router.
42+
43+
Example:
44+
forward "/jsonrpc", to: JSONRPC2Plug, init_opts: CalculatorService
45+
46+
@impl Plug.ErrorHandler
47+
def handle_errors(conn, %{kind: kind, reason: reason, stack: stacktrace}) do
48+
Logger.error(Exception.format(kind, reason, stacktrace))
49+
50+
case conn do
51+
%{request_path: "/jsonrpc"} ->
52+
JSONRPC2Plug.send_error(conn, kind, reason)
53+
54+
_ ->
55+
send_resp(conn, 500, "Someting went wrong")
56+
end
57+
end
58+
"""
3059
@spec send_error(Plug.Conn.t(), atom(), struct()) :: Plug.Conn.t()
3160
def send_error(conn, :error, %Plug.Parsers.ParseError{} = ex),
3261
do: send_error_response(conn, :parse_error, Exception.message(ex))

lib/jsonrpc2_plug/error.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,39 @@ defmodule JSONRPC2Plug.Error do
3131
def code2error(code),
3232
do: Keyword.get(@errors, code, {-32603, "Internal error"})
3333

34+
@doc """
35+
Create error struct with predefined errors.
36+
37+
Example:
38+
iex> Error.new("123", :invalid_request)
39+
%Error{id: "123", error: %{code: -32600, message: "Invalid request"}, jsonrpc: "2.0"}
40+
"""
3441
@spec new(id(), atom()) :: t()
3542
def new(id, code),
3643
do: %__MODULE__{id: id, error: error(code)}
3744

45+
@doc """
46+
Create error struct.
47+
48+
Example:
49+
iex> Error.new("123", :invalid_params, %{"x" => ["is not a integer"]})
50+
%Error{id: "123", error: %{code: -32602, message: "Invalid params", data: %{"x" => ["is not a integer"]}}, jsonrpc: "2.0"}
51+
52+
iex> Error.new("123", 500, "Some valuable error")
53+
%Error{id: "123", error: %{code: 500, message: "Some valuable error"}, jsonrpc: "2.0"}
54+
"""
3855
@spec new(id(), raw_code(), message() | data()) :: t()
3956
def new(id, code, message_or_data),
4057
do: %__MODULE__{id: id, error: error(code, message_or_data)}
4158

59+
@doc """
60+
Create error struct with custom errors.
61+
62+
Example:
63+
64+
iex> Error.new("123", 500, "Some valuable error", "details")
65+
%Error{id: "123", error: %{code: 500, message: "Some valuable error", data: "details"}, jsonrpc: "2.0"}
66+
"""
4267
@spec new(id(), raw_code(), message(), data()) :: t()
4368
def new(id, code, message, data),
4469
do: %__MODULE__{id: id, error: error(code, message, data)}

lib/jsonrpc2_plug/gettext.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
defmodule JSONRPC2Plug.Gettext do
2+
@moduledoc false
23
use Gettext, otp_app: :jsonrpc2_plug
34
end

lib/jsonrpc2_plug/method.ex

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,40 @@ defmodule JSONRPC2Plug.Method do
2020

2121
alias JSONRPC2Plug.Request
2222

23+
@doc """
24+
Call service method.
25+
26+
Example:
27+
28+
iex> DivideMethod.call(%{"x" => 13, "y" => 5})
29+
{:ok, 3}
30+
31+
iex> DivideMethod.call(%{"x" => 13, "y" => "5"})
32+
{:jsonrpc2_error, {:invalid_params, %{"y" => ["is not a integer"]}}}
33+
34+
iex> DivideMethod.call(%{"x" => 13, "y" => 0})
35+
{:jsonrpc2_error, {12345, "bad argument in arithmetic expression"}}
36+
"""
2337
@spec call(Request.params(), Plug.Conn.t()) :: result()
24-
def call(params, conn),
38+
def call(params, conn \\ %Plug.Conn{}),
2539
do: unquote(__MODULE__).handle({__MODULE__, :handle_call}, params, conn)
2640

41+
@doc """
42+
Cast service method.
43+
44+
Example:
45+
46+
iex> DivideMethod.call(%{"x" => 13, "y" => 5})
47+
{:ok, 3}
48+
49+
iex> DivideMethod.call(%{"x" => 13, "y" => "5"})
50+
{:jsonrpc2_error, {:invalid_params, %{"y" => ["is not a integer"]}}}
51+
52+
iex> DivideMethod.call(%{"x" => 13, "y" => 0})
53+
{:jsonrpc2_error, {12345, "bad argument in arithmetic expression"}}
54+
"""
2755
@spec cast(Request.params(), Plug.Conn.t()) :: result()
28-
def cast(params, conn),
56+
def cast(params, conn \\ %Plug.Conn{}),
2957
do: unquote(__MODULE__).handle({__MODULE__, :handle_cast}, params, conn)
3058

3159
defp error!(code),
@@ -81,7 +109,7 @@ defmodule JSONRPC2Plug.Method do
81109
{:ok, result}
82110
else
83111
{:invalid, errors} ->
84-
{:jsonrpc2_error, {:invalid_params, Enum.into(errors, %{})}}
112+
{:jsonrpc2_error, {:invalid_params, errors}}
85113

86114
error ->
87115
error

lib/jsonrpc2_plug/request.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ defmodule JSONRPC2Plug.Request do
1010

1111
defstruct [:id, :method, :params]
1212

13+
@doc """
14+
Build request struct from input data
15+
16+
Example:
17+
18+
iex> Request.parse(%{"id" => 123, "method" => "add", "params" => %{"x" => 15, "y" => 51}, "jsonrpc" => "2.0"})
19+
{:ok, %Request{id: 123, method: "add", params: %{"x" => 15, "y" => 51}, "jsonrpc" => "2.0"}}
20+
21+
iex> Request.parse(%{"id" => nil, "method" => "add", "params" => %{"x" => 15, "y" => 51}, "jsonrpc" => "2.0"})
22+
{:ok, %Request{id: nil, method: "add", params: %{"x" => 15, "y" => 51}, "jsonrpc" => "2.0"}}
23+
24+
iex> Request.parse(%{"id" => 123, "method" => "add", "params" => %{"x" => 15, "y" => 51}})
25+
{:invalid, 123}
26+
27+
iex> Request.parse(%{"method" => "add", "params" => %{"x" => 15, "y" => 51}, "jsonrpc" => "2.0"})
28+
{:invalid, nil}
29+
"""
1330
@spec parse(map()) :: {:ok, t()} | {:invalid, id()}
1431
def parse(%{"id" => id, "method" => method, "params" => params, "jsonrpc" => "2.0"}),
1532
do: {:ok, %__MODULE__{id: id, method: method, params: params}}

lib/jsonrpc2_plug/service.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ defmodule JSONRPC2Plug.Service do
1515

1616
@before_compile unquote(__MODULE__)
1717

18+
@doc """
19+
Handle service requests.
20+
"""
1821
def handle(%{"_json" => body_params}, conn) when is_list(body_params),
1922
do: Enum.map(body_params, fn(one) -> handle_one(one, conn) end) |> drop_nils()
2023
def handle(body_params, conn) when is_map(body_params),
@@ -100,6 +103,13 @@ defmodule JSONRPC2Plug.Service do
100103
end
101104
end
102105

106+
@doc """
107+
Define method and handler.
108+
109+
Example:
110+
method "add", AddMethod
111+
method :subtract, SubtractMethod
112+
"""
103113
@spec method(Request.method(), module()) :: term()
104114
defmacro method(name, handler) when is_binary(name),
105115
do: build_method(String.to_atom(name), handler)

test/jsonrpc2_plug/service_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ defmodule JSONRPC2Plug.ServiceTest do
4646
do: {:ok, "result"}
4747

4848
def validate(%{"invalid" => true}),
49-
do: {:invalid, [key: ["error 1", "error 2"]]}
49+
do: {:invalid, %{key: ["error 1", "error 2"]}}
5050
def validate(params),
5151
do: {:ok, params}
5252
end

0 commit comments

Comments
 (0)