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