├── .gitignore ├── LICENSE ├── README.md └── src ├── jsonrpc2.app.src ├── jsonrpc2.erl └── jsonrpc2_client.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | priv 4 | *.o 5 | *.beam 6 | *.plt 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | JSON-RPC 2.0 for Erlang 2 | ======================= 3 | 4 | Transport agnostic library for JSON-RPC 2.0 servers and clients. 5 | 6 | This page contains the manual for the server part, the `jsonrpc2` module. The client part has a 7 | separate module `jsonrpc2_client`. Client docs are yet to be written. For documentation on the 8 | client library, see the source code: [jsonrpc2_client.erl](src/jsonrpc2_client.erl). 9 | 10 | Features 11 | -------- 12 | 13 | * can use any JSON encoder and decoder that supports the eep0018 style terms 14 | format, 15 | * transport neutral 16 | * dispatches parsed requests to a simple callback function 17 | * supports an optional callback "map" function for batch requests, e.g. to 18 | support concurrent processing of the requests in a batch, 19 | * handles rpc calls and notifications, 20 | * supports named and unnamed parameters, 21 | * includes unit tests for all examples in the JSON-RPC 2.0 specification. 22 | 23 | Example 24 | ------- 25 | 26 | ``` erlang 27 | 1> Json = <<"{\"jsonrpc\": \"2.0\", \"method\": \"foo\", \"params\": [1,2,3], \"id\": 1}">>. 28 | <<"{\"jsonroc\": \"2.0\", \"method\": \"foo\", \"params\": [1,2,3], \"id\": 1}">> 29 | 2> 30 | 2> MyHandler = fun (<<"foo">>, Params) -> lists:reverse(Params); 31 | 2> (_, _) -> throw(method_not_found) 32 | 2> end. 33 | #Fun 34 | 3> 35 | 3> jsonrpc2:handle(Json, MyHandler, fun jiffy:decode/1, fun jiffy:encode/1). 36 | {reply,<<"{\"jsonrpc\":\"2.0\",\"result\":[3,2,1],\"id\":1}">>} 37 | 4> 38 | 4> jsonrpc2:handle(<<"dummy">>, MyHandler, fun jiffy:decode/1, fun jiffy:encode/1). 39 | {reply,<<"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error.\"},\"id\":null}">>} 40 | 5> 41 | 5> jsonrpc2:handle(<<"{\"x\":42}">>, MyHandler, fun jiffy:decode/1, fun jiffy:encode/1). 42 | {reply,<<"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"Invalid Request.\"},\"id\":null}">>} 43 | ``` 44 | 45 | Types 46 | ----- 47 | 48 | ```Erlang 49 | json() :: true | false | null | binary() | [json()] | {[{binary(), json()}]}. 50 | 51 | handlerfun() :: fun((method(), params()) -> json()). 52 | method() :: binary(). 53 | params() :: [json()] | {[{binary(), json()}]}. 54 | 55 | mapfun() :: fun((fun((A) -> B), [A]) -> [B]). %% the same as lists:map/2 56 | ``` 57 | 58 | Functions 59 | --------- 60 | 61 | Any of the `jsonrpc2:handle/2,3,4,5` functions can be used to handle JSON-RPC 62 | request by delegating the actual procedure call to a handler callback function. 63 | They all return `{reply, Data}` where Data is a result or an error response or 64 | `noreply` when no response should be sent to the client. The handler callback 65 | function must return a term that can be encoded to JSON using the 66 | representation explained on the page https://github.com/davisp/jiffy#data-format, 67 | as required by jiffy and other compatible JSON parses. 68 | 69 | ```Erlang 70 | handle(json(), handlerfun()) -> {reply, json()} | noreply 71 | ``` 72 | 73 | Handles decoded JSON and returns a reply as decoded JSON or noreply. Use 74 | this if you want to handle JSON encoding separately. 75 | 76 | ```Erlang 77 | handle(json(), handlerfun(), mapfun()) -> {reply, json()} | noreply 78 | ``` 79 | 80 | Like `handle/2`, handles decoded JSON, but takes an extra 81 | "map" function callback to be used instead of `lists:map/2` 82 | for batch processing. The map function should be a function that behaves 83 | similarly to `lists:map/2`, such as the `plists:map/2` 84 | from the plists library for concurrent batch handling. 85 | 86 | ```Erlang 87 | handle(Req::term(), handlerfun(), JsonDecode::fun(), JsonEncode::fun()) -> 88 | {reply, term()} | noreply 89 | ``` 90 | 91 | Handles JSON as binary or string. Uses the supplied functions 92 | JsonDecode to parse the JSON request and JsonEncode to encode the reply as JSON. 93 | 94 | ```Erlang 95 | handle(Req::term(), handlerfun(), mapfun(), JsonDecode::fun(), 96 | JsonEncode::fun()) -> {reply, term()} | noreply 97 | ``` 98 | 99 | Like `handle/4`, but also takes a map function for batch 100 | processing. See `handle/3` above. 101 | 102 | Error Handling 103 | -------------- 104 | 105 | A requests that is not valid JSON results in a "Parse error" JSON-RPC response. 106 | 107 | An invalid JSON-RPC request (though valid JSON) results in an "Invalid Request" 108 | response. In these two cases the handler callback function is never called. 109 | 110 | To produce an error response from the handler function, you may throw one of 111 | the exceptions below. They will be caught and turned into a corresponding 112 | JSON-RPC error response. 113 | 114 | * `throw(method_not_found)` is reported as "Method not found" (-32601) 115 | * `throw(invalid_params)` is reported as "Invalid params" (-32602) 116 | * `throw(internal_error)` is reported as "Internal error" (-32603) 117 | * `throw(server_error)` is reported as "Server error" (-32000) 118 | 119 | If you also want to include `data` in the JSON-RPC error response, throw a pair 120 | with the error type and the data, such as `{internal_error, Data}`. 121 | 122 | For your own *application-defined errors*, it is possible to set a custom error 123 | code by throwing a tuple with the atom `jsonrpc2`, an integer error code, a 124 | binary message and optional data. 125 | 126 | * `throw({jsonrpc2, Code, Message)` 127 | * `throw({jsonrpc2, Code, Message, Data})` 128 | 129 | If any other exception is thrown or an error occurs in the handler, this is 130 | caught, an error message is logged (using the standard error logger 131 | `error_logger:error_msg/2`) and an "Internal error" response is returned. 132 | 133 | If you're working with already parsed JSON, i.e. you're using `handle/2` or 134 | `handle/3`, you may want to produce an error message that you can use when the 135 | client sends invalid JSON that can't be parsed. Use `jsonrpc2:parseerror()` to 136 | create the appropriate error response for this purpose. 137 | 138 | Examples: 139 | 140 | ```erlang 141 | my_handler(<<"Foo">>, [X, Y]) when is_integer(X), is_integer(Y) -> 142 | {[{<<"Foo says">>}, X + Y + 42}]}; 143 | my_handler(<<"Foo">>, _SomeOtherParams) -> 144 | throw(invalid_params); 145 | my_handler(<<"Logout">>, [Username]) -> 146 | throw({jsonrpc2, 123, <<"Not logged in">>}); 147 | my_handler(_SomeOtherMethod, _) -> 148 | throw(method_not_found). 149 | ``` 150 | 151 | Compatible JSON parsers 152 | ----------------------- 153 | 154 | * Jiffy, https://github.com/davisp/jiffy 155 | * erlang-json, https://github.com/hio/erlang-json 156 | * Mochijson2 using ```mochijson2:decode(Bin, [{format, eep18}])``` 157 | * Probably more... 158 | 159 | Links 160 | ----- 161 | 162 | * The JSON-RPC 2.0 specification, http://www.jsonrpc.org/specification 163 | * rjsonrpc2, a "restricted" implementation of JSON-RPC 2.0, https://github.com/imprest/rjsonrpc2 164 | * ejrpc2, another JSON-RPC 2 library, https://github.com/jvliwanag/ejrpc2 165 | 166 | License 167 | ------- 168 | 169 | ``` 170 | Copyright 2013-2014 Viktor Söderqvist 171 | 172 | Licensed under the Apache License, Version 2.0 (the "License"); 173 | you may not use this file except in compliance with the License. 174 | You may obtain a copy of the License at 175 | 176 | http://www.apache.org/licenses/LICENSE-2.0 177 | 178 | Unless required by applicable law or agreed to in writing, software 179 | distributed under the License is distributed on an "AS IS" BASIS, 180 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 181 | See the License for the specific language governing permissions and 182 | limitations under the License. 183 | ``` 184 | 185 | **Author's note:** 186 | The Apache 2.0 is a very permissive license just like MIT and BSD, but as 187 | FSF notes, it includes "certain patent termination and indemnification 188 | provisions", which is a good thing. We (the authours) cannot come to you 189 | (the users) to claim any patents we might have on something in the code. 190 | 191 | If you have any compatibility issues with this license, keep in mind that if 192 | you're using this as an external dependency (e.g. with Rebar or Erlang.mk) 193 | you're not actually distributing this dependency anyway. Even if you do 194 | distribute dependencies, they are not actually linked together until they 195 | are loaded and run in the BEAM unless you compile the release with HiPE. 196 | -------------------------------------------------------------------------------- /src/jsonrpc2.app.src: -------------------------------------------------------------------------------- 1 | %% Copyright 2013-2014 Viktor Söderqvist 2 | %% 3 | %% Copying and distribution of this file, with or without modification, 4 | %% are permitted in any medium without royalty provided the copyright 5 | %% notice and this notice are preserved. This file is offered as-is, 6 | %% without any warranty. 7 | 8 | {application, jsonrpc2, [ 9 | {description, "JSON-RPC 2.0 request handler"}, 10 | {vsn, "0.9.2"}, 11 | {modules, [jsonrpc2, jsonrpc2_client]}, 12 | {registered, []}, 13 | {applications, [ 14 | kernel, 15 | stdlib 16 | ]} 17 | ]}. 18 | -------------------------------------------------------------------------------- /src/jsonrpc2.erl: -------------------------------------------------------------------------------- 1 | %% Copyright 2013-2014 Viktor Söderqvist 2 | %% 3 | %% Licensed under the Apache License, Version 2.0 (the "License"); 4 | %% you may not use this file except in compliance with the License. 5 | %% You may obtain a copy of the License at 6 | %% 7 | %% http://www.apache.org/licenses/LICENSE-2.0 8 | %% 9 | %% Unless required by applicable law or agreed to in writing, software 10 | %% distributed under the License is distributed on an "AS IS" BASIS, 11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | %% See the License for the specific language governing permissions and 13 | %% limitations under the License. 14 | 15 | %% @doc This module handles JSON-RPC 2.0 requests. 16 | %% 17 | %% JSON encoding and decoding is not handled by this module. Thus, it must be 18 | %% handled outside, by the caller. 19 | %% 20 | %% The format of the parsed JSON is the so called eep0018 style terms, where 21 | %% strings are represented as binaries and objects are represented as proplists 22 | %% wrapped in a single element tuple. This format is supported by several JSON 23 | %% parsers. 24 | -module(jsonrpc2). 25 | 26 | -export([handle/2, handle/3, handle/4, handle/5, parseerror/0]). 27 | 28 | -type json() :: true | false | null | binary() | [json()] | {[{binary(), json()}]}. 29 | -type method() :: binary(). 30 | -type params() :: [json()] | {[{binary(), json()}]}. 31 | -type id() :: number() | null. 32 | -type errortype() :: parse_error | method_not_found | invalid_params | 33 | internal_error | server_error. 34 | -type error() :: errortype() | {errortype(), json()} | {jsonrpc2, integer(), binary()} | 35 | {jsonrpc2, integer(), binary(), json()}. 36 | -type request() :: {method(), params(), id() | undefined} | invalid_request. 37 | -type response() :: {reply, json()} | noreply. 38 | 39 | -type handlerfun() :: fun((method(), params()) -> json()). 40 | -type mapfun() :: fun((fun((A) -> B), [A]) -> [B]). % should be the same as lists:map/2 41 | 42 | -export_type([json/0, method/0, params/0, id/0, handlerfun/0, mapfun/0, 43 | response/0, errortype/0, error/0]). 44 | 45 | %% @doc Handles a raw JSON-RPC request, using the supplied JSON decode and 46 | %% encode functions. 47 | -spec handle(Req::term(), handlerfun(), JsonDecode::fun(), JsonEncode::fun()) -> 48 | noreply | {reply, term()}. 49 | handle(Req, HandlerFun, JsonDecode, JsonEncode) 50 | when is_function(HandlerFun, 2), 51 | is_function(JsonDecode, 1), 52 | is_function(JsonEncode, 1) -> 53 | handle(Req, HandlerFun, fun lists:map/2, JsonDecode, JsonEncode). 54 | 55 | %% @doc Handles a raw JSON-RPC request, using the supplied JSON decode and 56 | %% encode functions and a custom map function. 57 | -spec handle(Req::term(), handlerfun(), mapfun(), JsonDecode::fun(), 58 | JsonEncode::fun()) -> noreply | {reply, term()}. 59 | handle(Req, HandlerFun, MapFun, JsonDecode, JsonEncode) 60 | when is_function(HandlerFun, 2), 61 | is_function(MapFun, 2), 62 | is_function(JsonDecode, 1), 63 | is_function(JsonEncode, 1) -> 64 | Response = try JsonDecode(Req) of 65 | DecodedJson -> handle(DecodedJson, HandlerFun, MapFun) 66 | catch 67 | _:_ -> {reply, parseerror()} 68 | end, 69 | case Response of 70 | noreply -> noreply; 71 | {reply, Reply} -> 72 | try JsonEncode(Reply) of 73 | EncodedReply -> {reply, EncodedReply} 74 | catch _:_ -> 75 | error_logger:error_msg("Failed encoding reply as JSON: ~p", 76 | [Reply]), 77 | {reply, Error} = make_standard_error_response(internal_error, null), 78 | {reply, JsonEncode(Error)} 79 | end 80 | end. 81 | 82 | %% @doc Handles the requests using the handler function. Batch requests are 83 | %% handled sequentially. Since this module doesn't handle the JSON encoding and 84 | %% decoding, the request must be JSON decoded before passing it to this 85 | %% function. Likewise, if a reply is returned, it should be JSON encoded before 86 | %% sending it to the client. 87 | -spec handle(json(), handlerfun()) -> response(). 88 | handle(Req, HandlerFun) -> 89 | handle(Req, HandlerFun, fun lists:map/2). 90 | 91 | %% @doc Handles the requests using the handler function and a custom map 92 | %% function for batch requests. The map function should be compatible with 93 | %% lists:map/2. This is useful for concurrent processing of batch requests. 94 | -spec handle(json(), handlerfun(), mapfun()) -> response(). 95 | handle(Req, HandlerFun, MapFun) -> 96 | case parse(Req) of 97 | BatchRpc when is_list(BatchRpc), length(BatchRpc) > 0 -> 98 | Responses = MapFun(fun(Rpc) -> dispatch(Rpc, HandlerFun) end, BatchRpc), 99 | merge_responses(Responses); 100 | Rpc -> 101 | dispatch(Rpc, HandlerFun) 102 | end. 103 | 104 | %% @doc Returns a jsonrpc2 parse error response. 105 | %% 106 | %% This function can be used to manually create a JSON-SPC error response, for 107 | %% the case when the client sends invalid JSON. This function is exported for 108 | %% completeness, since the JSON encoding and decoding is not handled by this 109 | %% module. 110 | -spec parseerror() -> json(). 111 | parseerror() -> 112 | make_error(-32700, <<"Parse error.">>, null). 113 | 114 | %% helpers 115 | 116 | -spec make_result_response(json(), id() | undefined) -> response(). 117 | make_result_response(_Result, undefined) -> 118 | noreply; 119 | make_result_response(Result, Id) -> 120 | {reply, {[{<<"jsonrpc">>, <<"2.0">>}, 121 | {<<"result">>, Result}, 122 | {<<"id">>, Id}]}}. 123 | 124 | -spec make_standard_error_response(errortype(), id() | undefined) -> response(). 125 | make_standard_error_response(ErrorType, Id) -> 126 | {Code, Msg} = error_code_and_message(ErrorType), 127 | make_error_response(Code, Msg, Id). 128 | 129 | -spec make_standard_error_response(errortype(), json(), id() | undefined) -> response(). 130 | make_standard_error_response(ErrorType, Data, Id) -> 131 | {Code, Msg} = error_code_and_message(ErrorType), 132 | make_error_response(Code, Msg, Data, Id). 133 | 134 | %% @doc Custom error, with data 135 | -spec make_error_response(integer(), binary(), json(), id() | undefined) -> response(). 136 | make_error_response(_Code, _Message, _Data, undefined) -> 137 | noreply; 138 | make_error_response(Code, Message, Data, Id) -> 139 | {reply, make_error(Code, Message, Data, Id)}. 140 | 141 | %% @doc Custom error, without data 142 | -spec make_error_response(integer(), binary(), id() | undefined) -> response(). 143 | make_error_response(_Code, _Message, undefined) -> 144 | noreply; 145 | make_error_response(Code, Message, Id) -> 146 | {reply, make_error(Code, Message, Id)}. 147 | 148 | %% @doc Make json-rpc error response, with data 149 | -spec make_error(integer(), binary(), json(), id()) -> json(). 150 | make_error(Code, Msg, Data, Id) -> 151 | {[{<<"jsonrpc">>, <<"2.0">>}, 152 | {<<"error">>, {[{<<"code">>, Code}, 153 | {<<"message">>, Msg}, 154 | {<<"data">>, Data}]}}, 155 | {<<"id">>, Id}]}. 156 | 157 | %% @doc Make json-rpc error response, without data 158 | -spec make_error(integer(), binary(), id()) -> json(). 159 | make_error(Code, Msg, Id) -> 160 | {[{<<"jsonrpc">>, <<"2.0">>}, 161 | {<<"error">>, {[{<<"code">>, Code}, 162 | {<<"message">>, Msg}]}}, 163 | {<<"id">>, Id}]}. 164 | 165 | %% @doc Parses the RPC part of an already JSON decoded request. Returns a tuple 166 | %% {Method, Params, Id} for a single request, 'invalid_request' for an invalid 167 | %% request and a list of these for a batch request. An Id value of 'undefined' 168 | %% is used when the id is not present in the request. 169 | -spec parse(json()) -> request() | [request()]. 170 | parse(Reqs) when is_list(Reqs) -> 171 | [parse(Req) || Req <- Reqs]; 172 | parse({Req}) -> 173 | Version = proplists:get_value(<<"jsonrpc">>, Req), 174 | Method = proplists:get_value(<<"method">>, Req), 175 | Params = proplists:get_value(<<"params">>, Req, []), 176 | Id = proplists:get_value(<<"id">>, Req, undefined), 177 | case Version =:= <<"2.0">> 178 | andalso is_binary(Method) 179 | andalso (is_list(Params) orelse is_tuple(Params)) 180 | andalso (Id =:= undefined orelse Id =:= null 181 | orelse is_binary(Id) 182 | orelse is_number(Id)) of 183 | true -> 184 | {Method, Params, Id}; 185 | false -> 186 | invalid_request 187 | end; 188 | parse(_) -> 189 | invalid_request. 190 | 191 | %% @doc Calls the handler function, catches errors and composes a json-rpc response. 192 | -spec dispatch(request(), handlerfun()) -> response(). 193 | dispatch({Method, Params, Id}, HandlerFun) -> 194 | try HandlerFun(Method, Params) of 195 | Response -> make_result_response(Response, Id) 196 | catch 197 | throw:E when E == method_not_found; E == invalid_params; 198 | E == internal_error; E == server_error -> 199 | make_standard_error_response(E, Id); 200 | throw:{E, Data} when E == method_not_found; E == invalid_params; 201 | E == internal_error; E == server_error -> 202 | make_standard_error_response(E, Data, Id); 203 | throw:{jsonrpc2, Code, Message} when is_integer(Code), is_binary(Message) -> 204 | %% Custom error, without data 205 | %% -32000 to -32099 Server error Reserved for implementation-defined server-errors. 206 | %% The remainder of the space is available for application defined errors. 207 | make_error_response(Code, Message, Id); 208 | throw:{jsonrpc2, Code, Message, Data} when is_integer(Code), is_binary(Message) -> 209 | %% Custom error, with data 210 | make_error_response(Code, Message, Data, Id); 211 | Class:Error -> 212 | error_logger:error_msg( 213 | "Error in JSON-RPC handler for method ~s with params ~p (id: ~p): ~p:~p from ~p", 214 | [Method, Params, Id, Class, Error, erlang:get_stacktrace()]), 215 | make_standard_error_response(internal_error, Id) 216 | end; 217 | dispatch(_, _HandlerFun) -> 218 | make_standard_error_response(invalid_request, null). 219 | 220 | %% @doc Returns JSON-RPC error code and error message 221 | error_code_and_message(invalid_request) -> {-32600, <<"Invalid Request.">>}; 222 | error_code_and_message(method_not_found) -> {-32601, <<"Method not found.">>}; 223 | error_code_and_message(invalid_params) -> {-32602, <<"Invalid params.">>}; 224 | error_code_and_message(internal_error) -> {-32603, <<"Internal error.">>}; 225 | error_code_and_message(server_error) -> {-32000, <<"Server error.">>}. 226 | 227 | %% @doc Transforms a list of responses into a single response. 228 | -spec merge_responses([response()]) -> response(). 229 | merge_responses(Responses) when is_list(Responses) -> 230 | case [Reply || {reply, Reply} <- Responses] of 231 | [] -> noreply; 232 | Replies -> {reply, Replies} 233 | end. 234 | 235 | 236 | -ifdef(TEST). 237 | -include_lib("eunit/include/eunit.hrl"). 238 | 239 | %% Testing the examples from http://www.jsonrpc.org/specification 240 | 241 | test_handler(<<"subtract">>, [42,23]) -> 19; 242 | test_handler(<<"subtract">>, [23,42]) -> -19; 243 | test_handler(<<"subtract">>, {[{<<"subtrahend">>,23},{<<"minuend">>,42}]}) -> 19; 244 | test_handler(<<"subtract">>, {[{<<"minuend">>,42},{<<"subtrahend">>,23}]}) -> 19; 245 | test_handler(<<"update">>, [1,2,3,4,5]) -> ok; 246 | test_handler(<<"sum">>, [1,2,4]) -> 7; 247 | test_handler(<<"get_data">>, []) -> [<<"hello">>,5]; 248 | test_handler(_, _) -> throw(method_not_found). 249 | 250 | %% rpc call with positional parameters 251 | call_test() -> 252 | Req = {[{<<"jsonrpc">>,<<"2.0">>}, 253 | {<<"method">>,<<"subtract">>}, 254 | {<<"params">>,[42,23]}, 255 | {<<"id">>,1}]}, 256 | Reply = {[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,19},{<<"id">>,1}]}, 257 | {reply, Reply} = handle(Req, fun test_handler/2). 258 | 259 | %% rpc call with positional parameters, reverse order 260 | call2_test() -> 261 | Req = {[{<<"jsonrpc">>,<<"2.0">>}, 262 | {<<"method">>,<<"subtract">>}, 263 | {<<"params">>,[23,42]}, 264 | {<<"id">>,2}]}, 265 | Reply = {[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,-19},{<<"id">>,2}]}, 266 | {reply, Reply} = handle(Req, fun test_handler/2). 267 | 268 | %% rpc call with named parameters 269 | named_test() -> 270 | Req = {[{<<"jsonrpc">>,<<"2.0">>}, 271 | {<<"method">>,<<"subtract">>}, 272 | {<<"params">>,{[{<<"subtrahend">>,23},{<<"minuend">>,42}]}}, 273 | {<<"id">>,3}]}, 274 | Reply = {[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,19},{<<"id">>,3}]}, 275 | {reply, Reply} = handle(Req, fun test_handler/2). 276 | 277 | %% rpc call with named parameters, reverse order 278 | named2_test() -> 279 | Req = {[{<<"jsonrpc">>,<<"2.0">>}, 280 | {<<"method">>,<<"subtract">>}, 281 | {<<"params">>,{[{<<"minuend">>,42},{<<"subtrahend">>,23}]}}, 282 | {<<"id">>,4}]}, 283 | Reply = {[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,19},{<<"id">>,4}]}, 284 | {reply, Reply} = handle(Req, fun test_handler/2). 285 | 286 | %% a Notification 287 | notif_test() -> 288 | Req = {[{<<"jsonrpc">>,<<"2.0">>}, 289 | {<<"method">>,<<"update">>}, 290 | {<<"params">>,[1,2,3,4,5]}]}, 291 | noreply = handle(Req, fun test_handler/2). 292 | 293 | %% a Notification + non-existent method 294 | notif2_test() -> 295 | Req = {[{<<"jsonrpc">>,<<"2.0">>},{<<"method">>,<<"foobar">>}]}, 296 | noreply = handle(Req, fun test_handler/2). 297 | 298 | %% rpc call of non-existent method 299 | bad_method_test() -> 300 | Req = {[{<<"jsonrpc">>,<<"2.0">>},{<<"method">>,<<"foobar">>},{<<"id">>,<<"1">>}]}, 301 | Reply = {[{<<"jsonrpc">>,<<"2.0">>}, 302 | {<<"error">>, 303 | {[{<<"code">>,-32601},{<<"message">>,<<"Method not found.">>}]}}, 304 | {<<"id">>,<<"1">>}]}, 305 | {reply, Reply} = handle(Req, fun test_handler/2). 306 | 307 | %% rpc call with invalid JSON 308 | %% Not applicable, since JSON parsing is not done in this module. We test the error 309 | %% response though. 310 | bad_json_test() -> 311 | Expected = {[{<<"jsonrpc">>,<<"2.0">>}, 312 | {<<"error">>,{[{<<"code">>,-32700},{<<"message">>,<<"Parse error.">>}]}}, 313 | {<<"id">>,null}]}, 314 | Reply = parseerror(), 315 | Reply = Expected. 316 | 317 | %% rpc call with invalid Request object 318 | bad_rpc_test() -> 319 | Req = {[{<<"jsonrpc">>,<<"2.0">>},{<<"method">>,1},{<<"params">>,<<"bar">>}]}, 320 | Reply = {[{<<"jsonrpc">>,<<"2.0">>}, 321 | {<<"error">>,{[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 322 | {<<"id">>,null}]}, 323 | {reply, Reply} = handle(Req, fun test_handler/2). 324 | 325 | %% rpc call Batch, invalid JSON: 326 | %% Not applicable, see bad_json_test/0 above. 327 | bad_json_batch_test() -> 328 | ok. 329 | 330 | %% rpc call with an empty Array 331 | empty_batch_test() -> 332 | Req = [], 333 | Reply = {[{<<"jsonrpc">>,<<"2.0">>}, 334 | {<<"error">>,{[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 335 | {<<"id">>,null}]}, 336 | {reply, Reply} = handle(Req, fun test_handler/2). 337 | 338 | %% rpc call with an invalid Batch (but not empty) 339 | invalid_batch_test() -> 340 | Req = [1], 341 | Reply = [{[{<<"jsonrpc">>,<<"2.0">>}, 342 | {<<"error">>, 343 | {[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 344 | {<<"id">>,null}]}], 345 | {reply, Reply} = handle(Req, fun test_handler/2). 346 | 347 | %% rpc call with invalid Batch 348 | invalid_batch2_test() -> 349 | Req = [1,2,3], 350 | Reply = [{[{<<"jsonrpc">>,<<"2.0">>}, 351 | {<<"error">>, 352 | {[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 353 | {<<"id">>,null}]}, 354 | {[{<<"jsonrpc">>,<<"2.0">>}, 355 | {<<"error">>, 356 | {[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 357 | {<<"id">>,null}]}, 358 | {[{<<"jsonrpc">>,<<"2.0">>}, 359 | {<<"error">>, 360 | {[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 361 | {<<"id">>,null}]}], 362 | {reply, Reply} = handle(Req, fun test_handler/2). 363 | 364 | %% rpc call Batch 365 | batch_test() -> 366 | Req = [{[{<<"jsonrpc">>,<<"2.0">>}, 367 | {<<"method">>,<<"sum">>}, 368 | {<<"params">>,[1,2,4]}, 369 | {<<"id">>,<<"1">>}]}, 370 | {[{<<"jsonrpc">>,<<"2.0">>}, 371 | {<<"method">>,<<"notify_hello">>}, 372 | {<<"params">>,[7]}]}, 373 | {[{<<"jsonrpc">>,<<"2.0">>}, 374 | {<<"method">>,<<"subtract">>}, 375 | {<<"params">>,[42,23]}, 376 | {<<"id">>,<<"2">>}]}, 377 | {[{<<"foo">>,<<"boo">>}]}, 378 | {[{<<"jsonrpc">>,<<"2.0">>}, 379 | {<<"method">>,<<"foo.get">>}, 380 | {<<"params">>,{[{<<"name">>,<<"myself">>}]}}, 381 | {<<"id">>,<<"5">>}]}, 382 | {[{<<"jsonrpc">>,<<"2.0">>}, 383 | {<<"method">>,<<"get_data">>}, 384 | {<<"id">>,<<"9">>}]}], 385 | Reply = [{[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,7},{<<"id">>,<<"1">>}]}, 386 | {[{<<"jsonrpc">>,<<"2.0">>},{<<"result">>,19},{<<"id">>,<<"2">>}]}, 387 | {[{<<"jsonrpc">>,<<"2.0">>}, 388 | {<<"error">>, 389 | {[{<<"code">>,-32600},{<<"message">>,<<"Invalid Request.">>}]}}, 390 | {<<"id">>,null}]}, 391 | {[{<<"jsonrpc">>,<<"2.0">>}, 392 | {<<"error">>, 393 | {[{<<"code">>,-32601},{<<"message">>,<<"Method not found.">>}]}}, 394 | {<<"id">>,<<"5">>}]}, 395 | {[{<<"jsonrpc">>,<<"2.0">>}, 396 | {<<"result">>,[<<"hello">>,5]}, 397 | {<<"id">>,<<"9">>}]}], 398 | {reply, Reply} = handle(Req, fun test_handler/2). 399 | 400 | %% rpc call Batch (all notifications) 401 | batch_notif_test() -> 402 | Req = [{[{<<"jsonrpc">>,<<"2.0">>}, 403 | {<<"method">>,<<"notify_sum">>}, 404 | {<<"params">>,[1,2,4]}]}, 405 | {[{<<"jsonrpc">>,<<"2.0">>}, 406 | {<<"method">>,<<"notify_hello">>}, 407 | {<<"params">>,[7]}]}], 408 | noreply = handle(Req, fun test_handler/2). 409 | 410 | -define(ENCODED_REQUEST, <<"{\"jsonrpc\":\"2.0\"," 411 | "\"method\":\"subtract\"," 412 | "\"params\":[42,23]," 413 | "\"id\":1}">>). 414 | -define(DECODED_REQUEST, {[{<<"jsonrpc">>,<<"2.0">>}, 415 | {<<"method">>,<<"subtract">>}, 416 | {<<"params">>,[42,23]}, 417 | {<<"id">>,1}]}). 418 | -define(ENCODED_RESPONSE, <<"{\"jsonrpc\":\"2.0\"," 419 | "\"result\":19," 420 | "\"id\":1}">>). 421 | -define(DECODED_RESPONSE, {[{<<"jsonrpc">>,<<"2.0">>}, 422 | {<<"result">>,19}, 423 | {<<"id">>,1}]}). 424 | -define(ENCODED_PARSE_ERROR, <<"{\"jsonrpc\":\"2.0\"," 425 | "\"error\":{\"code\":-32700," 426 | "\"message\":\"Parse error.\"}," 427 | "\"id\":null}">>). 428 | -define(DECODED_PARSE_ERROR, {[{<<"jsonrpc">>,<<"2.0">>}, 429 | {<<"error">>, 430 | {[{<<"code">>,-32700}, 431 | {<<"message">>,<<"Parse error.">>}]}}, 432 | {<<"id">>,null}]}). 433 | 434 | %% define json encode and decode only for the cases we need in the tests 435 | json_decode(?ENCODED_REQUEST) -> ?DECODED_REQUEST. 436 | json_encode(?DECODED_RESPONSE) -> ?ENCODED_RESPONSE; 437 | json_encode(?DECODED_PARSE_ERROR) -> ?ENCODED_PARSE_ERROR. 438 | 439 | %% test handle/4 with encode and decode callbacks 440 | json_callbacks_test() -> 441 | Req = ?ENCODED_REQUEST, 442 | Reply = ?ENCODED_RESPONSE, 443 | {reply, Reply} = handle(Req, fun test_handler/2, fun json_decode/1, 444 | fun json_encode/1). 445 | 446 | parse_error_test() -> 447 | Error = ?ENCODED_PARSE_ERROR, 448 | {reply, Error} = handle(<<"dummy">>, fun test_handler/2, fun json_decode/1, 449 | fun json_encode/1). 450 | 451 | -endif. 452 | -------------------------------------------------------------------------------- /src/jsonrpc2_client.erl: -------------------------------------------------------------------------------- 1 | %% Copyright 2013-2014 Viktor Söderqvist 2 | %% 3 | %% Licensed under the Apache License, Version 2.0 (the "License"); 4 | %% you may not use this file except in compliance with the License. 5 | %% You may obtain a copy of the License at 6 | %% 7 | %% http://www.apache.org/licenses/LICENSE-2.0 8 | %% 9 | %% Unless required by applicable law or agreed to in writing, software 10 | %% distributed under the License is distributed on an "AS IS" BASIS, 11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | %% See the License for the specific language governing permissions and 13 | %% limitations under the License. 14 | 15 | %% @doc JSON-RPC 2.0 client 16 | -module(jsonrpc2_client). 17 | 18 | -export_type([request/0, response/0]). 19 | -export([create_request/1, parse_response/1, batch_call/5]). 20 | 21 | -type call_req() :: {jsonrpc2:method(), jsonrpc2:params(), jsonrpc2:id()}. 22 | -type notification_req() :: {jsonrpc2:method(), jsonrpc2:params()}. 23 | -type batch_req() :: [call_req() | notification_req()]. 24 | -type request() :: call_req() | notification_req() | batch_req(). 25 | 26 | -type response() :: {ok, jsonrpc2:json()} | {error, jsonrpc2:error()}. 27 | 28 | -type transportfun() :: fun ((binary()) -> binary()). 29 | -type json_encode() :: fun ((jsonrpc2:json()) -> binary()). 30 | -type json_decode() :: fun ((binary()) -> jsonrpc2:json()). 31 | 32 | %% @doc Creates a call, notification or batch request, depending on the parameter. 33 | -spec create_request(request()) -> jsonrpc2:json(). 34 | create_request({Method, Params}) -> 35 | {[{<<"jsonrpc">>, <<"2.0">>}, 36 | {<<"method">>, Method}, 37 | {<<"params">>, Params}]}; 38 | create_request({Method, Params, Id}) -> 39 | {[{<<"jsonrpc">>, <<"2.0">>}, 40 | {<<"method">>, Method}, 41 | {<<"params">>, Params}, 42 | {<<"id">>, Id}]}; 43 | create_request(Reqs) when is_list(Reqs) -> 44 | lists:map(fun create_request/1, Reqs). 45 | 46 | %% @doc Parses a structured response (already json-decoded) and returns a list of pairs, with id 47 | %% and a tuple {ok, Reply} or {error, Error}. 48 | %% TODO: Define the structure of Error. 49 | -spec parse_response(jsonrpc2:json()) -> [{jsonrpc2:id(), response()}]. 50 | parse_response({_} = Response) -> 51 | [parse_single_response(Response)]; 52 | parse_response(BatchResponse) when is_list(BatchResponse) -> 53 | lists:map(fun parse_single_response/1, BatchResponse). 54 | 55 | %% @doc Calls multiple methods as a batch call and returns the results in the same order. 56 | %% TODO: Sort out what this function returns in the different error cases. 57 | -spec batch_call([{jsonrpc2:method(), jsonrpc2:params()}], transportfun(), 58 | json_decode(), json_encode(), FirstId :: integer()) -> 59 | [response()]. 60 | batch_call(MethodsAndParams, TransportFun, JsonDecode, JsonEncode, FirstId) -> 61 | MethodParamsIds = enumerate_call_tuples(MethodsAndParams, FirstId), 62 | JsonReq = create_request(MethodParamsIds), 63 | BinReq = JsonEncode(JsonReq), 64 | try 65 | %% The transport fun can fail gracefully by throwing {transport_error, binary()} 66 | BinResp = try TransportFun(BinReq) 67 | catch throw:{transport_error, TransportError} when is_binary(TransportError) -> 68 | throw({jsonrpc2_client, TransportError}) 69 | end, 70 | 71 | %% JsonDecode can fail (any kind of error) 72 | JsonResp = try JsonDecode(BinResp) 73 | catch _:_ -> throw({jsonrpc2_client, invalid_json}) 74 | end, 75 | 76 | %% parse_response can fail by throwing invalid_jsonrpc_response 77 | RepliesById = try parse_response(JsonResp) 78 | catch throw:invalid_jsonrpc_response -> 79 | throw({jsonrpc2_client, invalid_jsonrpc_response}) 80 | end, 81 | 82 | %% Decompose the replies into a list in the same order as MethodsAndParams. 83 | LastId = FirstId + length(MethodsAndParams) - 1, 84 | denumerate_replies(RepliesById, FirstId, LastId) 85 | 86 | catch throw:{jsonrpc2_client, ErrorData} -> 87 | %% Failure in transport function. Repeat the error data for each request to 88 | %% simulate a batch response. 89 | lists:duplicate(length(MethodsAndParams), {error, {server_error, ErrorData}}) 90 | end. 91 | 92 | %%---------- 93 | %% Internal 94 | %%---------- 95 | 96 | %% @doc Helper for parse_response/1. Returns a single pair {Id, Response}. 97 | -spec parse_single_response(jsonrpc2:json()) -> {jsonrpc2:id(), response()}. 98 | parse_single_response({Response}) -> 99 | <<"2.0">> == proplists:get_value(<<"jsonrpc">>, Response) 100 | orelse throw(invalid_jsonrpc_response), 101 | Id = proplists:get_value(<<"id">>, Response), 102 | is_number(Id) orelse Id == null 103 | orelse throw(invalid_jsonrpc_response), 104 | Result = proplists:get_value(<<"result">>, Response, undefined), 105 | Error = proplists:get_value(<<"error">>, Response, undefined), 106 | Reply = case {Result, Error} of 107 | {undefined, undefined} -> 108 | {error, {server_error, <<"Invalid JSON-RPC 2.0 response">>}}; 109 | {_, undefined} -> 110 | {ok, Result}; 111 | {undefined, {ErrorProplist}} -> 112 | Code = proplists:get_value(<<"code">>, ErrorProplist, -32000), 113 | Message = proplists:get_value(<<"message">>, ErrorProplist, <<"Unknown error">>), 114 | ErrorTuple = case proplists:get_value(<<"data">>, ErrorProplist) of 115 | undefined -> 116 | {jsonrpc2, Code, Message}; 117 | Data -> 118 | {jsonrpc2, Code, Message, Data} 119 | end, 120 | {error, ErrorTuple}; 121 | _ -> 122 | %% both error and result 123 | {error, {server_error, <<"Invalid JSON-RPC 2.0 response">>}} 124 | end, 125 | {Id, Reply}. 126 | 127 | %% @doc Gives each method-params pair a number. Returns a list of triples: method-params-id. 128 | enumerate_call_tuples(MethodParamsPairs, FirstId) -> 129 | enumerate_call_tuples(MethodParamsPairs, FirstId, []). 130 | 131 | %% @doc Helper for enumerate_call_tuples/2. 132 | enumerate_call_tuples([{Method, Params} | MPs], NextId, Acc) -> 133 | Triple = {Method, Params, NextId}, 134 | enumerate_call_tuples(MPs, NextId + 1, [Triple | Acc]); 135 | enumerate_call_tuples([], _, Acc) -> 136 | lists:reverse(Acc). 137 | 138 | %% @doc Finds each pair {Id, Reply} for each Id in the range FirstId..LastId in the proplist 139 | %% Replies. Removes the id and returns the only the replies in the correct order. 140 | denumerate_replies(Replies, FirstId, LastId) -> 141 | denumerate_replies(dict:from_list(Replies), FirstId, LastId, []). 142 | 143 | %% @doc Helper for denumerate_replies/3. 144 | denumerate_replies(ReplyDict, FirstId, LastId, Acc) when FirstId =< LastId -> 145 | Reply = dict:fetch(FirstId, ReplyDict), 146 | Acc1 = [Reply | Acc], 147 | denumerate_replies(ReplyDict, FirstId + 1, LastId, Acc1); 148 | denumerate_replies(_, _, _, Acc) -> 149 | lists:reverse(Acc). 150 | 151 | %%------------ 152 | %% Unit tests 153 | %%------------ 154 | 155 | -ifdef(TEST). 156 | -include_lib("eunit/include/eunit.hrl"). 157 | 158 | enumerate_call_tuples_test() -> 159 | Input = [{x, foo}, {y, bar}, {z, baz}], 160 | FirstId = 3, 161 | Expect = [{x, foo, 3}, {y, bar, 4}, {z, baz, 5}], 162 | Expect = enumerate_call_tuples(Input, FirstId). 163 | 164 | denumerate_replies_test() -> 165 | Input = [{3, foo}, {5, baz}, {4, bar}], 166 | FirstId = 3, 167 | LastId = 5 = FirstId + length(Input) - 1, 168 | Expect = [foo, bar, baz], 169 | Expect = denumerate_replies(Input, FirstId, LastId). 170 | 171 | transport_error_test() -> 172 | TransportFun = fun (_) -> throw({transport_error, <<"404 or whatever">>}) end, 173 | JsonEncode = fun (_) -> <<"foo">> end, 174 | JsonDecode = fun (_) -> [] end, 175 | MethodsAndParams = [{<<"foo">>, []}], 176 | Expect = [{error, {server_error, <<"404 or whatever">>}}], 177 | ?assertEqual(Expect, batch_call(MethodsAndParams, TransportFun, JsonDecode, JsonEncode, 1)). 178 | 179 | transport_return_invalid_json_test() -> 180 | TransportFun = fun (_) -> <<"some non-JSON junk">> end, 181 | JsonEncode = fun (_) -> <<"{\"foo\":\"bar\"}">> end, 182 | JsonDecode = fun (_) -> throw(invalid_json) end, 183 | MethodsAndParams = [{<<"foo">>, []}], 184 | Expect = [{error, {server_error, invalid_json}}], 185 | ?assertEqual(Expect, batch_call(MethodsAndParams, TransportFun, JsonDecode, JsonEncode, 1)). 186 | 187 | -endif. 188 | --------------------------------------------------------------------------------