1 /** JSON-RPC 2.0 protocol library.
2 
3     The JSON-RPC 2.0 specification may be found at
4     $(LINK http://www.jsonrpc.org/specification)
5 
6     Example:
7     --------------------------------------------
8     enum hostname = "127.0.0.1";
9     enum ushort port = 54321;
10 
11     interface API {
12         long add(int a, int b);
13     }
14 
15     class ServeFunctions {
16         long add(int a, int b) { return a + b; }
17     }
18 
19     void main(string[] args)
20     {
21         import core.thread : Thread;
22         import core.time : dur;
23 
24         auto t = new Thread({
25             auto rpc = new RPCServer!ServeFunctions(hostname, port);
26             rpc.listen();
27         });
28         t.isDaemon = true;
29         t.start();
30         Thread.sleep(dur!"seconds"(2));
31 
32         auto client = new RPCClient!API(hostname, port);
33         assert(client.add(2, 2) == 4);
34         assert(client.add(5, 6) == 11);
35     }
36     --------------------------------------------
37 
38     Authors:
39         Ryan Frame
40 
41     Copyright:
42         Copyright 2018 Ryan Frame
43 
44     License:
45         MIT
46  */
47 module jsonrpc.jsonrpc;
48 
49 import std.json;
50 import std.socket;
51 import std.traits : isAggregateType;
52 
53 import jsonrpc.transport;
54 import jsonrpc.exception;
55 
56 static if (__VERSION__ < 2079) {
57     // 2.079 was the first to allow traits(isDisabled, ...).
58     static assert(0, "The minimum supported version of the JSON-RPC library is the DMD 2.079.0 frontend.");
59 }
60 
61 version(Have_tested) import tested : test = name;
62 else private struct test { string name; }
63 
64 /** An RPC request constructed by the client to send to the RPC server.
65 
66     The Request contains the ID of the request, the method to call, and any
67     parameters to pass to the method. You should not need to manually create a
68     Request object; the RPCClient will do this for you.
69 */
70 struct Request {
71     import std.traits;
72     import std.typecons : Flag, Yes, No;
73 
74     /** Construct a Request with the specified remote method name and
75         arguments.
76 
77         The ID must be a string, a number, or null; null is not recommended for
78         use as an ID.
79 
80         Template_Parameters:
81             T = The type of the request ID.
82 
83         Params:
84             id =        The ID of the request.
85             method =    The name of the remote method to call.
86             params =    A JSONValue containing the method arguments as a JSON
87                         Object or array.
88     */
89     this(T = long)(T id, string method, JSONValue params = JSONValue())
90             if (isNumeric!T || isSomeString!T || is(T : typeof(null)))
91     in {
92         assert(method.length > 0);
93     } body {
94         this._data["jsonrpc"] = "2.0";
95         this._data["id"] = id;
96         this._data["method"] = method;
97         this.params = params;
98     }
99 
100     /** Construct a Request with the specified remote method name and
101         arguments.
102 
103         The value of the ID must be a string, a number, or null; null is not
104         recommended for use as an ID.
105 
106         Params:
107             id =        A JSONValue containing a scalar ID of the request.
108             method =    The name of the remote method to call.
109             params =    A JSONValue containing the method arguments as a JSON
110                         Object or array.
111     */
112     this(JSONValue id, string method, JSONValue params) in {
113         assert(method.length > 0);
114         assert(id.type != JSONType.object);
115         assert(id.type != JSONType.array);
116     } body {
117         _data["id"] = id;
118         _data["method"] = method;
119         _data["jsonrpc"] = "2.0";
120         this.params = params;
121     }
122 
123     /** Construct a notification with the specified remote method name and
124         arguments.
125 
126         A notification will receive no response from the server.
127 
128         Params:
129             method =    The name of the remote method to call.
130             params =    A JSON string containing the method arguments as a JSON
131                         Object or array.
132     */
133     this(string method, JSONValue params = JSONValue()) in {
134         assert(method.length > 0);
135     } body {
136         this._isNotification = true;
137         this._data["jsonrpc"] = "2.0";
138         this._data["method"] = method;
139         this.params = params;
140     }
141 
142     /** Get the ID of this request.
143 
144         If the ID is not of type long, it needs to be specified; if uncertain of
145         the underlying type, use idType to query for it.
146 
147         There is no ID for notifications.
148 
149         Template_Parameters:
150             T = The type of the ID; the default type is `long`, but JSON-RPC
151                 allows IDs to also be string or typeof(null).
152 
153         See_Also:
154             idType
155 
156         Throws:
157             IncorrectType if the underlying type of the ID is not the requested
158             type.
159 
160             IncorrectType if this request is a notification.
161     */
162     @property auto id(T = long)() {
163         if (this.isNotification) {
164             raise!(IncorrectType)("There is no ID in a notification.");
165         }
166         scope(failure) {
167             raise!(IncorrectType)("The ID is not of the specified type.");
168         }
169         return unwrapValue!T(_data["id"]);
170     }
171 
172     @test("Request string id can be created and read.")
173     unittest {
174         import std.exception : assertThrown;
175 
176         auto req = Request("my_id", "func", JSONValue(["params"]));
177         assert(req.id!string == "my_id");
178         assertThrown!IncorrectType(req.id!int);
179     }
180 
181     @test("Request null id can be created and read.")
182     unittest {
183         auto req = Request(null, "func", JSONValue(["params"]));
184         assert(req.id!(typeof(null)) is null);
185     }
186 
187     /** Get the type of the underlying ID. There is no type for notifications.
188 
189         See_Also:
190             id
191 
192         Throws:
193             IncorrectType if this request is a notification.
194     */
195     @property JSONType idType() {
196         if (this.isNotification) {
197             raise!(IncorrectType)("There is no ID in a notification.");
198         }
199         return _data["id"].type;
200     }
201 
202     /** Return true if this request is a notification; otherwise, return false.
203     */
204     @property bool isNotification() { return _isNotification; }
205 
206     package:
207 
208     /** Parse a JSON string into a Request object.
209 
210         Params:
211             str =   The JSON string to parse.
212 
213         Notes:
214             The ID, if present, must be integral; this is non-conformant to
215             the JSON-RPC specification.
216 
217         Throws:
218             InvalidDataReceived if the ID or method fields are missing.
219 
220             JSONException if the ID or method fields are an incorrect type. The
221             ID must be integral and the method must be a string.
222     */
223     static Request fromJSONString(const char[] str) {
224         auto json = str.parseJSON;
225         enforce!(InvalidDataReceived, str)
226                 (json.type != JSONType.null_
227                      && "method" in json
228                      && "jsonrpc" in json && json["jsonrpc"].str == "2.0",
229                 "Request is missing 'jsonrpc', 'id', and/or 'method' fields.");
230 
231         if ("params" !in json) json["params"] = JSONValue();
232         if ("id" in json) {
233             return Request(
234                     json["id"],
235                     json["method"].str,
236                     json["params"]);
237         } else {
238             return Request(json["method"].str, json["params"]);
239         }
240     }
241 
242     @test("[DOCTEST]: Request.fromJSONString")
243     ///
244     unittest {
245         auto req = Request.fromJSONString(
246                 `{"jsonrpc":"2.0","id":14,"method":"func","params":[0,1]}`);
247 
248         assert(req.id == 14);
249         assert(req.method == "func");
250         assert(req.params.array == [JSONValue(0), JSONValue(1)]);
251     }
252 
253     @test("Request.fromJSONString converts JSON to Request")
254     unittest {
255         auto req = Request.fromJSONString(
256                 `{"jsonrpc": "2.0", "id": 0, "method": "func", "params": [0, 1]}`);
257         assert(req.id == 0, "Incorrect ID.");
258         assert(req.method == "func", "Incorrect method.");
259         assert(req.params.array == [JSONValue(0), JSONValue(1)],
260                 "Incorrect params.");
261     }
262 
263     @test("Request.fromJSONString creates notifications")
264     unittest {
265         auto req = Request.fromJSONString(
266                 `{"jsonrpc": "2.0", "method": "func", "params": [0, 1]}`);
267 
268         assert(req.method == "func", "Incorrect method.");
269         assert(req.params.array == [JSONValue(0), JSONValue(1)],
270                 "Incorrect params.");
271     }
272 
273     /** Construct a Request with the specified remote method name and
274         arguments.
275 
276         Template_Parameters:
277             T = The type of the request ID.
278 
279         Params:
280             id =        The ID number of this request.
281             method =    The name of the remote method to call.
282             params =    A JSON string containing the method arguments as a JSON
283                         Object or array.
284 
285         Throws:
286             std.json.JSONException if the json string cannot be parsed.
287     */
288     this(T)(T id, string method, string params) in {
289         assert(method.length > 0);
290     } body {
291         this(id, method, params.parseJSON);
292     }
293 
294     /** Convert the Request to a JSON string to pass to the server.
295 
296         Params:
297             prettyPrint = Yes to pretty-print the JSON output; No for efficient
298                           output.
299     */
300     string toJSONString(Flag!"prettyPrint" prettyPrint = No.prettyPrint) {
301         return _data.toJSON(prettyPrint);
302     }
303 
304     private:
305 
306     /** Retrieve the method to execute on the RPC server. */
307     @property string method() { return _data["method"].str; }
308 
309     /** Retrieve the parameters that will be passed to the method. */
310     @property JSONValue params() { return _data["params"]; }
311 
312     /** Set the parameters to the remote method that will be called.
313 
314         Params:
315             val =   A JSON Object or array. Other value types will be wrapped
316                     in an array (e.g., 3 becomes [3]).
317     */
318     @property void params(JSONValue val)
319     {
320         if (val.type != JSONType.object
321                 && val.type != JSONType.array
322                 && val.type != JSONType.null_) {
323 
324             _data["params"] = JSONValue([val]);
325         } else _data["params"] = val;
326     }
327 
328     JSONValue _data;
329     bool _isNotification = false;
330 }
331 
332 /** The RPC server's response sent to clients.
333 
334     Example:
335     ---
336     auto response = rpc.call("func", [1, 2, 3]);
337 
338     if (response.hasError()) writeln(response.error);
339     else writeln(response.result);
340     ---
341 */
342 struct Response {
343     /** Get the id of the Response.
344 
345         If the id is not of type long, it needs to be specified; if uncertain of
346         the underlying type, use idType to query for it.
347 
348         Throws:
349             IncorrectType if the underlying type of the ID is not the requested
350             type.
351 
352         See_Also:
353             idType
354     */
355     @property T id(T = long)() {
356         scope(failure) {
357             raise!(IncorrectType)("The ID is not of the specified type.");
358         }
359         return unwrapValue!T(_data["id"]);
360     }
361 
362     @test("Response string id can be created and read.")
363     unittest {
364         import std.exception : assertThrown;
365 
366         auto resp = Response("my_id", JSONValue(["result"]));
367         assert(resp.id!string == "my_id");
368         assertThrown!IncorrectType(resp.id!int);
369     }
370 
371     @test("Response null id can be created and read.")
372     unittest {
373         auto resp = Response(null, JSONValue(["result"]));
374         assert(resp.id!(typeof(null)) == null);
375     }
376 
377     @property JSONType idType() { return _data["id"].type; }
378 
379     // TODO: These are mutually exclusive; I shouldn't have properties for both
380     // as valid accessors.
381     @property JSONValue result() { return _data["result"]; }
382     @property JSONValue error() { return _data["error"]; }
383 
384     @property bool hasError() {
385         return "error" in _data && !(_data["error"].isNull);
386     }
387 
388     /** Convert the Response to a JSON string to send to the client.
389 
390         Params:
391             prettyPrint = Yes to pretty-print the JSON output; No for efficient
392                           output.
393     */
394     string toJSONString(Flag!"prettyPrint" prettyPrint = No.prettyPrint) {
395         return _data.toJSON(prettyPrint);
396     }
397 
398     package:
399 
400     /** Construct a response to send to the client.
401 
402         The id must be the same as the Request to which the server is
403         responding, and can be numeric, string, or null.
404 
405         Template_Parameters:
406             T = The type of the response ID.
407 
408         Params:
409             id =        The ID of this response. This matches the relevant
410                         request.
411             result =    The return value(s) of the method executed.
412     */
413     this(T)(T id, JSONValue result) {
414         _data["jsonrpc"] = "2.0";
415         _data["id"] = id;
416         _data["result"] = result;
417     }
418 
419     /** Construct a predefined error response to send to the client.
420 
421         Template_Parameters:
422             T = The type of the response ID.
423 
424         Params:
425             id =      The ID of this response. This matches the relevant request.
426             error =   The code of the standard error to send.
427             errData = Any additional data to add to the error response.
428     */
429     this(T)(T id, StandardErrorCode error, JSONValue errData = JSONValue()) {
430         _data["jsonrpc"] = "2.0";
431         _data["id"] = id;
432         _data["error"] = getStandardError(error);
433         if (errData.type != JSONType.null_) _data["error"]["data"] = errData;
434     }
435 
436     /** Construct an Response from a JSON string.
437 
438         Params:
439             str = The string to convert to an Response object.
440 
441         Throws:
442             std.json.JSONException if the string cannot be parsed as JSON.
443     */
444     static package Response fromJSONString(const char[] str) in {
445         assert(str.length > 0, "Parsed JSON cannot be null.");
446     } body {
447         auto json = str.parseJSON;
448         if ("id" in json
449                 && ("result" in json) != ("error" in json)
450                 && "jsonrpc" in json && json["jsonrpc"].str == "2.0") {
451 
452             return Response(json);
453         } else {
454             return Response(json["id"].integer, StandardErrorCode.InvalidRequest);
455         }
456     }
457 
458     @test("Response.fromJSONString returns an error on invalid request.")
459     unittest {
460         auto resp = Response.fromJSONString(
461                 `{"jsonrpc": "2.0", "id": 0, "params": [0, 1]}`);
462         assert(resp.id == 0, "Incorrect ID.");
463         assert(resp.error["code"] == JSONValue(StandardErrorCode.InvalidRequest),
464                 "Incorrect error.");
465     }
466 
467     @test("Response.fromJSONString converts JSON to Response")
468     unittest {
469         auto res = Response.fromJSONString(
470                 `{"jsonrpc": "2.0", "id": 0, "method": "func", "result": 0}`);
471         assert(res.id == 0, "Incorrect ID.");
472         assert(res.result.integer == 0, "Incorrect result.");
473     }
474 
475     JSONValue _data;
476 
477     private:
478 
479     /** Construct an RPC response by taking a JSONValue response.
480 
481         Params:
482             data =  The JSONValue data that comprises the response. It must be a
483                     valid JSON-RPC 2.0 response.
484     */
485     this(JSONValue data) in {
486         assert("jsonrpc" in data && ("result" in data) != ("error" in data),
487                 "Malformed response: missing required field(s).");
488     } body {
489         _data = data;
490     }
491 }
492 
493 /** Standard error codes.
494 
495     Codes between -32768 and -32099 (inclusive) are reserved for pre-defined
496     errors; application-specific codes may be defined outside that range.
497 */
498 enum StandardErrorCode : int {
499     ParseError = -32700,
500     InvalidRequest = -32600,
501     MethodNotFound = -32601,
502     InvalidParams = -32602,
503     InternalError = -32603
504     // -32000 to -32099 are reserved for implementation-defined server
505     // errors.
506 }
507 
508 /** Implementation of a JSON-RPC client.
509 
510     Template_Parameters:
511         API =       An interface containing the function definitions to call on
512                     the remote server. <BR>
513         Transport = The network transport to use; by default, we use a
514                     TCPTransport.
515 
516     Example:
517     ---
518     // This is the list of functions on the RPC server.
519     interface MyRemoteFunctions {
520         long func(string param) { return 56789; }
521     }
522 
523     // Connect over TCP to a server on localhost.
524     auto rpc = new RPCClient!MyRemoteFunctions("127.0.0.1", 54321);
525     long retval = rpc.func("some string");
526     assert(retval == 56789);
527     ---
528 */
529 class RPCClient(API, Transport = TCPTransport)
530         if (is(API == interface) && isTransport!Transport) {
531 
532     /** Instantiate an RPCClient bound to the specified host.
533 
534         Params:
535             host =  The hostname or address of the RPC server.
536             port =  The port at which to connect to the server.
537     */
538     this(string host, ushort port) in {
539         assert(host.length > 0);
540     } body {
541         this(Transport(host, port));
542     }
543 
544     /** Make a blocking remote call with natural syntax.
545 
546         Any method (not part of the RPC client itself) that is present in
547         the remote API can be called as if it was a member of the RPC client,
548         and that function call will be forwarded to the remote server.
549 
550         Template_Parameters:
551             apiFunc = The name of the fake method to dispatch. <BR>
552             ARGS... = A list of parameter types.
553 
554         Params:
555             args = The arguments of the remote function to call.
556 
557         Returns:
558             The return value of the function call.
559 
560         Throws:
561             InvalidArgument if the argument types do not match the
562             remote interface.
563             RPCErrorResponse if the server returns an error response. Inspect
564             the exception payload for details.
565 
566         Example:
567         ---
568         interface RemoteFuncs {
569             void func1();
570             int func2(bool b, string s);
571         }
572 
573         auto rpc = new RPCClient!RemoteFuncs("127.0.0.1", 54321);
574         rpc.func1();
575         int retval = rpc.func2(false, "hello");
576         ---
577 
578         Notes:
579             If you want the full response from the server, use the `call`
580             function instead.
581     */
582     auto ref opDispatch(string apiFunc, ARGS...)(ARGS args) {
583         import std.traits;
584         static if (! hasMember!(API, apiFunc)) {
585             raise!(InvalidArgument, args)
586                     ("Argument does not match the remote function interface.");
587         }
588 
589         import std.conv : text;
590         import std.meta : AliasSeq;
591 
592         mixin(
593             "alias paramTypes = AliasSeq!(Parameters!(API."
594                     ~ apiFunc ~ "));\n" ~
595             "alias paramNames = AliasSeq!(ParameterIdentifierTuple!(API."
596                     ~ apiFunc ~ "));\n" ~
597             "alias returnType = ReturnType!(API." ~ apiFunc ~ ");\n"
598         );
599 
600         auto jsonArgs = JSONValue();
601         static foreach (i; 0..args.length) {
602             assert(is(typeof(args[i]) == paramTypes[i]));
603 
604             mixin("jsonArgs[\"" ~ paramNames[i] ~ "\"] = JSONValue(args[" ~
605                     i.text ~ "]);\n");
606         }
607 
608         // TODO: Need to reconstruct arrays and AAs too.
609         auto response = call(apiFunc, jsonArgs);
610         if (response.hasError()) {
611             raise!(RPCErrorResponse, response)
612                     ("Server error: " ~ response.error["message"].str);
613         }
614 
615         static if (is(returnType: void)) {
616             return;
617         } else return unwrapValue!(returnType)(response.result);
618     }
619 
620     /** Make a function call on the RPC server.
621 
622         Params:
623             func =   The name of the remote function to call.
624             params = A valid JSON array or Object containing the function
625                      parameters.
626 
627         Throws:
628             std.json.JSONException if the string cannot be parsed as JSON.
629 
630         Returns:
631             The server's response.
632 
633         Example:
634         ---
635         interface MyAPI { void func(int val); }
636         auto client = new RPCClient!MyAPI("127.0.0.1", 54321);
637 
638         import std.json : JSONValue, parseJSON;
639         auto resp = client.call("func", `{ "val": 3 }`.parseJSON);
640         auto resp2 = client.call("func", JSONValue(3));
641         ---
642     */
643     Response call(string func, JSONValue params = JSONValue()) in {
644         assert(func.length > 0);
645     } body {
646         auto req = Request(_nextId++, func, params);
647         _transport.send(req.toJSONString());
648 
649         auto respObj = _transport.receiveJSONObjectOrArray();
650         return Response.fromJSONString(respObj);
651     }
652 
653     /** Send a Request object to the server.
654 
655         Generally the only reason to use this overload is to send a request with
656         a non-integral ID.
657 
658         Params:
659             request = The Request to send.
660 
661         Example:
662         ---
663         interface MyAPI { bool my_func(int[] values); }
664 
665         auto client = RPCClient!MyAPI("127.0.0.1", 54321);
666         auto req = Request("my_id", "my_func", [1, 2, 3]);
667         auto response = client.call(req);
668         ---
669     */
670     Response call(Request request) {
671         _transport.send(request.toJSONString());
672 
673         auto respObj = _transport.receiveJSONObjectOrArray();
674         return Response.fromJSONString(respObj);
675     }
676 
677     /** Send a notification to the server.
678 
679         A notification is a function call with no reply requested. Note that
680         this is different than calling a function that returns void - in the
681         latter case a response is still received with a null result; if a
682         notification calls a function that returns a value, that return value is
683         not sent to the client.
684 
685         Params:
686             func =   The name of the remote function to call.
687             params = A valid JSON array or Object containing the function
688                      parameters.
689 
690         Throws:
691             std.json.JSONException if the string cannot be parsed as JSON.
692 
693         Example:
694         ---
695         interface MyAPI { void func(int val); }
696         auto client = new RPCClient!MyAPI("127.0.0.1", 54321);
697 
698         import std.json : JSONValue, parseJSON;
699         client.notify("func", `{ "val": 3 }`.parseJSON);
700         client.notify("func", JSONValue(3));
701         ---
702     */
703     void notify(string func, JSONValue params = JSONValue()) in {
704         assert(func.length > 0);
705     } body {
706         _transport.send(Request(func, params).toJSONString());
707     }
708 
709     /** Execute a batch of function calls.
710 
711         Params:
712             requests = A list of BatchRequests, each constructed via the
713                        `batchReq` function.
714 
715         Returns:
716             An array of Response objects, in the same order as the respective
717             request.
718 
719         Notes:
720             Notifications do not get responses; if three requests are made, and
721             one is a notification, only two responses will be returned.
722 
723         Example:
724         ---
725         import std.typecons : Yes;
726 
727         interface API {
728             void func1(int a);
729             long func2(string s);
730             long func3();
731         }
732         auto client = RPCClient!API("localhost", 54321);
733 
734         auto responses = client.batch(
735                 batchReq("func1", JSONValue(50)),
736                 batchReq("func1", JSONValue(-1), Yes.notify),
737                 batchReq("func2", JSONValue("hello")),
738                 batchReq("func3", JSONValue(), Yes.notify),
739                 batchReq("func1", JSONValue(123))
740         );
741         ---
742     */
743     Response[long] batch(BatchRequest[] requests ...) {
744         if (requests.length == 0) {
745             raise!(InvalidArgument)("requests cannot be an empty array.");
746         }
747 
748         Response[long] responses;
749         auto allAreNotifications = sendBatchRequest(requests);
750         if (! allAreNotifications) responses = receiveBatchResponse();
751         return responses;
752     }
753 
754     private:
755 
756     import std.typecons : Flag, Yes, No;
757 
758     long _nextId;
759     Transport _transport;
760 
761     /** Instantiate an RPCClient with the specified network transport.
762 
763         This is designed to allow mock objects for testing.
764     */
765     this(Transport transport) {
766         _transport = transport;
767     }
768 
769     Flag!"allAreNotifications" sendBatchRequest(BatchRequest[] requests) {
770         JSONValue[] reqs;
771         Flag!"allAreNotifications" allAreNotifications = Yes.allAreNotifications;
772 
773         foreach (request; requests) {
774             if (request.isNotification) {
775                 reqs ~= Request(request.method, request.params)._data;
776             } else {
777                 allAreNotifications = No.allAreNotifications;
778                 reqs ~= Request(
779                         _nextId++, request.method, request.params)._data;
780             }
781         }
782         auto batchReq = JSONValue(reqs);
783         _transport.send(batchReq.toJSON());
784         return allAreNotifications;
785     }
786 
787     Response[long] receiveBatchResponse() {
788         Response[long] responses;
789         auto resps = _transport.receiveJSONObjectOrArray().parseJSON;
790 
791         if (resps.type == JSONType.array) {
792             foreach (resp; resps.array) {
793                 auto r = Response(resp);
794                 responses[r.id] = r;
795             }
796         } else {
797             // Single non-array (error?) response due to a malformed or empty
798             // batch.
799             auto resp = Response(resps);
800             responses[resp.id] = resp;
801         }
802         return responses;
803     }
804 }
805 
806 import std.typecons : Flag, No;
807 /** Create a BatchRequest to pass to an RPCClient's `batch` function.
808 
809     Params:
810         method = The name of the remote method to call.
811         params = A JSONValue scalar or object/array containing the method
812                  parameters.
813         notify = Yes.notify if the request is a notification; No.notify
814                  otherwise.
815 
816     Returns:
817         A BatchRequest to be passed to an RPCClient's batch function.
818 
819     Example:
820     ---
821     import std.typecons : Yes;
822 
823     interface API {
824         void func1(int a);
825         long func2(string s);
826         long func3();
827     }
828     auto client = RPCClient!API("localhost", 54321);
829 
830     auto responses = client.batch(
831             batchReq("func1", JSONValue(50)),
832             batchReq("func1", JSONValue(-1), Yes.notify),
833             batchReq("func2", JSONValue("hello")),
834             batchReq("func3", JSONValue(), Yes.notify),
835             batchReq("func1", JSONValue(123))
836     );
837     ---
838 */
839 auto batchReq(
840         string method, JSONValue params, Flag!"notify" notify = No.notify) {
841     return BatchRequest(method, params, notify);
842 }
843 
844 /** Implementation of a JSON-RPC server.
845 
846     This implementation only supports communication via TCP sockets.
847 
848     Template_Parameters:
849         API =      A class or struct containing the functions available for the
850                    client to call. <BR>
851         Listener = The object to use to manage client connections. By default, a
852                    TCPTransport.
853 
854     Example:
855     ---
856     class MyFunctions {
857         long func(string param) { return 56789; }
858     }
859 
860     // Bind to a local port and serve func on a platter.
861     auto serve = new RPCServer!MyFunctions("127.0.0.1", 54321);
862     serve.listen();
863     ---
864 */
865 class RPCServer(API, Listener = TCPListener!API)
866         if (is(API == class) && isListener!(Listener!API)) {
867 
868     import std.socket;
869 
870     /** Construct an RPCServer!API object to communicate via TCP sockets.
871 
872         The API class must be constructable via a default constructor; if you
873         need to use an alternate constructor, create it first and pass it to the
874         RPCServer via another constructor.
875 
876         Params:
877             host =  The host interface on which to listen.
878             port =  The port on which to listen.
879     */
880     this(string host, ushort port) {
881         this(new API(), Listener(host, port));
882     }
883 
884     /** Construct an RPCServer!API object to communicate via TCP sockets.
885 
886         Params:
887             api =   The instantiated class or struct providing the RPC API.
888             host =  The host interface on which to listen.
889             port =  The port on which to listen.
890     */
891     this(API api, string host, ushort port) {
892         this(api, Listener(host, port));
893     }
894 
895     /** Listen for and respond to connections.
896 
897         Params:
898             maxQueuedConnections = The maximum number of clients to hold in
899                                    the backlog before rejecting connections.
900     */
901     void listen(int maxQueuedConnections = 10) {
902         _listener.listen!(handleClient!API)(_api, maxQueuedConnections);
903     }
904 
905     private:
906 
907     API _api;
908     Listener _listener;
909 
910     /** Construct an RPCServer!API object.
911 
912         By default, serve over a TCP connection; alternate network transports can
913         be specified.
914 
915         Params:
916             api =       The instantiated class or struct providing the RPC API.
917             transport = The network transport to use.
918     */
919     this(API api, Listener listener) {
920         _api = api;
921         _listener = listener;
922     }
923 }
924 
925 /** Handles all of an individual client's requests.
926 
927     The `listen` method of the RPCServer calls this in a new thread to handle
928     client requests. This is not intended to be called by user code.
929 
930     Template_Parameters:
931         API =       The class containing the RPC functions.
932         Transport = The type of the network transport to use; by default, this
933                     is a TCPTransport.
934 
935     Params:
936         transport = The network transport used for data transmission.
937         api       = An instantiated class containing the functions to execute.
938 */
939 void handleClient(API, Transport = TCPTransport)(Transport transport, API api)
940         if (is(API == class) && isTransport!Transport) {
941     while (transport.isAlive()) {
942         char[] received = receiveRequest(transport);
943         if (received.length == 0) continue;
944 
945         if (received[0] == '[') {
946             executeBatch(transport, api, received);
947         } else {
948             auto req = Request.fromJSONString(received);
949             if (req.isNotification) {
950                 executeMethod(req, api);
951             } else {
952                 transport.send(executeMethod(req, api).toJSONString);
953             }
954         }
955     }
956 }
957 
958 /** Execute an RPC method and return the server's response.
959 
960     Only public members of the API object are callable as a remote function.
961 
962     Template_Parameters:
963         API = The class containing the function to call.
964 
965     Params:
966         request =   The request from the client.
967         api =       An instance of the class or struct containing the function
968                     to call.
969 */
970 Response executeMethod(API)(Request request, API api)
971         if (is(API == class)) {
972     import std.traits : isFunction;
973 
974     foreach(method; __traits(derivedMembers, API)) {
975         mixin(
976             "enum isMethodAPublicFunction =\n" ~
977             "   isFunction!(api." ~ method ~ ") &&\n" ~
978             "   __traits(getProtection, api." ~ method ~ ") == `public`;\n"
979         );
980         mixin(
981             "enum isDisabledFunction = __traits(isDisabled, api." ~ method ~ ");"
982         );
983 
984         static if (isMethodAPublicFunction && !isDisabledFunction) {
985             if (method == request.method) {
986                 static if (isAggregateType!
987                             (typeof(execRPCMethod!(API, method)(request, api)))) {
988                     auto retval =
989                             execRPCMethod!(API, method)(request, api).serialize;
990                 } else {
991                     auto retval = JSONValue(
992                             execRPCMethod!(API, method)(request, api));
993                 }
994 
995                 if (request.isNotification) {
996                     // TODO: I'd rather return nothing; this is just thrown away.
997                     Response r;
998                     return r;
999                 } else if (request.idType == JSONType.integer) {
1000                     return Response(request.id, retval);
1001                 } else if (request.idType == JSONType.float_) {
1002                     return Response(request.id!double, retval);
1003                 } else if (request.idType == JSONType..string) {
1004                     return Response(request.id!string, retval);
1005                 } else if (request.idType == JSONType.null_) {
1006                     return Response(null, retval);
1007                 }
1008                 return Response(
1009                         request._data["id"],
1010                         StandardErrorCode.InvalidRequest,
1011                         JSONValue("Invalid request ID type."));
1012             }
1013         }
1014     }
1015     return Response(
1016             request.id,
1017             StandardErrorCode.MethodNotFound,
1018             request._data);
1019 }
1020 
1021 @("We can pass POD objects as RPC parameters.")
1022 unittest {
1023     struct MyData {
1024         int a;
1025         string b;
1026         float c;
1027     }
1028 
1029     MyData mydata = {
1030         a: 1,
1031         b: "2",
1032         c: 3.0,
1033     };
1034 
1035     interface API {
1036         MyData func(MyData params);
1037     }
1038 
1039     auto sock = new FakeSocket();
1040     auto transport = TCPTransport(sock);
1041     auto client = new RPCClient!API(transport);
1042 
1043     // TODO: I want automatic serialization: auto ret = client.func(mydata);
1044     auto ret = client.call("func", mydata.serialize);
1045 }
1046 
1047 @test("serialize converts simple structs to JSONValues")
1048 unittest {
1049     struct MyData {
1050         int a;
1051         string b;
1052         float c;
1053         int[] d;
1054         string[string] e;
1055     }
1056 
1057     string[string] _e; _e["asdf"] = ";lkj";
1058 
1059     MyData mydata = {
1060         a: 1,
1061         b: "2",
1062         c: 3.0,
1063         d: [1, 2, 3],
1064         e: _e
1065     };
1066 
1067     auto json = serialize(mydata);
1068     JSONValue expected = JSONValue(
1069             `{"a":1,"b":"2","c":3.0,"d":[1,2,3],"e":{"asdf":";lkj"}}`.parseJSON);
1070     assert(json == expected, json.toJSON);
1071 }
1072 
1073 @test("serialize converts nested structs to JSONValues")
1074 unittest {
1075     struct MoreData {
1076         int e;
1077         string f;
1078     }
1079 
1080     struct MyData {
1081         int a;
1082         string b;
1083         float c;
1084         MoreData d;
1085     }
1086 
1087     MyData mydata = {
1088         a: 1,
1089         b: "2",
1090         c: 3.0,
1091         d: MoreData(123, "g")
1092     };
1093 
1094     auto json = serialize(mydata);
1095     auto expected = JSONValue(`{"a":1,"b":"2","c":3.0,"d":{"e":123,"f":"g"}}`.parseJSON);
1096     assert(json == expected, json.toJSON);
1097 }
1098 
1099 /** Serialize a struct to a JSON Object.
1100 
1101     Template_Parameters:
1102         T = The type of object to serialize.
1103 
1104     Params:
1105         obj = The object to serialize to JSON.
1106 */
1107 JSONValue serialize(T)(T obj) if (isAggregateType!T) {
1108     import std.traits : Fields, FieldNameTuple;
1109     JSONValue json;
1110     alias fields = Fields!T;
1111     alias fieldNames = FieldNameTuple!T;
1112 
1113     static foreach (int i; 0..fields.length) {
1114         static if (isAggregateType!(fields[i])) {
1115             mixin(`json["` ~ fieldNames[i] ~ `"] = serialize(obj.` ~ fieldNames[i] ~ `);`);
1116         } else{
1117             mixin(`json["` ~ fieldNames[i] ~ `"] = obj.` ~ fieldNames[i] ~ `;`);
1118         }
1119     }
1120 
1121     return json;
1122 }
1123 
1124 @test("deserialize converts a JSONValue to a struct")
1125 unittest {
1126     struct MyData {
1127         int a;
1128         string b;
1129         float c;
1130         int[] d;
1131         string[string] e;
1132     }
1133 
1134     string[string] _e; _e["asdf"] = ";lkj";
1135 
1136     MyData expected = {
1137         a: 1,
1138         b: "2",
1139         c: 3.0,
1140         d: [1, 2, 3],
1141         e: _e
1142     };
1143 
1144     JSONValue input = JSONValue(
1145             `{"a":1,"b":"2","c":3.0,"d":[1,2,3],"e":{"asdf":";lkj"}}`.parseJSON);
1146     assert(input.deserialize!MyData == expected);
1147 }
1148 
1149 @test("deserialize converts a JSONValue to a nested struct")
1150 unittest {
1151     struct MoreData {
1152         int e;
1153         string f;
1154     }
1155 
1156     struct MyData {
1157         int a;
1158         string b;
1159         float c;
1160         MoreData d;
1161     }
1162 
1163     MyData expected = {
1164         a: 1,
1165         b: "2",
1166         c: 3.0,
1167         d: MoreData(123, "g")
1168     };
1169 
1170     auto input = JSONValue(`{"a":1,"b":"2","c":3.0,"d":{"e":123,"f":"g"}}`.parseJSON);
1171     assert(input.deserialize!MyData == expected);
1172 }
1173 
1174 /** Deserialize a JSON Object to the specified aggregate D type.
1175 
1176     Template_Parameters:
1177         T = The type of data object.
1178 
1179     Params:
1180         json = The JSON object to deserialize.
1181 */
1182 T deserialize(T)(JSONValue json) if (isAggregateType!T) {
1183     import std.traits : Fields, FieldNameTuple;
1184 
1185     alias types = Fields!T;
1186     alias names = FieldNameTuple!T;
1187     T newObject;
1188 
1189     static foreach (i; 0..types.length) {
1190         static if (isAggregateType!(types[i])) {
1191             mixin(`newObject.` ~ names[i]
1192                 ~ ` = deserialize!(typeof(newObject.` ~ names[i] ~ `))(json["`
1193                     ~ names[i] ~ `"]);`);
1194         } else {
1195             mixin(`newObject.` ~ names[i] ~
1196                 ` = unwrapValue!(` ~ types[i].stringof ~ `)(json["`
1197                     ~ names[i] ~ `"]);`);
1198         }
1199     }
1200     return newObject;
1201 }
1202 
1203 private:
1204 
1205 char[] receiveRequest(Transport)(Transport transport) {
1206     try {
1207         return transport.receiveJSONObjectOrArray();
1208     } catch (InvalidDataReceived ex) {
1209         transport.send(
1210                 Response(
1211                     null,
1212                     StandardErrorCode.InvalidRequest,
1213                     JSONValue(ex.msg)
1214                 ).toJSONString()
1215         );
1216     } catch (Exception) {
1217         transport.send(
1218                 Response(
1219                     null,
1220                     StandardErrorCode.InternalError,
1221                     JSONValue(
1222                         "Unknown exception occurred while receiving data.")
1223                 ).toJSONString()
1224         );
1225     }
1226     return [];
1227 }
1228 
1229 JSONValue parseBatch(Transport)(Transport transport, const char[] request) {
1230     scope(failure) {
1231         // TODO: The spec says to send a single response, but how do I
1232         // handle the ID? Need to check other implementations.
1233         transport.send(
1234                 Response(
1235                     null,
1236                     StandardErrorCode.ParseError,
1237                     JSONValue("Batch request is malformed.")
1238                 ).toJSONString()
1239         );
1240         return JSONValue();
1241     }
1242 
1243     auto batch = request.parseJSON();
1244     if (batch.array.length == 0) {
1245         // SPEC: Send a single response if the array is empty.
1246         // TODO: How do I handle the ID?
1247         transport.send(
1248                 Response(
1249                     null,
1250                     StandardErrorCode.InvalidRequest,
1251                     JSONValue("Received batch with no requests.")
1252                 ).toJSONString()
1253         );
1254         return JSONValue();
1255     }
1256     return batch;
1257 }
1258 
1259 void executeBatch(API, Transport)
1260         (Transport transport, API api, const char[] received) {
1261 
1262     JSONValue batch = parseBatch(transport, received);
1263     if (batch.type == JSONType.null_) return;
1264 
1265     JSONValue[] responses;
1266     // TODO: Could parallelize these. Probably use constructor flag(?)
1267     foreach (request; batch.array) {
1268         Request req;
1269         try {
1270             // TODO: Horribly inefficient. Need a new constructor.
1271             req = Request.fromJSONString(request.toJSON());
1272         } catch (InvalidDataReceived ex) {
1273             if ("id" in request) {
1274                 responses ~= Response(
1275                         request["id"],
1276                         StandardErrorCode.InvalidRequest,
1277                         JSONValue(ex.msg))._data;
1278             } // TODO: else... spec is silent. Example uses null ID.
1279             continue;
1280         } catch (JSONException ex) {
1281             if ("id" in request) {
1282                 responses ~= Response(
1283                         request["id"],
1284                         StandardErrorCode.ParseError,
1285                         JSONValue(ex.msg))._data;
1286             } // TODO: else... spec is silent. Example uses null ID.
1287             continue;
1288         }
1289         if (req.isNotification) {
1290             executeMethod(req, api);
1291         } else {
1292             responses ~= executeMethod(req, api)._data;
1293         }
1294     }
1295     if (responses.length > 0) {
1296         auto data = JSONValue(responses);
1297         transport.send(data.toJSON());
1298     } // else: they were all notifications.
1299 }
1300 
1301 /** Execute an RPC method and return its result.
1302 
1303     Template_Parameters:
1304         API    = The class providing the executable functions.
1305         method = The name of the method to call.
1306 
1307     Params:
1308         request = The request sent from the client.
1309         api     = The instantiated class with the method to execute.
1310 
1311     Returns:
1312         The return value of the RPC method.
1313 */
1314 auto execRPCMethod(API, string method)(Request request, API api) {
1315     import std.traits : ReturnType;
1316     mixin(
1317         "enum returnType = typeid(ReturnType!(API." ~ method ~ "));\n" ~
1318         GenCaller!(API, method)
1319     );
1320 
1321     static if(returnType is typeid(void)) {
1322         callRPCFunc!(method, JSONValue)(api, request.params);
1323         return null;
1324     } else {
1325         return callRPCFunc!(method, JSONValue)(api, request.params);
1326     }
1327 }
1328 
1329 /** Generate the function `callRPCFunc` that will call the API function
1330     specified by the client and return its return value, if present.
1331 
1332     Parameters are provided as a JSONValue array or Object; Objects will be
1333     converted to arrays.
1334 
1335     Example:
1336     ---
1337     mixin(GenCaller!(API, method));
1338     auto retval = callRPCFunc!(method, JSONValue)(api, request.params);
1339     ---
1340 */
1341 static string GenCaller(API, string method)() pure {
1342     import std.conv : text;
1343     import std.meta : AliasSeq;
1344     import std.traits;
1345 
1346     mixin(
1347         "alias paramNames = AliasSeq!(ParameterIdentifierTuple!(API."
1348                 ~ method ~ "));\n"
1349       ~ "alias paramTypes = AliasSeq!(Parameters!(API." ~ method ~ "));\n"
1350       ~ "\nenum returnType = typeid(ReturnType!(API." ~ method ~ "));\n"
1351      );
1352 
1353     // TODO: Validate against API - if a named param is passed that isn't on the
1354     // method we need to return an error response. See execRPCMethod.
1355     string func =
1356         "\nauto ref callRPCFunc(string method, ARGS)(API api, ARGS args) {\n";
1357 
1358     static foreach(i; 0..paramTypes.length) {
1359         static if (isAggregateType!(paramTypes[i]))
1360             func ~=
1361           "    import " ~ moduleName!(paramTypes[i]) ~ " : "
1362                     ~ paramTypes[i].stringof ~ ";\n";
1363     }
1364 
1365     func ~=
1366           "    JSONValue vals = args;\n"
1367         ~ "    if (args.type != JSONType.object) {\n"
1368         ~ "        vals = JSONValue(`{}`.parseJSON);\n";
1369 
1370     static foreach (i; 0..paramNames.length) {
1371         func ~=
1372           "        vals[`" ~ paramNames[i] ~ "`] = args[" ~ i.text ~ "];\n";
1373     }
1374         func ~=
1375           "    }\n"; // args.type != JSONType.object
1376 
1377     static if (returnType is typeid(void)) {
1378         func ~= "    ";
1379     } else static if (returnType is typeid(bool)) {
1380         func ~= ("    return cast(bool)");
1381     } else {
1382         func ~= ("    return ");
1383     }
1384 
1385     func ~= "api." ~ method ~ "(";
1386 
1387     static if (paramTypes.length > 0) {
1388         static foreach(i; 0..paramTypes.length) {
1389             static if (isAggregateType!(paramTypes[i])) {
1390                 func ~=
1391                     "vals.deserialize!(" ~ paramTypes[i].stringof ~ "), ";
1392             } else {
1393                 func ~=
1394                     "vals[`" ~ paramNames[i] ~ "`].unwrapValue!" ~ paramTypes[i].stringof ~ ", ";
1395             }
1396         }
1397     }
1398     func ~= ");\n}\n";
1399 
1400     return func;
1401 }
1402 
1403 /** Container for request data submitted in a batch.
1404 
1405     This allows me to send requests and notifications in a single object without
1406     doing crazy stuff.
1407 */
1408 struct BatchRequest {
1409     this(string method, JSONValue params, bool isNotification = false) {
1410         this.method = method;
1411         this.params = params;
1412         this.isNotification = isNotification;
1413     }
1414 
1415     string method;
1416     JSONValue params;
1417     bool isNotification;
1418 }
1419 
1420 /** Return error data for JSON-RPC and RPCServer standard error codes.
1421 
1422     Params:
1423         code = A StandardErrorCode set to the error number.
1424 
1425     Returns:
1426         The "error" dictionary to attach to the Response.
1427 
1428     Example:
1429     ---
1430     auto resp = Response(0, getStandardError(StandardErrorCode.InvalidRequest);
1431     ---
1432 */
1433 private JSONValue getStandardError(StandardErrorCode code) {
1434     auto err = JSONValue();
1435     err["code"] = code;
1436     final switch (code) {
1437         case StandardErrorCode.ParseError:
1438             err["message"] = "An error occurred while parsing the JSON text.";
1439             break;
1440         case StandardErrorCode.InvalidRequest:
1441             err["message"] = "The JSON sent is not a valid Request object.";
1442             break;
1443         case StandardErrorCode.MethodNotFound:
1444             err["message"] = "The called method is not available.";
1445             break;
1446         case StandardErrorCode.InvalidParams:
1447             err["message"] = "The method was called with invalid parameters.";
1448             break;
1449         case StandardErrorCode.InternalError:
1450             err["message"] = "Internal server error.";
1451             break;
1452     }
1453 
1454     return err;
1455 }
1456 
1457 /** Unwrap a D value from a JSONValue. */
1458 auto unwrapValue(T)(JSONValue value) {
1459     import std.traits;
1460     static if (isFloatingPoint!T) {
1461         return cast(T)value.floating;
1462     } else static if (isSomeString!T) {
1463         return cast(T)value.str;
1464     } else static if (isSigned!T) {
1465         return cast(T)value.integer;
1466     } else static if (isUnsigned!T) {
1467         // TODO: There has to be a better way to do all of this.
1468         // Positive signed values will take this branch, rather than the
1469         // isSigned! branch.
1470         try {
1471             return cast(T)value.uinteger;
1472         } catch (JSONException e) {
1473             return cast(T)value.integer;
1474         }
1475     } else static if (isBoolean!T) {
1476         if (value.type == JSONType.true_) return true;
1477         if (value.type == JSONType.false_) return false;
1478         raise!(InvalidArgument, value)("Expected a boolean value.");
1479     } else static if (is(T == typeof(null))) {
1480         return null;
1481     } else static if (isArray!T) {
1482         T elems;
1483         foreach (elem; value.array) {
1484             elems ~= unwrapValue!(ForeachType!T)(elem);
1485         }
1486         return elems;
1487     } else { // JSON Object; return an AA.
1488         T obj;
1489         foreach (key, val; value.object) {
1490             obj[key] = unwrapValue!(KeyType!T)(val);
1491         }
1492         return obj;
1493     }
1494     assert(0, "Missing JSON value type.");
1495 }
1496 
1497 @test("unwrapValue retrieves scalar values from a JSONValue")
1498 ///
1499 unittest {
1500     auto a = JSONValue("a");
1501     auto b = JSONValue(2);
1502     auto c = JSONValue(2u);
1503     auto d = JSONValue(2.3);
1504     auto e = JSONValue(true);
1505 
1506     assert(a.unwrapValue!string == "a");
1507     assert(b.unwrapValue!int == 2);
1508     assert(c.unwrapValue!uint == 2u);
1509     auto fl = d.unwrapValue!float;
1510     assert(fl > 2.2 && fl < 2.4);
1511     assert(e.unwrapValue!bool == true);
1512 }
1513 
1514 version(unittest) {
1515     class MyAPI {
1516         bool voidFunc_called = false;
1517         bool void3params_called = false;
1518         bool voidArray_called = false;
1519         bool voidWithString_called = false;
1520 
1521         private void dontCallThis() {
1522             throw new Exception("Private members should not be callable.");
1523         }
1524 
1525         @disable
1526         void disabledMethod() {
1527             throw new Exception("Disabled members should not be callable.");
1528         }
1529 
1530         bool retBool() { return true; }
1531         alias retTrue = retBool;
1532         bool retFalse() { return false; }
1533 
1534         ulong retUlong(string s) { return ("abc and " ~ s).length; }
1535 
1536         int retInt(int i) { return i+1; }
1537         float retFloat() { return 1.23; }
1538 
1539         void voidFunc() { voidFunc_called = true; }
1540         void void3params(int a, bool b, float c) { void3params_called = true; }
1541         void voidArray(int a, int b) { voidArray_called = true; }
1542         void voidWithString(string s) { voidWithString_called = true; }
1543 
1544         string retString() { return "testing"; }
1545 
1546         string øbæårößΓαζ(string input) { return input ~ "âいはろшь ж๏เ"; }
1547     }
1548 }
1549 
1550 @test("execRPCMethod executes void RPC functions")
1551 unittest {
1552     auto api = new MyAPI();
1553 
1554     auto r1 = execRPCMethod!(MyAPI, "void3params")
1555             (Request(
1556                 0,
1557                 "void3params",
1558                 JSONValue(`{"a": 3, "b": false, "c": 2.3}`.parseJSON)),
1559             api);
1560 
1561     auto r2 = execRPCMethod!(MyAPI, "voidArray")
1562             (Request(1, "voidArray", JSONValue([1, 2])), api);
1563 
1564     auto r3 = execRPCMethod!(MyAPI, "voidFunc")
1565             (Request(2, "voidFunc"), api);
1566 
1567     assert(api.void3params_called == true
1568             && api.voidArray_called == true
1569             && api.voidFunc_called == true);
1570 }
1571 
1572 @test("execRPCMethod executes non-void RPC functions")
1573 unittest {
1574     auto api = new MyAPI();
1575 
1576     auto r1 = execRPCMethod!(MyAPI, "retBool")
1577             (Request(0, "retBool"), api);
1578     assert(r1 == true);
1579 
1580     auto r2 = execRPCMethod!(MyAPI, "retUlong")
1581             (Request(1, "retUlong", JSONValue(["some string"])), api);
1582     assert(r2 == 19);
1583 }
1584 
1585 @test("executeMethod returns integral values")
1586 unittest {
1587     auto api = new MyAPI();
1588 
1589     auto r1 = executeMethod(
1590             Request(0, "retUlong", JSONValue(["some string"])), api);
1591     assert(r1.id == 0);
1592     assert(r1.result.unwrapValue!ulong == 19);
1593 
1594     auto r2 = executeMethod(Request(1, "retInt", JSONValue([5])), api);
1595     assert(r2.id == 1);
1596     assert(r2.result.integer == 6);
1597 }
1598 
1599 @test("executeMethod returns floating-point values")
1600 unittest {
1601     auto api = new MyAPI();
1602 
1603     auto resp = executeMethod(Request(0, "retFloat"), api);
1604     assert(resp.result.floating > 1.22 && resp.result.floating < 1.24);
1605 }
1606 
1607 @test("executeMethod returns boolean values")
1608 unittest {
1609     auto api = new MyAPI();
1610 
1611     auto r1 = executeMethod(Request(0, "retTrue"), api);
1612     assert(r1.id == 0);
1613     assert(r1.result == JSONValue(true));
1614 
1615     auto r2 = executeMethod(Request(1, "retFalse"), api);
1616     assert(r2.id == 1);
1617     assert(r2.result == JSONValue(false));
1618 }
1619 
1620 @test("executeMethod returns string values")
1621 unittest {
1622     auto api = new MyAPI();
1623 
1624     auto r1 = executeMethod(Request(0, "retString"), api);
1625     assert(r1.result.unwrapValue!string == "testing");
1626 }
1627 
1628 @test("executeMethod handles non-integral IDs")
1629 unittest {
1630     auto api = new MyAPI();
1631 
1632     auto r1 = executeMethod(Request("my_id", "retString"), api);
1633     assert(r1.id!string == "my_id");
1634 
1635     auto r2 = executeMethod(Request(null, "retString"), api);
1636     assert(r2.id!(typeof(null)) == null);
1637 }
1638 
1639 @test("executeMethod can execute functions and pass data not in the ASCII character range")
1640 unittest {
1641     auto api = new MyAPI();
1642 
1643     auto ret = executeMethod(
1644             Request("áðý", "øbæårößΓαζ", JSONValue(["éçφωτz"])), api);
1645     assert(ret.result == JSONValue("éçφωτzâいはろшь ж๏เ"));
1646 }
1647 
1648 @test("executeMethod returns error when the method doesn't exist")
1649 unittest {
1650     auto api = new MyAPI();
1651     auto r1 = executeMethod(Request(0, "noFunctionHere"), api);
1652 
1653     assert(r1.id!long == 0);
1654     assert(r1.error["code"].integer == StandardErrorCode.MethodNotFound,
1655             "Wrong error.");
1656 
1657     assert(r1.error["data"]["method"].str == "noFunctionHere",
1658             "Did not include method.");
1659 }
1660 
1661 @test("executeMethod returns error on invalid request ID")
1662 unittest {
1663     auto api = new MyAPI();
1664 
1665     auto req = Request(0, "retTrue");
1666     req._data["id"] = JSONValue([1, 2]);
1667     auto resp = executeMethod(req, api);
1668 
1669     assert(resp._data["id"] == JSONValue([1, 2]));
1670     assert(resp.error["code"].integer == StandardErrorCode.InvalidRequest,
1671             "Incorrect error.");
1672 }
1673 
1674 @test("executeMethod will not execute private methods")
1675 unittest {
1676     auto api = new MyAPI();
1677     auto r1 = executeMethod(Request(0, "dontCallThis"), api);
1678 
1679     assert(r1.id!long == 0);
1680     assert(r1.error["code"].integer == StandardErrorCode.MethodNotFound,
1681             "Wrong error.");
1682 
1683     assert(r1.error["data"]["method"].str == "dontCallThis",
1684             "Did not include method.");
1685 }
1686 
1687 @test("executeMethod will not execute disabled methods")
1688 unittest {
1689     auto api = new MyAPI();
1690     auto r1 = executeMethod(Request(0, "disabledMethod"), api);
1691 
1692     assert(r1.id!long == 0);
1693     assert(r1.error["code"].integer == StandardErrorCode.MethodNotFound,
1694             "Wrong error.");
1695 
1696     assert(r1.error["data"]["method"].str == "disabledMethod",
1697             "Did not include method.");
1698 }
1699 
1700 @test("[DOCTEST] RPCClient example: opDispatch")
1701 unittest {
1702     interface RemoteFuncs {
1703         void func1();
1704         int func2(bool b, string s);
1705     }
1706 
1707     auto sock = new FakeSocket();
1708     auto transport = TCPTransport(sock);
1709     auto rpc = new RPCClient!RemoteFuncs(transport);
1710 
1711     sock.receiveReturnValue = `{"jsonrpc":"2.0","id":0,"result":null}`;
1712     rpc.func1();
1713     sock.receiveReturnValue = `{"jsonrpc":"2.0","id":1,"result":3}`;
1714     assert(rpc.func2(false, "hello") == 3);
1715 }
1716 
1717 @test("[DOCTEST] RPCClient example: call")
1718 unittest {
1719     interface MyAPI { void func(int val); }
1720     auto sock = new FakeSocket();
1721     auto transport = TCPTransport(sock);
1722     auto client = new RPCClient!MyAPI(transport);
1723 
1724     sock.receiveReturnValue = `{"jsonrpc":"2.0","id":0,"result":null}`;
1725     auto resp = client.call("func", `{ "val": 3 }`.parseJSON);
1726     sock.receiveReturnValue = `{"jsonrpc":"2.0","id":1,"result":null}`;
1727     auto resp2 = client.call("func", JSONValue([3]));
1728 }
1729 
1730 @test("[DOCTEST] RPCClient example: call(Request) allows non-integral IDs")
1731 unittest {
1732     interface MyAPI { bool my_func(int[] values); }
1733     auto sock = new FakeSocket();
1734     auto transport = TCPTransport(sock);
1735     auto client = new RPCClient!MyAPI(transport);
1736 
1737     sock.receiveReturnValue = `{"jsonrpc":"2.0","id":"my_id","result":true}`;
1738     auto req = Request("my_id", "my_func", JSONValue([1, 2, 3]));
1739     auto response = client.call(req);
1740 
1741     assert(unwrapValue!bool(response.result) == true);
1742 }
1743 
1744 @test("[DOCTEST] RPCClient example: notify")
1745 unittest {
1746     interface MyAPI { void func(int val); }
1747     auto sock = new FakeSocket();
1748     auto transport = TCPTransport(sock);
1749     auto client = new RPCClient!MyAPI(transport);
1750 
1751     import std.json : JSONValue, parseJSON, toJSON;
1752 
1753     client.notify("func", `{"val": 3}`.parseJSON);
1754     assert(sock.lastDataSent ==
1755             Request("func", `{"val":3}`.parseJSON)._data.toJSON);
1756 
1757     client.notify("func", JSONValue([3]));
1758     assert(sock.lastDataSent == Request("func", `[3]`.parseJSON)._data.toJSON);
1759 }
1760 
1761 @test("[DOCTEST] Execute batches of requests.")
1762 unittest {
1763     interface API {
1764         void func1(int a);
1765         long func2(string s);
1766         long func3();
1767     }
1768     auto sock = new FakeSocket();
1769     auto transport = TCPTransport(sock);
1770     auto client = new RPCClient!API(transport);
1771 
1772     sock.receiveReturnValue =
1773         `[{"id":0,"jsonrpc":"2.0","result":null},
1774           {"id":1,"jsonrpc":"2.0","result":123},
1775           {"id":2,"jsonrpc":"2.0","result":0}]`;
1776 
1777     import std.typecons : Yes;
1778     auto responses = client.batch(
1779             batchReq("func1", JSONValue([50])),
1780             batchReq("func1", JSONValue([-1]), Yes.notify),
1781             batchReq("func2", JSONValue(["hello"])),
1782             batchReq("func3", JSONValue()),
1783             batchReq("func1", JSONValue([123]), Yes.notify)
1784     );
1785 
1786     assert(responses.length == 3);
1787     assert(responses[0].result == JSONValue(null), "Incorrect [0] result");
1788     assert(responses[1].result == JSONValue(123), "Incorrect [1] result");
1789     assert(responses[2].result == JSONValue(0), "Incorrect [2] result");
1790 }