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 : HTTPClientRequest, HTTPClientResponse, HTTPClientSettings; 321 import vibe.http.common : HTTPMethod, HTTPStatusException; 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 std.typetuple : staticMap; 686 687 private alias Info = RestInterface!I; 688 689 //pragma(msg, "imports for "~I.stringof~":"); 690 //pragma(msg, generateModuleImports!(I)()); 691 mixin(generateModuleImports!I()); 692 693 private { 694 // storing this struct directly causes a segfault when built with 695 // LDC 0.15.x, so we are using a pointer here: 696 RestInterface!I* m_intf; 697 RequestFilter m_requestFilter; 698 RequestBodyFilter m_requestBodyFilter; 699 staticMap!(RestInterfaceClient, Info.SubInterfaceTypes) m_subInterfaces; 700 } 701 702 alias RequestFilter = void delegate(HTTPClientRequest req) @safe; 703 704 alias RequestBodyFilter = void delegate(HTTPClientRequest req, scope InputStream body_contents) @safe; 705 706 /** 707 Creates a new REST client implementation of $(D I). 708 */ 709 this(RestInterfaceSettings settings) 710 { 711 m_intf = new Info(settings, true); 712 713 foreach (i, SI; Info.SubInterfaceTypes) 714 m_subInterfaces[i] = new RestInterfaceClient!SI(m_intf.subInterfaces[i].settings); 715 } 716 717 /// ditto 718 this(string base_url, MethodStyle style = MethodStyle.lowerUnderscored) 719 { 720 this(URL(base_url), style); 721 } 722 723 /// ditto 724 this(URL base_url, MethodStyle style = MethodStyle.lowerUnderscored) 725 { 726 scope settings = new RestInterfaceSettings; 727 settings.baseURL = base_url; 728 settings.methodStyle = style; 729 this(settings); 730 } 731 732 /** 733 An optional request filter that allows to modify each request before it is made. 734 */ 735 final @property RequestFilter requestFilter() 736 { 737 return m_requestFilter; 738 } 739 /// ditto 740 final @property void requestFilter(RequestFilter v) 741 { 742 m_requestFilter = v; 743 foreach (i, SI; Info.SubInterfaceTypes) 744 m_subInterfaces[i].requestFilter = v; 745 } 746 /// ditto 747 final @property void requestFilter(void delegate(HTTPClientRequest req) v) 748 { 749 this.requestFilter = cast(RequestFilter)v; 750 } 751 752 /** Optional request filter with access to the request body. 753 754 This callback allows to modify the request headers depending on the 755 contents of the body. 756 */ 757 final @property void requestBodyFilter(RequestBodyFilter del) 758 { 759 m_requestBodyFilter = del; 760 } 761 /// ditto 762 final @property RequestBodyFilter requestBodyFilter() 763 { 764 return m_requestBodyFilter; 765 } 766 767 //pragma(msg, "restinterface:"); 768 mixin(generateRestClientMethods!I()); 769 770 protected { 771 import vibe.data.json : Json; 772 import vibe.textfilter.urlencode; 773 774 /** 775 * Perform a request to the interface using the given parameters. 776 * 777 * Params: 778 * verb = Kind of request (See $(D HTTPMethod) enum). 779 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 780 * it will be '/rejectedsoftware/vibe.d/issues'. 781 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 782 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 783 * it will be 'author%3ASantaClaus'. 784 * 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 785 * the generic type "application/json". 786 * reqReturnHdrs = A map of required return headers. 787 * To avoid returning unused headers, nothing is written 788 * to this structure unless there's an (usually empty) 789 * entry (= the key exists) with the same key. 790 * If any key present in `reqReturnHdrs` is not present 791 * in the response, an Exception is thrown. 792 * optReturnHdrs = A map of optional return headers. 793 * This behaves almost as exactly as reqReturnHdrs, 794 * except that non-existent key in the response will 795 * not cause it to throw, but rather to set this entry 796 * to 'null'. 797 * 798 * Returns: 799 * The Json object returned by the request 800 */ 801 Json request(HTTPMethod verb, string name, 802 const scope ref InetHeaderMap hdrs, string query, string body_, 803 ref InetHeaderMap reqReturnHdrs, 804 ref InetHeaderMap optReturnHdrs) const 805 { 806 auto path = URL(m_intf.baseURL).pathString; 807 808 if (name.length) 809 { 810 if (path.length && path[$ - 1] == '/' && name[0] == '/') 811 path ~= name[1 .. $]; 812 else if (path.length && path[$ - 1] == '/' || name[0] == '/') 813 path ~= name; 814 else 815 path ~= '/' ~ name; 816 } 817 818 auto httpsettings = m_intf.settings.httpClientSettings; 819 820 auto http_resp = .request(URL(m_intf.baseURL), m_requestFilter, 821 m_requestBodyFilter, verb, path, 822 hdrs, query, body_, reqReturnHdrs, optReturnHdrs, httpsettings); 823 scope(exit) http_resp.dropBody(); 824 825 return http_resp.readJson(); 826 } 827 } 828 } 829 830 /// 831 unittest 832 { 833 interface IMyApi 834 { 835 // GET /status 836 string getStatus(); 837 838 // GET /greeting 839 @property string greeting(); 840 // PUT /greeting 841 @property void greeting(string text); 842 843 // POST /new_user 844 void addNewUser(string name); 845 // GET /users 846 @property string[] users(); 847 // GET /:id/name 848 string getName(int id); 849 850 Json getSomeCustomJson(); 851 } 852 853 void test() 854 { 855 auto api = new RestInterfaceClient!IMyApi("http://127.0.0.1/api/"); 856 857 logInfo("Status: %s", api.getStatus()); 858 api.greeting = "Hello, World!"; 859 logInfo("Greeting message: %s", api.greeting); 860 api.addNewUser("Peter"); 861 api.addNewUser("Igor"); 862 logInfo("Users: %s", api.users); 863 logInfo("First user name: %s", api.getName(0)); 864 } 865 } 866 867 868 /** 869 Encapsulates settings used to customize the generated REST interface. 870 */ 871 class RestInterfaceSettings { 872 /** The public URL below which the REST interface is registered. 873 */ 874 URL baseURL; 875 876 /** List of allowed origins for CORS 877 878 Empty list is interpreted as allowing all origins (e.g. *) 879 */ 880 string[] allowedOrigins; 881 882 /** Naming convention used for the generated URLs. 883 */ 884 MethodStyle methodStyle = MethodStyle.lowerUnderscored; 885 886 /** The content type the client would like to receive the data back 887 */ 888 string content_type = "application/json"; 889 890 /** Ignores a trailing underscore in method and function names. 891 892 With this setting set to $(D true), it's possible to use names in the 893 REST interface that are reserved words in D. 894 */ 895 bool stripTrailingUnderscore = true; 896 897 /// Overrides the default HTTP client settings used by the `RestInterfaceClient`. 898 HTTPClientSettings httpClientSettings; 899 900 /** Optional handler used to render custom replies in case of errors. 901 902 The handler needs to set the response status code to the provided 903 `RestErrorInformation.statusCode` value and can then write a custom 904 response body. 905 906 Note that the REST interface generator by default handles any exceptions thrown 907 during request handling and sents a JSON response with the error message. The 908 low level `HTTPServerSettings.errorPageHandler` is not invoked. 909 910 If `errorHandler` is not set, a JSON object with a single field "statusMessage" 911 will be sent. In debug builds, there may also be an additional 912 "statusDebugMessage" field that contains the full exception text, including a 913 possible stack trace. 914 */ 915 RestErrorHandler errorHandler; 916 917 @property RestInterfaceSettings dup() 918 const @safe { 919 auto ret = new RestInterfaceSettings; 920 ret.baseURL = this.baseURL; 921 ret.methodStyle = this.methodStyle; 922 ret.stripTrailingUnderscore = this.stripTrailingUnderscore; 923 ret.allowedOrigins = this.allowedOrigins.dup; 924 ret.content_type = this.content_type.dup; 925 ret.errorHandler = this.errorHandler; 926 if (this.httpClientSettings) { 927 ret.httpClientSettings = this.httpClientSettings.dup; 928 } 929 return ret; 930 } 931 } 932 933 /** Type of the optional handler used to render custom replies in case of errors. 934 935 The error handler for a REST interface can be set via 936 `RestInterfaceSettings.errorHandler`. 937 */ 938 alias RestErrorHandler = void delegate(HTTPServerRequest, HTTPServerResponse, RestErrorInformation error) @safe; 939 940 /** Contains detailed informations about the error 941 942 Used by the error handler (`RestInterfaceSettings.errorHandler`) to return 943 the correct information(s) to clients. 944 */ 945 struct RestErrorInformation { 946 /// The status code that the handler should send in the reply 947 HTTPStatus statusCode; 948 949 /** If triggered by an exception, this contains the catched exception 950 object. 951 */ 952 Exception exception; 953 954 private this(Exception e, HTTPStatus default_status) 955 @safe { 956 this.exception = e; 957 958 if (auto he = cast(HTTPStatusException)e) { 959 this.statusCode = cast(HTTPStatus)he.status; 960 } else { 961 this.statusCode = default_status; 962 } 963 } 964 } 965 966 967 /** 968 Models REST collection interfaces using natural D syntax. 969 970 Use this type as the return value of a REST interface getter method/property 971 to model a collection of objects. `opIndex` is used to make the individual 972 entries accessible using the `[index]` syntax. Nested collections are 973 supported. 974 975 The interface `I` needs to define a struct named `CollectionIndices`. The 976 members of this struct denote the types and names of the indexes that lead 977 to a particular resource. If a collection is nested within another 978 collection, the order of these members must match the nesting order 979 (outermost first). 980 981 The parameter list of all of `I`'s methods must begin with all but the last 982 entry in `CollectionIndices`. Methods that also match the last entry will be 983 considered methods of a collection item (`collection[index].method()`), 984 wheres all other methods will be considered methods of the collection 985 itself (`collection.method()`). 986 987 The name of the index parameters affects the default path of a method's 988 route. Normal parameter names will be subject to the same rules as usual 989 routes (see `registerRestInterface`) and will be mapped to query or form 990 parameters at the protocol level. Names starting with an underscore will 991 instead be mapped to path placeholders. For example, 992 `void getName(int __item_id)` will be mapped to a GET request to the 993 path `":item_id/name"`. 994 */ 995 struct Collection(I) 996 if (is(I == interface)) 997 { 998 static assert(is(I.CollectionIndices == struct), "Collection interfaces must define a CollectionIndices struct."); 999 1000 alias Interface = I; 1001 alias AllIDs = AliasSeq!(typeof(I.CollectionIndices.tupleof)); 1002 alias AllIDNames = FieldNameTuple!(I.CollectionIndices); 1003 static assert(AllIDs.length >= 1, I.stringof~".CollectionIndices must define at least one member."); 1004 static assert(AllIDNames.length == AllIDs.length); 1005 alias ItemID = AllIDs[$-1]; 1006 alias ParentIDs = AllIDs[0 .. $-1]; 1007 alias ParentIDNames = AllIDNames[0 .. $-1]; 1008 1009 private { 1010 I m_interface; 1011 ParentIDs m_parentIDs; 1012 } 1013 1014 /** Constructs a new collection instance that is tied to a particular 1015 parent collection entry. 1016 1017 Params: 1018 api = The target interface imstance to be mapped as a collection 1019 pids = The indexes of all collections in which this collection is 1020 nested (if any) 1021 */ 1022 this(I api, ParentIDs pids) 1023 { 1024 m_interface = api; 1025 m_parentIDs = pids; 1026 } 1027 1028 static struct Item { 1029 private { 1030 I m_interface; 1031 AllIDs m_id; 1032 } 1033 1034 this(I api, AllIDs id) 1035 { 1036 m_interface = api; 1037 m_id = id; 1038 } 1039 1040 // forward all item methods 1041 mixin(() { 1042 string ret; 1043 foreach (m; __traits(allMembers, I)) { 1044 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 1045 alias PT = ParameterTypeTuple!ovrld; 1046 static if (matchesAllIDs!ovrld) 1047 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_id, args); }\n"; 1048 } 1049 } 1050 return ret; 1051 } ()); 1052 } 1053 1054 // Note: the example causes a recursive template instantiation if done as a documented unit test: 1055 /** Accesses a single collection entry. 1056 1057 Example: 1058 --- 1059 interface IMain { 1060 @property Collection!IItem items(); 1061 } 1062 1063 interface IItem { 1064 struct CollectionIndices { 1065 int _itemID; 1066 } 1067 1068 @method(HTTPMethod.GET) 1069 string name(int _itemID); 1070 } 1071 1072 void test(IMain main) 1073 { 1074 auto item_name = main.items[23].name; // equivalent to IItem.name(23) 1075 } 1076 --- 1077 */ 1078 Item opIndex(ItemID id) 1079 { 1080 return Item(m_interface, m_parentIDs, id); 1081 } 1082 1083 // forward all non-item methods 1084 mixin(() { 1085 string ret; 1086 foreach (m; __traits(allMembers, I)) { 1087 foreach (ovrld; MemberFunctionsTuple!(I, m)) { 1088 alias PT = ParameterTypeTuple!ovrld; 1089 static if (!matchesAllIDs!ovrld && !hasUDA!(ovrld, NoRouteAttribute)) { 1090 static assert(matchesParentIDs!ovrld, 1091 "Collection methods must take all parent IDs as the first parameters."~PT.stringof~" "~ParentIDs.stringof); 1092 ret ~= "auto "~m~"(ARGS...)(ARGS args) { return m_interface."~m~"(m_parentIDs, args); }\n"; 1093 } 1094 } 1095 } 1096 return ret; 1097 } ()); 1098 1099 private template matchesParentIDs(alias func) { 1100 static if (is(ParameterTypeTuple!func[0 .. ParentIDs.length] == ParentIDs)) { 1101 static if (ParentIDNames.length == 0) enum matchesParentIDs = true; 1102 else static if (ParameterIdentifierTuple!func[0 .. ParentIDNames.length] == ParentIDNames) 1103 enum matchesParentIDs = true; 1104 else enum matchesParentIDs = false; 1105 } else enum matchesParentIDs = false; 1106 } 1107 1108 private template matchesAllIDs(alias func) { 1109 static if (is(ParameterTypeTuple!func[0 .. AllIDs.length] == AllIDs)) { 1110 static if (ParameterIdentifierTuple!func[0 .. AllIDNames.length] == AllIDNames) 1111 enum matchesAllIDs = true; 1112 else enum matchesAllIDs = false; 1113 } else enum matchesAllIDs = false; 1114 } 1115 } 1116 1117 /// Model two nested collections using path based indexes 1118 unittest { 1119 // 1120 // API definition 1121 // 1122 interface SubItemAPI { 1123 // Define the index path that leads to a sub item 1124 struct CollectionIndices { 1125 // The ID of the base item. This must match the definition in 1126 // ItemAPI.CollectionIndices 1127 string _item; 1128 // The index if the sub item 1129 int _index; 1130 } 1131 1132 // GET /items/:item/subItems/length 1133 @property int length(string _item); 1134 1135 // GET /items/:item/subItems/:index/squared_position 1136 int getSquaredPosition(string _item, int _index); 1137 } 1138 1139 interface ItemAPI { 1140 // Define the index that identifies an item 1141 struct CollectionIndices { 1142 string _item; 1143 } 1144 1145 // base path /items/:item/subItems 1146 Collection!SubItemAPI subItems(string _item); 1147 1148 // GET /items/:item/name 1149 @property string name(string _item); 1150 } 1151 1152 interface API { 1153 // a collection of items at the base path /items/ 1154 Collection!ItemAPI items(); 1155 } 1156 1157 // 1158 // Local API implementation 1159 // 1160 class SubItemAPIImpl : SubItemAPI { 1161 @property int length(string _item) { return 10; } 1162 1163 int getSquaredPosition(string _item, int _index) { return _index ^^ 2; } 1164 } 1165 1166 class ItemAPIImpl : ItemAPI { 1167 private SubItemAPIImpl m_subItems; 1168 1169 this() { m_subItems = new SubItemAPIImpl; } 1170 1171 Collection!SubItemAPI subItems(string _item) { return Collection!SubItemAPI(m_subItems, _item); } 1172 1173 string name(string _item) { return _item; } 1174 } 1175 1176 class APIImpl : API { 1177 private ItemAPIImpl m_items; 1178 1179 this() { m_items = new ItemAPIImpl; } 1180 1181 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 1182 } 1183 1184 // 1185 // Resulting API usage 1186 // 1187 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 1188 1189 // GET /items/foo/name 1190 assert(api.items["foo"].name == "foo"); 1191 // GET /items/foo/sub_items/length 1192 assert(api.items["foo"].subItems.length == 10); 1193 // GET /items/foo/sub_items/2/squared_position 1194 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 1195 } 1196 1197 unittest { 1198 interface I { 1199 struct CollectionIndices { 1200 int id1; 1201 string id2; 1202 } 1203 1204 void a(int id1, string id2); 1205 void b(int id1, int id2); 1206 void c(int id1, string p); 1207 void d(int id1, string id2, int p); 1208 void e(int id1, int id2, int p); 1209 void f(int id1, string p, int q); 1210 } 1211 1212 Collection!I coll; 1213 static assert(is(typeof(coll["x"].a()) == void)); 1214 static assert(is(typeof(coll.b(42)) == void)); 1215 static assert(is(typeof(coll.c("foo")) == void)); 1216 static assert(is(typeof(coll["x"].d(42)) == void)); 1217 static assert(is(typeof(coll.e(42, 42)) == void)); 1218 static assert(is(typeof(coll.f("foo", 42)) == void)); 1219 } 1220 1221 /// Model two nested collections using normal query parameters as indexes 1222 unittest { 1223 // 1224 // API definition 1225 // 1226 interface SubItemAPI { 1227 // Define the index path that leads to a sub item 1228 struct CollectionIndices { 1229 // The ID of the base item. This must match the definition in 1230 // ItemAPI.CollectionIndices 1231 string item; 1232 // The index if the sub item 1233 int index; 1234 } 1235 1236 // GET /items/subItems/length?item=... 1237 @property int length(string item); 1238 1239 // GET /items/subItems/squared_position?item=...&index=... 1240 int getSquaredPosition(string item, int index); 1241 } 1242 1243 interface ItemAPI { 1244 // Define the index that identifies an item 1245 struct CollectionIndices { 1246 string item; 1247 } 1248 1249 // base path /items/subItems?item=... 1250 Collection!SubItemAPI subItems(string item); 1251 1252 // GET /items/name?item=... 1253 @property string name(string item); 1254 } 1255 1256 interface API { 1257 // a collection of items at the base path /items/ 1258 Collection!ItemAPI items(); 1259 } 1260 1261 // 1262 // Local API implementation 1263 // 1264 class SubItemAPIImpl : SubItemAPI { 1265 @property int length(string item) { return 10; } 1266 1267 int getSquaredPosition(string item, int index) { return index ^^ 2; } 1268 } 1269 1270 class ItemAPIImpl : ItemAPI { 1271 private SubItemAPIImpl m_subItems; 1272 1273 this() { m_subItems = new SubItemAPIImpl; } 1274 1275 Collection!SubItemAPI subItems(string item) { return Collection!SubItemAPI(m_subItems, item); } 1276 1277 string name(string item) { return item; } 1278 } 1279 1280 class APIImpl : API { 1281 private ItemAPIImpl m_items; 1282 1283 this() { m_items = new ItemAPIImpl; } 1284 1285 Collection!ItemAPI items() { return Collection!ItemAPI(m_items); } 1286 } 1287 1288 // 1289 // Resulting API usage 1290 // 1291 API api = new APIImpl; // A RestInterfaceClient!API would work just as well 1292 1293 // GET /items/name?item=foo 1294 assert(api.items["foo"].name == "foo"); 1295 // GET /items/subitems/length?item=foo 1296 assert(api.items["foo"].subItems.length == 10); 1297 // GET /items/subitems/squared_position?item=foo&index=2 1298 assert(api.items["foo"].subItems[2].getSquaredPosition() == 4); 1299 } 1300 1301 unittest { 1302 interface C { 1303 struct CollectionIndices { 1304 int _ax; 1305 int _b; 1306 } 1307 void testB(int _ax, int _b); 1308 } 1309 1310 interface B { 1311 struct CollectionIndices { 1312 int _a; 1313 } 1314 Collection!C c(); 1315 void testA(int _a); 1316 } 1317 1318 interface A { 1319 Collection!B b(); 1320 } 1321 1322 static assert (!is(typeof(A.init.b[1].c[2].testB()))); 1323 } 1324 1325 /** Allows processing the server request/response before the handler method is called. 1326 1327 Note that this attribute is only used by `registerRestInterface`, but not 1328 by the client generators. This attribute expects the name of a parameter that 1329 will receive its return value. 1330 1331 Writing to the response body from within the specified hander function 1332 causes any further processing of the request to be skipped. In particular, 1333 the route handler method will not be called. 1334 1335 Note: 1336 The example shows the drawback of this attribute. It generally is a 1337 leaky abstraction that propagates to the base interface. For this 1338 reason the use of this attribute is not recommended, unless there is 1339 no suitable alternative. 1340 */ 1341 alias before = vibe.internal.meta.funcattr.before; 1342 1343 /// 1344 @safe unittest { 1345 import vibe.http.router; 1346 import vibe.http.server; 1347 import vibe.web.rest; 1348 1349 interface MyService { 1350 long getHeaderCount(size_t foo = 0) @safe; 1351 } 1352 1353 static size_t handler(scope HTTPServerRequest req, scope HTTPServerResponse res) 1354 { 1355 return req.headers.length; 1356 } 1357 1358 class MyServiceImpl : MyService { 1359 // the "foo" parameter will receive the number of request headers 1360 @before!handler("foo") 1361 long getHeaderCount(size_t foo) 1362 { 1363 return foo; 1364 } 1365 } 1366 1367 void test(URLRouter router) 1368 @safe { 1369 router.registerRestInterface(new MyServiceImpl); 1370 } 1371 } 1372 1373 1374 /** Allows processing the return value of a handler method and the request/response objects. 1375 1376 The value returned by the REST API will be the value returned by the last 1377 `@after` handler, which allows to post process the results of the handler 1378 method. 1379 1380 Writing to the response body from within the specified handler function 1381 causes any further processing of the request ot be skipped, including 1382 any other `@after` annotations and writing the result value. 1383 */ 1384 alias after = vibe.internal.meta.funcattr.after; 1385 1386 /// 1387 @safe unittest { 1388 import vibe.http.router; 1389 import vibe.http.server; 1390 import vibe.web.rest; 1391 1392 interface MyService { 1393 long getMagic() @safe; 1394 } 1395 1396 static long handler(long ret, HTTPServerRequest req, HTTPServerResponse res) 1397 @safe { 1398 return ret * 2; 1399 } 1400 1401 class MyServiceImpl : MyService{ 1402 // the result reported by the REST API will be 42 1403 @after!handler 1404 long getMagic() 1405 { 1406 return 21; 1407 } 1408 } 1409 1410 void test(URLRouter router) 1411 @safe { 1412 router.registerRestInterface(new MyServiceImpl); 1413 } 1414 } 1415 1416 /** 1417 * Generate an handler that will wrap the server's method 1418 * 1419 * This function returns an handler, generated at compile time, that 1420 * will deserialize the parameters, pass them to the function implemented 1421 * by the user, and return what it needs to return, be it header parameters 1422 * or body, which is at the moment either a pure string or a Json object. 1423 * 1424 * One thing that makes this method more complex that it needs be is the 1425 * inability for D to attach UDA to parameters. This means we have to roll 1426 * our own implementation, which tries to be as easy to use as possible. 1427 * We'll require the user to give the name of the parameter as a string to 1428 * our UDA. Hopefully, we're also able to detect at compile time if the user 1429 * made a typo of any kind (see $(D genInterfaceValidationError)). 1430 * 1431 * Note: 1432 * Lots of abbreviations are used to ease the code, such as 1433 * PTT (ParameterTypeTuple), WPAT (WebParamAttributeTuple) 1434 * and PWPAT (ParameterWebParamAttributeTuple). 1435 * 1436 * Params: 1437 * T = type of the object which represent the REST server (user implemented). 1438 * Func = An alias to the function of $(D T) to wrap. 1439 * 1440 * inst = REST server on which to call our $(D Func). 1441 * settings = REST server configuration. 1442 * 1443 * Returns: 1444 * A delegate suitable to use as an handler for an HTTP request. 1445 */ 1446 private HTTPServerRequestDelegate jsonMethodHandler(alias Func, size_t ridx, T)(T inst, ref RestInterface!T intf) 1447 { 1448 import std.encoding : sanitize; 1449 import std.string : format; 1450 import std.traits : Unqual; 1451 import vibe.http.common : enforceBadRequest; 1452 import vibe.web.internal.rest.common : ParameterKind; 1453 import vibe.internal.meta.funcattr : IsAttributedParameter, computeAttributedParameterCtx; 1454 import vibe.internal.meta.traits : derivedMethod; 1455 import vibe.textfilter.urlencode : urlDecode; 1456 1457 enum Method = __traits(identifier, Func); 1458 // We need mutable types for deserialization 1459 alias PTypes = staticMap!(Unqual, ParameterTypeTuple!Func); 1460 alias PDefaults = ParameterDefaultValueTuple!Func; 1461 alias CFuncRaw = derivedMethod!(T, Func); 1462 static if (AliasSeq!(CFuncRaw).length > 0) alias CFunc = CFuncRaw; 1463 else alias CFunc = Func; 1464 alias RT = ReturnType!(FunctionTypeOf!Func); 1465 static const sroute = RestInterface!T.staticRoutes[ridx]; 1466 auto route = intf.routes[ridx]; 1467 auto settings = intf.settings; 1468 alias SerPolicyType = SerPolicyT!(RestInterface!T.I).PolicyTemplate; 1469 1470 void handler(HTTPServerRequest req, HTTPServerResponse res) 1471 @safe { 1472 if (route.bodyParameters.length) { 1473 /*enforceBadRequest(req.contentType == "application/json", 1474 "The Content-Type header needs to be set to application/json.");*/ 1475 enforceBadRequest(req.json.type != Json.Type.undefined, 1476 "The request body does not contain a valid JSON value."); 1477 enforceBadRequest(req.json.type == Json.Type.object, 1478 "The request body must contain a JSON object."); 1479 } 1480 1481 void handleException(Exception e, HTTPStatus default_status) 1482 @safe { 1483 logDebug("REST handler exception: %s", () @trusted { return e.toString(); } ()); 1484 if (res.headerWritten) { 1485 logDebug("Response already started. Client will not receive an error code!"); 1486 return; 1487 } 1488 1489 if (settings.errorHandler) { 1490 settings.errorHandler(req, res, RestErrorInformation(e, default_status)); 1491 } else { 1492 import std.algorithm : among; 1493 debug string debugMsg; 1494 1495 if (auto se = cast(HTTPStatusException)e) 1496 res.statusCode = se.status; 1497 else debug { 1498 res.statusCode = HTTPStatus.internalServerError; 1499 debugMsg = e.toString().sanitize(); 1500 } 1501 else 1502 res.statusCode = default_status; 1503 1504 // All 1xx(informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body. 1505 // See: https://tools.ietf.org/html/rfc2616#section-4.3 1506 if (res.statusCode < 200 || res.statusCode.among(204, 304)) { 1507 res.writeVoidBody(); 1508 return; 1509 } 1510 1511 debug { 1512 if (debugMsg) { 1513 res.writeJsonBody(["statusMessage": e.msg, "statusDebugMessage": debugMsg]); 1514 return; 1515 } 1516 } 1517 res.writeJsonBody(["statusMessage": e.msg]); 1518 } 1519 } 1520 1521 static if (isAuthenticated!(T, Func)) { 1522 typeof(handleAuthentication!Func(inst, req, res)) auth_info; 1523 1524 try auth_info = handleAuthentication!Func(inst, req, res); 1525 catch (Exception e) { 1526 handleException(e, HTTPStatus.unauthorized); 1527 return; 1528 } 1529 1530 if (res.headerWritten) return; 1531 } 1532 1533 1534 PTypes params; 1535 1536 try { 1537 foreach (i, PT; PTypes) { 1538 enum sparam = sroute.parameters[i]; 1539 1540 static if (sparam.isIn) { 1541 enum pname = sparam.name; 1542 auto fieldname = route.parameters[i].fieldName; 1543 static if (isInstanceOf!(Nullable, PT)) PT v; 1544 else Nullable!PT v; 1545 1546 static if (sparam.kind == ParameterKind.auth) { 1547 v = auth_info; 1548 } else static if (sparam.kind == ParameterKind.query) { 1549 if (auto pv = fieldname in req.query) 1550 v = fromRestString!(PT, SerPolicyType)(*pv); 1551 } else static if (sparam.kind == ParameterKind.wholeBody) { 1552 try v = deserializeWithPolicy!(JsonSerializer, SerPolicyType, PT)(req.json); 1553 catch (JSONException e) enforceBadRequest(false, e.msg); 1554 } else static if (sparam.kind == ParameterKind.body_) { 1555 try { 1556 if (auto pv = fieldname in req.json) 1557 v = deserializeWithPolicy!(JsonSerializer, SerPolicyType, PT)(*pv); 1558 } catch (JSONException e) 1559 enforceBadRequest(false, e.msg); 1560 } else static if (sparam.kind == ParameterKind.header) { 1561 if (auto pv = fieldname in req.headers) 1562 v = fromRestString!(PT, SerPolicyType)(*pv); 1563 } else static if (sparam.kind == ParameterKind.attributed) { 1564 static if (!__traits(compiles, () @safe { computeAttributedParameterCtx!(CFunc, pname)(inst, req, res); } ())) 1565 static assert(false, "`@before` evaluator for REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be marked `@safe`."); 1566 v = computeAttributedParameterCtx!(CFunc, pname)(inst, req, res); 1567 } else static if (sparam.kind == ParameterKind.internal) { 1568 if (auto pv = fieldname in req.params) 1569 v = fromRestString!(PT, DefaultPolicy)(urlDecode(*pv)); 1570 } else static assert(false, "Unhandled parameter kind."); 1571 1572 static if (isInstanceOf!(Nullable, PT)) params[i] = v; 1573 else if (v.isNull()) { 1574 static if (!is(PDefaults[i] == void)) params[i] = PDefaults[i]; 1575 else enforceBadRequest(false, "Missing non-optional "~sparam.kind.to!string~" parameter '"~(fieldname.length?fieldname:sparam.name)~"'."); 1576 } else params[i] = v.get; 1577 } 1578 } 1579 } catch (Exception e) { 1580 handleException(e, HTTPStatus.badRequest); 1581 return; 1582 } 1583 1584 static if (isAuthenticated!(T, Func)) { 1585 try handleAuthorization!(T, Func, params)(auth_info); 1586 catch (Exception e) { 1587 handleException(e, HTTPStatus.forbidden); 1588 return; 1589 } 1590 } 1591 1592 void handleCors() 1593 { 1594 import std.algorithm : any; 1595 import std.uni : sicmp; 1596 1597 if (req.method == HTTPMethod.OPTIONS) 1598 return; 1599 auto origin = "Origin" in req.headers; 1600 if (origin is null) 1601 return; 1602 1603 if (settings.allowedOrigins.length != 0 && 1604 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1605 return; 1606 1607 res.headers["Access-Control-Allow-Origin"] = *origin; 1608 res.headers["Access-Control-Allow-Credentials"] = "true"; 1609 } 1610 // Anti copy-paste 1611 void returnHeaders() 1612 { 1613 handleCors(); 1614 foreach (i, P; PTypes) { 1615 static if (sroute.parameters[i].isOut) { 1616 static assert (sroute.parameters[i].kind == ParameterKind.header); 1617 static if (isInstanceOf!(Nullable, typeof(params[i]))) { 1618 if (!params[i].isNull) 1619 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1620 } else { 1621 res.headers[route.parameters[i].fieldName] = to!string(params[i]); 1622 } 1623 } 1624 } 1625 } 1626 1627 try { 1628 import vibe.internal.meta.funcattr; 1629 1630 static if (!__traits(compiles, () @safe { __traits(getMember, inst, Method)(params); })) 1631 static assert(false, "REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be marked `@safe`."); 1632 1633 static if (is(RT == void)) { 1634 // TODO: remove after deprecation period 1635 __traits(getMember, inst, Method)(params); 1636 returnHeaders(); 1637 res.writeBody(cast(ubyte[])null); 1638 } else static if (isInputStream!RT) { 1639 returnHeaders(); 1640 auto ret = evaluateOutputModifiers!CFunc( 1641 __traits(getMember, inst, Method)(params), req, res); 1642 res.headers["Content-Type"] = "application/octet-stream"; 1643 ret.pipe(res.bodyWriter); 1644 } else { 1645 // TODO: remove after deprecation period 1646 static if (!__traits(compiles, () @safe { evaluateOutputModifiers!Func(RT.init, req, res); } ())) 1647 static assert(false, "`@after` evaluator for REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be marked `@safe`."); 1648 1649 auto ret = evaluateOutputModifiers!CFunc( 1650 __traits(getMember, inst, Method)(params), req, res); 1651 returnHeaders(); 1652 1653 string accept_str; 1654 if (const accept_header = "Accept" in req.headers) 1655 accept_str = *accept_header; 1656 alias result_serializers = ResultSerializersT!Func; 1657 immutable serializer_ind = get_matching_content_type!(result_serializers)(accept_str); 1658 foreach (i, serializer; result_serializers) 1659 if (serializer_ind == i) { 1660 auto serialized_output = appender!(ubyte[]); 1661 static if ( 1662 __traits(compiles, () @trusted { 1663 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1664 }) 1665 && !__traits(compiles, () @safe { 1666 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1667 })) 1668 { 1669 static assert(false, "Serialization of return type `"~RT.stringof~"` of REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be `@safe`."); 1670 } 1671 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1672 res.writeBody(serialized_output.data, serializer.contentType); 1673 } 1674 res.statusCode = HTTPStatus.notAcceptable; // will trigger RestException on the client side 1675 res.writeBody(cast(ubyte[])null); 1676 } 1677 } catch (Exception e) { 1678 returnHeaders(); 1679 handleException(e, HTTPStatus.internalServerError); 1680 } 1681 } 1682 1683 return &handler; 1684 } 1685 1686 /** 1687 * Generate an handler that will wrap the server's method 1688 * 1689 * This function returns an handler that handles the http OPTIONS method. 1690 * 1691 * It will return the ALLOW header with all the methods on this resource 1692 * And it will handle Preflight CORS. 1693 * 1694 * Params: 1695 * routes = a range of Routes were each route has the same resource/URI 1696 * just different method. 1697 * settings = REST server configuration. 1698 * 1699 * Returns: 1700 * A delegate suitable to use as an handler for an HTTP request. 1701 */ 1702 private HTTPServerRequestDelegate optionsMethodHandler(RouteRange)(RouteRange routes, RestInterfaceSettings settings = null) 1703 { 1704 import std.algorithm : map, joiner, any; 1705 import std.array : array; 1706 import std.conv : text; 1707 import vibe.http.common : httpMethodString, httpMethodFromString; 1708 // NOTE: don't know what is better, to keep this in memory, or generate on each request 1709 auto allow = routes.map!(r => r.method.httpMethodString).joiner(",").text(); 1710 auto methods = routes.map!(r => r.method).array(); 1711 1712 void handlePreflightedCors(HTTPServerRequest req, HTTPServerResponse res, ref HTTPMethod[] methods, RestInterfaceSettings settings = null) 1713 { 1714 import std.algorithm : among; 1715 import std.uni : sicmp; 1716 1717 auto origin = "Origin" in req.headers; 1718 if (origin is null) 1719 return; 1720 1721 if (settings !is null && 1722 settings.allowedOrigins.length != 0 && 1723 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1724 return; 1725 1726 auto method = "Access-Control-Request-Method" in req.headers; 1727 if (method is null) 1728 return; 1729 1730 auto httpMethod = httpMethodFromString(*method); 1731 1732 if (!methods.any!(m => m == httpMethod)) 1733 return; 1734 1735 res.headers["Access-Control-Allow-Origin"] = *origin; 1736 1737 // there is no way to know if the specific resource supports credentials 1738 // (either cookies, HTTP authentication, or client-side SSL certificates), 1739 // so we always assume it does 1740 res.headers["Access-Control-Allow-Credentials"] = "true"; 1741 res.headers["Access-Control-Max-Age"] = "1728000"; 1742 res.headers["Access-Control-Allow-Methods"] = *method; 1743 1744 // we have no way to reliably determine what headers the resource allows 1745 // so we simply copy whatever the client requested 1746 if (auto headers = "Access-Control-Request-Headers" in req.headers) 1747 res.headers["Access-Control-Allow-Headers"] = *headers; 1748 } 1749 1750 void handler(HTTPServerRequest req, HTTPServerResponse res) 1751 { 1752 // since this is a OPTIONS request, we have to return the ALLOW headers to tell which methods we have 1753 res.headers["Allow"] = allow; 1754 1755 // handle CORS preflighted requests 1756 handlePreflightedCors(req,res,methods,settings); 1757 1758 // NOTE: besides just returning the allowed methods and handling CORS preflighted requests, 1759 // this would be a nice place to describe what kind of resources are on this route, 1760 // the params each accepts, the headers, etc... think WSDL but then for REST. 1761 res.writeBody(""); 1762 } 1763 return &handler; 1764 } 1765 1766 private string generateRestClientMethods(I)() 1767 { 1768 import std.array : join; 1769 import std.string : format; 1770 import std.traits : fullyQualifiedName, isInstanceOf; 1771 1772 alias Info = RestInterface!I; 1773 1774 string ret = q{ 1775 import vibe.internal.meta.codegen : CloneFunction; 1776 }; 1777 1778 // generate sub interface methods 1779 foreach (i, SI; Info.SubInterfaceTypes) { 1780 alias F = Info.SubInterfaceFunctions[i]; 1781 alias RT = ReturnType!F; 1782 alias ParamNames = ParameterIdentifierTuple!F; 1783 static if (ParamNames.length == 0) enum pnames = ""; 1784 else enum pnames = ", " ~ [ParamNames].join(", "); 1785 static if (isInstanceOf!(Collection, RT)) { 1786 ret ~= q{ 1787 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1788 return Collection!(%2$s)(m_subInterfaces[%1$s]%3$s); 1789 }); 1790 }.format(i, fullyQualifiedName!SI, pnames); 1791 } else { 1792 ret ~= q{ 1793 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1794 return m_subInterfaces[%1$s]; 1795 }); 1796 }.format(i); 1797 } 1798 } 1799 1800 // generate route methods 1801 foreach (i, F; Info.RouteFunctions) { 1802 alias ParamNames = ParameterIdentifierTuple!F; 1803 static if (ParamNames.length == 0) enum pnames = ""; 1804 else enum pnames = ", " ~ [ParamNames].join(", "); 1805 1806 ret ~= q{ 1807 mixin CloneFunction!(Info.RouteFunctions[%1$s], q{ 1808 return executeClientMethod!(I, %1$s%2$s)(*m_intf, m_requestFilter, m_requestBodyFilter); 1809 }); 1810 }.format(i, pnames); 1811 } 1812 1813 // generate stubs for non-route functions 1814 static foreach (m; __traits(allMembers, I)) 1815 foreach (i, fun; MemberFunctionsTuple!(I, m)) 1816 static if (hasUDA!(fun, NoRouteAttribute)) 1817 ret ~= q{ 1818 mixin CloneFunction!(MemberFunctionsTuple!(I, "%s")[%s], q{ 1819 assert(false); 1820 }); 1821 }.format(m, i); 1822 1823 return ret; 1824 } 1825 1826 1827 private auto executeClientMethod(I, size_t ridx, ARGS...) 1828 (const ref RestInterface!I intf, scope void delegate(HTTPClientRequest) @safe request_filter, 1829 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter) 1830 { 1831 import vibe.internal.interfaceproxy : asInterface; 1832 import vibe.web.internal.rest.common : ParameterKind; 1833 import vibe.stream.operations : readAll; 1834 import vibe.textfilter.urlencode : filterURLEncode, urlEncode; 1835 1836 alias Info = RestInterface!I; 1837 alias Func = Info.RouteFunctions[ridx]; 1838 alias RT = ReturnType!Func; 1839 alias PTT = ParameterTypeTuple!Func; 1840 alias SerPolicyType = SerPolicyT!I.PolicyTemplate; 1841 enum sroute = Info.staticRoutes[ridx]; 1842 auto route = intf.routes[ridx]; 1843 auto settings = intf.settings; 1844 1845 InetHeaderMap headers; 1846 InetHeaderMap reqhdrs; 1847 InetHeaderMap opthdrs; 1848 1849 string url_prefix; 1850 1851 auto query = appender!string(); 1852 auto jsonBody = Json.emptyObject; 1853 string body_; 1854 1855 void addQueryParam(size_t i)(string name) 1856 { 1857 if (query.data.length) query.put('&'); 1858 query.filterURLEncode(name); 1859 query.put("="); 1860 static if (is(PT == Json)) 1861 query.filterURLEncode(ARGS[i].toString()); 1862 else 1863 // Note: CTFE triggers compiler bug here (think we are returning Json, not string). 1864 query.filterURLEncode(toRestString!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(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 static if (!is(RT == void)) {{ 1938 alias result_serializers = ResultSerializersT!Func; 1939 static if (is(result_serializers == AliasSeq!(DefaultSerializerT))) 1940 headers["Accept"] = settings.content_type; 1941 else 1942 headers["Accept"] = result_serializers[0].contentType; 1943 }} else { 1944 headers["Accept"] = settings.content_type; 1945 } 1946 1947 // Do not require a `Content-Type` header if no response is expected 1948 // https://github.com/vibe-d/vibe.d/issues/2521 1949 static if (!is(RT == void)) 1950 // Don't override it if set from the parameter 1951 if ("Content-Type" !in opthdrs) 1952 opthdrs["Content-Type"] = null; 1953 1954 auto ret = request(URL(intf.baseURL), request_filter, request_body_filter, 1955 sroute.method, url, headers, query.data, body_, reqhdrs, opthdrs, 1956 intf.settings.httpClientSettings); 1957 1958 static if (is(RT == InputStream)) { 1959 return ret.bodyReader.asInterface!InputStream; 1960 } else static if (isInputStream!RT) { 1961 return RT(ret.bodyReader); 1962 } else static if (!is(RT == void)) { 1963 scope(exit) ret.dropBody(); 1964 1965 string content_type; 1966 if (const hdr = "Content-Type" in opthdrs) 1967 content_type = *hdr; 1968 if (!content_type.length) 1969 content_type = "application/octet-stream"; 1970 1971 alias result_serializers = ResultSerializersT!Func; 1972 immutable serializer_ind = get_matching_content_type!(result_serializers)(content_type); 1973 foreach (i, serializer; result_serializers) 1974 if (serializer_ind == i) { 1975 // TODO: The JSON deserialiation code requires a forward range, 1976 // but streamInputRange is currently just a bare input 1977 // range, so for now we need to read everything into a 1978 // buffer instead. 1979 //import vibe.stream.wrapper : streamInputRange; 1980 //auto rng = streamInputRange(ret.bodyReader); 1981 auto rng = ret.bodyReader.readAll(); 1982 return serializer.deserialize!(SerPolicyT!I.PolicyTemplate, RT)(rng); 1983 } 1984 1985 throw new Exception("Unrecognized content type: " ~ content_type); 1986 } else ret.dropBody(); 1987 } 1988 1989 /** 1990 * Perform a request to the interface using the given parameters. 1991 * 1992 * Params: 1993 * verb = Kind of request (See $(D HTTPMethod) enum). 1994 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1995 * it will be '/rejectedsoftware/vibe.d/issues'. 1996 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 1997 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 1998 * it will be 'author%3ASantaClaus'. 1999 * 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 2000 * the generic type "application/json". 2001 * reqReturnHdrs = A map of required return headers. 2002 * To avoid returning unused headers, nothing is written 2003 * to this structure unless there's an (usually empty) 2004 * entry (= the key exists) with the same key. 2005 * If any key present in `reqReturnHdrs` is not present 2006 * in the response, an Exception is thrown. 2007 * optReturnHdrs = A map of optional return headers. 2008 * This behaves almost as exactly as reqReturnHdrs, 2009 * except that non-existent key in the response will 2010 * not cause it to throw, but rather to set this entry 2011 * to 'null'. 2012 * 2013 * Returns: 2014 * The Json object returned by the request 2015 */ 2016 private HTTPClientResponse request(URL base_url, 2017 scope void delegate(HTTPClientRequest) @safe request_filter, 2018 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter, 2019 HTTPMethod verb, string path, const scope ref InetHeaderMap hdrs, string query, 2020 string body_, ref InetHeaderMap reqReturnHdrs, 2021 ref InetHeaderMap optReturnHdrs, in HTTPClientSettings http_settings) 2022 @safe { 2023 import std.uni : sicmp; 2024 import vibe.http.client : requestHTTP; 2025 import vibe.http.common : HTTPStatus, httpMethodString, httpStatusText; 2026 import vibe.stream.memory : createMemoryStream; 2027 import vibe.stream.operations; 2028 2029 URL url = base_url; 2030 url.pathString = path; 2031 2032 if (query.length) url.queryString = query; 2033 2034 scope reqdg = (scope HTTPClientRequest req) { 2035 req.method = verb; 2036 foreach (k, v; hdrs.byKeyValue) 2037 req.headers[k] = v; 2038 2039 if (request_body_filter) { 2040 scope str = createMemoryStream(() @trusted { return cast(ubyte[])body_; } (), false); 2041 request_body_filter(req, str); 2042 } 2043 2044 if (request_filter) request_filter(req); 2045 2046 if (body_ != "") 2047 req.writeBody(cast(const(ubyte)[])body_, hdrs.get("Content-Type", "application/json; charset=UTF-8")); 2048 }; 2049 2050 HTTPClientResponse client_res; 2051 if (http_settings) client_res = requestHTTP(url, reqdg, http_settings); 2052 else client_res = requestHTTP(url, reqdg); 2053 2054 logDebug( 2055 "REST call: %s %s -> %d, %s", 2056 httpMethodString(verb), 2057 url.toString(), 2058 client_res.statusCode, 2059 client_res.statusPhrase 2060 ); 2061 2062 // Get required headers - Don't throw yet 2063 string[] missingKeys; 2064 foreach (k, ref v; reqReturnHdrs.byKeyValue) 2065 if (auto ptr = k in client_res.headers) 2066 v = (*ptr).idup; 2067 else 2068 missingKeys ~= k; 2069 2070 // Get optional headers 2071 foreach (k, ref v; optReturnHdrs.byKeyValue) 2072 if (auto ptr = k in client_res.headers) 2073 v = (*ptr).idup; 2074 else 2075 v = null; 2076 if (missingKeys.length) 2077 throw new Exception( 2078 "REST interface mismatch: Missing required header field(s): " 2079 ~ missingKeys.to!string); 2080 2081 if (!isSuccessCode(cast(HTTPStatus)client_res.statusCode)) 2082 { 2083 Json msg = Json(["statusMessage": Json(client_res.statusPhrase)]); 2084 if (client_res.contentType.length) 2085 if (client_res.contentType.splitter(";").front.strip.sicmp("application/json") == 0) 2086 msg = client_res.readJson(); 2087 client_res.dropBody(); 2088 throw new RestException(client_res.statusCode, msg); 2089 } 2090 2091 return client_res; 2092 } 2093 2094 private { 2095 import vibe.data.json; 2096 import std.conv : to; 2097 2098 string toRestString(alias SerPolicyType = DefaultPolicy, T)(T value) 2099 @safe { 2100 import std.array : Appender, appender; 2101 import std.uuid : UUID; 2102 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 2103 else static if (is(T == bool)) return value ? "true" : "false"; 2104 else static if (is(T : int)) return to!string(value); 2105 else static if (is(T : double)) return to!string(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 2106 else static if (is(string : T)) return value; 2107 else static if (__traits(compiles, value.toISOExtString)) return value.toISOExtString; 2108 else static if (__traits(compiles, value.toString)) return value.toString; 2109 else static if (is(T == UUID)) return value.toString(); 2110 else { 2111 auto ret = appender!string; 2112 serializeWithPolicy!(JsonStringSerializer!(Appender!string), SerPolicyType, T)(value, ret); 2113 return ret.data; 2114 } 2115 } 2116 2117 T fromRestString(T, alias SerPolicyType = DefaultPolicy)(string value) 2118 { 2119 import std.conv : ConvException; 2120 import std.uuid : UUID, UUIDParsingException; 2121 import vibe.http.status : HTTPStatus; 2122 try { 2123 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 2124 else static if (is(T == bool)) return value == "1" || value.to!T; 2125 else static if (is(T : int)) return to!T(value); 2126 else static if (is(T : double)) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 2127 else static if (is(string : T)) return value; 2128 else static if (__traits(compiles, T.fromISOExtString("hello"))) return T.fromISOExtString(value); 2129 else static if (__traits(compiles, T.fromString("hello"))) return T.fromString(value); 2130 else static if (is(T == UUID)) return UUID(value); 2131 else return deserializeWithPolicy!(JsonStringSerializer!string, SerPolicyType, T)(value); 2132 } catch (ConvException e) { 2133 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2134 } catch (JSONException e) { 2135 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2136 } catch (UUIDParsingException e) { 2137 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2138 } 2139 } 2140 2141 // Converting from invalid JSON string to aggregate should throw bad request 2142 unittest { 2143 void assertHTTPStatus(E)(lazy E expression, HTTPStatus expectedStatus, 2144 string file = __FILE__, size_t line = __LINE__) 2145 { 2146 import core.exception : AssertError; 2147 import std.format : format; 2148 2149 try 2150 expression(); 2151 catch (HTTPStatusException e) 2152 { 2153 if (e.status != expectedStatus) 2154 throw new AssertError(format("assertHTTPStatus failed: " ~ 2155 "status expected %d but was %d", expectedStatus, e.status), 2156 file, line); 2157 2158 return; 2159 } 2160 2161 throw new AssertError("assertHTTPStatus failed: No " ~ 2162 "'HTTPStatusException' exception was thrown", file, line); 2163 } 2164 2165 struct Foo { int bar; } 2166 assertHTTPStatus(fromRestString!(Foo)("foo"), HTTPStatus.badRequest); 2167 2168 enum Bar { foo, bar } 2169 assert(fromRestString!(Bar)("bar") == Bar.bar); 2170 assertHTTPStatus(fromRestString!(Bar)("foobarbaz"), HTTPStatus.badRequest); 2171 } 2172 } 2173 2174 private string generateModuleImports(I)() 2175 { 2176 if (!__ctfe) 2177 assert (false); 2178 2179 import vibe.internal.meta.codegen : getRequiredImports; 2180 import std.algorithm : map; 2181 import std.array : join; 2182 2183 auto modules = getRequiredImports!I(); 2184 return join(map!(a => "static import " ~ a ~ ";")(modules), "\n"); 2185 } 2186 2187 /*************************************************************************** 2188 2189 The client sends the list of allowed content types in the 'Allow' http header 2190 and the response will contain a 'Content-Type' header. This function will 2191 try to find the best matching @SerializationResult UDA based on the allowed 2192 content types. 2193 2194 Note: 2195 2196 Comment 1: if the request doesn't specify any allowed content types, then * / * 2197 is assumed 2198 2199 Comment 2: if there are no UDA's matching the client's allowed content types, -1 2200 is returned 2201 2202 Comment 3: if there are more than 1 matching UDA, for ONE specific client's allowed 2203 content type(and their priority is the same - see below), 2204 then the one specified earlier in the code gets chosen 2205 2206 Comment 4: accept-params(quality factor) and accept-extensions are ignored 2207 https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 2208 2209 Comment 5: matching the most specific content type without any wildcard has priority 2210 2211 Comment 6: the request's content type can be in the format of 2212 2213 $(UL 2214 $(LI major type / minor type) 2215 $(LI major type / *) 2216 $(LI * / *) 2217 ) 2218 2219 Params: 2220 T = compile time known ResultSerializer classes 2221 req_content_types_str = list of allowed content types for example: 2222 text/*;q=0.3, text/html;q=0.7, text/html;level=1 2223 2224 Returns: 2225 index of the result serializers in the T... AliasSeq if matching found, 2226 -1 otherwise 2227 2228 ***************************************************************************/ 2229 2230 package int get_matching_content_type (T...)(string req_content_types_str) pure @safe 2231 { 2232 if (!req_content_types_str.strip().length) 2233 req_content_types_str = "*/*"; 2234 struct ContentType 2235 { 2236 this (string major_type, string minor_type) 2237 { 2238 this.major_type = major_type; 2239 this.minor_type = minor_type; 2240 this.full_type = major_type ~ "/" ~ minor_type; 2241 this.star_num = this.full_type.count('*'); // serves as priority 2242 } 2243 string major_type; 2244 string minor_type; 2245 string full_type; 2246 ulong star_num; 2247 } 2248 2249 // processing ResultSerializers 2250 alias packed_UDAs = AliasSeq!(T); 2251 ContentType[] UDA_content_types; 2252 foreach (UDA; packed_UDAs) 2253 { 2254 auto ctype = UDA.contentType.splitter(';').front; 2255 auto content_type_split = ctype.toLower().split("/"); 2256 assert(content_type_split.length == 2); 2257 UDA_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2258 } 2259 2260 // processing request content typess 2261 ContentType[] req_content_types; 2262 foreach (content_type; req_content_types_str.toLower().split(",")) 2263 { 2264 immutable semicolon_pos = content_type.indexOf(';'); 2265 if (semicolon_pos != -1) 2266 content_type = content_type[0 .. semicolon_pos]; // quality factor ignored 2267 auto content_type_split = content_type.split("/"); 2268 if (content_type_split.length == 2) 2269 req_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2270 } 2271 // sorting content types by matching preference 2272 req_content_types.sort!(( x, y) => (x.star_num < y.star_num)); 2273 2274 int res = -1; 2275 ulong min_star_num = ulong.max; 2276 foreach (UDA_ind, UDA_content_type; UDA_content_types) 2277 foreach (const ref content_type; req_content_types) 2278 if ( 2279 ( 2280 ( 2281 UDA_content_type.major_type == content_type.major_type && 2282 UDA_content_type.minor_type == content_type.minor_type 2283 ) || 2284 ( 2285 UDA_content_type.major_type == content_type.major_type && 2286 content_type.minor_type == "*" 2287 ) || 2288 ( 2289 content_type.major_type == "*" && content_type.minor_type == "*" 2290 ) 2291 ) && 2292 ( 2293 content_type.star_num < min_star_num 2294 ) 2295 ) 2296 { 2297 res = cast(int) UDA_ind; 2298 min_star_num = content_type.star_num; 2299 } 2300 return res; 2301 } 2302 2303 version(unittest) 2304 { 2305 import std.range.interfaces : OutputRange; 2306 import vibe.internal.interfaceproxy : InterfaceProxy; 2307 2308 void s (OutputRange!char, int){}; 2309 int d (InterfaceProxy!(InputStream)){return 1;} 2310 2311 int test1(); 2312 2313 @resultSerializer!(s,d,"text/plain") 2314 @resultSerializer!(s,d," aPPliCatIon / jsOn ") 2315 int test2(); 2316 } 2317 2318 unittest 2319 { 2320 alias res = ResultSerializersT!(test1); 2321 assert(res.length == 1); 2322 assert(res[0].contentType == "application/json; charset=UTF-8"); 2323 2324 assert(get_matching_content_type!(res)("application/json") == 0); 2325 assert(get_matching_content_type!(res)("application/*") == 0); 2326 assert(get_matching_content_type!(res)(" appliCAtIon / *") == 0); 2327 assert(get_matching_content_type!(res)("*/*") == 0); 2328 assert(get_matching_content_type!(res)("") == 0); 2329 assert(get_matching_content_type!(res)("application/blabla") == -1); 2330 2331 alias res2 = ResultSerializersT!(test2); 2332 assert(res2.length == 2); 2333 assert(res2[0].contentType == "text/plain"); 2334 assert(res2[1].contentType == " aPPliCatIon / jsOn "); 2335 2336 assert(get_matching_content_type!(res2)("text/plain, application/json") == 0); 2337 assert(get_matching_content_type!(res2)("text/*, application/json") == 1); 2338 assert(get_matching_content_type!(res2)("*/*, application/json") == 1); 2339 assert(get_matching_content_type!(res2)("*/*") == 0); 2340 assert(get_matching_content_type!(res2)("") == 0); 2341 assert(get_matching_content_type!(res2)("blabla/blabla, blublu/blublu") == -1); 2342 } 2343 2344 version(unittest) 2345 { 2346 private struct Aggregate { } 2347 private interface Interface 2348 { 2349 Aggregate[] foo(); 2350 } 2351 } 2352 2353 unittest 2354 { 2355 enum imports = generateModuleImports!Interface; 2356 static assert (imports == "static import vibe.web.rest;"); 2357 } 2358 2359 // Check that the interface is valid. Every checks on the correctness of the 2360 // interface should be put in checkRestInterface, which allows to have consistent 2361 // errors in the server and client. 2362 package string getInterfaceValidationError(I)() 2363 out (result) { assert((result is null) == !result.length); } 2364 do { 2365 import vibe.web.internal.rest.common : ParameterKind, WebParamUDATuple; 2366 import std.algorithm : strip; 2367 2368 // The hack parameter is to kill "Statement is not reachable" warnings. 2369 string validateMethod(alias Func)(bool hack = true) { 2370 import vibe.internal.meta.uda; 2371 import std.string : format; 2372 2373 static assert(is(FunctionTypeOf!Func), "Internal error"); 2374 2375 if (!__ctfe) 2376 assert(false, "Internal error"); 2377 2378 enum FuncId = (fullyQualifiedName!I~ "." ~ __traits(identifier, Func)); 2379 alias PT = ParameterTypeTuple!Func; 2380 static if (!__traits(compiles, ParameterIdentifierTuple!Func)) { 2381 if (hack) return "%s: A parameter has no name.".format(FuncId); 2382 alias PN = AliasSeq!("-DummyInvalid-"); 2383 } else 2384 alias PN = ParameterIdentifierTuple!Func; 2385 alias WPAT = UDATuple!(WebParamAttribute, Func); 2386 2387 // Check if there is no orphan UDATuple (e.g. typo while writing the name of the parameter). 2388 foreach (i, uda; WPAT) { 2389 // Note: static foreach gets unrolled, generating multiple nested sub-scope. 2390 // The spec / DMD doesn't like when you have the same symbol in those, 2391 // leading to wrong codegen / wrong template being reused. 2392 // That's why those templates need different names. 2393 // See DMD bug #9748. 2394 mixin(GenOrphan!(i).Decl); 2395 // template CmpOrphan(string name) { enum CmpOrphan = (uda.identifier == name); } 2396 static if (!anySatisfy!(mixin(GenOrphan!(i).Name), PN)) { 2397 if (hack) return "%s: No parameter '%s' (referenced by attribute @%sParam)" 2398 .format(FuncId, uda.identifier, uda.origin); 2399 } 2400 } 2401 2402 foreach (i, P; PT) { 2403 static if (!PN[i].length) 2404 if (hack) return "%s: Parameter %d has no name." 2405 .format(FuncId, i); 2406 // Check for multiple origins 2407 static if (WPAT.length) { 2408 // It's okay to reuse GenCmp, as the order of params won't change. 2409 // It should/might not be reinstantiated by the compiler. 2410 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2411 alias WPA = Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT); 2412 static if (WPA.length > 1) 2413 if (hack) return "%s: Parameter '%s' has multiple @*Param attributes on it." 2414 .format(FuncId, PN[i]); 2415 } 2416 } 2417 2418 // Check for misplaced out and non-const ref 2419 alias PSC = ParameterStorageClass; 2420 foreach (i, SC; ParameterStorageClassTuple!Func) { 2421 static if (SC & PSC.out_ || (SC & PSC.ref_ && !is(ConstOf!(PT[i]) == PT[i])) ) { 2422 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2423 alias Attr = AliasSeq!( 2424 WebParamUDATuple!(Func, i), 2425 Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT), 2426 ); 2427 static if (Attr.length != 1) { 2428 if (hack) return "%s: Parameter '%s' cannot be %s" 2429 .format(FuncId, PN[i], SC & PSC.out_ ? "out" : "ref"); 2430 } else static if (Attr[0].origin != ParameterKind.header) { 2431 if (hack) return "%s: %s parameter '%s' cannot be %s" 2432 .format(FuncId, Attr[0].origin, PN[i], 2433 SC & PSC.out_ ? "out" : "ref"); 2434 } 2435 } 2436 } 2437 2438 // Check for @path(":name") 2439 enum pathAttr = findFirstUDA!(PathAttribute, Func); 2440 static if (pathAttr.found) { 2441 static if (!pathAttr.value.length) { 2442 if (hack) 2443 return "%s: Path is null or empty".format(FuncId); 2444 } else { 2445 import std.algorithm : canFind, splitter; 2446 // splitter doesn't work with alias this ? 2447 auto str = pathAttr.value.data; 2448 if (str.canFind("//")) return "%s: Path '%s' contains empty entries.".format(FuncId, pathAttr.value); 2449 str = str.strip('/'); 2450 if (!str.length) return null; 2451 foreach (elem; str.splitter('/')) { 2452 assert(elem.length, "Empty path entry not caught yet!?"); 2453 2454 if (elem[0] == ':') { 2455 // typeof(PN) is void when length is 0. 2456 static if (!PN.length) { 2457 if (hack) 2458 return "%s: Path contains '%s', but no parameter '_%s' defined." 2459 .format(FuncId, elem, elem[1..$]); 2460 } else { 2461 if (![PN].canFind("_"~elem[1..$])) 2462 if (hack) return "%s: Path contains '%s', but no parameter '_%s' defined." 2463 .format(FuncId, elem, elem[1..$]); 2464 elem = elem[1..$]; 2465 } 2466 } 2467 } 2468 // TODO: Check for validity of the subpath. 2469 } 2470 } 2471 return null; 2472 } 2473 2474 if (!__ctfe) 2475 assert(false, "Internal error"); 2476 bool hack = true; 2477 foreach (method; __traits(allMembers, I)) { 2478 // WORKAROUND #1045 / @@BUG14375@@ 2479 static if (method.length != 0) 2480 foreach (overload; MemberFunctionsTuple!(I, method)) { 2481 static if (validateMethod!(overload)()) 2482 if (hack) return validateMethod!(overload)(); 2483 } 2484 } 2485 return null; 2486 } 2487 2488 // Test detection of user typos (e.g., if the attribute is on a parameter that doesn't exist). 2489 unittest { 2490 enum msg = "No parameter 'ath' (referenced by attribute @headerParam)"; 2491 2492 interface ITypo { 2493 @headerParam("ath", "Authorization") // mistyped parameter name 2494 string getResponse(string auth); 2495 } 2496 enum err = getInterfaceValidationError!ITypo; 2497 static assert(err !is null && stripTestIdent(err) == msg, 2498 "Expected validation error for getResponse, got: "~stripTestIdent(err)); 2499 } 2500 2501 // Multiple origin for a parameter 2502 unittest { 2503 enum msg = "Parameter 'arg1' has multiple @*Param attributes on it."; 2504 2505 interface IMultipleOrigin { 2506 @headerParam("arg1", "Authorization") @bodyParam("arg1", "Authorization") 2507 string getResponse(string arg1, int arg2); 2508 } 2509 enum err = getInterfaceValidationError!IMultipleOrigin; 2510 static assert(err !is null && stripTestIdent(err) == msg, err); 2511 } 2512 2513 // Missing parameter name 2514 unittest { 2515 enum msg = "Parameter 0 has no name."; 2516 2517 interface IMissingName1 { 2518 string getResponse(string = "troublemaker"); 2519 } 2520 interface IMissingName2 { 2521 string getResponse(string); 2522 } 2523 enum err1 = getInterfaceValidationError!IMissingName1; 2524 static assert(err1 !is null && stripTestIdent(err1) == msg, err1); 2525 enum err2 = getInterfaceValidationError!IMissingName2; 2526 static assert(err2 !is null && stripTestIdent(err2) == msg, err2); 2527 } 2528 2529 // Issue 949 2530 unittest { 2531 enum msg = "Path contains ':owner', but no parameter '_owner' defined."; 2532 2533 @path("/repos/") 2534 interface IGithubPR { 2535 @path(":owner/:repo/pulls") 2536 string getPullRequests(string owner, string repo); 2537 } 2538 enum err = getInterfaceValidationError!IGithubPR; 2539 static assert(err !is null && stripTestIdent(err) == msg, err); 2540 } 2541 2542 // Issue 1017 2543 unittest { 2544 interface TestSuccess { @path("/") void test(); } 2545 interface TestSuccess2 { @path("/test/") void test(); } 2546 interface TestFail { @path("//") void test(); } 2547 interface TestFail2 { @path("/test//it/") void test(); } 2548 static assert(getInterfaceValidationError!TestSuccess is null); 2549 static assert(getInterfaceValidationError!TestSuccess2 is null); 2550 static assert(stripTestIdent(getInterfaceValidationError!TestFail) 2551 == "Path '//' contains empty entries."); 2552 static assert(stripTestIdent(getInterfaceValidationError!TestFail2) 2553 == "Path '/test//it/' contains empty entries."); 2554 } 2555 2556 unittest { 2557 interface NullPath { @path(null) void test(); } 2558 interface ExplicitlyEmptyPath { @path("") void test(); } 2559 static assert(stripTestIdent(getInterfaceValidationError!NullPath) 2560 == "Path is null or empty"); 2561 static assert(stripTestIdent(getInterfaceValidationError!ExplicitlyEmptyPath) 2562 == "Path is null or empty"); 2563 2564 // Note: Implicitly empty path are valid: 2565 // interface ImplicitlyEmptyPath { void get(); } 2566 } 2567 2568 // Accept @headerParam ref / out parameters 2569 unittest { 2570 interface HeaderRef { 2571 @headerParam("auth", "auth") 2572 string getData(ref string auth); 2573 string getData2(@viaHeader("auth") ref string auth); 2574 } 2575 static assert(getInterfaceValidationError!HeaderRef is null, 2576 stripTestIdent(getInterfaceValidationError!HeaderRef)); 2577 2578 interface HeaderOut { 2579 @headerParam("auth", "auth") 2580 void getData(out string auth); 2581 void getData(@viaHeader("auth") out string auth); 2582 } 2583 static assert(getInterfaceValidationError!HeaderOut is null, 2584 stripTestIdent(getInterfaceValidationError!HeaderOut)); 2585 } 2586 2587 // Reject unattributed / @queryParam or @bodyParam ref / out parameters 2588 unittest { 2589 interface QueryRef { 2590 @queryParam("auth", "auth") 2591 string getData(ref string auth); 2592 } 2593 static assert(stripTestIdent(getInterfaceValidationError!QueryRef) 2594 == "query parameter 'auth' cannot be ref"); 2595 2596 interface QueryRefConst { 2597 @queryParam("auth", "auth") 2598 string getData(const ref string auth); 2599 } 2600 enum err1 = getInterfaceValidationError!QueryRefConst; 2601 static assert(err1 is null, err1); 2602 2603 interface QueryOut { 2604 @queryParam("auth", "auth") 2605 void getData(out string auth); 2606 } 2607 static assert(stripTestIdent(getInterfaceValidationError!QueryOut) 2608 == "query parameter 'auth' cannot be out"); 2609 2610 interface BodyRef { 2611 @bodyParam("auth", "auth") 2612 string getData(ref string auth); 2613 } 2614 static assert(stripTestIdent(getInterfaceValidationError!BodyRef) 2615 == "body_ parameter 'auth' cannot be ref",x); 2616 2617 interface BodyRefConst { 2618 @bodyParam("auth", "auth") 2619 string getData(const ref string auth); 2620 } 2621 enum err2 = getInterfaceValidationError!BodyRefConst; 2622 static assert(err2 is null, err2); 2623 2624 interface BodyOut { 2625 @bodyParam("auth", "auth") 2626 void getData(out string auth); 2627 } 2628 static assert(stripTestIdent(getInterfaceValidationError!BodyOut) 2629 == "body_ parameter 'auth' cannot be out"); 2630 2631 // There's also the possibility of someone using an out unnamed 2632 // parameter (don't ask me why), but this is catched as unnamed 2633 // parameter, so we don't need to check it here. 2634 } 2635 2636 private string stripTestIdent(string msg) 2637 @safe { 2638 import std.string; 2639 auto idx = msg.indexOf(": "); 2640 return idx >= 0 ? msg[idx+2 .. $] : msg; 2641 } 2642 2643 // Small helper for client code generation 2644 private string paramCTMap(string[string] params) 2645 @safe { 2646 import std.array : appender, join; 2647 if (!__ctfe) 2648 assert (false, "This helper is only supposed to be called for codegen in RestClientInterface."); 2649 auto app = appender!(string[]); 2650 foreach (key, val; params) { 2651 app ~= "\""~key~"\""; 2652 app ~= val; 2653 } 2654 return app.data.join(", "); 2655 } 2656 2657 package string stripTUnderscore(string name, RestInterfaceSettings settings) 2658 @safe { 2659 if ((settings is null || settings.stripTrailingUnderscore) 2660 && name.endsWith("_")) 2661 return name[0 .. $-1]; 2662 else return name; 2663 } 2664 2665 // Workarounds @@DMD:9748@@, and maybe more 2666 package template GenCmp(string name, int id, string cmpTo) { 2667 import std.string : format; 2668 import std.conv : to; 2669 enum Decl = q{ 2670 template %1$s(alias uda) { 2671 enum %1$s = (uda.identifier == "%2$s"); 2672 } 2673 }.format(Name, cmpTo); 2674 enum Name = name~to!string(id); 2675 } 2676 2677 // Ditto 2678 private template GenOrphan(int id) { 2679 import std.string : format; 2680 import std.conv : to; 2681 enum Decl = q{ 2682 template %1$s(string name) { 2683 enum %1$s = (uda.identifier == name); 2684 } 2685 }.format(Name); 2686 enum Name = "OrphanCheck"~to!string(id); 2687 } 2688 2689 // Workaround for issue #1045 / DMD bug 14375 2690 // Also, an example of policy-based design using this module. 2691 @safe unittest { 2692 import std.traits, std.typetuple; 2693 import vibe.internal.meta.codegen; 2694 import vibe.internal.meta.typetuple; 2695 import vibe.web.internal.rest.common : ParameterKind; 2696 2697 interface Policies { 2698 @headerParam("auth", "Authorization") 2699 string BasicAuth(string auth, ulong expiry) @safe; 2700 } 2701 2702 @path("/keys/") 2703 interface IKeys(alias AuthenticationPolicy = Policies.BasicAuth) { 2704 static assert(is(FunctionTypeOf!AuthenticationPolicy == function), 2705 "Policies needs to be functions"); 2706 @path("/") @method(HTTPMethod.POST) @safe 2707 mixin CloneFunctionDecl!(AuthenticationPolicy, true, "create"); 2708 } 2709 2710 class KeysImpl : IKeys!() { 2711 override: 2712 string create(string auth, ulong expiry) @safe { 2713 return "4242-4242"; 2714 } 2715 } 2716 2717 // Some sanity checks 2718 // Note: order is most likely implementation dependent. 2719 // Good thing we only have one frontend... 2720 alias WPA = WebParamAttribute; 2721 static assert(Compare!( 2722 Group!(__traits(getAttributes, IKeys!().create)), 2723 Group!(PathAttribute("/"), 2724 MethodAttribute(HTTPMethod.POST), 2725 WPA(ParameterKind.header, "auth", "Authorization")))); 2726 2727 void register() @safe { 2728 auto router = new URLRouter(); 2729 router.registerRestInterface(new KeysImpl()); 2730 } 2731 2732 void query() @safe { 2733 auto client = new RestInterfaceClient!(IKeys!())("http://127.0.0.1:8080"); 2734 assert(client.create("Hello", 0) == "4242-4242"); 2735 } 2736 } 2737 2738 @safe unittest { // @noRoute support in RestInterfaceClient 2739 interface I { 2740 void foo(); 2741 @noRoute int bar(void* someparam); 2742 } 2743 auto cli = new RestInterfaceClient!I("http://127.0.0.1/"); 2744 }