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 Serialization: 152 By default the return values of the interface methods are serialized 153 as a JSON text and sent back to the REST client. To override this, you 154 can use the @resultSerializer attribute 155 156 --- 157 struct TestStruct {int i;} 158 159 interface IService { 160 @safe: 161 @resultSerializer!( 162 // output_stream implements OutputRange 163 function (output_stream, test_struct) { 164 output_stream ~= serializeToJsonString(test_struct); 165 }, 166 // input_stream implements InputStream 167 function (input_stream) { 168 return deserializeJson!TestStruct(input_stream.readAllUTF8()); 169 }, 170 "application/json")() 171 @resultSerializer!( 172 // output_stream implements OutputRange 173 function (output_stream, test_struct) { 174 output_stream ~= test_struct.i.to!string(); 175 }, 176 // input_stream implements InputStream 177 function (input_stream) { 178 TestStruct test_struct; 179 test_struct.i = input_stream.readAllUTF8().to!int(); 180 return test_struct; 181 }, 182 "plain/text")() 183 TestStruct getTest(); 184 } 185 186 class Service : IService { 187 @safe: 188 TestStruct getTest() { 189 TestStruct test_struct = {42}; 190 return test_struct; 191 } 192 } 193 --- 194 195 Serialization_policies: 196 You can customize the serialization of any type used by an interface 197 by using serialization policies. The following example is using 198 the `Base64ArrayPolicy`, which means if `X` contains any ubyte arrays, 199 they will be serialized to their base64 encoding instead of 200 their normal string representation (e.g. `"[1, 2, 255]"`). 201 202 --- 203 @serializationPolicy!(Base64ArrayPolicy) 204 interface ITestBase64 205 { 206 @safe X getTest(); 207 } 208 --- 209 210 Parameters: 211 Function parameters may be populated from the route, query string, 212 request body, or headers. They may optionally affect the route URL itself. 213 214 By default, parameters are passed differently depending on the type of 215 request (i.e., HTTP method). For GET and PUT, parameters are passed 216 via the query string (`<route>?paramA=valueA[?paramB=...]`), 217 while for POST and PATCH, they are passed via the request body 218 as a JSON object. 219 220 The default behavior can be overridden using one of the following 221 annotations, put as UDA on the relevant parameter: 222 223 $(UL 224 $(LI `@viaHeader("field")`: Will source the parameter on which it is 225 applied from the request headers named "field". If the parameter 226 is `ref`, it will also be set as a response header. Parameters 227 declared as `out` will $(I only) be set as a response header.) 228 $(LI `@viaQuery("field")`: Will source the parameter on which it is 229 applied from a field named "field" of the query string.) 230 $(LI `@viaBody("field")`: Will source the parameter on which it is 231 applied from a field named "field" of the request body 232 in JSON format, or, if no field is passed, will represent the 233 whole body. Note that in the later case, there can be no other 234 `viaBody` parameters.) 235 ) 236 237 ---- 238 @path("/api/") 239 interface APIRoot { 240 // GET /api/header with 'Authorization' set 241 string getHeader(@viaBody("Authorization") string param); 242 243 // GET /api/foo?param=... 244 string getFoo(@viaQuery("param") int param); 245 246 // GET /api/body with body set to { "myFoo": {...} } 247 string getBody(@viaBody("parameter") FooType myFoo); 248 249 // GET /api/full_body with body set to {...} 250 string getFullBody(@viaBody() FooType myFoo); 251 } 252 ---- 253 254 Further, how function parameters are named may affect the route: 255 256 $(UL 257 $(LI $(P Parameters with leading underscores (e.g. `_slug`) are also 258 interpreted as a route component, but only in the presence of 259 a `@path` UDA annotation. See Manual endpoint specification above.)) 260 $(LI $(P Other function parameters do not affect or come from the path 261 portion of the URL, and are are passed according to the default 262 rules above: query string for GET and PUT; request body JSON 263 for POST and PATCH.)) 264 $(LI $(P $(B Deprecated:) If the first parameter is named `id`, this is 265 interpreted as a leading route component. For example, 266 `getName(int id)` becomes `/:id/name`.) 267 $(P Note that this style of parameter-based URL routing is 268 different than in many other web frameworks, where instead 269 this example would be routed as `/name/:id`.) 270 $(P See `Collection` for the preferred way to represent object 271 collections in REST interfaces)) 272 ) 273 274 275 Default_values: 276 Parameters with default values behave as optional parameters. If one is 277 set in the interface declaration of a method, the client can omit a 278 value for the corresponding field in the request and the default value 279 is used instead. 280 281 Note that if default parameters are not evaluable by CTFE, compilation 282 may fail due to DMD bug #14369 (Vibe.d tracking issue: #1043). 283 284 Aggregates: 285 When passing aggregates as parameters, those are serialized differently 286 depending on the way they are passed, which may be especially important 287 when interfacing with an existing RESTful API: 288 289 $(UL 290 $(LI If the parameter is passed via the headers or the query, either 291 implicitly or explicitly, the aggregate is serialized to JSON. 292 If the JSON representation is a single string, the string value 293 will be used verbatim. Otherwise the JSON representation will be 294 used) 295 $(LI If the parameter is passed via the body, the datastructure is 296 serialized to JSON and set as a field of the main JSON object 297 that is expected in the request body. Its field name equals the 298 parameter name, unless an explicit `@bodyParam` annotation is 299 used.) 300 ) 301 302 See_Also: 303 To see how to implement the server side in detail, jump to 304 `registerRestInterface`. 305 306 To see how to implement the client side in detail, jump to 307 the `RestInterfaceClient` documentation. 308 309 Copyright: © 2012-2018 Sönke Ludwig 310 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 311 Authors: Sönke Ludwig, Михаил Страшун, Mathias 'Geod24' Lang 312 */ 313 module vibe.web.rest; 314 315 public import vibe.web.common; 316 317 import vibe.core.log; 318 import vibe.core.stream : InputStream, isInputStream, pipe; 319 import vibe.http.router : URLRouter; 320 import vibe.http.client : HTTPClientResponse, HTTPClientSettings; 321 import vibe.http.common : HTTPMethod; 322 import vibe.http.server : HTTPServerRequestDelegate, HTTPServerRequest, HTTPServerResponse; 323 import vibe.http.status : HTTPStatus, isSuccessCode; 324 import vibe.internal.meta.uda; 325 import vibe.internal.meta.funcattr; 326 import vibe.inet.url; 327 import vibe.inet.message : InetHeaderMap; 328 import vibe.web.internal.rest.common : RestInterface, Route, SubInterfaceType; 329 import vibe.web.auth : AuthInfo, handleAuthentication, handleAuthorization, isAuthenticated; 330 331 import std.algorithm : count, startsWith, endsWith, sort, splitter; 332 import std.array : appender, split; 333 import std.meta : AliasSeq; 334 import std.range : isOutputRange; 335 import std.string : strip, indexOf, toLower; 336 import std.typecons : No, Nullable, Yes; 337 import std.typetuple : anySatisfy, Filter; 338 import std.traits; 339 340 /** Registers a server matching a certain REST interface. 341 342 Servers are implementation of the D interface that defines the RESTful API. 343 The methods of this class are invoked by the code that is generated for 344 each endpoint of the API, with parameters and return values being translated 345 according to the rules documented in the `vibe.web.rest` module 346 documentation. 347 348 A basic 'hello world' API can be defined as follows: 349 ---- 350 @path("/api/") 351 interface APIRoot { 352 string get(); 353 } 354 355 class API : APIRoot { 356 override string get() { return "Hello, World"; } 357 } 358 359 void main() 360 { 361 // -- Where the magic happens -- 362 router.registerRestInterface(new API()); 363 // GET http://127.0.0.1:8080/api/ and 'Hello, World' will be replied 364 listenHTTP("127.0.0.1:8080", router); 365 366 runApplication(); 367 } 368 ---- 369 370 As can be seen here, the RESTful logic can be written inside the class 371 without any concern for the actual HTTP representation. 372 373 Return_value: 374 By default, all methods that return a value send a 200 (OK) status code, 375 or 204 if no value is being returned for the body. 376 377 Non-success: 378 In the cases where an error code should be signaled to the user, a 379 `HTTPStatusException` can be thrown from within the method. It will be 380 turned into a JSON object that has a `statusMessage` field with the 381 exception message. In case of other exception types being thrown, the 382 status code will be set to 500 (internal server error), the 383 `statusMessage` field will again contain the exception's message, and, 384 in debug mode, an additional `statusDebugMessage` field will be set to 385 the complete string representation of the exception 386 (`Exception.toString`), which usually contains a stack trace useful for 387 debugging. 388 389 Returning_data: 390 To return data, it is possible to either use the return value, which 391 will be sent as the response body, or individual `ref`/`out` parameters 392 can be used. The way they are represented in the response can be 393 customized by adding `@viaBody`/`@viaHeader` annotations on the 394 parameter declaration of the method within the interface. 395 396 In case of errors, any `@viaHeader` parameters are guaranteed to 397 be set in the response, so that applications such as HTTP basic 398 authentication can be implemented. 399 400 Template_Params: 401 TImpl = Either an interface type, or a class that derives from an 402 interface. If the class derives from multiple interfaces, 403 the first one will be assumed to be the API description 404 and a warning will be issued. 405 406 Params: 407 router = The HTTP router on which the interface will be registered 408 instance = Server instance to use 409 settings = Additional settings, such as the `MethodStyle` or the prefix 410 411 See_Also: 412 `RestInterfaceClient` class for an automated way to generate the 413 matching client-side implementation. 414 */ 415 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, RestInterfaceSettings settings = null) 416 { 417 import std.algorithm : filter, map, all; 418 import std.array : array; 419 import std.range : front; 420 import vibe.web.internal.rest.common : ParameterKind; 421 422 auto intf = RestInterface!TImpl(settings, false); 423 424 foreach (i, ovrld; intf.SubInterfaceFunctions) { 425 enum fname = __traits(identifier, intf.SubInterfaceFunctions[i]); 426 alias R = ReturnType!ovrld; 427 428 static if (isInstanceOf!(Collection, R)) { 429 auto ret = __traits(getMember, instance, fname)(R.ParentIDs.init); 430 router.registerRestInterface!(R.Interface)(ret.m_interface, intf.subInterfaces[i].settings); 431 } else { 432 auto ret = __traits(getMember, instance, fname)(); 433 router.registerRestInterface!R(ret, intf.subInterfaces[i].settings); 434 } 435 } 436 437 438 foreach (i, func; intf.RouteFunctions) { 439 auto route = intf.routes[i]; 440 441 // normal handler 442 auto handler = jsonMethodHandler!(func, i)(instance, intf); 443 444 auto diagparams = route.parameters.filter!(p => p.kind != ParameterKind.internal).map!(p => p.fieldName).array; 445 logDiagnostic("REST route: %s %s %s", route.method, route.fullPattern, diagparams); 446 router.match(route.method, route.fullPattern, handler); 447 } 448 449 // here we filter our already existing OPTIONS routes, so we don't overwrite whenever the user explicitly made his own OPTIONS route 450 auto routesGroupedByPattern = intf.getRoutesGroupedByPattern.filter!(rs => rs.all!(r => r.method != HTTPMethod.OPTIONS)); 451 452 foreach(routes; routesGroupedByPattern){ 453 auto route = routes.front; 454 auto handler = optionsMethodHandler(routes, settings); 455 456 auto diagparams = route.parameters.filter!(p => p.kind != ParameterKind.internal).map!(p => p.fieldName).array; 457 logDiagnostic("REST route: %s %s %s", HTTPMethod.OPTIONS, route.fullPattern, diagparams); 458 router.match(HTTPMethod.OPTIONS, route.fullPattern, handler); 459 } 460 return router; 461 } 462 463 /// ditto 464 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, MethodStyle style) 465 { 466 return registerRestInterface(router, instance, "/", style); 467 } 468 469 /// ditto 470 URLRouter registerRestInterface(TImpl)(URLRouter router, TImpl instance, string url_prefix, 471 MethodStyle style = MethodStyle.lowerUnderscored) 472 { 473 auto settings = new RestInterfaceSettings; 474 if (!url_prefix.startsWith("/")) url_prefix = "/"~url_prefix; 475 settings.baseURL = URL("http://127.0.0.1"~url_prefix); 476 settings.methodStyle = style; 477 return registerRestInterface(router, instance, settings); 478 } 479 480 481 /** 482 This is a very limited example of REST interface features. Please refer to 483 the "rest" project in the "examples" folder for a full overview. 484 485 All details related to HTTP are inferred from the interface declaration. 486 */ 487 @safe unittest 488 { 489 @path("/") 490 interface IMyAPI 491 { 492 @safe: 493 // GET /api/greeting 494 @property string greeting(); 495 496 // PUT /api/greeting 497 @property void greeting(string text); 498 499 // POST /api/users 500 @path("/users") 501 void addNewUser(string name); 502 503 // GET /api/users 504 @property string[] users(); 505 506 // GET /api/:id/name 507 string getName(int id); 508 509 // GET /some_custom_json 510 Json getSomeCustomJson(); 511 } 512 513 // vibe.d takes care of all JSON encoding/decoding 514 // and actual API implementation can work directly 515 // with native types 516 517 class API : IMyAPI 518 { 519 private { 520 string m_greeting; 521 string[] m_users; 522 } 523 524 @property string greeting() { return m_greeting; } 525 @property void greeting(string text) { m_greeting = text; } 526 527 void addNewUser(string name) { m_users ~= name; } 528 529 @property string[] users() { return m_users; } 530 531 string getName(int id) { return m_users[id]; } 532 533 Json getSomeCustomJson() 534 { 535 Json ret = Json.emptyObject; 536 ret["somefield"] = "Hello, World!"; 537 return ret; 538 } 539 } 540 541 // actual usage, this is usually done in app.d module 542 // constructor 543 544 void static_this() 545 { 546 import vibe.http.server, vibe.http.router; 547 548 auto router = new URLRouter; 549 router.registerRestInterface(new API()); 550 listenHTTP(new HTTPServerSettings(), router); 551 } 552 } 553 554 555 /** 556 Returns a HTTP handler delegate that serves a JavaScript REST client. 557 */ 558 HTTPServerRequestDelegate serveRestJSClient(I)(RestInterfaceSettings settings) 559 if (is(I == interface)) 560 { 561 import std.datetime.systime : SysTime; 562 import std.array : appender; 563 564 import vibe.http.fileserver : ETag, handleCache; 565 566 auto app = appender!string(); 567 generateRestJSClient!I(app, settings); 568 ETag tag = ETag.md5(No.weak, app.data); 569 570 void serve(HTTPServerRequest req, HTTPServerResponse res) 571 { 572 if (handleCache(req, res, tag, SysTime.init, "public")) 573 return; 574 575 res.writeBody(app.data, "application/javascript; charset=UTF-8"); 576 } 577 578 return &serve; 579 } 580 /// ditto 581 HTTPServerRequestDelegate serveRestJSClient(I)(URL base_url) 582 { 583 auto settings = new RestInterfaceSettings; 584 settings.baseURL = base_url; 585 return serveRestJSClient!I(settings); 586 } 587 /// ditto 588 HTTPServerRequestDelegate serveRestJSClient(I)(string base_url) 589 { 590 auto settings = new RestInterfaceSettings; 591 settings.baseURL = URL(base_url); 592 return serveRestJSClient!I(settings); 593 } 594 /// ditto 595 HTTPServerRequestDelegate serveRestJSClient(I)() 596 { 597 auto settings = new RestInterfaceSettings; 598 return serveRestJSClient!I(settings); 599 } 600 601 /// 602 unittest { 603 import vibe.http.server; 604 605 interface MyAPI { 606 string getFoo(); 607 void postBar(string param); 608 } 609 610 void test() 611 { 612 auto restsettings = new RestInterfaceSettings; 613 restsettings.baseURL = URL("http://api.example.org/"); 614 615 auto router = new URLRouter; 616 router.get("/myapi.js", serveRestJSClient!MyAPI(restsettings)); 617 //router.get("/myapi.js", serveRestJSClient!MyAPI(URL("http://api.example.org/"))); 618 //router.get("/myapi.js", serveRestJSClient!MyAPI("http://api.example.org/")); 619 //router.get("/myapi.js", serveRestJSClient!MyAPI()); // if want to request to self server 620 //router.get("/", staticTemplate!"index.dt"); 621 622 listenHTTP(new HTTPServerSettings, router); 623 } 624 625 /* 626 index.dt: 627 html 628 head 629 title JS REST client test 630 script(src="myapi.js") 631 body 632 button(onclick="MyAPI.postBar('hello');") 633 */ 634 } 635 636 637 /** 638 Generates JavaScript code to access a REST interface from the browser. 639 */ 640 void generateRestJSClient(I, R)(ref R output, RestInterfaceSettings settings = null) 641 if (is(I == interface) && isOutputRange!(R, char)) 642 { 643 import vibe.web.internal.rest.jsclient : generateInterface, JSRestClientSettings; 644 auto jsgenset = new JSRestClientSettings; 645 output.generateInterface!I(settings, jsgenset, true); 646 } 647 648 /// Writes a JavaScript REST client to a local .js file. 649 unittest { 650 import vibe.core.file; 651 652 interface MyAPI { 653 void getFoo(); 654 void postBar(string param); 655 } 656 657 void generateJSClientImpl() 658 { 659 import std.array : appender; 660 661 auto app = appender!string; 662 auto settings = new RestInterfaceSettings; 663 settings.baseURL = URL("http://localhost/"); 664 generateRestJSClient!MyAPI(app, settings); 665 } 666 667 generateJSClientImpl(); 668 } 669 670 671 /** 672 Implements the given interface by forwarding all public methods to a REST server. 673 674 The server must talk the same protocol as registerRestInterface() generates. Be sure to set 675 the matching method style for this. The RestInterfaceClient class will derive from the 676 interface that is passed as a template argument. It can be used as a drop-in replacement 677 of the real implementation of the API this way. 678 679 Non-success: 680 If a request failed, timed out, or the server returned an non-success status code, 681 an `vibe.web.common.RestException` will be thrown. 682 */ 683 class RestInterfaceClient(I) : I 684 { 685 import vibe.inet.url : URL; 686 import vibe.http.client : HTTPClientRequest; 687 import std.typetuple : staticMap; 688 689 private alias Info = RestInterface!I; 690 691 //pragma(msg, "imports for "~I.stringof~":"); 692 //pragma(msg, generateModuleImports!(I)()); 693 mixin(generateModuleImports!I()); 694 695 private { 696 // storing this struct directly causes a segfault when built with 697 // LDC 0.15.x, so we are using a pointer here: 698 RestInterface!I* m_intf; 699 RequestFilter m_requestFilter; 700 RequestBodyFilter m_requestBodyFilter; 701 staticMap!(RestInterfaceClient, Info.SubInterfaceTypes) m_subInterfaces; 702 } 703 704 alias RequestFilter = void delegate(HTTPClientRequest req) @safe; 705 706 alias RequestBodyFilter = void delegate(HTTPClientRequest req, scope InputStream body_contents) @safe; 707 708 /** 709 Creates a new REST client implementation of $(D I). 710 */ 711 this(RestInterfaceSettings settings) 712 { 713 m_intf = new Info(settings, true); 714 715 foreach (i, SI; Info.SubInterfaceTypes) 716 m_subInterfaces[i] = new RestInterfaceClient!SI(m_intf.subInterfaces[i].settings); 717 } 718 719 /// ditto 720 this(string base_url, MethodStyle style = MethodStyle.lowerUnderscored) 721 { 722 this(URL(base_url), style); 723 } 724 725 /// ditto 726 this(URL base_url, MethodStyle style = MethodStyle.lowerUnderscored) 727 { 728 scope settings = new RestInterfaceSettings; 729 settings.baseURL = base_url; 730 settings.methodStyle = style; 731 this(settings); 732 } 733 734 /** 735 An optional request filter that allows to modify each request before it is made. 736 */ 737 final @property RequestFilter requestFilter() 738 { 739 return m_requestFilter; 740 } 741 /// ditto 742 final @property void requestFilter(RequestFilter v) 743 { 744 m_requestFilter = v; 745 foreach (i, SI; Info.SubInterfaceTypes) 746 m_subInterfaces[i].requestFilter = v; 747 } 748 /// ditto 749 final @property void requestFilter(void delegate(HTTPClientRequest req) v) 750 { 751 this.requestFilter = cast(RequestFilter)v; 752 } 753 754 /** Optional request filter with access to the request body. 755 756 This callback allows to modify the request headers depending on the 757 contents of the body. 758 */ 759 final @property void requestBodyFilter(RequestBodyFilter del) 760 { 761 m_requestBodyFilter = del; 762 } 763 /// ditto 764 final @property RequestBodyFilter requestBodyFilter() 765 { 766 return m_requestBodyFilter; 767 } 768 769 //pragma(msg, "restinterface:"); 770 mixin(generateRestClientMethods!I()); 771 772 protected { 773 import vibe.data.json : Json; 774 import vibe.textfilter.urlencode; 775 776 /** 777 * Perform a request to the interface using the given parameters. 778 * 779 * Params: 780 * verb = Kind of request (See $(D HTTPMethod) enum). 781 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 782 * it will be '/rejectedsoftware/vibe.d/issues'. 783 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 784 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 785 * it will be 'author%3ASantaClaus'. 786 * 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 787 * the generic type "application/json". 788 * reqReturnHdrs = A map of required return headers. 789 * To avoid returning unused headers, nothing is written 790 * to this structure unless there's an (usually empty) 791 * entry (= the key exists) with the same key. 792 * If any key present in `reqReturnHdrs` is not present 793 * in the response, an Exception is thrown. 794 * optReturnHdrs = A map of optional return headers. 795 * This behaves almost as exactly as reqReturnHdrs, 796 * except that non-existent key in the response will 797 * not cause it to throw, but rather to set this entry 798 * to 'null'. 799 * 800 * Returns: 801 * The Json object returned by the request 802 */ 803 Json request(HTTPMethod verb, string name, 804 const scope ref InetHeaderMap hdrs, string query, string body_, 805 ref InetHeaderMap reqReturnHdrs, 806 ref InetHeaderMap optReturnHdrs) const 807 { 808 auto path = URL(m_intf.baseURL).pathString; 809 810 if (name.length) 811 { 812 if (path.length && path[$ - 1] == '/' && name[0] == '/') 813 path ~= name[1 .. $]; 814 else if (path.length && path[$ - 1] == '/' || name[0] == '/') 815 path ~= name; 816 else 817 path ~= '/' ~ name; 818 } 819 820 auto httpsettings = m_intf.settings.httpClientSettings; 821 822 auto http_resp = .request(URL(m_intf.baseURL), m_requestFilter, 823 m_requestBodyFilter, verb, path, 824 hdrs, query, body_, reqReturnHdrs, optReturnHdrs, httpsettings); 825 scope(exit) http_resp.dropBody(); 826 827 return http_resp.readJson(); 828 } 829 } 830 } 831 832 /// 833 unittest 834 { 835 interface IMyApi 836 { 837 // GET /status 838 string getStatus(); 839 840 // GET /greeting 841 @property string greeting(); 842 // PUT /greeting 843 @property void greeting(string text); 844 845 // POST /new_user 846 void addNewUser(string name); 847 // GET /users 848 @property string[] users(); 849 // GET /:id/name 850 string getName(int id); 851 852 Json getSomeCustomJson(); 853 } 854 855 void test() 856 { 857 auto api = new RestInterfaceClient!IMyApi("http://127.0.0.1/api/"); 858 859 logInfo("Status: %s", api.getStatus()); 860 api.greeting = "Hello, World!"; 861 logInfo("Greeting message: %s", api.greeting); 862 api.addNewUser("Peter"); 863 api.addNewUser("Igor"); 864 logInfo("Users: %s", api.users); 865 logInfo("First user name: %s", api.getName(0)); 866 } 867 } 868 869 870 /** 871 Encapsulates settings used to customize the generated REST interface. 872 */ 873 class RestInterfaceSettings { 874 /** The public URL below which the REST interface is registered. 875 */ 876 URL baseURL; 877 878 /** List of allowed origins for CORS 879 880 Empty list is interpreted as allowing all origins (e.g. *) 881 */ 882 string[] allowedOrigins; 883 884 /** Naming convention used for the generated URLs. 885 */ 886 MethodStyle methodStyle = MethodStyle.lowerUnderscored; 887 888 /** The content type the client would like to receive the data back 889 */ 890 string content_type = "application/json"; 891 892 /** Ignores a trailing underscore in method and function names. 893 894 With this setting set to $(D true), it's possible to use names in the 895 REST interface that are reserved words in D. 896 */ 897 bool stripTrailingUnderscore = true; 898 899 /// Overrides the default HTTP client settings used by the `RestInterfaceClient`. 900 HTTPClientSettings httpClientSettings; 901 902 /** Optional handler used to render custom replies in case of errors. 903 904 The handler needs to set the response status code to the provided 905 `RestErrorInformation.statusCode` value and can then write a custom 906 response body. 907 908 Note that the REST interface generator by default handles any exceptions thrown 909 during request handling and sents a JSON response with the error message. The 910 low level `HTTPServerSettings.errorPageHandler` is not invoked. 911 912 If `errorHandler` is not set, a JSON object with a single field "statusMessage" 913 will be sent. In debug builds, there may also be an additional 914 "statusDebugMessage" field that contains the full exception text, including a 915 possible stack trace. 916 */ 917 RestErrorHandler errorHandler; 918 919 @property RestInterfaceSettings dup() 920 const @safe { 921 auto ret = new RestInterfaceSettings; 922 ret.baseURL = this.baseURL; 923 ret.methodStyle = this.methodStyle; 924 ret.stripTrailingUnderscore = this.stripTrailingUnderscore; 925 ret.allowedOrigins = this.allowedOrigins.dup; 926 ret.content_type = this.content_type.dup; 927 ret.errorHandler = this.errorHandler; 928 if (this.httpClientSettings) { 929 ret.httpClientSettings = this.httpClientSettings.dup; 930 } 931 return ret; 932 } 933 } 934 935 alias RestErrorHandler = void delegate(HTTPServerRequest, HTTPServerResponse, RestErrorInformation error) @safe; 936 937 struct RestErrorInformation { 938 /// The status code that the handler should send in the reply 939 HTTPStatus statusCode; 940 941 /** If triggered by an exception, this contains the catched exception 942 object. 943 */ 944 Exception exception; 945 946 private this(Exception e, HTTPStatus default_status) 947 @safe { 948 import vibe.http.common : HTTPStatusException; 949 950 this.exception = e; 951 952 if (auto he = cast(HTTPStatusException)e) { 953 this.statusCode = cast(HTTPStatus)he.status; 954 } else { 955 this.statusCode = default_status; 956 } 957 } 958 } 959 960 961 /** 962 Models REST collection interfaces using natural D syntax. 963 964 Use this type as the return value of a REST interface getter method/property 965 to model a collection of objects. `opIndex` is used to make the individual 966 entries accessible using the `[index]` syntax. Nested collections are 967 supported. 968 969 The interface `I` needs to define a struct named `CollectionIndices`. The 970 members of this struct denote the types and names of the indexes that lead 971 to a particular resource. If a collection is nested within another 972 collection, the order of these members must match the nesting order 973 (outermost first). 974 975 The parameter list of all of `I`'s methods must begin with all but the last 976 entry in `CollectionIndices`. Methods that also match the last entry will be 977 considered methods of a collection item (`collection[index].method()`), 978 wheres all other methods will be considered methods of the collection 979 itself (`collection.method()`). 980 981 The name of the index parameters affects the default path of a method's 982 route. Normal parameter names will be subject to the same rules as usual 983 routes (see `registerRestInterface`) and will be mapped to query or form 984 parameters at the protocol level. Names starting with an underscore will 985 instead be mapped to path placeholders. For example, 986 `void getName(int __item_id)` will be mapped to a GET request to the 987 path `":item_id/name"`. 988 */ 989 struct Collection(I) 990 if (is(I == interface)) 991 { 992 import std.typetuple; 993 994 static assert(is(I.CollectionIndices == struct), "Collection interfaces must define a CollectionIndices struct."); 995 996 alias Interface = I; 997 alias AllIDs = TypeTuple!(typeof(I.CollectionIndices.tupleof)); 998 alias AllIDNames = FieldNameTuple!(I.CollectionIndices); 999 static assert(AllIDs.length >= 1, I.stringof~".CollectionIndices must define at least one member."); 1000 static assert(AllIDNames.length == AllIDs.length); 1001 alias ItemID = AllIDs[$-1]; 1002 alias ParentIDs = AllIDs[0 .. $-1]; 1003 alias ParentIDNames = AllIDNames[0 .. $-1]; 1004 1005 private { 1006 I m_interface; 1007 ParentIDs m_parentIDs; 1008 } 1009 1010 /** Constructs a new collection instance that is tied to a particular 1011 parent collection entry. 1012 1013 Params: 1014 api = The target interface imstance to be mapped as a collection 1015 pids = The indexes of all collections in which this collection is 1016 nested (if any) 1017 */ 1018 this(I api, ParentIDs pids) 1019 { 1020 m_interface = api; 1021 m_parentIDs = pids; 1022 } 1023 1024 static struct Item { 1025 private { 1026 I m_interface; 1027 AllIDs m_id; 1028 } 1029 1030 this(I api, AllIDs id) 1031 { 1032 m_interface = api; 1033 m_id = id; 1034 } 1035 1036 // forward all item methods 1037 mixin(() { 1038 string ret; 1039 foreach (m; __traits(allMembers, I)) { 1040 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 1041 alias PT = ParameterTypeTuple!ovrld; 1042 static if (matchesAllIDs!ovrld) 1043 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_id, args); }\n"; 1044 } 1045 } 1046 return ret; 1047 } ()); 1048 } 1049 1050 // Note: the example causes a recursive template instantiation if done as a documented unit test: 1051 /** Accesses a single collection entry. 1052 1053 Example: 1054 --- 1055 interface IMain { 1056 @property Collection!IItem items(); 1057 } 1058 1059 interface IItem { 1060 struct CollectionIndices { 1061 int _itemID; 1062 } 1063 1064 @method(HTTPMethod.GET) 1065 string name(int _itemID); 1066 } 1067 1068 void test(IMain main) 1069 { 1070 auto item_name = main.items[23].name; // equivalent to IItem.name(23) 1071 } 1072 --- 1073 */ 1074 Item opIndex(ItemID id) 1075 { 1076 return Item(m_interface, m_parentIDs, id); 1077 } 1078 1079 // forward all non-item methods 1080 mixin(() { 1081 string ret; 1082 foreach (m; __traits(allMembers, I)) { 1083 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 1084 alias PT = ParameterTypeTuple!ovrld; 1085 static if (!matchesAllIDs!ovrld && !hasUDA!(ovrld, NoRouteAttribute)) { 1086 static assert(matchesParentIDs!ovrld, 1087 "Collection methods must take all parent IDs as the first parameters."~PT.stringof~" "~ParentIDs.stringof); 1088 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_parentIDs, args); }\n"; 1089 } 1090 } 1091 } 1092 return ret; 1093 } ()); 1094 1095 private template matchesParentIDs(alias func) { 1096 static if (is(ParameterTypeTuple!func[0 .. ParentIDs.length] == ParentIDs)) { 1097 static if (ParentIDNames.length == 0) enum matchesParentIDs = true; 1098 else static if (ParameterIdentifierTuple!func[0 .. ParentIDNames.length] == ParentIDNames) 1099 enum matchesParentIDs = true; 1100 else enum matchesParentIDs = false; 1101 } else enum matchesParentIDs = false; 1102 } 1103 1104 private template matchesAllIDs(alias func) { 1105 static if (is(ParameterTypeTuple!func[0 .. AllIDs.length] == AllIDs)) { 1106 static if (ParameterIdentifierTuple!func[0 .. AllIDNames.length] == AllIDNames) 1107 enum matchesAllIDs = true; 1108 else enum matchesAllIDs = false; 1109 } else enum matchesAllIDs = false; 1110 } 1111 } 1112 1113 /// Model two nested collections using path based indexes 1114 unittest { 1115 // 1116 // API definition 1117 // 1118 interface SubItemAPI { 1119 // Define the index path that leads to a sub item 1120 struct CollectionIndices { 1121 // The ID of the base item. This must match the definition in 1122 // ItemAPI.CollectionIndices 1123 string _item; 1124 // The index if the sub item 1125 int _index; 1126 } 1127 1128 // GET /items/:item/subItems/length 1129 @property int length(string _item); 1130 1131 // GET /items/:item/subItems/:index/squared_position 1132 int getSquaredPosition(string _item, int _index); 1133 } 1134 1135 interface ItemAPI { 1136 // Define the index that identifies an item 1137 struct CollectionIndices { 1138 string _item; 1139 } 1140 1141 // base path /items/:item/subItems 1142 Collection!SubItemAPI subItems(string _item); 1143 1144 // GET /items/:item/name 1145 @property string name(string _item); 1146 } 1147 1148 interface API { 1149 // a collection of items at the base path /items/ 1150 Collection!ItemAPI items(); 1151 } 1152 1153 // 1154 // Local API implementation 1155 // 1156 class SubItemAPIImpl : SubItemAPI { 1157 @property int length(string _item) { return 10; } 1158 1159 int getSquaredPosition(string _item, int _index) { return _index ^^ 2; } 1160 } 1161 1162 class ItemAPIImpl : ItemAPI { 1163 private SubItemAPIImpl m_subItems; 1164 1165 this() { m_subItems = new SubItemAPIImpl; } 1166 1167 Collection!SubItemAPI subItems(string _item) { return Collection!SubItemAPI(m_subItems, _item); } 1168 1169 string name(string _item) { return _item; } 1170 } 1171 1172 class APIImpl : API { 1173 private ItemAPIImpl m_items; 1174 1175 this() { m_items = new ItemAPIImpl; } 1176 1177 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 1178 } 1179 1180 // 1181 // Resulting API usage 1182 // 1183 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 1184 1185 // GET /items/foo/name 1186 assert(api.items["foo"].name == "foo"); 1187 // GET /items/foo/sub_items/length 1188 assert(api.items["foo"].subItems.length == 10); 1189 // GET /items/foo/sub_items/2/squared_position 1190 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 1191 } 1192 1193 unittest { 1194 interface I { 1195 struct CollectionIndices { 1196 int id1; 1197 string id2; 1198 } 1199 1200 void a(int id1, string id2); 1201 void b(int id1, int id2); 1202 void c(int id1, string p); 1203 void d(int id1, string id2, int p); 1204 void e(int id1, int id2, int p); 1205 void f(int id1, string p, int q); 1206 } 1207 1208 Collection!I coll; 1209 static assert(is(typeof(coll["x"].a()) == void)); 1210 static assert(is(typeof(coll.b(42)) == void)); 1211 static assert(is(typeof(coll.c("foo")) == void)); 1212 static assert(is(typeof(coll["x"].d(42)) == void)); 1213 static assert(is(typeof(coll.e(42, 42)) == void)); 1214 static assert(is(typeof(coll.f("foo", 42)) == void)); 1215 } 1216 1217 /// Model two nested collections using normal query parameters as indexes 1218 unittest { 1219 // 1220 // API definition 1221 // 1222 interface SubItemAPI { 1223 // Define the index path that leads to a sub item 1224 struct CollectionIndices { 1225 // The ID of the base item. This must match the definition in 1226 // ItemAPI.CollectionIndices 1227 string item; 1228 // The index if the sub item 1229 int index; 1230 } 1231 1232 // GET /items/subItems/length?item=... 1233 @property int length(string item); 1234 1235 // GET /items/subItems/squared_position?item=...&index=... 1236 int getSquaredPosition(string item, int index); 1237 } 1238 1239 interface ItemAPI { 1240 // Define the index that identifies an item 1241 struct CollectionIndices { 1242 string item; 1243 } 1244 1245 // base path /items/subItems?item=... 1246 Collection!SubItemAPI subItems(string item); 1247 1248 // GET /items/name?item=... 1249 @property string name(string item); 1250 } 1251 1252 interface API { 1253 // a collection of items at the base path /items/ 1254 Collection!ItemAPI items(); 1255 } 1256 1257 // 1258 // Local API implementation 1259 // 1260 class SubItemAPIImpl : SubItemAPI { 1261 @property int length(string item) { return 10; } 1262 1263 int getSquaredPosition(string item, int index) { return index ^^ 2; } 1264 } 1265 1266 class ItemAPIImpl : ItemAPI { 1267 private SubItemAPIImpl m_subItems; 1268 1269 this() { m_subItems = new SubItemAPIImpl; } 1270 1271 Collection!SubItemAPI subItems(string item) { return Collection!SubItemAPI(m_subItems, item); } 1272 1273 string name(string item) { return item; } 1274 } 1275 1276 class APIImpl : API { 1277 private ItemAPIImpl m_items; 1278 1279 this() { m_items = new ItemAPIImpl; } 1280 1281 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 1282 } 1283 1284 // 1285 // Resulting API usage 1286 // 1287 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 1288 1289 // GET /items/name?item=foo 1290 assert(api.items["foo"].name == "foo"); 1291 // GET /items/subitems/length?item=foo 1292 assert(api.items["foo"].subItems.length == 10); 1293 // GET /items/subitems/squared_position?item=foo&index=2 1294 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 1295 } 1296 1297 unittest { 1298 interface C { 1299 struct CollectionIndices { 1300 int _ax; 1301 int _b; 1302 } 1303 void testB(int _ax, int _b); 1304 } 1305 1306 interface B { 1307 struct CollectionIndices { 1308 int _a; 1309 } 1310 Collection!C c(); 1311 void testA(int _a); 1312 } 1313 1314 interface A { 1315 Collection!B b(); 1316 } 1317 1318 static assert (!is(typeof(A.init.b[1].c[2].testB()))); 1319 } 1320 1321 /** Allows processing the server request/response before the handler method is called. 1322 1323 Note that this attribute is only used by `registerRestInterface`, but not 1324 by the client generators. This attribute expects the name of a parameter that 1325 will receive its return value. 1326 1327 Writing to the response body from within the specified hander function 1328 causes any further processing of the request to be skipped. In particular, 1329 the route handler method will not be called. 1330 1331 Note: 1332 The example shows the drawback of this attribute. It generally is a 1333 leaky abstraction that propagates to the base interface. For this 1334 reason the use of this attribute is not recommended, unless there is 1335 no suitable alternative. 1336 */ 1337 alias before = vibe.internal.meta.funcattr.before; 1338 1339 /// 1340 @safe unittest { 1341 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 1342 1343 interface MyService { 1344 long getHeaderCount(size_t foo = 0) @safe; 1345 } 1346 1347 static size_t handler(HTTPServerRequest req, HTTPServerResponse res) 1348 { 1349 return req.headers.length; 1350 } 1351 1352 class MyServiceImpl : MyService { 1353 // the "foo" parameter will receive the number of request headers 1354 @before!handler("foo") 1355 long getHeaderCount(size_t foo) 1356 { 1357 return foo; 1358 } 1359 } 1360 1361 void test(URLRouter router) 1362 @safe { 1363 router.registerRestInterface(new MyServiceImpl); 1364 } 1365 } 1366 1367 1368 /** Allows processing the return value of a handler method and the request/response objects. 1369 1370 The value returned by the REST API will be the value returned by the last 1371 `@after` handler, which allows to post process the results of the handler 1372 method. 1373 1374 Writing to the response body from within the specified handler function 1375 causes any further processing of the request ot be skipped, including 1376 any other `@after` annotations and writing the result value. 1377 */ 1378 alias after = vibe.internal.meta.funcattr.after; 1379 1380 /// 1381 @safe unittest { 1382 import vibe.http.server : HTTPServerRequest, HTTPServerResponse; 1383 1384 interface MyService { 1385 long getMagic() @safe; 1386 } 1387 1388 static long handler(long ret, HTTPServerRequest req, HTTPServerResponse res) 1389 @safe { 1390 return ret * 2; 1391 } 1392 1393 class MyServiceImpl : MyService{ 1394 // the result reported by the REST API will be 42 1395 @after!handler 1396 long getMagic() 1397 { 1398 return 21; 1399 } 1400 } 1401 1402 void test(URLRouter router) 1403 @safe { 1404 router.registerRestInterface(new MyServiceImpl); 1405 } 1406 } 1407 1408 /** 1409 * Generate an handler that will wrap the server's method 1410 * 1411 * This function returns an handler, generated at compile time, that 1412 * will deserialize the parameters, pass them to the function implemented 1413 * by the user, and return what it needs to return, be it header parameters 1414 * or body, which is at the moment either a pure string or a Json object. 1415 * 1416 * One thing that makes this method more complex that it needs be is the 1417 * inability for D to attach UDA to parameters. This means we have to roll 1418 * our own implementation, which tries to be as easy to use as possible. 1419 * We'll require the user to give the name of the parameter as a string to 1420 * our UDA. Hopefully, we're also able to detect at compile time if the user 1421 * made a typo of any kind (see $(D genInterfaceValidationError)). 1422 * 1423 * Note: 1424 * Lots of abbreviations are used to ease the code, such as 1425 * PTT (ParameterTypeTuple), WPAT (WebParamAttributeTuple) 1426 * and PWPAT (ParameterWebParamAttributeTuple). 1427 * 1428 * Params: 1429 * T = type of the object which represent the REST server (user implemented). 1430 * Func = An alias to the function of $(D T) to wrap. 1431 * 1432 * inst = REST server on which to call our $(D Func). 1433 * settings = REST server configuration. 1434 * 1435 * Returns: 1436 * A delegate suitable to use as an handler for an HTTP request. 1437 */ 1438 private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(T inst, ref RestInterface!T intf) 1439 { 1440 import std.meta : AliasSeq; 1441 import std.string : format; 1442 import std.traits : Unqual; 1443 import vibe.http.common : HTTPStatusException, enforceBadRequest; 1444 import vibe.utils.string : sanitizeUTF8; 1445 import vibe.web.internal.rest.common : ParameterKind; 1446 import vibe.internal.meta.funcattr : IsAttributedParameter, computeAttributedParameterCtx; 1447 import vibe.internal.meta.traits : derivedMethod; 1448 import vibe.textfilter.urlencode : urlDecode; 1449 1450 enum Method = __traits(identifier, Func); 1451 // We need mutable types for deserialization 1452 alias PTypes = staticMap!(Unqual, ParameterTypeTuple!Func); 1453 alias PDefaults = ParameterDefaultValueTuple!Func; 1454 alias CFuncRaw = derivedMethod!(T, Func); 1455 static if (AliasSeq!(CFuncRaw).length > 0) alias CFunc = CFuncRaw; 1456 else alias CFunc = Func; 1457 alias RT = ReturnType!(FunctionTypeOf!Func); 1458 static const sroute = RestInterface!T.staticRoutes[ridx]; 1459 auto route = intf.routes[ridx]; 1460 auto settings = intf.settings; 1461 alias SerPolicyType = SerPolicyT!(RestInterface!T.I).PolicyTemplate; 1462 1463 void handler(HTTPServerRequest req, HTTPServerResponse res) 1464 @safe { 1465 if (route.bodyParameters.length) { 1466 /*enforceBadRequest(req.contentType == "application/json", 1467 "The Content-Type header needs to be set to application/json.");*/ 1468 enforceBadRequest(req.json.type != Json.Type.undefined, 1469 "The request body does not contain a valid JSON value."); 1470 enforceBadRequest(req.json.type == Json.Type.object, 1471 "The request body must contain a JSON object."); 1472 } 1473 1474 void handleException(Exception e, HTTPStatus default_status) 1475 @safe { 1476 logDebug("REST handler exception: %s", () @trusted { return e.toString(); } ()); 1477 if (res.headerWritten) { 1478 logDebug("Response already started. Client will not receive an error code!"); 1479 return; 1480 } 1481 1482 if (settings.errorHandler) { 1483 settings.errorHandler(req, res, RestErrorInformation(e, default_status)); 1484 } else { 1485 import std.algorithm : among; 1486 debug string debugMsg; 1487 1488 if (auto se = cast(HTTPStatusException)e) 1489 res.statusCode = se.status; 1490 else debug { 1491 res.statusCode = HTTPStatus.internalServerError; 1492 debugMsg = () @trusted { return sanitizeUTF8(cast(ubyte[])e.toString()); }(); 1493 } 1494 else 1495 res.statusCode = default_status; 1496 1497 // All 1xx(informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body. 1498 // See: https://tools.ietf.org/html/rfc2616#section-4.3 1499 if (res.statusCode < 200 || res.statusCode.among(204, 304)) { 1500 res.writeVoidBody(); 1501 return; 1502 } 1503 1504 debug { 1505 if (debugMsg) { 1506 res.writeJsonBody(["statusMessage": e.msg, "statusDebugMessage": debugMsg]); 1507 return; 1508 } 1509 } 1510 res.writeJsonBody(["statusMessage": e.msg]); 1511 } 1512 } 1513 1514 static if (isAuthenticated!(T, Func)) { 1515 typeof(handleAuthentication!Func(inst, req, res)) auth_info; 1516 1517 try auth_info = handleAuthentication!Func(inst, req, res); 1518 catch (Exception e) { 1519 handleException(e, HTTPStatus.unauthorized); 1520 return; 1521 } 1522 1523 if (res.headerWritten) return; 1524 } 1525 1526 1527 PTypes params; 1528 1529 try { 1530 foreach (i, PT; PTypes) { 1531 enum sparam = sroute.parameters[i]; 1532 1533 static if (sparam.isIn) { 1534 enum pname = sparam.name; 1535 auto fieldname = route.parameters[i].fieldName; 1536 static if (isInstanceOf!(Nullable, PT)) PT v; 1537 else Nullable!PT v; 1538 1539 static if (sparam.kind == ParameterKind.auth) { 1540 v = auth_info; 1541 } else static if (sparam.kind == ParameterKind.query) { 1542 if (auto pv = fieldname in req.query) 1543 v = fromRestString!(PT, SerPolicyType)(*pv); 1544 } else static if (sparam.kind == ParameterKind.wholeBody) { 1545 try v = deserializeWithPolicy!(JsonSerializer, SerPolicyType, PT)(req.json); 1546 catch (JSONException e) enforceBadRequest(false, e.msg); 1547 } else static if (sparam.kind == ParameterKind.body_) { 1548 try { 1549 if (auto pv = fieldname in req.json) 1550 v = deserializeWithPolicy!(JsonSerializer, SerPolicyType, PT)(*pv); 1551 } catch (JSONException e) 1552 enforceBadRequest(false, e.msg); 1553 } else static if (sparam.kind == ParameterKind.header) { 1554 if (auto pv = fieldname in req.headers) 1555 v = fromRestString!(PT, SerPolicyType)(*pv); 1556 } else static if (sparam.kind == ParameterKind.attributed) { 1557 static if (!__traits(compiles, () @safe { computeAttributedParameterCtx!(CFunc, pname)(inst, req, res); } ())) 1558 pragma(msg, "Non-@safe @before evaluators are deprecated - annotate evaluator function for parameter "~pname~" of "~T.stringof~"."~Method~" as @safe."); 1559 v = () @trusted { return computeAttributedParameterCtx!(CFunc, pname)(inst, req, res); } (); 1560 } else static if (sparam.kind == ParameterKind.internal) { 1561 if (auto pv = fieldname in req.params) 1562 v = fromRestString!(PT, DefaultPolicy)(urlDecode(*pv)); 1563 } else static assert(false, "Unhandled parameter kind."); 1564 1565 static if (isInstanceOf!(Nullable, PT)) params[i] = v; 1566 else if (v.isNull()) { 1567 static if (!is(PDefaults[i] == void)) params[i] = PDefaults[i]; 1568 else enforceBadRequest(false, "Missing non-optional "~sparam.kind.to!string~" parameter '"~(fieldname.length?fieldname:sparam.name)~"'."); 1569 } else params[i] = v.get; 1570 } 1571 } 1572 } catch (Exception e) { 1573 handleException(e, HTTPStatus.badRequest); 1574 return; 1575 } 1576 1577 static if (isAuthenticated!(T, Func)) { 1578 try handleAuthorization!(T, Func, params)(auth_info); 1579 catch (Exception e) { 1580 handleException(e, HTTPStatus.forbidden); 1581 return; 1582 } 1583 } 1584 1585 void handleCors() 1586 { 1587 import std.algorithm : any; 1588 import std.uni : sicmp; 1589 1590 if (req.method == HTTPMethod.OPTIONS) 1591 return; 1592 auto origin = "Origin" in req.headers; 1593 if (origin is null) 1594 return; 1595 1596 if (settings.allowedOrigins.length != 0 && 1597 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1598 return; 1599 1600 res.headers["Access-Control-Allow-Origin"] = *origin; 1601 res.headers["Access-Control-Allow-Credentials"] = "true"; 1602 } 1603 // Anti copy-paste 1604 void returnHeaders() 1605 { 1606 handleCors(); 1607 foreach (i, P; PTypes) { 1608 static if (sroute.parameters[i].isOut) { 1609 static assert (sroute.parameters[i].kind == ParameterKind.header); 1610 static if (isInstanceOf!(Nullable, typeof(params[i]))) { 1611 if (!params[i].isNull) 1612 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1613 } else { 1614 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1615 } 1616 } 1617 } 1618 } 1619 1620 try { 1621 import vibe.internal.meta.funcattr; 1622 1623 static if (!__traits(compiles, () @safe { __traits(getMember, inst, Method)(params); })) 1624 pragma(msg, "Non-@safe methods are deprecated in REST interfaces - Mark " ~ 1625 T.stringof ~ "." ~ Method ~ " as @safe."); 1626 1627 static if (is(RT == void)) { 1628 // TODO: remove after deprecation period 1629 () @trusted { __traits(getMember, inst, Method)(params); } (); 1630 returnHeaders(); 1631 res.writeBody(cast(ubyte[])null); 1632 } else static if (isInputStream!RT) { 1633 returnHeaders(); 1634 auto ret = () @trusted { 1635 return evaluateOutputModifiers!CFunc( 1636 __traits(getMember, inst, Method)(params), req, res); } (); 1637 ret.pipe(res.bodyWriter); 1638 } else { 1639 // TODO: remove after deprecation period 1640 static if (!__traits(compiles, () @safe { evaluateOutputModifiers!Func(RT.init, req, res); } ())) 1641 pragma(msg, "Non-@safe @after evaluators are deprecated - annotate @after evaluator function for " ~ 1642 T.stringof ~ "." ~ Method ~ " as @safe."); 1643 1644 auto ret = () @trusted { 1645 return evaluateOutputModifiers!CFunc( 1646 __traits(getMember, inst, Method)(params), req, res); } (); 1647 returnHeaders(); 1648 1649 string accept_str; 1650 if (const accept_header = "Accept" in req.headers) 1651 accept_str = *accept_header; 1652 alias result_serializers = ResultSerializersT!Func; 1653 immutable serializer_ind = get_matching_content_type!(result_serializers)(accept_str); 1654 foreach (i, serializer; result_serializers) 1655 if (serializer_ind == i) { 1656 auto serialized_output = appender!(ubyte[]); 1657 static if ( 1658 __traits(compiles, () @trusted { 1659 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1660 }) 1661 && !__traits(compiles, () @safe { 1662 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1663 })) 1664 { 1665 pragma(msg, "Non-@safe serialization of REST return types deprecated - ensure that " ~ 1666 RT.stringof~" is safely serializable."); 1667 } 1668 () @trusted { 1669 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1670 }(); 1671 res.writeBody(serialized_output.data, serializer.contentType); 1672 } 1673 res.statusCode = HTTPStatus.notAcceptable; // will trigger RestException on the client side 1674 res.writeBody(cast(ubyte[])null); 1675 } 1676 } catch (Exception e) { 1677 returnHeaders(); 1678 handleException(e, HTTPStatus.internalServerError); 1679 } 1680 } 1681 1682 return &handler; 1683 } 1684 1685 /** 1686 * Generate an handler that will wrap the server's method 1687 * 1688 * This function returns an handler that handles the http OPTIONS method. 1689 * 1690 * It will return the ALLOW header with all the methods on this resource 1691 * And it will handle Preflight CORS. 1692 * 1693 * Params: 1694 * routes = a range of Routes were each route has the same resource/URI 1695 * just different method. 1696 * settings = REST server configuration. 1697 * 1698 * Returns: 1699 * A delegate suitable to use as an handler for an HTTP request. 1700 */ 1701 private HTTPServerRequestDelegate optionsMethodHandler(RouteRange)(RouteRange routes, RestInterfaceSettings settings = null) 1702 { 1703 import std.algorithm : map, joiner, any; 1704 import std.conv : text; 1705 import std.array : array; 1706 import vibe.http.common : httpMethodString, httpMethodFromString; 1707 // NOTE: don't know what is better, to keep this in memory, or generate on each request 1708 auto allow = routes.map!(r => r.method.httpMethodString).joiner(",").text(); 1709 auto methods = routes.map!(r => r.method).array(); 1710 1711 void handlePreflightedCors(HTTPServerRequest req, HTTPServerResponse res, ref HTTPMethod[] methods, RestInterfaceSettings settings = null) 1712 { 1713 import std.algorithm : among; 1714 import std.uni : sicmp; 1715 1716 auto origin = "Origin" in req.headers; 1717 if (origin is null) 1718 return; 1719 1720 if (settings !is null && 1721 settings.allowedOrigins.length != 0 && 1722 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1723 return; 1724 1725 auto method = "Access-Control-Request-Method" in req.headers; 1726 if (method is null) 1727 return; 1728 1729 auto httpMethod = httpMethodFromString(*method); 1730 1731 if (!methods.any!(m => m == httpMethod)) 1732 return; 1733 1734 res.headers["Access-Control-Allow-Origin"] = *origin; 1735 1736 // there is no way to know if the specific resource supports credentials 1737 // (either cookies, HTTP authentication, or client-side SSL certificates), 1738 // so we always assume it does 1739 res.headers["Access-Control-Allow-Credentials"] = "true"; 1740 res.headers["Access-Control-Max-Age"] = "1728000"; 1741 res.headers["Access-Control-Allow-Methods"] = *method; 1742 1743 // we have no way to reliably determine what headers the resource allows 1744 // so we simply copy whatever the client requested 1745 if (auto headers = "Access-Control-Request-Headers" in req.headers) 1746 res.headers["Access-Control-Allow-Headers"] = *headers; 1747 } 1748 1749 void handler(HTTPServerRequest req, HTTPServerResponse res) 1750 { 1751 // since this is a OPTIONS request, we have to return the ALLOW headers to tell which methods we have 1752 res.headers["Allow"] = allow; 1753 1754 // handle CORS preflighted requests 1755 handlePreflightedCors(req,res,methods,settings); 1756 1757 // NOTE: besides just returning the allowed methods and handling CORS preflighted requests, 1758 // this would be a nice place to describe what kind of resources are on this route, 1759 // the params each accepts, the headers, etc... think WSDL but then for REST. 1760 res.writeBody(""); 1761 } 1762 return &handler; 1763 } 1764 1765 private string generateRestClientMethods(I)() 1766 { 1767 import std.array : join; 1768 import std.string : format; 1769 import std.traits : fullyQualifiedName, isInstanceOf; 1770 1771 alias Info = RestInterface!I; 1772 1773 string ret = q{ 1774 import vibe.internal.meta.codegen : CloneFunction; 1775 }; 1776 1777 // generate sub interface methods 1778 foreach (i, SI; Info.SubInterfaceTypes) { 1779 alias F = Info.SubInterfaceFunctions[i]; 1780 alias RT = ReturnType!F; 1781 alias ParamNames = ParameterIdentifierTuple!F; 1782 static if (ParamNames.length == 0) enum pnames = ""; 1783 else enum pnames = ", " ~ [ParamNames].join(", "); 1784 static if (isInstanceOf!(Collection, RT)) { 1785 ret ~= q{ 1786 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1787 return Collection!(%2$s)(m_subInterfaces[%1$s]%3$s); 1788 }); 1789 }.format(i, fullyQualifiedName!SI, pnames); 1790 } else { 1791 ret ~= q{ 1792 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1793 return m_subInterfaces[%1$s]; 1794 }); 1795 }.format(i); 1796 } 1797 } 1798 1799 // generate route methods 1800 foreach (i, F; Info.RouteFunctions) { 1801 alias ParamNames = ParameterIdentifierTuple!F; 1802 static if (ParamNames.length == 0) enum pnames = ""; 1803 else enum pnames = ", " ~ [ParamNames].join(", "); 1804 1805 ret ~= q{ 1806 mixin CloneFunction!(Info.RouteFunctions[%1$s], q{ 1807 return executeClientMethod!(I, %1$s%2$s)(*m_intf, m_requestFilter, m_requestBodyFilter); 1808 }); 1809 }.format(i, pnames); 1810 } 1811 1812 // generate stubs for non-route functions 1813 static foreach (m; __traits(allMembers, I)) 1814 foreach (i, fun; MemberFunctionsTuple!(I, m)) 1815 static if (hasUDA!(fun, NoRouteAttribute)) 1816 ret ~= q{ 1817 mixin CloneFunction!(MemberFunctionsTuple!(I, "%s")[%s], q{ 1818 assert(false); 1819 }); 1820 }.format(m, i); 1821 1822 return ret; 1823 } 1824 1825 1826 private auto executeClientMethod(I, size_t ridx, ARGS...) 1827 (const scope ref RestInterface!I intf, scope void delegate(HTTPClientRequest) @safe request_filter, 1828 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter) 1829 { 1830 import vibe.web.internal.rest.common : ParameterKind; 1831 import vibe.stream.operations : readAll; 1832 import vibe.textfilter.urlencode : filterURLEncode, urlEncode; 1833 import std.array : appender; 1834 1835 alias Info = RestInterface!I; 1836 alias Func = Info.RouteFunctions[ridx]; 1837 alias RT = ReturnType!Func; 1838 alias PTT = ParameterTypeTuple!Func; 1839 alias SerPolicyType = SerPolicyT!I.PolicyTemplate; 1840 enum sroute = Info.staticRoutes[ridx]; 1841 auto route = intf.routes[ridx]; 1842 auto settings = intf.settings; 1843 1844 InetHeaderMap headers; 1845 InetHeaderMap reqhdrs; 1846 InetHeaderMap opthdrs; 1847 1848 string url_prefix; 1849 1850 auto query = appender!string(); 1851 auto jsonBody = Json.emptyObject; 1852 string body_; 1853 1854 void addQueryParam(size_t i)(string name) 1855 { 1856 if (query.data.length) query.put('&'); 1857 query.filterURLEncode(name); 1858 query.put("="); 1859 static if (is(PT == Json)) 1860 query.filterURLEncode(ARGS[i].toString()); 1861 else 1862 // Note: CTFE triggers compiler bug here (think we are returning Json, not string). 1863 query.filterURLEncode(toRestString( 1864 serializeWithPolicy!(JsonSerializer, SerPolicyType)(ARGS[i]))); 1865 } 1866 1867 foreach (i, PT; PTT) { 1868 enum sparam = sroute.parameters[i]; 1869 auto fieldname = route.parameters[i].fieldName; 1870 static if (sparam.kind == ParameterKind.query) { 1871 addQueryParam!i(fieldname); 1872 } else static if (sparam.kind == ParameterKind.wholeBody) { 1873 jsonBody = serializeWithPolicy!(JsonSerializer, SerPolicyType)(ARGS[i]); 1874 } else static if (sparam.kind == ParameterKind.body_) { 1875 jsonBody[fieldname] = serializeWithPolicy!(JsonSerializer, SerPolicyType)(ARGS[i]); 1876 } else static if (sparam.kind == ParameterKind.header) { 1877 // Don't send 'out' parameter, as they should be default init anyway and it might confuse some server 1878 static if (sparam.isIn) { 1879 static if (isInstanceOf!(Nullable, PT)) { 1880 if (!ARGS[i].isNull) 1881 headers[fieldname] = to!string(ARGS[i]); 1882 } else headers[fieldname] = to!string(ARGS[i]); 1883 } 1884 static if (sparam.isOut) { 1885 // Optional parameter 1886 static if (isInstanceOf!(Nullable, PT)) { 1887 opthdrs[fieldname] = null; 1888 } else { 1889 reqhdrs[fieldname] = null; 1890 } 1891 } 1892 } 1893 } 1894 1895 static if (sroute.method == HTTPMethod.GET) { 1896 assert(jsonBody == Json.emptyObject, "GET request trying to send body parameters."); 1897 } else { 1898 debug body_ = jsonBody.toPrettyString(); 1899 else body_ = jsonBody.toString(); 1900 } 1901 1902 string url; 1903 foreach (i, p; route.fullPathParts) { 1904 if (p.isParameter) { 1905 switch (p.text) { 1906 foreach (j, PT; PTT) { 1907 static if (sroute.parameters[j].name[0] == '_' || sroute.parameters[j].name == "id") { 1908 case sroute.parameters[j].name: 1909 url ~= urlEncode(toRestString(serializeToJson(ARGS[j]))); 1910 goto sbrk; 1911 } 1912 } 1913 default: url ~= ":" ~ p.text; break; 1914 } 1915 sbrk:; 1916 } else url ~= p.text; 1917 } 1918 1919 scope (exit) { 1920 foreach (i, PT; PTT) { 1921 enum sparam = sroute.parameters[i]; 1922 auto fieldname = route.parameters[i].fieldName; 1923 static if (sparam.kind == ParameterKind.header) { 1924 static if (sparam.isOut) { 1925 static if (isInstanceOf!(Nullable, PT)) { 1926 ARGS[i] = to!(TemplateArgsOf!PT)( 1927 opthdrs.get(fieldname, null)); 1928 } else { 1929 if (auto ptr = fieldname in reqhdrs) 1930 ARGS[i] = to!PT(*ptr); 1931 } 1932 } 1933 } 1934 } 1935 } 1936 1937 headers["Accept"] = settings.content_type; 1938 1939 // Do not require a `Content-Type` header if no response is expected 1940 // https://github.com/vibe-d/vibe.d/issues/2521 1941 static if (!is(RT == void)) 1942 // Don't override it if set from the parameter 1943 if ("Content-Type" !in opthdrs) 1944 opthdrs["Content-Type"] = null; 1945 1946 auto ret = request(URL(intf.baseURL), request_filter, request_body_filter, 1947 sroute.method, url, headers, query.data, body_, reqhdrs, opthdrs, 1948 intf.settings.httpClientSettings); 1949 1950 static if (isInputStream!RT) { 1951 return RT(ret.bodyReader); 1952 } else static if (!is(RT == void)) { 1953 scope(exit) ret.dropBody(); 1954 1955 string content_type; 1956 if (const hdr = "Content-Type" in opthdrs) 1957 content_type = *hdr; 1958 if (!content_type.length) 1959 content_type = "application/octet-stream"; 1960 1961 alias result_serializers = ResultSerializersT!Func; 1962 immutable serializer_ind = get_matching_content_type!(result_serializers)(content_type); 1963 foreach (i, serializer; result_serializers) 1964 if (serializer_ind == i) { 1965 // TODO: The JSON deserialiation code requires a forward range, 1966 // but streamInputRange is currently just a bare input 1967 // range, so for now we need to read everything into a 1968 // buffer instead. 1969 //import vibe.stream.wrapper : streamInputRange; 1970 //auto rng = streamInputRange(ret.bodyReader); 1971 auto rng = ret.bodyReader.readAll(); 1972 return serializer.deserialize!(SerPolicyT!I.PolicyTemplate, RT)(rng); 1973 } 1974 1975 throw new Exception("Unrecognized content type: " ~ content_type); 1976 } else ret.dropBody(); 1977 } 1978 1979 1980 import vibe.http.client : HTTPClientRequest; 1981 /** 1982 * Perform a request to the interface using the given parameters. 1983 * 1984 * Params: 1985 * verb = Kind of request (See $(D HTTPMethod) enum). 1986 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1987 * it will be '/rejectedsoftware/vibe.d/issues'. 1988 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 1989 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1990 * it will be 'author%3ASantaClaus'. 1991 * 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 1992 * the generic type "application/json". 1993 * reqReturnHdrs = A map of required return headers. 1994 * To avoid returning unused headers, nothing is written 1995 * to this structure unless there's an (usually empty) 1996 * entry (= the key exists) with the same key. 1997 * If any key present in `reqReturnHdrs` is not present 1998 * in the response, an Exception is thrown. 1999 * optReturnHdrs = A map of optional return headers. 2000 * This behaves almost as exactly as reqReturnHdrs, 2001 * except that non-existent key in the response will 2002 * not cause it to throw, but rather to set this entry 2003 * to 'null'. 2004 * 2005 * Returns: 2006 * The Json object returned by the request 2007 */ 2008 private HTTPClientResponse request(URL base_url, 2009 scope void delegate(HTTPClientRequest) @safe request_filter, 2010 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter, 2011 HTTPMethod verb, string path, const scope ref InetHeaderMap hdrs, string query, 2012 string body_, ref InetHeaderMap reqReturnHdrs, 2013 ref InetHeaderMap optReturnHdrs, in HTTPClientSettings http_settings) 2014 @safe { 2015 import std.uni : sicmp; 2016 import vibe.http.client : HTTPClientRequest, HTTPClientResponse, requestHTTP; 2017 import vibe.http.common : HTTPStatusException, HTTPStatus, httpMethodString, httpStatusText; 2018 2019 URL url = base_url; 2020 url.pathString = path; 2021 2022 if (query.length) url.queryString = query; 2023 2024 auto reqdg = (scope HTTPClientRequest req) { 2025 req.method = verb; 2026 foreach (k, v; hdrs.byKeyValue) 2027 req.headers[k] = v; 2028 2029 if (request_body_filter) { 2030 import vibe.stream.memory : createMemoryStream; 2031 scope str = createMemoryStream(() @trusted { return cast(ubyte[])body_; } (), false); 2032 request_body_filter(req, str); 2033 } 2034 2035 if (request_filter) request_filter(req); 2036 2037 if (body_ != "") 2038 req.writeBody(cast(const(ubyte)[])body_, hdrs.get("Content-Type", "application/json; charset=UTF-8")); 2039 }; 2040 2041 HTTPClientResponse client_res; 2042 if (http_settings) client_res = requestHTTP(url, reqdg, http_settings); 2043 else client_res = requestHTTP(url, reqdg); 2044 2045 import vibe.stream.operations; 2046 2047 logDebug( 2048 "REST call: %s %s -> %d, %s", 2049 httpMethodString(verb), 2050 url.toString(), 2051 client_res.statusCode, 2052 client_res.statusPhrase 2053 ); 2054 2055 // Get required headers - Don't throw yet 2056 string[] missingKeys; 2057 foreach (k, ref v; reqReturnHdrs.byKeyValue) 2058 if (auto ptr = k in client_res.headers) 2059 v = (*ptr).idup; 2060 else 2061 missingKeys ~= k; 2062 2063 // Get optional headers 2064 foreach (k, ref v; optReturnHdrs.byKeyValue) 2065 if (auto ptr = k in client_res.headers) 2066 v = (*ptr).idup; 2067 else 2068 v = null; 2069 if (missingKeys.length) 2070 throw new Exception( 2071 "REST interface mismatch: Missing required header field(s): " 2072 ~ missingKeys.to!string); 2073 2074 if (!isSuccessCode(cast(HTTPStatus)client_res.statusCode)) 2075 { 2076 Json msg = Json(["statusMessage": Json(client_res.statusPhrase)]); 2077 if (client_res.contentType.length) 2078 if (client_res.contentType.splitter(";").front.strip.sicmp("application/json") == 0) 2079 msg = client_res.readJson(); 2080 client_res.dropBody(); 2081 throw new RestException(client_res.statusCode, msg); 2082 } 2083 2084 return client_res; 2085 } 2086 2087 private { 2088 import vibe.data.json; 2089 import std.conv : to; 2090 2091 string toRestString(Json value) 2092 @safe { 2093 switch (value.type) { 2094 default: return value.toString(); 2095 case Json.Type.Bool: return value.get!bool ? "true" : "false"; 2096 case Json.Type.Int: return to!string(value.get!long); 2097 case Json.Type.Float: return to!string(value.get!double); 2098 case Json.Type.String: return value.get!string; 2099 } 2100 } 2101 2102 T fromRestString(T, alias SerPolicyType = DefaultPolicy)(string value) 2103 { 2104 import std.conv : ConvException; 2105 import std.uuid : UUID, UUIDParsingException; 2106 import vibe.http.common : HTTPStatusException; 2107 import vibe.http.status : HTTPStatus; 2108 try { 2109 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 2110 else static if (is(T == bool)) return value == "1" || value.to!T; 2111 else static if (is(T : int)) return to!T(value); 2112 else static if (is(T : double)) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 2113 else static if (is(string : T)) return value; 2114 else static if (__traits(compiles, T.fromISOExtString("hello"))) return T.fromISOExtString(value); 2115 else static if (__traits(compiles, T.fromString("hello"))) return T.fromString(value); 2116 else static if (is(T == UUID)) return UUID(value); 2117 else return deserializeWithPolicy!(JsonStringSerializer!string, SerPolicyType, T)(value); 2118 } catch (ConvException e) { 2119 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2120 } catch (JSONException e) { 2121 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2122 } catch (UUIDParsingException e) { 2123 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2124 } 2125 } 2126 2127 // Converting from invalid JSON string to aggregate should throw bad request 2128 unittest { 2129 import vibe.http.common : HTTPStatusException; 2130 import vibe.http.status : HTTPStatus; 2131 2132 void assertHTTPStatus(E)(lazy E expression, HTTPStatus expectedStatus, 2133 string file = __FILE__, size_t line = __LINE__) 2134 { 2135 import core.exception : AssertError; 2136 import std.format : format; 2137 2138 try 2139 expression(); 2140 catch (HTTPStatusException e) 2141 { 2142 if (e.status != expectedStatus) 2143 throw new AssertError(format("assertHTTPStatus failed: " ~ 2144 "status expected %d but was %d", expectedStatus, e.status), 2145 file, line); 2146 2147 return; 2148 } 2149 2150 throw new AssertError("assertHTTPStatus failed: No " ~ 2151 "'HTTPStatusException' exception was thrown", file, line); 2152 } 2153 2154 struct Foo { int bar; } 2155 assertHTTPStatus(fromRestString!(Foo)("foo"), HTTPStatus.badRequest); 2156 } 2157 } 2158 2159 private string generateModuleImports(I)() 2160 { 2161 if (!__ctfe) 2162 assert (false); 2163 2164 import vibe.internal.meta.codegen : getRequiredImports; 2165 import std.algorithm : map; 2166 import std.array : join; 2167 2168 auto modules = getRequiredImports!I(); 2169 return join(map!(a => "static import " ~ a ~ ";")(modules), "\n"); 2170 } 2171 2172 /*************************************************************************** 2173 2174 The client sends the list of allowed content types in the 'Allow' http header 2175 and the response will contain a 'Content-Type' header. This function will 2176 try to find the best matching @SerializationResult UDA based on the allowed 2177 content types. 2178 2179 Note: 2180 2181 Comment 1: if the request doesn't specify any allowed content types, then * / * 2182 is assumed 2183 2184 Comment 2: if there are no UDA's matching the client's allowed content types, -1 2185 is returned 2186 2187 Comment 3: if there are more than 1 matching UDA, for ONE specific client's allowed 2188 content type(and their priority is the same - see below), 2189 then the one specified earlier in the code gets chosen 2190 2191 Comment 4: accept-params(quality factor) and accept-extensions are ignored 2192 https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 2193 2194 Comment 5: matching the most specific content type without any wildcard has priority 2195 2196 Comment 6: the request's content type can be in the format of 2197 2198 $(UL 2199 $(LI major type / minor type) 2200 $(LI major type / *) 2201 $(LI * / *) 2202 ) 2203 2204 Params: 2205 T = compile time known ResultSerializer classes 2206 req_content_types_str = list of allowed content types for example: 2207 text/*;q=0.3, text/html;q=0.7, text/html;level=1 2208 2209 Returns: 2210 index of the result serializers in the T... AliasSeq if matching found, 2211 -1 otherwise 2212 2213 ***************************************************************************/ 2214 2215 package int get_matching_content_type (T...)(string req_content_types_str) pure @safe 2216 { 2217 if (!req_content_types_str.strip().length) 2218 req_content_types_str = "*/*"; 2219 struct ContentType 2220 { 2221 this (string major_type, string minor_type) 2222 { 2223 this.major_type = major_type; 2224 this.minor_type = minor_type; 2225 this.full_type = major_type ~ "/" ~ minor_type; 2226 this.star_num = this.full_type.count('*'); // serves as priority 2227 } 2228 string major_type; 2229 string minor_type; 2230 string full_type; 2231 ulong star_num; 2232 } 2233 2234 // processing ResultSerializers 2235 alias packed_UDAs = AliasSeq!(T); 2236 ContentType[] UDA_content_types; 2237 foreach (UDA; packed_UDAs) 2238 { 2239 auto ctype = UDA.contentType.splitter(';').front; 2240 auto content_type_split = ctype.toLower().split("/"); 2241 assert(content_type_split.length == 2); 2242 UDA_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2243 } 2244 2245 // processing request content typess 2246 ContentType[] req_content_types; 2247 foreach (content_type; req_content_types_str.toLower().split(",")) 2248 { 2249 immutable semicolon_pos = content_type.indexOf(';'); 2250 if (semicolon_pos != -1) 2251 content_type = content_type[0 .. semicolon_pos]; // quality factor ignored 2252 auto content_type_split = content_type.split("/"); 2253 if (content_type_split.length == 2) 2254 req_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2255 } 2256 // sorting content types by matching preference 2257 req_content_types.sort!(( x, y) => (x.star_num < y.star_num)); 2258 2259 int res = -1; 2260 ulong min_star_num = ulong.max; 2261 foreach (UDA_ind, UDA_content_type; UDA_content_types) 2262 foreach (const ref content_type; req_content_types) 2263 if ( 2264 ( 2265 ( 2266 UDA_content_type.major_type == content_type.major_type && 2267 UDA_content_type.minor_type == content_type.minor_type 2268 ) || 2269 ( 2270 UDA_content_type.major_type == content_type.major_type && 2271 content_type.minor_type == "*" 2272 ) || 2273 ( 2274 content_type.major_type == "*" && content_type.minor_type == "*" 2275 ) 2276 ) && 2277 ( 2278 content_type.star_num < min_star_num 2279 ) 2280 ) 2281 { 2282 res = cast(int) UDA_ind; 2283 min_star_num = content_type.star_num; 2284 } 2285 return res; 2286 } 2287 2288 version(unittest) 2289 { 2290 import std.range.interfaces : OutputRange; 2291 import vibe.internal.interfaceproxy : InterfaceProxy; 2292 2293 void s (OutputRange!char, int){}; 2294 int d (InterfaceProxy!(InputStream)){return 1;} 2295 2296 int test1(); 2297 2298 @resultSerializer!(s,d,"text/plain") 2299 @resultSerializer!(s,d," aPPliCatIon / jsOn ") 2300 int test2(); 2301 } 2302 2303 unittest 2304 { 2305 alias res = ResultSerializersT!(test1); 2306 assert(res.length == 1); 2307 assert(res[0].contentType == "application/json; charset=UTF-8"); 2308 2309 assert(get_matching_content_type!(res)("application/json") == 0); 2310 assert(get_matching_content_type!(res)("application/*") == 0); 2311 assert(get_matching_content_type!(res)(" appliCAtIon / *") == 0); 2312 assert(get_matching_content_type!(res)("*/*") == 0); 2313 assert(get_matching_content_type!(res)("") == 0); 2314 assert(get_matching_content_type!(res)("application/blabla") == -1); 2315 2316 alias res2 = ResultSerializersT!(test2); 2317 assert(res2.length == 2); 2318 assert(res2[0].contentType == "text/plain"); 2319 assert(res2[1].contentType == " aPPliCatIon / jsOn "); 2320 2321 assert(get_matching_content_type!(res2)("text/plain, application/json") == 0); 2322 assert(get_matching_content_type!(res2)("text/*, application/json") == 1); 2323 assert(get_matching_content_type!(res2)("*/*, application/json") == 1); 2324 assert(get_matching_content_type!(res2)("*/*") == 0); 2325 assert(get_matching_content_type!(res2)("") == 0); 2326 assert(get_matching_content_type!(res2)("blabla/blabla, blublu/blublu") == -1); 2327 } 2328 2329 version(unittest) 2330 { 2331 private struct Aggregate { } 2332 private interface Interface 2333 { 2334 Aggregate[] foo(); 2335 } 2336 } 2337 2338 unittest 2339 { 2340 enum imports = generateModuleImports!Interface; 2341 static assert (imports == "static import vibe.web.rest;"); 2342 } 2343 2344 // Check that the interface is valid. Every checks on the correctness of the 2345 // interface should be put in checkRestInterface, which allows to have consistent 2346 // errors in the server and client. 2347 package string getInterfaceValidationError(I)() 2348 out (result) { assert((result is null) == !result.length); } 2349 do { 2350 import vibe.web.internal.rest.common : ParameterKind, WebParamUDATuple; 2351 import std.typetuple : TypeTuple; 2352 import std.algorithm : strip; 2353 2354 // The hack parameter is to kill "Statement is not reachable" warnings. 2355 string validateMethod(alias Func)(bool hack = true) { 2356 import vibe.internal.meta.uda; 2357 import std.string : format; 2358 2359 static assert(is(FunctionTypeOf!Func), "Internal error"); 2360 2361 if (!__ctfe) 2362 assert(false, "Internal error"); 2363 2364 enum FuncId = (fullyQualifiedName!I~ "." ~ __traits(identifier, Func)); 2365 alias PT = ParameterTypeTuple!Func; 2366 static if (!__traits(compiles, ParameterIdentifierTuple!Func)) { 2367 if (hack) return "%s: A parameter has no name.".format(FuncId); 2368 alias PN = TypeTuple!("-DummyInvalid-"); 2369 } else 2370 alias PN = ParameterIdentifierTuple!Func; 2371 alias WPAT = UDATuple!(WebParamAttribute, Func); 2372 2373 // Check if there is no orphan UDATuple (e.g. typo while writing the name of the parameter). 2374 foreach (i, uda; WPAT) { 2375 // Note: static foreach gets unrolled, generating multiple nested sub-scope. 2376 // The spec / DMD doesn't like when you have the same symbol in those, 2377 // leading to wrong codegen / wrong template being reused. 2378 // That's why those templates need different names. 2379 // See DMD bug #9748. 2380 mixin(GenOrphan!(i).Decl); 2381 // template CmpOrphan(string name) { enum CmpOrphan = (uda.identifier == name); } 2382 static if (!anySatisfy!(mixin(GenOrphan!(i).Name), PN)) { 2383 if (hack) return "%s: No parameter '%s' (referenced by attribute @%sParam)" 2384 .format(FuncId, uda.identifier, uda.origin); 2385 } 2386 } 2387 2388 foreach (i, P; PT) { 2389 static if (!PN[i].length) 2390 if (hack) return "%s: Parameter %d has no name." 2391 .format(FuncId, i); 2392 // Check for multiple origins 2393 static if (WPAT.length) { 2394 // It's okay to reuse GenCmp, as the order of params won't change. 2395 // It should/might not be reinstantiated by the compiler. 2396 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2397 alias WPA = Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT); 2398 static if (WPA.length > 1) 2399 if (hack) return "%s: Parameter '%s' has multiple @*Param attributes on it." 2400 .format(FuncId, PN[i]); 2401 } 2402 } 2403 2404 // Check for misplaced out and non-const ref 2405 alias PSC = ParameterStorageClass; 2406 foreach (i, SC; ParameterStorageClassTuple!Func) { 2407 static if (SC & PSC.out_ || (SC & PSC.ref_ && !is(ConstOf!(PT[i]) == PT[i])) ) { 2408 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2409 alias Attr = TypeTuple!( 2410 WebParamUDATuple!(Func, i), 2411 Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT), 2412 ); 2413 static if (Attr.length != 1) { 2414 if (hack) return "%s: Parameter '%s' cannot be %s" 2415 .format(FuncId, PN[i], SC & PSC.out_ ? "out" : "ref"); 2416 } else static if (Attr[0].origin != ParameterKind.header) { 2417 if (hack) return "%s: %s parameter '%s' cannot be %s" 2418 .format(FuncId, Attr[0].origin, PN[i], 2419 SC & PSC.out_ ? "out" : "ref"); 2420 } 2421 } 2422 } 2423 2424 // Check for @path(":name") 2425 enum pathAttr = findFirstUDA!(PathAttribute, Func); 2426 static if (pathAttr.found) { 2427 static if (!pathAttr.value.length) { 2428 if (hack) 2429 return "%s: Path is null or empty".format(FuncId); 2430 } else { 2431 import std.algorithm : canFind, splitter; 2432 // splitter doesn't work with alias this ? 2433 auto str = pathAttr.value.data; 2434 if (str.canFind("//")) return "%s: Path '%s' contains empty entries.".format(FuncId, pathAttr.value); 2435 str = str.strip('/'); 2436 if (!str.length) return null; 2437 foreach (elem; str.splitter('/')) { 2438 assert(elem.length, "Empty path entry not caught yet!?"); 2439 2440 if (elem[0] == ':') { 2441 // typeof(PN) is void when length is 0. 2442 static if (!PN.length) { 2443 if (hack) 2444 return "%s: Path contains '%s', but no parameter '_%s' defined." 2445 .format(FuncId, elem, elem[1..$]); 2446 } else { 2447 if (![PN].canFind("_"~elem[1..$])) 2448 if (hack) return "%s: Path contains '%s', but no parameter '_%s' defined." 2449 .format(FuncId, elem, elem[1..$]); 2450 elem = elem[1..$]; 2451 } 2452 } 2453 } 2454 // TODO: Check for validity of the subpath. 2455 } 2456 } 2457 return null; 2458 } 2459 2460 if (!__ctfe) 2461 assert(false, "Internal error"); 2462 bool hack = true; 2463 foreach (method; __traits(allMembers, I)) { 2464 // WORKAROUND #1045 / @@BUG14375@@ 2465 static if (method.length != 0) 2466 foreach (overload; MemberFunctionsTuple!(I, method)) { 2467 static if (validateMethod!(overload)()) 2468 if (hack) return validateMethod!(overload)(); 2469 } 2470 } 2471 return null; 2472 } 2473 2474 // Test detection of user typos (e.g., if the attribute is on a parameter that doesn't exist). 2475 unittest { 2476 enum msg = "No parameter 'ath' (referenced by attribute @headerParam)"; 2477 2478 interface ITypo { 2479 @headerParam("ath", "Authorization") // mistyped parameter name 2480 string getResponse(string auth); 2481 } 2482 enum err = getInterfaceValidationError!ITypo; 2483 static assert(err !is null && stripTestIdent(err) == msg, 2484 "Expected validation error for getResponse, got: "~stripTestIdent(err)); 2485 } 2486 2487 // Multiple origin for a parameter 2488 unittest { 2489 enum msg = "Parameter 'arg1' has multiple @*Param attributes on it."; 2490 2491 interface IMultipleOrigin { 2492 @headerParam("arg1", "Authorization") @bodyParam("arg1", "Authorization") 2493 string getResponse(string arg1, int arg2); 2494 } 2495 enum err = getInterfaceValidationError!IMultipleOrigin; 2496 static assert(err !is null && stripTestIdent(err) == msg, err); 2497 } 2498 2499 // Missing parameter name 2500 unittest { 2501 enum msg = "Parameter 0 has no name."; 2502 2503 interface IMissingName1 { 2504 string getResponse(string = "troublemaker"); 2505 } 2506 interface IMissingName2 { 2507 string getResponse(string); 2508 } 2509 enum err1 = getInterfaceValidationError!IMissingName1; 2510 static assert(err1 !is null && stripTestIdent(err1) == msg, err1); 2511 enum err2 = getInterfaceValidationError!IMissingName2; 2512 static assert(err2 !is null && stripTestIdent(err2) == msg, err2); 2513 } 2514 2515 // Issue 949 2516 unittest { 2517 enum msg = "Path contains ':owner', but no parameter '_owner' defined."; 2518 2519 @path("/repos/") 2520 interface IGithubPR { 2521 @path(":owner/:repo/pulls") 2522 string getPullRequests(string owner, string repo); 2523 } 2524 enum err = getInterfaceValidationError!IGithubPR; 2525 static assert(err !is null && stripTestIdent(err) == msg, err); 2526 } 2527 2528 // Issue 1017 2529 unittest { 2530 interface TestSuccess { @path("/") void test(); } 2531 interface TestSuccess2 { @path("/test/") void test(); } 2532 interface TestFail { @path("//") void test(); } 2533 interface TestFail2 { @path("/test//it/") void test(); } 2534 static assert(getInterfaceValidationError!TestSuccess is null); 2535 static assert(getInterfaceValidationError!TestSuccess2 is null); 2536 static assert(stripTestIdent(getInterfaceValidationError!TestFail) 2537 == "Path '//' contains empty entries."); 2538 static assert(stripTestIdent(getInterfaceValidationError!TestFail2) 2539 == "Path '/test//it/' contains empty entries."); 2540 } 2541 2542 unittest { 2543 interface NullPath { @path(null) void test(); } 2544 interface ExplicitlyEmptyPath { @path("") void test(); } 2545 static assert(stripTestIdent(getInterfaceValidationError!NullPath) 2546 == "Path is null or empty"); 2547 static assert(stripTestIdent(getInterfaceValidationError!ExplicitlyEmptyPath) 2548 == "Path is null or empty"); 2549 2550 // Note: Implicitly empty path are valid: 2551 // interface ImplicitlyEmptyPath { void get(); } 2552 } 2553 2554 // Accept @headerParam ref / out parameters 2555 unittest { 2556 interface HeaderRef { 2557 @headerParam("auth", "auth") 2558 string getData(ref string auth); 2559 string getData2(@viaHeader("auth") ref string auth); 2560 } 2561 static assert(getInterfaceValidationError!HeaderRef is null, 2562 stripTestIdent(getInterfaceValidationError!HeaderRef)); 2563 2564 interface HeaderOut { 2565 @headerParam("auth", "auth") 2566 void getData(out string auth); 2567 void getData(@viaHeader("auth") out string auth); 2568 } 2569 static assert(getInterfaceValidationError!HeaderOut is null, 2570 stripTestIdent(getInterfaceValidationError!HeaderOut)); 2571 } 2572 2573 // Reject unattributed / @queryParam or @bodyParam ref / out parameters 2574 unittest { 2575 interface QueryRef { 2576 @queryParam("auth", "auth") 2577 string getData(ref string auth); 2578 } 2579 static assert(stripTestIdent(getInterfaceValidationError!QueryRef) 2580 == "query parameter 'auth' cannot be ref"); 2581 2582 interface QueryRefConst { 2583 @queryParam("auth", "auth") 2584 string getData(const ref string auth); 2585 } 2586 enum err1 = getInterfaceValidationError!QueryRefConst; 2587 static assert(err1 is null, err1); 2588 2589 interface QueryOut { 2590 @queryParam("auth", "auth") 2591 void getData(out string auth); 2592 } 2593 static assert(stripTestIdent(getInterfaceValidationError!QueryOut) 2594 == "query parameter 'auth' cannot be out"); 2595 2596 interface BodyRef { 2597 @bodyParam("auth", "auth") 2598 string getData(ref string auth); 2599 } 2600 static assert(stripTestIdent(getInterfaceValidationError!BodyRef) 2601 == "body_ parameter 'auth' cannot be ref",x); 2602 2603 interface BodyRefConst { 2604 @bodyParam("auth", "auth") 2605 string getData(const ref string auth); 2606 } 2607 enum err2 = getInterfaceValidationError!BodyRefConst; 2608 static assert(err2 is null, err2); 2609 2610 interface BodyOut { 2611 @bodyParam("auth", "auth") 2612 void getData(out string auth); 2613 } 2614 static assert(stripTestIdent(getInterfaceValidationError!BodyOut) 2615 == "body_ parameter 'auth' cannot be out"); 2616 2617 // There's also the possibility of someone using an out unnamed 2618 // parameter (don't ask me why), but this is catched as unnamed 2619 // parameter, so we don't need to check it here. 2620 } 2621 2622 private string stripTestIdent(string msg) 2623 @safe { 2624 import std.string; 2625 auto idx = msg.indexOf(": "); 2626 return idx >= 0 ? msg[idx+2 .. $] : msg; 2627 } 2628 2629 // Small helper for client code generation 2630 private string paramCTMap(string[string] params) 2631 @safe { 2632 import std.array : appender, join; 2633 if (!__ctfe) 2634 assert (false, "This helper is only supposed to be called for codegen in RestClientInterface."); 2635 auto app = appender!(string[]); 2636 foreach (key, val; params) { 2637 app ~= "\""~key~"\""; 2638 app ~= val; 2639 } 2640 return app.data.join(", "); 2641 } 2642 2643 package string stripTUnderscore(string name, RestInterfaceSettings settings) 2644 @safe { 2645 if ((settings is null || settings.stripTrailingUnderscore) 2646 && name.endsWith("_")) 2647 return name[0 .. $-1]; 2648 else return name; 2649 } 2650 2651 // Workarounds @@DMD:9748@@, and maybe more 2652 package template GenCmp(string name, int id, string cmpTo) { 2653 import std.string : format; 2654 import std.conv : to; 2655 enum Decl = q{ 2656 template %1$s(alias uda) { 2657 enum %1$s = (uda.identifier == "%2$s"); 2658 } 2659 }.format(Name, cmpTo); 2660 enum Name = name~to!string(id); 2661 } 2662 2663 // Ditto 2664 private template GenOrphan(int id) { 2665 import std.string : format; 2666 import std.conv : to; 2667 enum Decl = q{ 2668 template %1$s(string name) { 2669 enum %1$s = (uda.identifier == name); 2670 } 2671 }.format(Name); 2672 enum Name = "OrphanCheck"~to!string(id); 2673 } 2674 2675 // Workaround for issue #1045 / DMD bug 14375 2676 // Also, an example of policy-based design using this module. 2677 @safe unittest { 2678 import std.traits, std.typetuple; 2679 import vibe.internal.meta.codegen; 2680 import vibe.internal.meta.typetuple; 2681 import vibe.web.internal.rest.common : ParameterKind; 2682 2683 interface Policies { 2684 @headerParam("auth", "Authorization") 2685 string BasicAuth(string auth, ulong expiry) @safe; 2686 } 2687 2688 @path("/keys/") 2689 interface IKeys(alias AuthenticationPolicy = Policies.BasicAuth) { 2690 static assert(is(FunctionTypeOf!AuthenticationPolicy == function), 2691 "Policies needs to be functions"); 2692 @path("/") @method(HTTPMethod.POST) @safe 2693 mixin CloneFunctionDecl!(AuthenticationPolicy, true, "create"); 2694 } 2695 2696 class KeysImpl : IKeys!() { 2697 override: 2698 string create(string auth, ulong expiry) @safe { 2699 return "4242-4242"; 2700 } 2701 } 2702 2703 // Some sanity checks 2704 // Note: order is most likely implementation dependent. 2705 // Good thing we only have one frontend... 2706 alias WPA = WebParamAttribute; 2707 static assert(Compare!( 2708 Group!(__traits(getAttributes, IKeys!().create)), 2709 Group!(PathAttribute("/"), 2710 MethodAttribute(HTTPMethod.POST), 2711 WPA(ParameterKind.header, "auth", "Authorization")))); 2712 2713 void register() @safe { 2714 auto router = new URLRouter(); 2715 router.registerRestInterface(new KeysImpl()); 2716 } 2717 2718 void query() @safe { 2719 auto client = new RestInterfaceClient!(IKeys!())("http://127.0.0.1:8080"); 2720 assert(client.create("Hello", 0) == "4242-4242"); 2721 } 2722 } 2723 2724 @safe unittest { // @noRoute support in RestInterfaceClient 2725 interface I { 2726 void foo(); 2727 @noRoute int bar(void* someparam); 2728 } 2729 auto cli = new RestInterfaceClient!I("http://127.0.0.1/"); 2730 }