1 /** 2 Automatic REST interface and client code generation facilities. 3 4 Copyright: © 2012-2016 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Михаил Страшун 7 */ 8 module hb.web.rest; 9 10 public import hb.web.common; 11 12 import vibe.core.log; 13 import vibe.http.router : URLRouter; 14 import vibe.http.client : HTTPClientSettings; 15 import vibe.http.common : HTTPMethod; 16 import vibe.http.server : HTTPServerRequestDelegate; 17 import vibe.http.status : isSuccessCode; 18 import vibe.internal.meta.uda; 19 import vibe.internal.meta.funcattr; 20 import vibe.inet.url; 21 import vibe.inet.message : InetHeaderMap; 22 import hb.web.internal.rest.common : RestInterface, Route, SubInterfaceType; 23 import hb.web.auth : AuthInfo, handleAuthentication, handleAuthorization, isAuthenticated; 24 25 import std.algorithm : startsWith, endsWith; 26 import std.range : isOutputRange; 27 import std.typecons : Nullable; 28 import std.typetuple : anySatisfy, Filter; 29 import std.traits; 30 31 /** 32 Registers a REST interface and connects it the the given instance. 33 34 Each method of the given class instance is mapped to the corresponing HTTP 35 verb. Property methods are mapped to GET/PUT and all other methods are 36 mapped according to their prefix verb. If the method has no known prefix, 37 POST is used. The rest of the name is mapped to the path of the route 38 according to the given `method_style`. Note that the prefix word must be 39 all-lowercase and is delimited by either an upper case character, a 40 non-alphabetic character, or the end of the string. 41 42 The following table lists the mappings from prefix verb to HTTP verb: 43 44 $(TABLE 45 $(TR $(TH HTTP method) $(TH Recognized prefixes)) 46 $(TR $(TD GET) $(TD get, query)) 47 $(TR $(TD PUT) $(TD set, put)) 48 $(TR $(TD POST) $(TD add, create, post)) 49 $(TR $(TD DELETE) $(TD remove, erase, delete)) 50 $(TR $(TD PATCH) $(TD update, patch)) 51 ) 52 53 If a method has its first parameter named 'id', it will be mapped to ':id/method' and 54 'id' is expected to be part of the URL instead of a JSON request. Parameters with default 55 values will be optional in the corresponding JSON request. 56 57 Any interface that you return from a getter will be made available with the 58 base url and its name appended. 59 60 Params: 61 router = The HTTP router on which the interface will be registered 62 instance = Class instance to use for the REST mapping - Note that TImpl 63 must either be an interface type, or a class which derives from a 64 single interface 65 settings = Additional settings, such as the $(D MethodStyle), or the prefix. 66 See $(D RestInterfaceSettings) for more details. 67 68 See_Also: 69 $(D RestInterfaceClient) class for a seamless way to access such a generated API 70 71 */ 72 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfaceSettings settings = null) 73 { 74 import std.algorithm : filter, map, all; 75 import std.array : array; 76 import std.range : front; 77 import hb.web.internal.rest.common : ParameterKind; 78 79 auto intf = RestInterface!TImpl(settings, false); 80 81 foreach (i, ovrld; intf.SubInterfaceFunctions) { 82 enum fname = __traits(identifier, intf.SubInterfaceFunctions[i]); 83 alias R = ReturnType!ovrld; 84 85 static if (isInstanceOf!(Collection, R)) { 86 auto ret = __traits(getMember, instance, fname)(R.ParentIDs.init); 87 router.registerRestInterface!(R.Interface)(ret.m_interface, intf.subInterfaces[i].settings); 88 } else { 89 auto ret = __traits(getMember, instance, fname)(); 90 router.registerRestInterface!R(ret, intf.subInterfaces[i].settings); 91 } 92 } 93 94 95 foreach (i, func; intf.RouteFunctions) { 96 auto route = intf.routes[i]; 97 98 // normal handler 99 auto handler = jsonMethodHandler!(func, i)(instance, intf); 100 101 auto diagparams = route.parameters.filter!(p => p.kind != ParameterKind.internal).map!(p => p.fieldName).array; 102 logDiagnostic("REST route: %s %s %s", route.method, route.fullPattern, diagparams); 103 router.match(route.method, route.fullPattern, handler); 104 } 105 106 // here we filter our already existing OPTIONS routes, so we don't overwrite whenever the user explicitly made his own OPTIONS route 107 auto routesGroupedByPattern = intf.getRoutesGroupedByPattern.filter!(rs => rs.all!(r => r.method != HTTPMethod.OPTIONS)); 108 109 foreach(routes; routesGroupedByPattern){ 110 auto route = routes.front; 111 auto handler = optionsMethodHandler(routes, settings); 112 113 auto diagparams = route.parameters.filter!(p => p.kind != ParameterKind.internal).map!(p => p.fieldName).array; 114 logDiagnostic("REST route: %s %s %s", HTTPMethod.OPTIONS, route.fullPattern, diagparams); 115 router.match(HTTPMethod.OPTIONS, route.fullPattern, handler); 116 } 117 return router; 118 } 119 120 /// ditto 121 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, MethodStyle style) 122 { 123 return registerRestInterface(router, instance, "/", style); 124 } 125 126 /// ditto 127 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, string url_prefix, 128 MethodStyle style = MethodStyle.lowerUnderscored) 129 { 130 auto settings = new RestInterfaceSettings; 131 if (!url_prefix.startsWith("/")) url_prefix = "/"~url_prefix; 132 settings.baseURL = URL("http://127.0.0.1"~url_prefix); 133 settings.methodStyle = style; 134 return registerRestInterface(router, instance, settings); 135 } 136 137 138 /** 139 This is a very limited example of REST interface features. Please refer to 140 the "rest" project in the "examples" folder for a full overview. 141 142 All details related to HTTP are inferred from the interface declaration. 143 */ 144 @safe unittest 145 { 146 @path("/") 147 interface IMyAPI 148 { 149 @safe: 150 // GET /api/greeting 151 @property string greeting(); 152 153 // PUT /api/greeting 154 @property void greeting(string text); 155 156 // POST /api/users 157 @path("/users") 158 void addNewUser(string name); 159 160 // GET /api/users 161 @property string[] users(); 162 163 // GET /api/:id/name 164 string getName(int id); 165 166 // GET /some_custom_json 167 Json getSomeCustomJson(); 168 } 169 170 // vibe.d takes care of all JSON encoding/decoding 171 // and actual API implementation can work directly 172 // with native types 173 174 class API : IMyAPI 175 { 176 private { 177 string m_greeting; 178 string[] m_users; 179 } 180 181 @property string greeting() { return m_greeting; } 182 @property void greeting(string text) { m_greeting = text; } 183 184 void addNewUser(string name) { m_users ~= name; } 185 186 @property string[] users() { return m_users; } 187 188 string getName(int id) { return m_users[id]; } 189 190 Json getSomeCustomJson() 191 { 192 Json ret = Json.emptyObject; 193 ret["somefield"] = "Hello, World!"; 194 return ret; 195 } 196 } 197 198 // actual usage, this is usually done in app.d module 199 // constructor 200 201 void static_this() 202 { 203 import vibe.http.server, vibe.http.router; 204 205 auto router = new URLRouter; 206 router.registerRestInterface(new API()); 207 listenHTTP(new HTTPServerSettings(), router); 208 } 209 } 210 211 212 /** 213 Returns a HTTP handler delegate that serves a JavaScript REST client. 214 */ 215 HTTPServerRequestDelegate serveRestJSClient(I)(RestInterfaceSettings settings) 216 if (is(I == interface)) 217 { 218 import std.digest.md : md5Of; 219 import std.digest.digest : toHexString; 220 import std.array : appender; 221 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 222 import vibe.http.status : HTTPStatus; 223 224 auto app = appender!string(); 225 generateRestJSClient!I(app, settings); 226 auto hash = app.data.md5Of.toHexString.idup; 227 228 void serve(HTTPServerRequest req, HTTPServerResponse res) 229 { 230 if (auto pv = "If-None-Match" in res.headers) { 231 res.statusCode = HTTPStatus.notModified; 232 res.writeVoidBody(); 233 return; 234 } 235 236 res.headers["Etag"] = hash; 237 res.writeBody(app.data, "application/javascript; charset=UTF-8"); 238 } 239 240 return &serve; 241 } 242 /// ditto 243 HTTPServerRequestDelegate serveRestJSClient(I)(URL base_url) 244 { 245 auto settings = new RestInterfaceSettings; 246 settings.baseURL = base_url; 247 return serveRestJSClient!I(settings); 248 } 249 /// ditto 250 HTTPServerRequestDelegate serveRestJSClient(I)(string base_url) 251 { 252 auto settings = new RestInterfaceSettings; 253 settings.baseURL = URL(base_url); 254 return serveRestJSClient!I(settings); 255 } 256 257 /// 258 unittest { 259 import vibe.http.server; 260 261 interface MyAPI { 262 string getFoo(); 263 void postBar(string param); 264 } 265 266 void test() 267 { 268 auto restsettings = new RestInterfaceSettings; 269 restsettings.baseURL = URL("http://api.example.org/"); 270 271 auto router = new URLRouter; 272 router.get("/myapi.js", serveRestJSClient!MyAPI(restsettings)); 273 //router.get("/myapi.js", serveRestJSClient!MyAPI(URL("http://api.example.org/"))); 274 //router.get("/myapi.js", serveRestJSClient!MyAPI("http://api.example.org/")); 275 //router.get("/", staticTemplate!"index.dt"); 276 277 listenHTTP(new HTTPServerSettings, router); 278 } 279 280 /* 281 index.dt: 282 html 283 head 284 title JS REST client test 285 script(src="myapi.js") 286 body 287 button(onclick="MyAPI.postBar('hello');") 288 */ 289 } 290 291 292 /** 293 Generates JavaScript code to access a REST interface from the browser. 294 */ 295 void generateRestJSClient(I, R)(ref R output, RestInterfaceSettings settings = null) 296 if (is(I == interface) && isOutputRange!(R, char)) 297 { 298 import hb.web.internal.rest.jsclient : generateInterface, JSRestClientSettings; 299 auto jsgenset = new JSRestClientSettings; 300 output.generateInterface!I(settings, jsgenset, true); 301 } 302 303 /// Writes a JavaScript REST client to a local .js file. 304 unittest { 305 import vibe.core.file; 306 307 interface MyAPI { 308 void getFoo(); 309 void postBar(string param); 310 } 311 312 void generateJSClientImpl() 313 { 314 import std.array : appender; 315 316 auto app = appender!string; 317 auto settings = new RestInterfaceSettings; 318 settings.baseURL = URL("http://localhost/"); 319 generateRestJSClient!MyAPI(app, settings); 320 } 321 322 generateJSClientImpl(); 323 } 324 325 326 /** 327 Implements the given interface by forwarding all public methods to a REST server. 328 329 The server must talk the same protocol as registerRestInterface() generates. Be sure to set 330 the matching method style for this. The RestInterfaceClient class will derive from the 331 interface that is passed as a template argument. It can be used as a drop-in replacement 332 of the real implementation of the API this way. 333 */ 334 class RestInterfaceClient(I) : I 335 { 336 import vibe.inet.url : URL, PathEntry; 337 import vibe.http.client : HTTPClientRequest; 338 import std.typetuple : staticMap; 339 340 private alias Info = RestInterface!I; 341 342 //pragma(msg, "imports for "~I.stringof~":"); 343 //pragma(msg, generateModuleImports!(I)()); 344 mixin(generateModuleImports!I()); 345 346 private { 347 // storing this struct directly causes a segfault when built with 348 // LDC 0.15.x, so we are using a pointer here: 349 RestInterface!I* m_intf; 350 RequestFilter m_requestFilter; 351 staticMap!(RestInterfaceClient, Info.SubInterfaceTypes) m_subInterfaces; 352 } 353 354 alias RequestFilter = void delegate(HTTPClientRequest req); 355 356 /** 357 Creates a new REST client implementation of $(D I). 358 */ 359 this(RestInterfaceSettings settings) 360 { 361 m_intf = new Info(settings, true); 362 363 foreach (i, SI; Info.SubInterfaceTypes) 364 m_subInterfaces[i] = new RestInterfaceClient!SI(m_intf.subInterfaces[i].settings); 365 } 366 367 /// ditto 368 this(string base_url, MethodStyle style = MethodStyle.lowerUnderscored) 369 { 370 this(URL(base_url), style); 371 } 372 373 /// ditto 374 this(URL base_url, MethodStyle style = MethodStyle.lowerUnderscored) 375 { 376 scope settings = new RestInterfaceSettings; 377 settings.baseURL = base_url; 378 settings.methodStyle = style; 379 this(settings); 380 } 381 382 /** 383 An optional request filter that allows to modify each request before it is made. 384 */ 385 final @property RequestFilter requestFilter() 386 { 387 return m_requestFilter; 388 } 389 390 /// ditto 391 final @property void requestFilter(RequestFilter v) 392 { 393 m_requestFilter = v; 394 foreach (i, SI; Info.SubInterfaceTypes) 395 m_subInterfaces[i].requestFilter = v; 396 } 397 398 //pragma(msg, "restinterface:"); 399 mixin(generateRestClientMethods!I()); 400 401 protected { 402 import vibe.data.json : Json; 403 import vibe.textfilter.urlencode; 404 405 /** 406 * Perform a request to the interface using the given parameters. 407 * 408 * Params: 409 * verb = Kind of request (See $(D HTTPMethod) enum). 410 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 411 * it will be '/rejectedsoftware/vibe.d/issues'. 412 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 413 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 414 * it will be 'author%3ASantaClaus'. 415 * body_ = The body to send, as a string. If a Content-Type is present in $(D hdrs), it will be used, otherwise it will default to 416 * the generic type "application/json". 417 * reqReturnHdrs = A map of required return headers. 418 * To avoid returning unused headers, nothing is written 419 * to this structure unless there's an (usually empty) 420 * entry (= the key exists) with the same key. 421 * If any key present in `reqReturnHdrs` is not present 422 * in the response, an Exception is thrown. 423 * optReturnHdrs = A map of optional return headers. 424 * This behaves almost as exactly as reqReturnHdrs, 425 * except that non-existent key in the response will 426 * not cause it to throw, but rather to set this entry 427 * to 'null'. 428 * 429 * Returns: 430 * The Json object returned by the request 431 */ 432 Json request(HTTPMethod verb, string name, 433 in ref InetHeaderMap hdrs, string query, string body_, 434 ref InetHeaderMap reqReturnHdrs, 435 ref InetHeaderMap optReturnHdrs) const 436 { 437 auto path = URL(m_intf.baseURL).pathString; 438 439 if (name.length) 440 { 441 if (path.length && path[$ - 1] == '/' && name[0] == '/') 442 path ~= name[1 .. $]; 443 else if (path.length && path[$ - 1] == '/' || name[0] == '/') 444 path ~= name; 445 else 446 path ~= '/' ~ name; 447 } 448 449 auto httpsettings = m_intf.settings.httpClientSettings; 450 451 return .request(URL(m_intf.baseURL), m_requestFilter, verb, path, 452 hdrs, query, body_, reqReturnHdrs, optReturnHdrs, httpsettings); 453 } 454 } 455 } 456 457 /// 458 unittest 459 { 460 interface IMyApi 461 { 462 // GET /status 463 string getStatus(); 464 465 // GET /greeting 466 @property string greeting(); 467 // PUT /greeting 468 @property void greeting(string text); 469 470 // POST /new_user 471 void addNewUser(string name); 472 // GET /users 473 @property string[] users(); 474 // GET /:id/name 475 string getName(int id); 476 477 Json getSomeCustomJson(); 478 } 479 480 void test() 481 { 482 auto api = new RestInterfaceClient!IMyApi("http://127.0.0.1/api/"); 483 484 logInfo("Status: %s", api.getStatus()); 485 api.greeting = "Hello, World!"; 486 logInfo("Greeting message: %s", api.greeting); 487 api.addNewUser("Peter"); 488 api.addNewUser("Igor"); 489 logInfo("Users: %s", api.users); 490 logInfo("First user name: %s", api.getName(0)); 491 } 492 } 493 494 495 /** 496 Encapsulates settings used to customize the generated REST interface. 497 */ 498 class RestInterfaceSettings { 499 /** The public URL below which the REST interface is registered. 500 */ 501 URL baseURL; 502 503 /** List of allowed origins for CORS 504 505 Empty list is interpreted as allowing all origins (e.g. *) 506 */ 507 string[] allowedOrigins; 508 509 /** Naming convention used for the generated URLs. 510 */ 511 MethodStyle methodStyle = MethodStyle.lowerUnderscored; 512 513 /** Ignores a trailing underscore in method and function names. 514 515 With this setting set to $(D true), it's possible to use names in the 516 REST interface that are reserved words in D. 517 */ 518 bool stripTrailingUnderscore = true; 519 520 /// Overrides the default HTTP client settings used by the `RestInterfaceClient`. 521 HTTPClientSettings httpClientSettings; 522 523 @property RestInterfaceSettings dup() 524 const @safe { 525 auto ret = new RestInterfaceSettings; 526 ret.baseURL = this.baseURL; 527 ret.methodStyle = this.methodStyle; 528 ret.stripTrailingUnderscore = this.stripTrailingUnderscore; 529 ret.allowedOrigins = this.allowedOrigins.dup; 530 return ret; 531 } 532 } 533 534 535 /** 536 Models REST collection interfaces using natural D syntax. 537 538 Use this type as the return value of a REST interface getter method/property 539 to model a collection of objects. `opIndex` is used to make the individual 540 entries accessible using the `[index]` syntax. Nested collections are 541 supported. 542 543 The interface `I` needs to define a struct named `CollectionIndices`. The 544 members of this struct denote the types and names of the indexes that lead 545 to a particular resource. If a collection is nested within another 546 collection, the order of these members must match the nesting order 547 (outermost first). 548 549 The parameter list of all of `I`'s methods must begin with all but the last 550 entry in `CollectionIndices`. Methods that also match the last entry will be 551 considered methods of a collection item (`collection[index].method()`), 552 wheres all other methods will be considered methods of the collection 553 itself (`collection.method()`). 554 555 The name of the index parameters affects the default path of a method's 556 route. Normal parameter names will be subject to the same rules as usual 557 routes (see `registerRestInterface`) and will be mapped to query or form 558 parameters at the protocol level. Names starting with an underscore will 559 instead be mapped to path placeholders. For example, 560 `void getName(int __item_id)` will be mapped to a GET request to the 561 path `":item_id/name"`. 562 */ 563 struct Collection(I) 564 if (is(I == interface)) 565 { 566 import std.typetuple; 567 568 static assert(is(I.CollectionIndices == struct), "Collection interfaces must define a CollectionIndices struct."); 569 570 alias Interface = I; 571 alias AllIDs = TypeTuple!(typeof(I.CollectionIndices.tupleof)); 572 alias AllIDNames = FieldNameTuple!(I.CollectionIndices); 573 static assert(AllIDs.length >= 1, I.stringof~".CollectionIndices must define at least one member."); 574 static assert(AllIDNames.length == AllIDs.length); 575 alias ItemID = AllIDs[$-1]; 576 alias ParentIDs = AllIDs[0 .. $-1]; 577 alias ParentIDNames = AllIDNames[0 .. $-1]; 578 579 private { 580 I m_interface; 581 ParentIDs m_parentIDs; 582 } 583 584 /** Constructs a new collection instance that is tied to a particular 585 parent collection entry. 586 587 Params: 588 api = The target interface imstance to be mapped as a collection 589 pids = The indexes of all collections in which this collection is 590 nested (if any) 591 */ 592 this(I api, ParentIDs pids) 593 { 594 m_interface = api; 595 m_parentIDs = pids; 596 } 597 598 static struct Item { 599 private { 600 I m_interface; 601 AllIDs m_id; 602 } 603 604 this(I api, AllIDs id) 605 { 606 m_interface = api; 607 m_id = id; 608 } 609 610 // forward all item methods 611 mixin(() { 612 string ret; 613 foreach (m; __traits(allMembers, I)) { 614 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 615 alias PT = ParameterTypeTuple!ovrld; 616 static if (matchesAllIDs!ovrld) 617 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_id, args); }\n"; 618 } 619 } 620 return ret; 621 } ()); 622 } 623 624 // Note: the example causes a recursive template instantiation if done as a documented unit test: 625 /** Accesses a single collection entry. 626 627 Example: 628 --- 629 interface IMain { 630 @property Collection!IItem items(); 631 } 632 633 interface IItem { 634 struct CollectionIndices { 635 int _itemID; 636 } 637 638 @method(HTTPMethod.GET) 639 string name(int _itemID); 640 } 641 642 void test(IMain main) 643 { 644 auto item_name = main.items[23].name; // equivalent to IItem.name(23) 645 } 646 --- 647 */ 648 Item opIndex(ItemID id) 649 { 650 return Item(m_interface, m_parentIDs, id); 651 } 652 653 // forward all non-item methods 654 mixin(() { 655 string ret; 656 foreach (m; __traits(allMembers, I)) { 657 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 658 alias PT = ParameterTypeTuple!ovrld; 659 static if (!matchesAllIDs!ovrld) { 660 static assert(matchesParentIDs!ovrld, 661 "Collection methods must take all parent IDs as the first parameters."~PT.stringof~" "~ParentIDs.stringof); 662 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_parentIDs, args); }\n"; 663 } 664 } 665 } 666 return ret; 667 } ()); 668 669 private template matchesParentIDs(alias func) { 670 static if (is(ParameterTypeTuple!func[0 .. ParentIDs.length] == ParentIDs)) { 671 static if (ParentIDNames.length == 0) enum matchesParentIDs = true; 672 else static if (ParameterIdentifierTuple!func[0 .. ParentIDNames.length] == ParentIDNames) 673 enum matchesParentIDs = true; 674 else enum matchesParentIDs = false; 675 } else enum matchesParentIDs = false; 676 } 677 678 private template matchesAllIDs(alias func) { 679 static if (is(ParameterTypeTuple!func[0 .. AllIDs.length] == AllIDs)) { 680 static if (ParameterIdentifierTuple!func[0 .. AllIDNames.length] == AllIDNames) 681 enum matchesAllIDs = true; 682 else enum matchesAllIDs = false; 683 } else enum matchesAllIDs = false; 684 } 685 } 686 687 /// Model two nested collections using path based indexes 688 unittest { 689 // 690 // API definition 691 // 692 interface SubItemAPI { 693 // Define the index path that leads to a sub item 694 struct CollectionIndices { 695 // The ID of the base item. This must match the definition in 696 // ItemAPI.CollectionIndices 697 string _item; 698 // The index if the sub item 699 int _index; 700 } 701 702 // GET /items/:item/subItems/length 703 @property int length(string _item); 704 705 // GET /items/:item/subItems/:index/squared_position 706 int getSquaredPosition(string _item, int _index); 707 } 708 709 interface ItemAPI { 710 // Define the index that identifies an item 711 struct CollectionIndices { 712 string _item; 713 } 714 715 // base path /items/:item/subItems 716 Collection!SubItemAPI subItems(string _item); 717 718 // GET /items/:item/name 719 @property string name(string _item); 720 } 721 722 interface API { 723 // a collection of items at the base path /items/ 724 Collection!ItemAPI items(); 725 } 726 727 // 728 // Local API implementation 729 // 730 class SubItemAPIImpl : SubItemAPI { 731 @property int length(string _item) { return 10; } 732 733 int getSquaredPosition(string _item, int _index) { return _index ^^ 2; } 734 } 735 736 class ItemAPIImpl : ItemAPI { 737 private SubItemAPIImpl m_subItems; 738 739 this() { m_subItems = new SubItemAPIImpl; } 740 741 Collection!SubItemAPI subItems(string _item) { return Collection!SubItemAPI(m_subItems, _item); } 742 743 string name(string _item) { return _item; } 744 } 745 746 class APIImpl : API { 747 private ItemAPIImpl m_items; 748 749 this() { m_items = new ItemAPIImpl; } 750 751 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 752 } 753 754 // 755 // Resulting API usage 756 // 757 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 758 assert(api.items["foo"].name == "foo"); 759 assert(api.items["foo"].subItems.length == 10); 760 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 761 } 762 763 unittest { 764 interface I { 765 struct CollectionIndices { 766 int id1; 767 string id2; 768 } 769 770 void a(int id1, string id2); 771 void b(int id1, int id2); 772 void c(int id1, string p); 773 void d(int id1, string id2, int p); 774 void e(int id1, int id2, int p); 775 void f(int id1, string p, int q); 776 } 777 778 Collection!I coll; 779 static assert(is(typeof(coll["x"].a()) == void)); 780 static assert(is(typeof(coll.b(42)) == void)); 781 static assert(is(typeof(coll.c("foo")) == void)); 782 static assert(is(typeof(coll["x"].d(42)) == void)); 783 static assert(is(typeof(coll.e(42, 42)) == void)); 784 static assert(is(typeof(coll.f("foo", 42)) == void)); 785 } 786 787 /// Model two nested collections using normal query parameters as indexes 788 unittest { 789 // 790 // API definition 791 // 792 interface SubItemAPI { 793 // Define the index path that leads to a sub item 794 struct CollectionIndices { 795 // The ID of the base item. This must match the definition in 796 // ItemAPI.CollectionIndices 797 string item; 798 // The index if the sub item 799 int index; 800 } 801 802 // GET /items/subItems/length?item=... 803 @property int length(string item); 804 805 // GET /items/subItems/squared_position?item=...&index=... 806 int getSquaredPosition(string item, int index); 807 } 808 809 interface ItemAPI { 810 // Define the index that identifies an item 811 struct CollectionIndices { 812 string item; 813 } 814 815 // base path /items/subItems?item=... 816 Collection!SubItemAPI subItems(string item); 817 818 // GET /items/name?item=... 819 @property string name(string item); 820 } 821 822 interface API { 823 // a collection of items at the base path /items/ 824 Collection!ItemAPI items(); 825 } 826 827 // 828 // Local API implementation 829 // 830 class SubItemAPIImpl : SubItemAPI { 831 @property int length(string item) { return 10; } 832 833 int getSquaredPosition(string item, int index) { return index ^^ 2; } 834 } 835 836 class ItemAPIImpl : ItemAPI { 837 private SubItemAPIImpl m_subItems; 838 839 this() { m_subItems = new SubItemAPIImpl; } 840 841 Collection!SubItemAPI subItems(string item) { return Collection!SubItemAPI(m_subItems, item); } 842 843 string name(string item) { return item; } 844 } 845 846 class APIImpl : API { 847 private ItemAPIImpl m_items; 848 849 this() { m_items = new ItemAPIImpl; } 850 851 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 852 } 853 854 // 855 // Resulting API usage 856 // 857 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 858 assert(api.items["foo"].name == "foo"); 859 assert(api.items["foo"].subItems.length == 10); 860 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 861 } 862 863 unittest { 864 interface C { 865 struct CollectionIndices { 866 int _ax; 867 int _b; 868 } 869 void testB(int _ax, int _b); 870 } 871 872 interface B { 873 struct CollectionIndices { 874 int _a; 875 } 876 Collection!C c(); 877 void testA(int _a); 878 } 879 880 interface A { 881 Collection!B b(); 882 } 883 884 static assert (!is(typeof(A.init.b[1].c[2].testB()))); 885 } 886 887 /** Allows processing the server request/response before the handler method is called. 888 889 Note that this attribute is only used by `registerRestInterface`, but not 890 by the client generators. This attribute expects the name of a parameter that 891 will receive its return value. 892 893 Writing to the response body from within the specified hander function 894 causes any further processing of the request to be skipped. In particular, 895 the route handler method will not be called. 896 897 Note: 898 The example shows the drawback of this attribute. It generally is a 899 leaky abstraction that propagates to the base interface. For this 900 reason the use of this attribute is not recommended, unless there is 901 no suitable alternative. 902 */ 903 alias before = vibe.internal.meta.funcattr.before; 904 905 /// 906 unittest { 907 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 908 909 interface MyService { 910 long getHeaderCount(size_t foo = 0); 911 } 912 913 size_t handler(HTTPServerRequest req, HTTPServerResponse res) 914 { 915 return req.headers.length; 916 } 917 918 class MyServiceImpl : MyService { 919 // the "foo" parameter will receive the number of request headers 920 @before!handler("foo") 921 long getHeaderCount(size_t foo) 922 { 923 return foo; 924 } 925 } 926 927 void test(URLRouter router) 928 { 929 router.registerRestInterface(new MyServiceImpl); 930 } 931 } 932 933 934 /** Allows processing the return value of a handler method and the request/response objects. 935 936 The value returned by the REST API will be the value returned by the last 937 `@after` handler, which allows to post process the results of the handler 938 method. 939 940 Writing to the response body from within the specified handler function 941 causes any further processing of the request ot be skipped, including 942 any other `@after` annotations and writing the result value. 943 */ 944 alias after = vibe.internal.meta.funcattr.after; 945 946 /// 947 unittest { 948 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 949 950 interface MyService { 951 long getMagic(); 952 } 953 954 long handler(long ret, HTTPServerRequest req, HTTPServerResponse res) 955 { 956 return ret * 2; 957 } 958 959 class MyServiceImpl : MyService{ 960 // the result reported by the REST API will be 42 961 @after!handler 962 long getMagic() 963 { 964 return 21; 965 } 966 } 967 968 void test(URLRouter router) 969 { 970 router.registerRestInterface(new MyServiceImpl); 971 } 972 } 973 974 /** 975 * Generate an handler that will wrap the server's method 976 * 977 * This function returns an handler, generated at compile time, that 978 * will deserialize the parameters, pass them to the function implemented 979 * by the user, and return what it needs to return, be it header parameters 980 * or body, which is at the moment either a pure string or a Json object. 981 * 982 * One thing that makes this method more complex that it needs be is the 983 * inability for D to attach UDA to parameters. This means we have to roll 984 * our own implementation, which tries to be as easy to use as possible. 985 * We'll require the user to give the name of the parameter as a string to 986 * our UDA. Hopefully, we're also able to detect at compile time if the user 987 * made a typo of any kind (see $(D genInterfaceValidationError)). 988 * 989 * Note: 990 * Lots of abbreviations are used to ease the code, such as 991 * PTT (ParameterTypeTuple), WPAT (WebParamAttributeTuple) 992 * and PWPAT (ParameterWebParamAttributeTuple). 993 * 994 * Params: 995 * T = type of the object which represent the REST server (user implemented). 996 * Func = An alias to the function of $(D T) to wrap. 997 * 998 * inst = REST server on which to call our $(D Func). 999 * settings = REST server configuration. 1000 * 1001 * Returns: 1002 * A delegate suitable to use as an handler for an HTTP request. 1003 */ 1004 private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(T inst, ref RestInterface!T intf) 1005 { 1006 import std.string : format; 1007 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 1008 import vibe.http.common : HTTPStatusException, HTTPStatus, enforceBadRequest; 1009 import vibe.utils.string : sanitizeUTF8; 1010 import hb.web.internal.rest.common : ParameterKind; 1011 import vibe.internal.meta.funcattr : IsAttributedParameter, computeAttributedParameterCtx; 1012 import vibe.textfilter.urlencode : urlDecode; 1013 1014 enum Method = __traits(identifier, Func); 1015 alias PTypes = ParameterTypeTuple!Func; 1016 alias PDefaults = ParameterDefaultValueTuple!Func; 1017 alias RT = ReturnType!(FunctionTypeOf!Func); 1018 static const sroute = RestInterface!T.staticRoutes[ridx]; 1019 auto route = intf.routes[ridx]; 1020 auto settings = intf.settings; 1021 1022 void handler(HTTPServerRequest req, HTTPServerResponse res) 1023 @safe { 1024 if (route.bodyParameters.length) { 1025 logDebug("BODYPARAMS: %s %s", Method, route.bodyParameters.length); 1026 /*enforceBadRequest(req.contentType == "application/json", 1027 "The Content-Type header needs to be set to application/json.");*/ 1028 enforceBadRequest(req.json.type != Json.Type.undefined, 1029 "The request body does not contain a valid JSON value."); 1030 enforceBadRequest(req.json.type == Json.Type.object, 1031 "The request body must contain a JSON object with an entry for each parameter."); 1032 } 1033 1034 static if (isAuthenticated!(T, Func)) { 1035 auto auth_info = handleAuthentication!Func(inst, req, res); 1036 if (res.headerWritten) return; 1037 } 1038 1039 PTypes params; 1040 1041 enum bool hasSingleBodyParam = { 1042 int i; 1043 foreach (j, PT; PTypes) 1044 if (sroute.parameters[j].kind == ParameterKind.body_) 1045 i++; 1046 1047 return i == 1; 1048 }(); 1049 1050 foreach (i, PT; PTypes) { 1051 enum sparam = sroute.parameters[i]; 1052 enum pname = sparam.name; 1053 auto fieldname = route.parameters[i].fieldName; 1054 static if (isInstanceOf!(Nullable, PT)) PT v; 1055 else Nullable!PT v; 1056 1057 static if (sparam.kind == ParameterKind.auth) { 1058 v = auth_info; 1059 } else static if (sparam.kind == ParameterKind.query) { 1060 if (auto pv = fieldname in req.query) 1061 v = fromRestString!PT(*pv); 1062 } else static if (sparam.kind == ParameterKind.body_) { 1063 try { 1064 // for @bodyParam("s") and by default the entire body should be serialized 1065 if (sparam.fieldName == bodyParamWholeName || hasSingleBodyParam && sparam.fieldName.length == 0) 1066 v = deserializeJson!PT(req.json); 1067 else if (auto pv = fieldname in req.json) 1068 v = deserializeJson!PT(*pv); 1069 } catch (JSONException e) 1070 enforceBadRequest(false, e.msg); 1071 } else static if (sparam.kind == ParameterKind.header) { 1072 if (auto pv = fieldname in req.headers) 1073 v = fromRestString!PT(*pv); 1074 } else static if (sparam.kind == ParameterKind.attributed) { 1075 static if (!__traits(compiles, () @safe { computeAttributedParameterCtx!(Func, pname)(inst, req, res); } ())) 1076 pragma(msg, "Non-@safe @before evaluators are deprecated - annotate evaluator function for parameter "~pname~" of "~T.stringof~"."~Method~" as @safe."); 1077 v = () @trusted { return computeAttributedParameterCtx!(Func, pname)(inst, req, res); } (); 1078 } else static if (sparam.kind == ParameterKind.internal) { 1079 if (auto pv = fieldname in req.params) 1080 v = fromRestString!PT(urlDecode(*pv)); 1081 } else static assert(false, "Unhandled parameter kind."); 1082 1083 static if (isInstanceOf!(Nullable, PT)) params[i] = v; 1084 else if (v.isNull()) { 1085 static if (!is(PDefaults[i] == void)) params[i] = PDefaults[i]; 1086 else enforceBadRequest(false, "Missing non-optional "~sparam.kind.to!string~" parameter '"~(fieldname.length?fieldname:sparam.name)~"'."); 1087 } else params[i] = v; 1088 } 1089 1090 static if (isAuthenticated!(T, Func)) 1091 handleAuthorization!(T, Func, params)(auth_info); 1092 1093 void handleCors() 1094 { 1095 import std.algorithm : any; 1096 import std.uni : sicmp; 1097 1098 if (req.method == HTTPMethod.OPTIONS) 1099 return; 1100 auto origin = "Origin" in req.headers; 1101 if (origin is null) 1102 return; 1103 1104 if (settings.allowedOrigins.length != 0 && 1105 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1106 return; 1107 1108 res.headers["Access-Control-Allow-Origin"] = *origin; 1109 res.headers["Access-Control-Allow-Credentials"] = "true"; 1110 } 1111 // Anti copy-paste 1112 void returnHeaders() 1113 { 1114 handleCors(); 1115 foreach (i, P; PTypes) { 1116 static if (sroute.parameters[i].isOut) { 1117 static assert (sroute.parameters[i].kind == ParameterKind.header); 1118 static if (isInstanceOf!(Nullable, typeof(params[i]))) { 1119 if (!params[i].isNull) 1120 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1121 } else { 1122 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1123 } 1124 } 1125 } 1126 } 1127 1128 try { 1129 import vibe.internal.meta.funcattr; 1130 1131 static if (!__traits(compiles, () @safe { __traits(getMember, inst, Method)(params); })) 1132 pragma(msg, "Non-@safe methods are deprecated in REST interfaces - Mark "~T.stringof~"."~Method~" as @safe."); 1133 1134 static if (is(RT == void)) { 1135 () @trusted { __traits(getMember, inst, Method)(params); } (); // TODO: remove after deprecation period 1136 returnHeaders(); 1137 res.writeVoidBody(); 1138 } else { 1139 auto ret = () @trusted { return __traits(getMember, inst, Method)(params); } (); // TODO: remove after deprecation period 1140 1141 static if (!__traits(compiles, () @safe { evaluateOutputModifiers!Func(ret, req, res); } ())) 1142 pragma(msg, "Non-@safe @after evaluators are deprecated - annotate @after evaluator function for "~T.stringof~"."~Method~" as @safe."); 1143 1144 ret = () @trusted { return evaluateOutputModifiers!Func(ret, req, res); } (); 1145 returnHeaders(); 1146 debug res.writePrettyJsonBody(ret); 1147 else res.writeJsonBody(ret); 1148 } 1149 } catch (HTTPStatusException e) { 1150 if (res.headerWritten) 1151 logDebug("Response already started when a HTTPStatusException was thrown. Client will not receive the proper error code (%s)!", e.status); 1152 else { 1153 returnHeaders(); 1154 res.writeJsonBody([ "statusMessage": e.msg ], e.status); 1155 } 1156 } catch (Exception e) { 1157 // TODO: better error description! 1158 logDebug("REST handler exception: %s", () @trusted { return e.toString(); } ()); 1159 if (res.headerWritten) logDebug("Response already started. Client will not receive an error code!"); 1160 else 1161 { 1162 returnHeaders(); 1163 debug res.writeJsonBody( 1164 [ "statusMessage": e.msg, "statusDebugMessage": () @trusted { return sanitizeUTF8(cast(ubyte[])e.toString()); } () ], 1165 HTTPStatus.internalServerError 1166 ); 1167 else res.writeJsonBody(["statusMessage": e.msg], HTTPStatus.internalServerError); 1168 } 1169 } 1170 } 1171 1172 return &handler; 1173 } 1174 1175 /** 1176 * Generate an handler that will wrap the server's method 1177 * 1178 * This function returns an handler that handles the http OPTIONS method. 1179 * 1180 * It will return the ALLOW header with all the methods on this resource 1181 * And it will handle Preflight CORS. 1182 * 1183 * Params: 1184 * routes = a range of Routes were each route has the same resource/URI 1185 * just different method. 1186 * settings = REST server configuration. 1187 * 1188 * Returns: 1189 * A delegate suitable to use as an handler for an HTTP request. 1190 */ 1191 private HTTPServerRequestDelegate optionsMethodHandler(RouteRange)(RouteRange routes, RestInterfaceSettings settings = null) 1192 { 1193 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 1194 import std.algorithm : map, joiner, any; 1195 import std.conv : text; 1196 import std.array : array; 1197 import vibe.http.common : httpMethodString, httpMethodFromString; 1198 // NOTE: don't know what is better, to keep this in memory, or generate on each request 1199 auto allow = routes.map!(r => r.method.httpMethodString).joiner(",").text(); 1200 auto methods = routes.map!(r => r.method).array(); 1201 1202 void handlePreflightedCors(HTTPServerRequest req, HTTPServerResponse res, ref HTTPMethod[] methods, RestInterfaceSettings settings = null) 1203 { 1204 import std.algorithm : among; 1205 import std.uni : sicmp; 1206 1207 auto origin = "Origin" in req.headers; 1208 if (origin is null) 1209 return; 1210 1211 if (settings !is null && 1212 settings.allowedOrigins.length != 0 && 1213 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1214 return; 1215 1216 auto method = "Access-Control-Request-Method" in req.headers; 1217 if (method is null) 1218 return; 1219 1220 auto httpMethod = httpMethodFromString(*method); 1221 1222 if (!methods.any!(m => m == httpMethod)) 1223 return; 1224 1225 res.headers["Access-Control-Allow-Origin"] = *origin; 1226 1227 // there is no way to know if the specific resource supports credentials 1228 // (either cookies, HTTP authentication, or client-side SSL certificates), 1229 // so we always assume it does 1230 res.headers["Access-Control-Allow-Credentials"] = "true"; 1231 res.headers["Access-Control-Max-Age"] = "1728000"; 1232 res.headers["Access-Control-Allow-Methods"] = *method; 1233 1234 // we have no way to reliably determine what headers the resource allows 1235 // so we simply copy whatever the client requested 1236 if (auto headers = "Access-Control-Request-Headers" in req.headers) 1237 res.headers["Access-Control-Allow-Headers"] = *headers; 1238 } 1239 1240 void handler(HTTPServerRequest req, HTTPServerResponse res) 1241 { 1242 // since this is a OPTIONS request, we have to return the ALLOW headers to tell which methods we have 1243 res.headers["Allow"] = allow; 1244 1245 // handle CORS preflighted requests 1246 handlePreflightedCors(req,res,methods,settings); 1247 1248 // NOTE: besides just returning the allowed methods and handling CORS preflighted requests, 1249 // this would be a nice place to describe what kind of resources are on this route, 1250 // the params each accepts, the headers, etc... think WSDL but then for REST. 1251 res.writeBody(""); 1252 } 1253 return &handler; 1254 } 1255 1256 private string generateRestClientMethods(I)() 1257 { 1258 import std.array : join; 1259 import std.string : format; 1260 import std.traits : fullyQualifiedName, isInstanceOf; 1261 1262 alias Info = RestInterface!I; 1263 1264 string ret = q{ 1265 import vibe.internal.meta.codegen : CloneFunction; 1266 }; 1267 1268 // generate sub interface methods 1269 foreach (i, SI; Info.SubInterfaceTypes) { 1270 alias F = Info.SubInterfaceFunctions[i]; 1271 alias RT = ReturnType!F; 1272 alias ParamNames = ParameterIdentifierTuple!F; 1273 static if (ParamNames.length == 0) enum pnames = ""; 1274 else enum pnames = ", " ~ [ParamNames].join(", "); 1275 static if (isInstanceOf!(Collection, RT)) { 1276 ret ~= q{ 1277 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1278 return Collection!(%2$s)(m_subInterfaces[%1$s]%3$s); 1279 }); 1280 }.format(i, fullyQualifiedName!SI, pnames); 1281 } else { 1282 ret ~= q{ 1283 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1284 return m_subInterfaces[%1$s]; 1285 }); 1286 }.format(i); 1287 } 1288 } 1289 1290 // generate route methods 1291 foreach (i, F; Info.RouteFunctions) { 1292 alias ParamNames = ParameterIdentifierTuple!F; 1293 static if (ParamNames.length == 0) enum pnames = ""; 1294 else enum pnames = ", " ~ [ParamNames].join(", "); 1295 1296 ret ~= q{ 1297 mixin CloneFunction!(Info.RouteFunctions[%1$s], q{ 1298 return executeClientMethod!(I, %1$s%2$s)(*m_intf, m_requestFilter); 1299 }); 1300 }.format(i, pnames); 1301 } 1302 1303 return ret; 1304 } 1305 1306 1307 private auto executeClientMethod(I, size_t ridx, ARGS...) 1308 (in ref RestInterface!I intf, void delegate(HTTPClientRequest) request_filter) 1309 { 1310 import hb.web.internal.rest.common : ParameterKind; 1311 import vibe.textfilter.urlencode : filterURLEncode, urlEncode; 1312 import std.array : appender; 1313 1314 alias Info = RestInterface!I; 1315 alias Func = Info.RouteFunctions[ridx]; 1316 alias RT = ReturnType!Func; 1317 alias PTT = ParameterTypeTuple!Func; 1318 enum sroute = Info.staticRoutes[ridx]; 1319 auto route = intf.routes[ridx]; 1320 1321 InetHeaderMap headers; 1322 InetHeaderMap reqhdrs; 1323 InetHeaderMap opthdrs; 1324 1325 string url_prefix; 1326 1327 auto query = appender!string(); 1328 auto jsonBody = Json.emptyObject; 1329 string body_; 1330 1331 enum bool hasSingleBodyParam = { 1332 int i; 1333 foreach (j, PT; PTT) 1334 if (sroute.parameters[j].kind == ParameterKind.body_) 1335 i++; 1336 1337 return i == 1; 1338 }(); 1339 1340 void addQueryParam(size_t i)(string name) 1341 { 1342 if (query.data.length) query.put('&'); 1343 query.filterURLEncode(name); 1344 query.put("="); 1345 static if (is(PT == Json)) 1346 query.filterURLEncode(ARGS[i].toString()); 1347 else // Note: CTFE triggers compiler bug here (think we are returning Json, not string). 1348 query.filterURLEncode(toRestString(serializeToJson(ARGS[i]))); 1349 } 1350 1351 foreach (i, PT; PTT) { 1352 enum sparam = sroute.parameters[i]; 1353 auto fieldname = route.parameters[i].fieldName; 1354 static if (sparam.kind == ParameterKind.query) { 1355 addQueryParam!i(fieldname); 1356 } else static if (sparam.kind == ParameterKind.body_) { 1357 // for @bodyParam("s") and by default the entire body should be serialized 1358 if (sparam.fieldName == bodyParamWholeName || hasSingleBodyParam && sparam.fieldName.length == 0) 1359 jsonBody = serializeToJson(ARGS[i]); 1360 else 1361 jsonBody[fieldname] = serializeToJson(ARGS[i]); 1362 } else static if (sparam.kind == ParameterKind.header) { 1363 // Don't send 'out' parameter, as they should be default init anyway and it might confuse some server 1364 static if (sparam.isIn) { 1365 static if (isInstanceOf!(Nullable, PT)) { 1366 if (!ARGS[i].isNull) 1367 headers[fieldname] = to!string(ARGS[i]); 1368 } else headers[fieldname] = to!string(ARGS[i]); 1369 } 1370 static if (sparam.isOut) { 1371 // Optional parameter 1372 static if (isInstanceOf!(Nullable, PT)) { 1373 opthdrs[fieldname] = null; 1374 } else { 1375 reqhdrs[fieldname] = null; 1376 } 1377 } 1378 } 1379 } 1380 1381 static if (sroute.method == HTTPMethod.GET) { 1382 assert(jsonBody == Json.emptyObject, "GET request trying to send body parameters."); 1383 } else { 1384 debug body_ = jsonBody.toPrettyString(); 1385 else body_ = jsonBody.toString(); 1386 } 1387 1388 string url; 1389 foreach (i, p; route.fullPathParts) { 1390 if (p.isParameter) { 1391 switch (p.text) { 1392 foreach (j, PT; PTT) { 1393 case sroute.parameters[j].name: 1394 url ~= urlEncode(toRestString(serializeToJson(ARGS[j]))); 1395 goto sbrk; 1396 } 1397 default: url ~= ":" ~ p.text; break; 1398 } 1399 sbrk:; 1400 } else url ~= p.text; 1401 } 1402 1403 scope (exit) { 1404 foreach (i, PT; PTT) { 1405 enum sparam = sroute.parameters[i]; 1406 auto fieldname = route.parameters[i].fieldName; 1407 static if (sparam.kind == ParameterKind.header) { 1408 static if (sparam.isOut) { 1409 static if (isInstanceOf!(Nullable, PT)) { 1410 ARGS[i] = to!(TemplateArgsOf!PT)( 1411 opthdrs.get(fieldname, null)); 1412 } else { 1413 if (auto ptr = fieldname in reqhdrs) 1414 ARGS[i] = to!PT(*ptr); 1415 } 1416 } 1417 } 1418 } 1419 } 1420 1421 auto jret = request(URL(intf.baseURL), request_filter, sroute.method, url, headers, query.data, body_, reqhdrs, opthdrs, intf.settings.httpClientSettings); 1422 1423 static if (!is(RT == void)) 1424 return deserializeJson!RT(jret); 1425 } 1426 1427 1428 import vibe.http.client : HTTPClientRequest; 1429 /** 1430 * Perform a request to the interface using the given parameters. 1431 * 1432 * Params: 1433 * verb = Kind of request (See $(D HTTPMethod) enum). 1434 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1435 * it will be '/rejectedsoftware/vibe.d/issues'. 1436 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 1437 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1438 * it will be 'author%3ASantaClaus'. 1439 * body_ = The body to send, as a string. If a Content-Type is present in $(D hdrs), it will be used, otherwise it will default to 1440 * the generic type "application/json". 1441 * reqReturnHdrs = A map of required return headers. 1442 * To avoid returning unused headers, nothing is written 1443 * to this structure unless there's an (usually empty) 1444 * entry (= the key exists) with the same key. 1445 * If any key present in `reqReturnHdrs` is not present 1446 * in the response, an Exception is thrown. 1447 * optReturnHdrs = A map of optional return headers. 1448 * This behaves almost as exactly as reqReturnHdrs, 1449 * except that non-existent key in the response will 1450 * not cause it to throw, but rather to set this entry 1451 * to 'null'. 1452 * 1453 * Returns: 1454 * The Json object returned by the request 1455 */ 1456 private Json request(URL base_url, 1457 void delegate(HTTPClientRequest) request_filter, HTTPMethod verb, 1458 string path, in ref InetHeaderMap hdrs, string query, string body_, 1459 ref InetHeaderMap reqReturnHdrs, ref InetHeaderMap optReturnHdrs, 1460 in HTTPClientSettings http_settings) 1461 { 1462 import vibe.http.client : HTTPClientRequest, HTTPClientResponse, requestHTTP; 1463 import vibe.http.common : HTTPStatusException, HTTPStatus, httpMethodString, httpStatusText; 1464 import vibe.inet.url : Path; 1465 1466 URL url = base_url; 1467 url.pathString = path; 1468 1469 if (query.length) url.queryString = query; 1470 1471 Json ret; 1472 1473 auto reqdg = (scope HTTPClientRequest req) { 1474 req.method = verb; 1475 foreach (k, v; hdrs) 1476 req.headers[k] = v; 1477 1478 if (request_filter) request_filter(req); 1479 1480 if (body_ != "") 1481 req.writeBody(cast(ubyte[])body_, hdrs.get("Content-Type", "application/json")); 1482 }; 1483 1484 auto resdg = (scope HTTPClientResponse res) { 1485 if (!res.bodyReader.empty) 1486 ret = res.readJson(); 1487 1488 logDebug( 1489 "REST call: %s %s -> %d, %s", 1490 httpMethodString(verb), 1491 url.toString(), 1492 res.statusCode, 1493 ret.toString() 1494 ); 1495 1496 // Get required headers - Don't throw yet 1497 string[] missingKeys; 1498 foreach (k, ref v; reqReturnHdrs) 1499 if (auto ptr = k in res.headers) 1500 v = (*ptr).idup; 1501 else 1502 missingKeys ~= k; 1503 1504 // Get optional headers 1505 foreach (k, ref v; optReturnHdrs) 1506 if (auto ptr = k in res.headers) 1507 v = (*ptr).idup; 1508 else 1509 v = null; 1510 1511 if (missingKeys.length) 1512 throw new Exception( 1513 "REST interface mismatch: Missing required header field(s): " 1514 ~ missingKeys.to!string); 1515 1516 1517 if (!isSuccessCode(cast(HTTPStatus)res.statusCode)) 1518 throw new RestException(res.statusCode, ret); 1519 }; 1520 1521 if (http_settings) requestHTTP(url, reqdg, resdg, http_settings); 1522 else requestHTTP(url, reqdg, resdg); 1523 1524 return ret; 1525 } 1526 1527 private { 1528 import vibe.data.json; 1529 import std.conv : to; 1530 1531 string toRestString(Json value) 1532 { 1533 switch (value.type) { 1534 default: return value.toString(); 1535 case Json.Type.Bool: return value.get!bool ? "true" : "false"; 1536 case Json.Type.Int: return to!string(value.get!long); 1537 case Json.Type.Float: return to!string(value.get!double); 1538 case Json.Type.String: return value.get!string; 1539 } 1540 } 1541 1542 T fromRestString(T)(string value) 1543 { 1544 import std.conv : ConvException; 1545 import hb.web.common : HTTPStatusException, HTTPStatus; 1546 try { 1547 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 1548 else static if (is(T == bool)) return value == "true"; 1549 else static if (is(T : int)) return to!T(value); 1550 else static if (is(T : double)) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 1551 else static if (is(string : T)) return value; 1552 else static if (__traits(compiles, T.fromISOExtString("hello"))) return T.fromISOExtString(value); 1553 else static if (__traits(compiles, T.fromString("hello"))) return T.fromString(value); 1554 else return deserializeJson!T(parseJson(value)); 1555 } catch (ConvException e) { 1556 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 1557 } catch (JSONException e) { 1558 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 1559 } 1560 } 1561 1562 // Converting from invalid JSON string to aggregate should throw bad request 1563 unittest { 1564 import hb.web.common : HTTPStatusException, HTTPStatus; 1565 1566 void assertHTTPStatus(E)(lazy E expression, HTTPStatus expectedStatus, 1567 string file = __FILE__, size_t line = __LINE__) 1568 { 1569 import core.exception : AssertError; 1570 import std.format : format; 1571 1572 try 1573 expression(); 1574 catch (HTTPStatusException e) 1575 { 1576 if (e.status != expectedStatus) 1577 throw new AssertError(format("assertHTTPStatus failed: " ~ 1578 "status expected %d but was %d", expectedStatus, e.status), 1579 file, line); 1580 1581 return; 1582 } 1583 1584 throw new AssertError("assertHTTPStatus failed: No " ~ 1585 "'HTTPStatusException' exception was thrown", file, line); 1586 } 1587 1588 struct Foo { int bar; } 1589 assertHTTPStatus(fromRestString!(Foo)("foo"), HTTPStatus.badRequest); 1590 } 1591 } 1592 1593 private string generateModuleImports(I)() 1594 { 1595 if (!__ctfe) 1596 assert (false); 1597 1598 import vibe.internal.meta.codegen : getRequiredImports; 1599 import std.algorithm : map; 1600 import std.array : join; 1601 1602 auto modules = getRequiredImports!I(); 1603 return join(map!(a => "static import " ~ a ~ ";")(modules), "\n"); 1604 } 1605 1606 version(unittest) 1607 { 1608 private struct Aggregate { } 1609 private interface Interface 1610 { 1611 Aggregate[] foo(); 1612 } 1613 } 1614 1615 unittest 1616 { 1617 enum imports = generateModuleImports!Interface; 1618 static assert (imports == "static import hb.web.rest;"); 1619 } 1620 1621 // Check that the interface is valid. Every checks on the correctness of the 1622 // interface should be put in checkRestInterface, which allows to have consistent 1623 // errors in the server and client. 1624 package string getInterfaceValidationError(I)() 1625 out (result) { assert((result is null) == !result.length); } 1626 body { 1627 import hb.web.internal.rest.common : ParameterKind; 1628 import std.typetuple : TypeTuple; 1629 import std.algorithm : strip; 1630 1631 // The hack parameter is to kill "Statement is not reachable" warnings. 1632 string validateMethod(alias Func)(bool hack = true) { 1633 import vibe.internal.meta.uda; 1634 import std.string : format; 1635 1636 static assert(is(FunctionTypeOf!Func), "Internal error"); 1637 1638 if (!__ctfe) 1639 assert(false, "Internal error"); 1640 1641 enum FuncId = (fullyQualifiedName!I~ "." ~ __traits(identifier, Func)); 1642 alias PT = ParameterTypeTuple!Func; 1643 static if (!__traits(compiles, ParameterIdentifierTuple!Func)) { 1644 if (hack) return "%s: A parameter has no name.".format(FuncId); 1645 alias PN = TypeTuple!("-DummyInvalid-"); 1646 } else 1647 alias PN = ParameterIdentifierTuple!Func; 1648 alias WPAT = UDATuple!(WebParamAttribute, Func); 1649 1650 // Check if there is no orphan UDATuple (e.g. typo while writing the name of the parameter). 1651 foreach (i, uda; WPAT) { 1652 // Note: static foreach gets unrolled, generating multiple nested sub-scope. 1653 // The spec / DMD doesn't like when you have the same symbol in those, 1654 // leading to wrong codegen / wrong template being reused. 1655 // That's why those templates need different names. 1656 // See DMD bug #9748. 1657 mixin(GenOrphan!(i).Decl); 1658 // template CmpOrphan(string name) { enum CmpOrphan = (uda.identifier == name); } 1659 static if (!anySatisfy!(mixin(GenOrphan!(i).Name), PN)) { 1660 if (hack) return "%s: No parameter '%s' (referenced by attribute @%sParam)" 1661 .format(FuncId, uda.identifier, uda.origin); 1662 } 1663 } 1664 1665 foreach (i, P; PT) { 1666 static if (!PN[i].length) 1667 if (hack) return "%s: Parameter %d has no name." 1668 .format(FuncId, i); 1669 // Check for multiple origins 1670 static if (WPAT.length) { 1671 // It's okay to reuse GenCmp, as the order of params won't change. 1672 // It should/might not be reinstantiated by the compiler. 1673 mixin(GenCmp!("Loop", i, PN[i]).Decl); 1674 alias WPA = Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT); 1675 static if (WPA.length > 1) 1676 if (hack) return "%s: Parameter '%s' has multiple @*Param attributes on it." 1677 .format(FuncId, PN[i]); 1678 } 1679 } 1680 1681 // Check for misplaced ref / out 1682 alias PSC = ParameterStorageClass; 1683 foreach (i, SC; ParameterStorageClassTuple!Func) { 1684 static if (SC & PSC.out_ || SC & PSC.ref_) { 1685 mixin(GenCmp!("Loop", i, PN[i]).Decl); 1686 alias Attr 1687 = Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT); 1688 static if (Attr.length != 1) { 1689 if (hack) return "%s: Parameter '%s' cannot be %s" 1690 .format(FuncId, PN[i], SC & PSC.out_ ? "out" : "ref"); 1691 } else static if (Attr[0].origin != ParameterKind.header) { 1692 if (hack) return "%s: %s parameter '%s' cannot be %s" 1693 .format(FuncId, Attr[0].origin, PN[i], 1694 SC & PSC.out_ ? "out" : "ref"); 1695 } 1696 } 1697 } 1698 1699 // Check for @path(":name") 1700 enum pathAttr = findFirstUDA!(PathAttribute, Func); 1701 static if (pathAttr.found) { 1702 static if (!pathAttr.value.length) { 1703 if (hack) 1704 return "%s: Path is null or empty".format(FuncId); 1705 } else { 1706 import std.algorithm : canFind, splitter; 1707 // splitter doesn't work with alias this ? 1708 auto str = pathAttr.value.data; 1709 if (str.canFind("//")) return "%s: Path '%s' contains empty entries.".format(FuncId, pathAttr.value); 1710 str = str.strip('/'); 1711 if (!str.length) return null; 1712 foreach (elem; str.splitter('/')) { 1713 assert(elem.length, "Empty path entry not caught yet!?"); 1714 1715 if (elem[0] == ':') { 1716 // typeof(PN) is void when length is 0. 1717 static if (!PN.length) { 1718 if (hack) 1719 return "%s: Path contains '%s', but no parameter '_%s' defined." 1720 .format(FuncId, elem, elem[1..$]); 1721 } else { 1722 if (![PN].canFind("_"~elem[1..$])) 1723 if (hack) return "%s: Path contains '%s', but no parameter '_%s' defined." 1724 .format(FuncId, elem, elem[1..$]); 1725 elem = elem[1..$]; 1726 } 1727 } 1728 } 1729 // TODO: Check for validity of the subpath. 1730 } 1731 } 1732 return null; 1733 } 1734 1735 if (!__ctfe) 1736 assert(false, "Internal error"); 1737 bool hack = true; 1738 foreach (method; __traits(allMembers, I)) { 1739 // WORKAROUND #1045 / @@BUG14375@@ 1740 static if (method.length != 0) 1741 foreach (overload; MemberFunctionsTuple!(I, method)) { 1742 static if (validateMethod!(overload)()) 1743 if (hack) return validateMethod!(overload)(); 1744 } 1745 } 1746 return null; 1747 } 1748 1749 // Test detection of user typos (e.g., if the attribute is on a parameter that doesn't exist). 1750 unittest { 1751 enum msg = "No parameter 'ath' (referenced by attribute @headerParam)"; 1752 1753 interface ITypo { 1754 @headerParam("ath", "Authorization") // mistyped parameter name 1755 string getResponse(string auth); 1756 } 1757 enum err = getInterfaceValidationError!ITypo; 1758 static assert(err !is null && stripTestIdent(err) == msg, 1759 "Expected validation error for getResponse, got: "~stripTestIdent(err)); 1760 } 1761 1762 // Multiple origin for a parameter 1763 unittest { 1764 enum msg = "Parameter 'arg1' has multiple @*Param attributes on it."; 1765 1766 interface IMultipleOrigin { 1767 @headerParam("arg1", "Authorization") @bodyParam("arg1", "Authorization") 1768 string getResponse(string arg1, int arg2); 1769 } 1770 enum err = getInterfaceValidationError!IMultipleOrigin; 1771 static assert(err !is null && stripTestIdent(err) == msg, err); 1772 } 1773 1774 // Missing parameter name 1775 unittest { 1776 enum msg = "Parameter 0 has no name."; 1777 1778 interface IMissingName1 { 1779 string getResponse(string = "troublemaker"); 1780 } 1781 interface IMissingName2 { 1782 string getResponse(string); 1783 } 1784 enum err1 = getInterfaceValidationError!IMissingName1; 1785 static assert(err1 !is null && stripTestIdent(err1) == msg, err1); 1786 enum err2 = getInterfaceValidationError!IMissingName2; 1787 static assert(err2 !is null && stripTestIdent(err2) == msg, err2); 1788 } 1789 1790 // Issue 949 1791 unittest { 1792 enum msg = "Path contains ':owner', but no parameter '_owner' defined."; 1793 1794 @path("/repos/") 1795 interface IGithubPR { 1796 @path(":owner/:repo/pulls") 1797 string getPullRequests(string owner, string repo); 1798 } 1799 enum err = getInterfaceValidationError!IGithubPR; 1800 static assert(err !is null && stripTestIdent(err) == msg, err); 1801 } 1802 1803 // Issue 1017 1804 unittest { 1805 interface TestSuccess { @path("/") void test(); } 1806 interface TestSuccess2 { @path("/test/") void test(); } 1807 interface TestFail { @path("//") void test(); } 1808 interface TestFail2 { @path("/test//it/") void test(); } 1809 static assert(getInterfaceValidationError!TestSuccess is null); 1810 static assert(getInterfaceValidationError!TestSuccess2 is null); 1811 static assert(stripTestIdent(getInterfaceValidationError!TestFail) 1812 == "Path '//' contains empty entries."); 1813 static assert(stripTestIdent(getInterfaceValidationError!TestFail2) 1814 == "Path '/test//it/' contains empty entries."); 1815 } 1816 1817 unittest { 1818 interface NullPath { @path(null) void test(); } 1819 interface ExplicitlyEmptyPath { @path("") void test(); } 1820 static assert(stripTestIdent(getInterfaceValidationError!NullPath) 1821 == "Path is null or empty"); 1822 static assert(stripTestIdent(getInterfaceValidationError!ExplicitlyEmptyPath) 1823 == "Path is null or empty"); 1824 1825 // Note: Implicitly empty path are valid: 1826 // interface ImplicitlyEmptyPath { void get(); } 1827 } 1828 1829 // Accept @headerParam ref / out parameters 1830 unittest { 1831 interface HeaderRef { 1832 @headerParam("auth", "auth") 1833 string getData(ref string auth); 1834 } 1835 static assert(getInterfaceValidationError!HeaderRef is null, 1836 stripTestIdent(getInterfaceValidationError!HeaderRef)); 1837 1838 interface HeaderOut { 1839 @headerParam("auth", "auth") 1840 void getData(out string auth); 1841 } 1842 static assert(getInterfaceValidationError!HeaderOut is null, 1843 stripTestIdent(getInterfaceValidationError!HeaderOut)); 1844 } 1845 1846 // Reject unattributed / @queryParam or @bodyParam ref / out parameters 1847 unittest { 1848 interface QueryRef { 1849 @queryParam("auth", "auth") 1850 string getData(ref string auth); 1851 } 1852 static assert(stripTestIdent(getInterfaceValidationError!QueryRef) 1853 == "query parameter 'auth' cannot be ref"); 1854 1855 interface QueryOut { 1856 @queryParam("auth", "auth") 1857 void getData(out string auth); 1858 } 1859 static assert(stripTestIdent(getInterfaceValidationError!QueryOut) 1860 == "query parameter 'auth' cannot be out"); 1861 1862 interface BodyRef { 1863 @bodyParam("auth", "auth") 1864 string getData(ref string auth); 1865 } 1866 static assert(stripTestIdent(getInterfaceValidationError!BodyRef) 1867 == "body_ parameter 'auth' cannot be ref"); 1868 1869 interface BodyOut { 1870 @bodyParam("auth", "auth") 1871 void getData(out string auth); 1872 } 1873 static assert(stripTestIdent(getInterfaceValidationError!BodyOut) 1874 == "body_ parameter 'auth' cannot be out"); 1875 1876 // There's also the possibility of someone using an out unnamed 1877 // parameter (don't ask me why), but this is catched as unnamed 1878 // parameter, so we don't need to check it here. 1879 } 1880 1881 private string stripTestIdent(string msg) 1882 @safe { 1883 import std.string; 1884 auto idx = msg.indexOf(": "); 1885 return idx >= 0 ? msg[idx+2 .. $] : msg; 1886 } 1887 1888 // Small helper for client code generation 1889 private string paramCTMap(string[string] params) 1890 @safe { 1891 import std.array : appender, join; 1892 if (!__ctfe) 1893 assert (false, "This helper is only supposed to be called for codegen in RestClientInterface."); 1894 auto app = appender!(string[]); 1895 foreach (key, val; params) { 1896 app ~= "\""~key~"\""; 1897 app ~= val; 1898 } 1899 return app.data.join(", "); 1900 } 1901 1902 package string stripTUnderscore(string name, RestInterfaceSettings settings) 1903 @safe { 1904 if ((settings is null || settings.stripTrailingUnderscore) 1905 && name.endsWith("_")) 1906 return name[0 .. $-1]; 1907 else return name; 1908 } 1909 1910 // Workarounds @@DMD:9748@@, and maybe more 1911 package template GenCmp(string name, int id, string cmpTo) { 1912 import std.string : format; 1913 import std.conv : to; 1914 enum Decl = q{ 1915 template %1$s(alias uda) { 1916 enum %1$s = (uda.identifier == "%2$s"); 1917 } 1918 }.format(Name, cmpTo); 1919 enum Name = name~to!string(id); 1920 } 1921 1922 // Ditto 1923 private template GenOrphan(int id) { 1924 import std.string : format; 1925 import std.conv : to; 1926 enum Decl = q{ 1927 template %1$s(string name) { 1928 enum %1$s = (uda.identifier == name); 1929 } 1930 }.format(Name); 1931 enum Name = "OrphanCheck"~to!string(id); 1932 } 1933 1934 // Workaround for issue #1045 / DMD bug 14375 1935 // Also, an example of policy-based design using this module. 1936 unittest { 1937 import std.traits, std.typetuple; 1938 import vibe.internal.meta.codegen; 1939 import vibe.internal.meta.typetuple; 1940 import hb.web.internal.rest.common : ParameterKind; 1941 1942 interface Policies { 1943 @headerParam("auth", "Authorization") 1944 string BasicAuth(string auth, ulong expiry); 1945 } 1946 1947 @path("/keys/") 1948 interface IKeys(alias AuthenticationPolicy = Policies.BasicAuth) { 1949 static assert(is(FunctionTypeOf!AuthenticationPolicy == function), 1950 "Policies needs to be functions"); 1951 @path("/") @method(HTTPMethod.POST) 1952 mixin CloneFunctionDecl!(AuthenticationPolicy, true, "create"); 1953 } 1954 1955 class KeysImpl : IKeys!() { 1956 override: 1957 string create(string auth, ulong expiry) { 1958 return "4242-4242"; 1959 } 1960 } 1961 1962 // Some sanity checks 1963 // Note: order is most likely implementation dependent. 1964 // Good thing we only have one frontend... 1965 alias WPA = WebParamAttribute; 1966 static assert(Compare!( 1967 Group!(__traits(getAttributes, IKeys!().create)), 1968 Group!(PathAttribute("/"), 1969 MethodAttribute(HTTPMethod.POST), 1970 WPA(ParameterKind.header, "auth", "Authorization")))); 1971 1972 void register() { 1973 auto router = new URLRouter(); 1974 router.registerRestInterface(new KeysImpl()); 1975 } 1976 1977 void query() { 1978 auto client = new RestInterfaceClient!(IKeys!())("http://127.0.0.1:8080"); 1979 assert(client.create("Hello", 0) == "4242-4242"); 1980 } 1981 }