Packages

Provides the ability to create services in accordance with the JSONRPC 2.0 specification. There is no transport layer, but only tools for creating transport-independent JSONRPC 2.0 services.

Current section

Files

Jump to
jsonrpc2_service lib jsonrpc2 service.ex
Raw

lib/jsonrpc2/service.ex

defmodule JSONRPC2.Service do
@moduledoc """
`JSONRPC2.Service` helps to describe methods of service. Each method is a module which use `JSONRPC2.Service.Method`.
Examples:
defmodule CalculatorService do
use JSONRPC2.Service
method "add", AddMethod
method "subtract", SubtractMethod
method "multiply", MultiplyMethod
method "divide", DivideMethod
end
defmodule AddMethod do
use JSONRPC2.Service.Method
end
# and so on...
It privides `handle` function as a single entry point to the service.
iex> CalculatorService.handle(%{"jsonrpc" => "2.0", "id" => 2, "method" => "add", "params" => [1, 2]})
%Result{jsonrpc: "2.0", id: 2, result: 3}
iex> CalculatorService.handle(%{"jsonrpc" => "2.0", "id" => 2, "method" => "divide", "params" => [10, 0]})
%Error{jsonrpc: "2.0", id: 2, error: %{code: 12345, message: "divided by zero"}}
"""
require Logger
alias JSONRPC2.Spec.Request
alias JSONRPC2.Spec.Response
defmacro __using__(_) do
quote location: :keep do
@__service_methods__ []
Module.register_attribute(__MODULE__, :__service_methods__, accumulate: true)
import unquote(__MODULE__), only: [method: 2]
@before_compile unquote(__MODULE__)
@doc """
Handle service requests.
Example:
iex> CalculatorService.handle(%{"jsonrpc" => "2.0", "id" => 2, "method" => "add", "params" => [1, 2]})
%Result{jsonrpc: "2.0", id: 2, result: 3}
iex> CalculatorService.handle(%{"jsonrpc" => "2.0", "id" => 2, "method" => "divide", "params" => [10, 0]})
%Error{jsonrpc: "2.0", id: 2, error: %{code: 12345, message: "divided by zero"}}
"""
def handle(%{"_json" => body_params}, conn) when is_list(body_params) do
Enum.map(body_params, fn(one) -> handle_one(one, conn) end) |> drop_nils()
end
def handle(body_params, conn) when is_map(body_params) do
handle_one(body_params, conn)
end
defp handle_one(body_params, conn) do
with {:ok, request} <- Request.parse(body_params) do
with :ok <- Request.valid?(request),
{:ok, handler} <- handler_lookup(request.method),
{:ok, result} <- exec_handler(handler, request, conn) do
response(request.id, result)
else
{:jsonrpc2_error, code_or_tuple} ->
error(request.id, code_or_tuple)
{:error, error} ->
error(request.id, {:internal_error, inspect(error)})
:error ->
error(request.id, {:method_not_found, inspect(request.method)})
:invalid ->
error(request.id, :invalid_request)
error ->
error(request.id, {:internal_error, inspect(error)})
end
else
{:invalid, id} ->
error(id, :invalid_request)
error ->
error
end
end
defp handler_lookup(name) do
Keyword.fetch(__service_methods__(), String.to_atom(name))
end
defp exec_handler(handler, %Request{id: id, method: method, params: params} = request, conn) do
try do
if is_nil(id) do
handler.cast(params, conn)
else
handler.call(params, conn)
end
rescue
exception ->
handler.handle_exception(request, exception, __STACKTRACE__)
catch
:throw, {:jsonrpc2_error, code_or_tuple} ->
{:jsonrpc2_error, code_or_tuple}
kind, payload ->
handler.handle_error(request, {kind, payload}, __STACKTRACE__)
end
end
defp drop_nils(responses) do
Enum.reject(responses, &is_nil/1)
end
defp response(nil, _result) do
nil
end
defp response(id, result) do
Response.result(id, result)
end
defp error(nil, _) do
nil
end
defp error(id, {code, message_or_data}) do
Response.error(id, code, message_or_data)
end
defp error(id, {code, message, data}) do
Response.error(id, code, message, data)
end
defp error(id, code) do
Response.error(id, code)
end
end
end
defmacro __before_compile__(_env) do
quote location: :keep do
def __service_methods__ do
@__service_methods__
end
end
end
@doc """
Define method and handler.
Example:
method "add", AddMethod
method :subtract, SubtractMethod
"""
@spec method(String.t(), module()) :: term()
defmacro method(name, handler) when is_binary(name) do
build_method(String.to_atom(name), handler)
end
@spec method(atom(), module()) :: term()
defmacro method(name, handler) when is_atom(name) do
build_method(name, handler)
end
defp build_method(name, handler) do
quote location: :keep do
@__service_methods__ {unquote(name), unquote(handler)}
end
end
end