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