From 56de5ee634a2414a6e3151e6fcc52e6bd4bde7a3 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Wed, 13 Nov 2024 20:12:35 +0100 Subject: [PATCH 1/4] Cleanup JsonRPC codec * Move the error formating out. * pre-process and post-process the message to handle null/undefined conversion. * Always return a list of message, no special case for single/batch. * Return decoding errors as proper jsonrpc errors that could be sent right away to the peer. --- src/grisp_connect_api.erl | 30 ++-- src/grisp_connect_jsonrpc.erl | 205 ++++++++++++++++++--------- test/grisp_connect_jsonrpc_SUITE.erl | 37 +++-- 3 files changed, 178 insertions(+), 94 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index f67d0c9..41c6862 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -46,10 +46,19 @@ 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}. + +handle_jsonrpc(Messages) -> + handle_rpc_messages(Messages, []). handle_rpc_messages([], Replies) -> lists:reverse(Replies); handle_rpc_messages([{request, M, Params, ID} | Batch], Replies) @@ -61,8 +70,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 +89,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 +97,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 +107,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 +125,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..3ac1c53 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()}. + + + +%--- Macros -------------------------------------------------------------------- + -define(V, jsonrpc => <<"2.0">>). -define(is_valid(Message), (map_get(jsonrpc, Message) == <<"2.0">>) @@ -17,103 +31,156 @@ -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 returns a list of decoded messages. +%% If some decoding errors occure while doing do, a special error message with +%% the tag `decoding_error` that can be encoded and sent back directly to the +%% JSONRpc 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()]. +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} -> + [{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)). + encode([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_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom). + +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); +as_id(Atom) when is_atom(Atom) -> atom_to_binary(Atom). 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(Atom) when is_atom(Atom) -> Atom; +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:from_list([{K, postprocess(V)} || {K, V} <- maps:to_list(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:from_list([{K, preprocess(V)} || {K, V} <- maps:to_list(Map)]). diff --git a/test/grisp_connect_jsonrpc_SUITE.erl b/test/grisp_connect_jsonrpc_SUITE.erl index 1f7efab..2e43fc9 100644 --- a/test/grisp_connect_jsonrpc_SUITE.erl +++ b/test/grisp_connect_jsonrpc_SUITE.erl @@ -6,6 +6,7 @@ -export([positional_parameters/1, named_parameters/1, + using_existing_atoms/1, notification/1, invalid_json/1, invalid_request/1, @@ -15,6 +16,7 @@ all() -> [ positional_parameters, named_parameters, + using_existing_atoms, notification, invalid_json, invalid_request, @@ -25,7 +27,7 @@ all() -> [ 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 +36,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,10 +81,10 @@ 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]">>, @@ -86,12 +96,11 @@ 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)). - jsonrpc_check(Elements, JsonString) -> Elements2 = [<<"\"jsonrpc\":\"2.0\"">>| Elements], lists:all(fun(E) -> binary:match(JsonString, E) =/= nomatch end, Elements2). From ac8207004d3d522e0f3653fafaf2d6f92cd8ed34 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Thu, 14 Nov 2024 09:33:29 +0100 Subject: [PATCH 2/4] Fix doc generation --- rebar.config | 2 ++ src/grisp_connect_jsonrpc.erl | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) 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_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl index 3ac1c53..ae6d899 100644 --- a/src/grisp_connect_jsonrpc.erl +++ b/src/grisp_connect_jsonrpc.erl @@ -40,23 +40,23 @@ %% @doc Decode a JSONRpc text packet and returns a list of decoded messages. %% If some decoding errors occure while doing do, a special error message with -%% the tag `decoding_error` that can be encoded and sent back directly to the +%% the tag `decoding_error' that can be encoded and sent back directly to the %% JSONRpc peer. %% -%% During JSON decoding, the `null` values are changed to `undefined` and when -%% encoding, `undefined` values are changed back to `null`. +%% 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 +%% The `method' will always be a binary, and `id' will always be either %% a binary or an integer. %% %%

The possible decoded messages are: %%

+%%
  • `{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()}'
  • +%%

    -spec decode(Data :: iodata()) -> [json_rpc_message()]. decode(Data) -> case json_to_term(iolist_to_binary(Data)) of @@ -71,7 +71,7 @@ decode(Data) -> end. %% @doc Encode a JSONRpc message or a list of JSONRpc messages to JSON text. -%% For backward compatibility, the `method` can be an atom. +%% 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]); From 1743f40b09ae44f7ce523e3b51d59d184fda01d7 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Thu, 14 Nov 2024 13:20:44 +0100 Subject: [PATCH 3/4] Fixes from review --- src/grisp_connect_jsonrpc.erl | 17 +++++++---------- test/grisp_connect_jsonrpc_SUITE.erl | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl index ae6d899..be0f5ce 100644 --- a/src/grisp_connect_jsonrpc.erl +++ b/src/grisp_connect_jsonrpc.erl @@ -15,7 +15,7 @@ | {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()}. + Data :: undefined | term(), ReqRef :: undefined | binary() | integer()}. @@ -83,14 +83,12 @@ encode(Message) -> as_bin(undefined) -> undefined; as_bin(Binary) when is_binary(Binary) -> Binary; -as_bin(List) when is_list(List) -> list_to_binary(List); -as_bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom). +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); -as_id(Atom) when is_atom(Atom) -> atom_to_binary(Atom). +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), ID =/= undefined -> @@ -152,8 +150,8 @@ 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}). +pack(Message) -> + erlang:error({badarg, Message}). json_to_term(Bin) -> try jsx:decode(Bin, [{labels, attempt_atom}, return_maps]) of @@ -166,14 +164,13 @@ term_to_json(Term) -> jsx:encode(preprocess(Term)). postprocess(null) -> undefined; -postprocess(Atom) when is_atom(Atom) -> Atom; 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:from_list([{K, postprocess(V)} || {K, V} <- maps:to_list(Map)]). + maps:map(fun(_K, V) -> postprocess(V) end, Map). preprocess(undefined) -> null; preprocess(Atom) when is_atom(Atom) -> Atom; @@ -183,4 +180,4 @@ preprocess(Binary) when is_binary(Binary) -> Binary; preprocess(List) when is_list(List) -> [preprocess(E) || E <- List]; preprocess(Map) when is_map(Map) -> - maps:from_list([{K, preprocess(V)} || {K, V} <- maps:to_list(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 2e43fc9..9273333 100644 --- a/test/grisp_connect_jsonrpc_SUITE.erl +++ b/test/grisp_connect_jsonrpc_SUITE.erl @@ -11,7 +11,8 @@ invalid_json/1, invalid_request/1, batch/1, - result/1]). + result/1, + null_values/1]). all() -> [ positional_parameters, @@ -21,7 +22,8 @@ all() -> [ invalid_json, invalid_request, batch, - result + result, + null_values ]. positional_parameters(_) -> @@ -87,7 +89,7 @@ batch(_) -> 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\"">>, @@ -101,6 +103,16 @@ result(_) -> ?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], lists:all(fun(E) -> binary:match(JsonString, E) =/= nomatch end, Elements2). From cac413f5a60fbed5119b9d1e12c1015d24ce53e7 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Thu, 14 Nov 2024 14:34:35 +0100 Subject: [PATCH 4/4] Fix batch handling --- src/grisp_connect_api.erl | 9 +++++++-- src/grisp_connect_jsonrpc.erl | 23 +++++++++++++++-------- test/grisp_connect_jsonrpc_SUITE.erl | 16 ++++++++-------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/grisp_connect_api.erl b/src/grisp_connect_api.erl index 41c6862..f9bb96c 100644 --- a/src/grisp_connect_api.erl +++ b/src/grisp_connect_api.erl @@ -57,8 +57,13 @@ format_error({internal_error, invalid_params, ID}) -> format_error({internal_error, Reason, ID}) -> {error, -32603, <<"Internal error">>, Reason, ID}. -handle_jsonrpc(Messages) -> - handle_rpc_messages(Messages, []). +%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) diff --git a/src/grisp_connect_jsonrpc.erl b/src/grisp_connect_jsonrpc.erl index be0f5ce..f37d1f8 100644 --- a/src/grisp_connect_jsonrpc.erl +++ b/src/grisp_connect_jsonrpc.erl @@ -38,10 +38,15 @@ %--- API ----------------------------------------------------------------------- -%% @doc Decode a JSONRpc text packet and returns a list of decoded messages. -%% If some decoding errors occure while doing do, a special error message with -%% the tag `decoding_error' that can be encoded and sent back directly to the -%% JSONRpc peer. +%% @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'. @@ -57,7 +62,7 @@ %%
  • `{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()}'
  • %%

    --spec decode(Data :: iodata()) -> [json_rpc_message()]. +-spec decode(Data :: iodata()) -> json_rpc_message() | [json_rpc_message()]. decode(Data) -> case json_to_term(iolist_to_binary(Data)) of [] -> @@ -65,9 +70,11 @@ decode(Data) -> Messages when is_list(Messages) -> [unpack(M) || M <- Messages]; Message when is_map(Message) -> - [unpack(Message)]; + unpack(Message); {error, _Reason} -> - [{decoding_error, -32700, <<"Parse error">>, undefined, undefined}] + % 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. %% @doc Encode a JSONRpc message or a list of JSONRpc messages to JSON text. @@ -76,7 +83,7 @@ decode(Data) -> encode(Messages) when is_list(Messages) -> term_to_json([pack(M) || M <- Messages]); encode(Message) -> - encode([Message]). + term_to_json(pack(Message)). %--- Internal ------------------------------------------------------------------ diff --git a/test/grisp_connect_jsonrpc_SUITE.erl b/test/grisp_connect_jsonrpc_SUITE.erl index 9273333..16853a1 100644 --- a/test/grisp_connect_jsonrpc_SUITE.erl +++ b/test/grisp_connect_jsonrpc_SUITE.erl @@ -29,7 +29,7 @@ all() -> [ positional_parameters(_) -> Term = {request, <<"subtract">>, [42,23], 1}, Json = <<"{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"subtract\",\"params\":[42,23]}">>, - ?assertMatch([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\"">>, @@ -38,7 +38,7 @@ 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([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\"">>, @@ -49,14 +49,14 @@ 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)), + ?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([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)). @@ -64,7 +64,7 @@ notification(_) -> invalid_json(_) -> Term = {decoding_error, -32700, <<"Parse error">>, undefined, undefined}, Json = <<"{\"jsonrpc\":\"2.0\",\"method\":\"foobar,\"params\":\"bar\",\"baz]">>, - ?assertMatch([Term], grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), JsonError = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"error\":{">>, <<"\"code\":-32700">>, @@ -74,7 +74,7 @@ invalid_json(_) -> invalid_request(_) -> Term = {decoding_error, -32600, <<"Invalid request">>, undefined, undefined}, Json = <<"{\"jsonrpc\":\"2.0\",\"method\":1,\"params\":\"bar\"}">>, - ?assertMatch([Term], grisp_connect_jsonrpc:decode(Json)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), JsonError = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"error\":{">>, <<"\"code\":-32600">>, @@ -98,7 +98,7 @@ batch(_) -> result(_) -> Term = {result, 7, 45}, Json = <<"{\"id\":45,\"jsonrpc\":\"2.0\",\"result\":7}">>, - ?assertMatch([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)). @@ -106,7 +106,7 @@ result(_) -> 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)), + ?assertMatch(Term, grisp_connect_jsonrpc:decode(Json)), Json2 = grisp_connect_jsonrpc:encode(Term), ?assert(jsonrpc_check([<<"\"array\":[null]">>, <<"\"foo\":null">>,