diff --git a/rebar.config b/rebar.config index ecee336..03c5966 100644 --- a/rebar.config +++ b/rebar.config @@ -22,6 +22,8 @@ {hex, [{doc, #{provider => ex_doc}}]}. +{edoc_opts, [{preprocess, true}]}. + {ex_doc, [ {extras, [ {"CHANGELOG.md", #{title => "Changelog"}}, diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index f67d0c9..f9bb96c 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -46,10 +46,24 @@ handle_msg(JSON) -> %--- Internal Funcitons -------------------------------------------------------- -handle_jsonrpc({batch, Batch}) -> - handle_rpc_messages(Batch, []); -handle_jsonrpc({single, Rpc}) -> - handle_rpc_messages([Rpc], []). +format_error({internal_error, parse_error, ID}) -> + {error, -32700, <<"Parse error">>, undefined, ID}; +format_error({internal_error, invalid_request, ID}) -> + {error, -32600, <<"Invalid request">>, undefined, ID}; +format_error({internal_error, method_not_found, ID}) -> + {error, -32601, <<"Method not found">>, undefined, ID}; +format_error({internal_error, invalid_params, ID}) -> + {error, -32602, <<"Invalid params">>, undefined, ID}; +format_error({internal_error, Reason, ID}) -> + {error, -32603, <<"Internal error">>, Reason, ID}. + +%FIXME: Batch are not supported yet. When receiving a batch of messages, as per +% the JSON-RPC standard, all the responses should goes in a single batch +% of responses. +handle_jsonrpc(Messages) when is_list(Messages) -> + handle_rpc_messages(Messages, []); +handle_jsonrpc(Message) -> + handle_rpc_messages([Message], []). handle_rpc_messages([], Replies) -> lists:reverse(Replies); handle_rpc_messages([{request, M, Params, ID} | Batch], Replies) @@ -61,8 +75,8 @@ handle_rpc_messages([{result, _, _} = Res| Batch], Replies) -> handle_rpc_messages([{error, _Code, _Msg, _Data, _ID} = E | Batch], Replies) -> ?LOG_INFO("Received JsonRPC error: ~p",[E]), handle_rpc_messages(Batch, [handle_response(E)| Replies]); -handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) -> - ?LOG_ERROR("JsonRPC: ~p",[E]), +handle_rpc_messages([{decoding_error, _, _, _, _} = E | Batch], Replies) -> + ?LOG_ERROR("JsonRPC decoding error: ~p",[E]), handle_rpc_messages(Batch, Replies). handle_request(?method_get, #{type := <<"system_info">>} = _Params, ID) -> @@ -80,7 +94,7 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> {error, -12, boot_system_not_validated, undefined, ID}; {error, Reason} -> ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), - grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); + format_error({internal_error, ReasonBinary, ID}); ok -> {result, ok, ID} end, @@ -88,8 +102,7 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) -> catch throw:bad_key -> {send_response, - grisp_connect_jsonrpc:format_error( - {internal_error, invalid_params, ID})} + format_error({internal_error, invalid_params, ID})} end; handle_request(?method_post, #{type := <<"validate">>}, ID) -> Reply = case grisp_connect_updater:validate() of @@ -99,7 +112,7 @@ handle_request(?method_post, #{type := <<"validate">>}, ID) -> {error, -13, validate_from_unbooted, PartitionIndex, ID}; {error, Reason} -> ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])), - grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID}); + format_error({internal_error, ReasonBinary, ID}); ok -> {result, ok, ID} end, @@ -117,7 +130,7 @@ handle_request(?method_post, #{type := <<"cancel">>}, ID) -> {send_response, grisp_connect_jsonrpc:encode(Reply)}; handle_request(_T, _P, ID) -> Error = {internal_error, method_not_found, ID}, - FormattedError = grisp_connect_jsonrpc:format_error(Error), + FormattedError = format_error(Error), {send_response, grisp_connect_jsonrpc:encode(FormattedError)}. handle_response(Response) -> diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl index 3e83b09..f37d1f8 100644 --- a/src/grisp_connect_jsonrpc.erl +++ b/src/grisp_connect_jsonrpc.erl @@ -3,10 +3,24 @@ % API -export([decode/1]). -export([encode/1]). --export([format_error/1]). + %--- Types --------------------------------------------------------------------- +-type json_rpc_message() :: + {request, Method :: binary(), Params :: map() | list(), + ReqRef :: binary() | integer()} + | {result, Result :: term(), ReqRef :: binary()} + | {notification, Method :: binary(), Params :: map() | list()} + | {error, Code :: integer(), Message :: undefined | binary(), + Data :: undefined | term(), ReqRef :: undefined | binary() | integer()} + | {decoding_error, Code :: integer(), Message :: undefined | binary(), + Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}. + + + +%--- Macros -------------------------------------------------------------------- + -define(V, jsonrpc => <<"2.0">>). -define(is_valid(Message), (map_get(jsonrpc, Message) == <<"2.0">>) @@ -17,103 +31,160 @@ -define(is_params(Params), (is_map(Params) orelse is_list(Params)) ). +-define(is_id(ID), + (is_binary(ID) orelse is_integer(ID)) +). + -%--- API ---------------------------------------------------------------------- +%--- API ----------------------------------------------------------------------- -decode(Term) -> - case json_to_term(Term) of +%% @doc Decode a JSONRpc text packet and decoded message or a list of decoded +%% messages in the case of a batch. +%% +%% If it returns a list, the all the responses are supposed to be sent back in +%% a batch too, as per the JSONRpc 2.0 specifications. +%% +%% If some decoding errors occure, a special error message with the tag +%% `decoding_error' will be returned, this message can be encoded and sent back +%% directly to the JSON-RPC peer. +%% +%% During JSON decoding, the `null' values are changed to `undefined' and when +%% encoding, `undefined' values are changed back to `null'. +%% +%% The `method' will always be a binary, and `id' will always be either +%% a binary or an integer. +%% +%%

The possible decoded messages are: +%%

+-spec decode(Data :: iodata()) -> json_rpc_message() | [json_rpc_message()]. +decode(Data) -> + case json_to_term(iolist_to_binary(Data)) of [] -> - {single, {internal_error, invalid_request, null}}; + [{decoding_error, -32600, <<"Invalid Request">>, undefined, undefined}]; Messages when is_list(Messages) -> - {batch, [unpack(M) || M <- Messages]}; + [unpack(M) || M <- Messages]; Message when is_map(Message) -> - {single, unpack(Message)}; - {error, _E} -> - {single, {internal_error, parse_error, null}} + unpack(Message); + {error, _Reason} -> + % Even though the data could have been a batch, we return a single + % error, as per JSON-RPC specifications + {decoding_error, -32700, <<"Parse error">>, undefined, undefined} end. -encode([Message]) -> - encode(Message); +%% @doc Encode a JSONRpc message or a list of JSONRpc messages to JSON text. +%% For backward compatibility, the `method' can be an atom. +-spec encode(Messages :: json_rpc_message() | [json_rpc_message()]) -> iodata(). encode(Messages) when is_list(Messages) -> term_to_json([pack(M) || M <- Messages]); encode(Message) -> term_to_json(pack(Message)). -format_error({internal_error, parse_error, ID}) -> - {error, -32700, <<"Parse error">>, undefined, ID}; -format_error({internal_error, invalid_request, ID}) -> - {error, -32600, <<"Invalid request">>, undefined, ID}; -format_error({internal_error, method_not_found, ID}) -> - {error, -32601, <<"Method not found">>, undefined, ID}; -format_error({internal_error, invalid_params, ID}) -> - {error, -32602, <<"Invalid params">>, undefined, ID}; -format_error({internal_error, Reason, ID}) -> - {error, -32603, <<"Internal error">>, Reason, ID}. -%--- Internal ----------------------------------------------------------------- +%--- Internal ------------------------------------------------------------------ + +as_bin(undefined) -> undefined; +as_bin(Binary) when is_binary(Binary) -> Binary; +as_bin(List) when is_list(List) -> list_to_binary(List). + +as_id(undefined) -> undefined; +as_id(Integer) when is_integer(Integer) -> Integer; +as_id(Binary) when is_binary(Binary) -> Binary; +as_id(List) when is_list(List) -> list_to_binary(List). unpack(#{method := Method, params := Params, id := ID} = M) - when ?is_valid(M), ?is_method(Method), ?is_params(Params) -> - {request, Method, Params, ID}; + when ?is_valid(M), ?is_method(Method), ?is_params(Params), ID =/= undefined -> + {request, as_bin(Method), Params, as_id(ID)}; unpack(#{method := Method, id := ID} = M) - when ?is_valid(M), ?is_method(Method) -> - {request, Method, undefined, ID}; + when ?is_valid(M), ?is_method(Method), ID =/= undefined -> + {request, as_bin(Method), undefined, as_id(ID)}; unpack(#{method := Method, params := Params} = M) - when ?is_valid(M), ?is_method(Method), ?is_params(Params) -> - {notification, Method, Params}; + when ?is_valid(M), ?is_method(Method), ?is_params(Params) -> + {notification, as_bin(Method), Params}; unpack(#{method := Method} = M) - when ?is_valid(M), ?is_method(Method) -> - {notification, Method, undefined}; -unpack(#{method := Method, params := _Params, id := ID} = M) - when ?is_valid(M), ?is_method(Method) -> - {internal_error, invalid_params, ID}; + when ?is_valid(M), ?is_method(Method) -> + {notification, as_bin(Method), undefined}; unpack(#{result := Result, id := ID} = M) - when ?is_valid(M) -> - {result, Result, ID}; -unpack(#{error := #{code := Code, - message := Message, - data := Data}, - id := ID} = M) - when ?is_valid(M) -> - {error, Code, Message, Data, ID}; -unpack(#{error := #{code := Code, - message := Message}, - id := ID} = M) - when ?is_valid(M) -> - {error, Code, Message, undefined, ID}; -unpack(M) -> - {internal_error, invalid_request, id(M)}. - -pack({request, Method, undefined, ID}) -> + when ?is_valid(M) -> + {result, Result, as_id(ID)}; +unpack(#{error := #{code := Code, message := Message, data := Data}, + id := ID} = M) + when ?is_valid(M), is_integer(Code) -> + {error, Code, as_bin(Message), Data, as_id(ID)}; +unpack(#{error := #{code := Code, message := Message}, id := ID} = M) + when ?is_valid(M), is_integer(Code) -> + {error, Code, as_bin(Message), undefined, as_id(ID)}; +unpack(#{id := ID}) -> + {decoding_error, -32600, <<"Invalid request">>, undefined, as_id(ID)}; +unpack(_M) -> + {decoding_error, -32600, <<"Invalid request">>, undefined, undefined}. + +pack({request, Method, undefined, ID}) + when is_binary(Method) orelse is_atom(Method), ?is_id(ID) -> #{?V, method => Method, id => ID}; -pack({request, Method, Params, ID}) -> +pack({request, Method, Params, ID}) + when is_binary(Method) orelse is_atom(Method), + Params =:= undefined orelse ?is_params(Params), + ?is_id(ID) -> #{?V, method => Method, params => Params, id => ID}; -pack({notification, Method, undefined}) -> +pack({notification, Method, undefined}) + when is_binary(Method) orelse is_atom(Method) -> #{?V, method => Method}; -pack({notification, Method, Params}) -> +pack({notification, Method, Params}) + when is_binary(Method), Params =:= undefined orelse ?is_params(Params) -> #{?V, method => Method, params => Params}; -pack({result, Result, ID}) -> +pack({result, Result, ID}) + when ?is_id(ID) -> #{?V, result => Result, id => ID}; -pack({error, Type, ID}) -> - pack(format_error({internal_error, Type, ID})); -pack({error, Code, Message, undefined, undefined}) -> +pack({ErrorTag, Code, Message, undefined, undefined}) + when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code), + Message =:= undefined orelse is_binary(Message) -> #{?V, error => #{code => Code, message => Message}, id => null}; -pack({error, Code, Message, undefined, ID}) -> +pack({ErrorTag, Code, Message, undefined, ID}) + when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code), + Message =:= undefined orelse is_binary(Message), ?is_id(ID) -> #{?V, error => #{code => Code, message => Message}, id => ID}; -pack({error, Code, Message, Data, undefined}) -> +pack({ErrorTag, Code, Message, Data, undefined}) + when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code), + Message =:= undefined orelse is_binary(Message) -> #{?V, error => #{code => Code, message => Message, data => Data, id => null}}; -pack({error, Code, Message, Data, ID}) -> - #{?V, error => #{code => Code, message => Message, data => Data}, id => ID}. - -id(Object) when is_map(Object) -> maps:get(id, Object, null); -id(_Object) -> null. - +pack({ErrorTag, Code, Message, Data, ID}) + when ErrorTag =:= error orelse ErrorTag =:= decoding_error, is_integer(Code), + Message =:= undefined orelse is_binary(Message), ?is_id(ID) -> + #{?V, error => #{code => Code, message => Message, data => Data}, id => ID}; +pack(Message) -> + erlang:error({badarg, Message}). json_to_term(Bin) -> - try jsx:decode(Bin, [{labels, attempt_atom}, return_maps]) + try jsx:decode(Bin, [{labels, attempt_atom}, return_maps]) of + Json -> postprocess(Json) catch error:E -> {error, E} end. -term_to_json(Map) -> - jsx:encode(Map). +term_to_json(Term) -> + jsx:encode(preprocess(Term)). + +postprocess(null) -> undefined; +postprocess(Integer) when is_integer(Integer) -> Integer; +postprocess(Float) when is_float(Float) -> Float; +postprocess(Binary) when is_binary(Binary) -> Binary; +postprocess(List) when is_list(List) -> + [postprocess(E) || E <- List]; +postprocess(Map) when is_map(Map) -> + maps:map(fun(_K, V) -> postprocess(V) end, Map). + +preprocess(undefined) -> null; +preprocess(Atom) when is_atom(Atom) -> Atom; +preprocess(Integer) when is_integer(Integer) -> Integer; +preprocess(Float) when is_float(Float) -> Float; +preprocess(Binary) when is_binary(Binary) -> Binary; +preprocess(List) when is_list(List) -> + [preprocess(E) || E <- List]; +preprocess(Map) when is_map(Map) -> + maps:map(fun(_K, V) -> preprocess(V) end, Map). diff --git a/test/grisp_connect_jsonrpc_SUITE.erl b/test/grisp_connect_jsonrpc_SUITE.erl index 1f7efab..16853a1 100644 --- a/test/grisp_connect_jsonrpc_SUITE.erl +++ b/test/grisp_connect_jsonrpc_SUITE.erl @@ -6,26 +6,30 @@ -export([positional_parameters/1, named_parameters/1, + using_existing_atoms/1, notification/1, invalid_json/1, invalid_request/1, batch/1, - result/1]). + result/1, + null_values/1]). all() -> [ positional_parameters, named_parameters, + using_existing_atoms, notification, invalid_json, invalid_request, batch, - result + result, + null_values ]. positional_parameters(_) -> Term = {request, <<"subtract">>, [42,23], 1}, Json = <<"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"subtract\",\"params\":[42,23]}">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), Json2 = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"id\":1">>, <<"\"method\":\"subtract\"">>, @@ -34,36 +38,44 @@ positional_parameters(_) -> named_parameters(_) -> Term = {request, <<"divide">>, #{<<"dividend">> => 42, <<"divisor">> => 2}, 2}, Json = <<"{\"id\":2,\"jsonrpc\":\"2.0\",\"method\":\"divide\",\"params\":{\"dividend\":42,\"divisor\":2}}">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), Json2 = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"id\":2">>, <<"\"method\":\"divide\"">>, <<"\"dividend\":42">>, <<"\"divisor\":2">>], Json2)). +using_existing_atoms(_) -> + % The ID and method are matching existing atoms, checks they are not atoms + Term = {request, <<"notification">>, #{}, <<"request">>}, + Json = <<"{\"id\":\"request\",\"jsonrpc\":\"2.0\",\"method\":\"notification\",\"params\":{}}">>, + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), + Json2 = grisp_connect_jsonrpc:encode(Term), + ?assert(jsonrpc_check([<<"\"id\":\"request\"">>, <<"\"method\":\"notification\"">>], Json2)). + notification(_) -> Term = {notification, <<"update">>, [1,2,3,4,5]}, Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"update\",\"params\":[1,2,3,4,5]}">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), Json2 = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"method\":\"update\"">>, <<"\"params\":[1,2,3,4,5]">>], Json2)). invalid_json(_) -> - Term = {internal_error, parse_error, null}, + Term = {decoding_error, -32700, <<"Parse error">>, undefined, undefined}, Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"foobar,\"params\":\"bar\",\"baz]">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), - JsonError = grisp_connect_jsonrpc:encode(grisp_connect_jsonrpc:format_error(Term)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), + JsonError = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"error\":{">>, <<"\"code\":-32700">>, <<"\"message\":\"Parse error\"">>, <<"\"id\":null">>], JsonError)). invalid_request(_) -> - Term = {internal_error, invalid_request, null}, + Term = {decoding_error, -32600, <<"Invalid request">>, undefined, undefined}, Json = <<"{\"jsonrpc\":\"2.0\",\"method\":1,\"params\":\"bar\"}">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), - JsonError = grisp_connect_jsonrpc:encode(grisp_connect_jsonrpc:format_error(Term)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), + JsonError = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"error\":{">>, <<"\"code\":-32600">>, <<"\"message\":\"Invalid request\"">>, @@ -71,13 +83,13 @@ invalid_request(_) -> batch(_) -> Term1 = {request, <<"sum">>, [1,2,4], <<"1">>}, - Term2 = {internal_error, invalid_request, null}, + Term2 = {decoding_error, -32600, <<"Invalid request">>, undefined, undefined}, Json = <<"[{\"jsonrpc\":\"2.0\",\"method\":\"sum\",\"params\":[1,2,4],\"id\":\"1\"},{\"foo\":\"boo\"}]">>, - ?assertMatch({batch, [Term1,Term2]}, grisp_connect_jsonrpc:decode(Json)), - JsonError = grisp_connect_jsonrpc:encode([Term1, grisp_connect_jsonrpc:format_error(Term2)]), + ?assertMatch([Term1, Term2], grisp_connect_jsonrpc:decode(Json)), + JsonError = grisp_connect_jsonrpc:encode([Term1, Term2]), ?assert(jsonrpc_check([<<"\"id\":\"1\"">>, <<"\"method\":\"sum\"">>, - <<"params\":[1,2,4]">>, + <<"\"params\":[1,2,4]">>, <<"\"error\":{">>, <<"\"code\":-32600">>, <<"\"message\":\"Invalid request\"">>, @@ -86,11 +98,20 @@ batch(_) -> result(_) -> Term = {result, 7, 45}, Json = <<"{\"id\":45,\"jsonrpc\":\"2.0\",\"result\":7}">>, - ?assertMatch({single, Term}, grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), Json2 = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"id\":45">>, <<"\"result\":7">>], Json2)). +null_values(_) -> + Term = {notification, <<"test_null">>, #{array => [undefined], object => #{foo => undefined}, value => undefined}}, + Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"test_null\",\"params\":{\"array\":[null],\"object\":{\"foo\":null},\"value\":null}}">>, + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), + Json2 = grisp_connect_jsonrpc:encode(Term), + ?assert(jsonrpc_check([<<"\"array\":[null]">>, + <<"\"foo\":null">>, + <<"\"value\":null">>], + Json2)). jsonrpc_check(Elements, JsonString) -> Elements2 = [<<"\"jsonrpc\":\"2.0\"">>| Elements],