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 }