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 if (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 } else static if (sroute.parameters[i].kind == ParameterKind.status) { 1634 static if (is(typeof(params[i]) == HTTPStatus) || is(typeof(params[i]) == int)) 1635 res.statusCode = params[i]; 1636 else static if (is(typeof(params[i]) == string)) 1637 res.statusPhrase = params[i]; 1638 else static assert(false, "`@viaStatus` parameters must be of type `HTTPStatus`/`int` or `string`"); 1639 } else static assert (false, "`out` parameters must be annotated with either `@viaHeader` or `@viaStatus`"); 1640 } 1641 } 1642 } 1643 1644 try { 1645 import vibe.internal.meta.funcattr; 1646 1647 static if (!__traits(compiles, () @safe { __traits(getMember, inst, Method)(params); })) 1648 static assert(false, "REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be marked `@safe`."); 1649 1650 static if (is(RT == void)) { 1651 // TODO: remove after deprecation period 1652 __traits(getMember, inst, Method)(params); 1653 returnHeaders(); 1654 res.writeBody(cast(ubyte[])null); 1655 } else static if (isInputStream!RT) { 1656 auto ret = evaluateOutputModifiers!CFunc( 1657 __traits(getMember, inst, Method)(params), req, res); 1658 returnHeaders(); 1659 res.headers["Content-Type"] = res.headers.get("Content-Type", "application/octet-stream"); 1660 ret.pipe(res.bodyWriter); 1661 } else { 1662 // TODO: remove after deprecation period 1663 static if (!__traits(compiles, () @safe { evaluateOutputModifiers!Func(RT.init, req, res); } ())) 1664 static assert(false, "`@after` evaluator for REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be marked `@safe`."); 1665 1666 auto ret = evaluateOutputModifiers!CFunc( 1667 __traits(getMember, inst, Method)(params), req, res); 1668 returnHeaders(); 1669 1670 string accept_str; 1671 if (const accept_header = "Accept" in req.headers) 1672 accept_str = *accept_header; 1673 alias result_serializers = ResultSerializersT!Func; 1674 immutable serializer_ind = get_matching_content_type!(result_serializers)(accept_str); 1675 foreach (i, serializer; result_serializers) 1676 if (serializer_ind == i) { 1677 auto serialized_output = appender!(ubyte[]); 1678 static if ( 1679 __traits(compiles, () @trusted { 1680 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1681 }) 1682 && !__traits(compiles, () @safe { 1683 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1684 })) 1685 { 1686 static assert(false, "Serialization of return type `"~RT.stringof~"` of REST interface method `" ~ fullyQualifiedName!T ~ "." ~ Method ~ "` must be `@safe`."); 1687 } 1688 serializer.serialize!(SerPolicyT!(RestInterface!T.I).PolicyTemplate)(serialized_output, ret); 1689 res.writeBody(serialized_output.data, serializer.contentType); 1690 } 1691 res.statusCode = HTTPStatus.notAcceptable; // will trigger RestException on the client side 1692 res.writeBody(cast(ubyte[])null); 1693 } 1694 } catch (Exception e) { 1695 returnHeaders(); 1696 handleException(e, HTTPStatus.internalServerError); 1697 } 1698 } 1699 1700 return &handler; 1701 } 1702 1703 /** 1704 * Generate an handler that will wrap the server's method 1705 * 1706 * This function returns an handler that handles the http OPTIONS method. 1707 * 1708 * It will return the ALLOW header with all the methods on this resource 1709 * And it will handle Preflight CORS. 1710 * 1711 * Params: 1712 * routes = a range of Routes were each route has the same resource/URI 1713 * just different method. 1714 * settings = REST server configuration. 1715 * 1716 * Returns: 1717 * A delegate suitable to use as an handler for an HTTP request. 1718 */ 1719 private HTTPServerRequestDelegate optionsMethodHandler(RouteRange)(RouteRange routes, RestInterfaceSettings settings = null) 1720 { 1721 import std.algorithm : map, joiner, any; 1722 import std.array : array; 1723 import std.conv : text; 1724 import vibe.http.common : httpMethodString, httpMethodFromString; 1725 // NOTE: don't know what is better, to keep this in memory, or generate on each request 1726 auto allow = routes.map!(r => r.method.httpMethodString).joiner(",").text(); 1727 auto methods = routes.map!(r => r.method).array(); 1728 1729 void handlePreflightedCors(HTTPServerRequest req, HTTPServerResponse res, ref HTTPMethod[] methods, RestInterfaceSettings settings = null) 1730 { 1731 import std.algorithm : among; 1732 import std.uni : sicmp; 1733 1734 auto origin = "Origin" in req.headers; 1735 if (origin is null) 1736 return; 1737 1738 if (settings !is null && 1739 settings.allowedOrigins.length != 0 && 1740 !settings.allowedOrigins.any!(org => org.sicmp((*origin)) == 0)) 1741 return; 1742 1743 auto method = "Access-Control-Request-Method" in req.headers; 1744 if (method is null) 1745 return; 1746 1747 auto httpMethod = httpMethodFromString(*method); 1748 1749 if (!methods.any!(m => m == httpMethod)) 1750 return; 1751 1752 res.headers["Access-Control-Allow-Origin"] = *origin; 1753 1754 // there is no way to know if the specific resource supports credentials 1755 // (either cookies, HTTP authentication, or client-side SSL certificates), 1756 // so we always assume it does 1757 res.headers["Access-Control-Allow-Credentials"] = "true"; 1758 res.headers["Access-Control-Max-Age"] = "1728000"; 1759 res.headers["Access-Control-Allow-Methods"] = *method; 1760 1761 // we have no way to reliably determine what headers the resource allows 1762 // so we simply copy whatever the client requested 1763 if (auto headers = "Access-Control-Request-Headers" in req.headers) 1764 res.headers["Access-Control-Allow-Headers"] = *headers; 1765 } 1766 1767 void handler(HTTPServerRequest req, HTTPServerResponse res) 1768 { 1769 // since this is a OPTIONS request, we have to return the ALLOW headers to tell which methods we have 1770 res.headers["Allow"] = allow; 1771 1772 // handle CORS preflighted requests 1773 handlePreflightedCors(req,res,methods,settings); 1774 1775 // NOTE: besides just returning the allowed methods and handling CORS preflighted requests, 1776 // this would be a nice place to describe what kind of resources are on this route, 1777 // the params each accepts, the headers, etc... think WSDL but then for REST. 1778 res.writeBody(""); 1779 } 1780 return &handler; 1781 } 1782 1783 private string generateRestClientMethods(I)() 1784 { 1785 import std.array : join; 1786 import std.string : format; 1787 import std.traits : fullyQualifiedName, isInstanceOf; 1788 1789 alias Info = RestInterface!I; 1790 1791 string ret = q{ 1792 import vibe.internal.meta.codegen : CloneFunction; 1793 }; 1794 1795 // generate sub interface methods 1796 foreach (i, SI; Info.SubInterfaceTypes) { 1797 alias F = Info.SubInterfaceFunctions[i]; 1798 alias RT = ReturnType!F; 1799 alias ParamNames = ParameterIdentifierTuple!F; 1800 static if (ParamNames.length == 0) enum pnames = ""; 1801 else enum pnames = ", " ~ [ParamNames].join(", "); 1802 static if (isInstanceOf!(Collection, RT)) { 1803 ret ~= q{ 1804 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1805 return Collection!(%2$s)(m_subInterfaces[%1$s]%3$s); 1806 }); 1807 }.format(i, fullyQualifiedName!SI, pnames); 1808 } else { 1809 ret ~= q{ 1810 mixin CloneFunction!(Info.SubInterfaceFunctions[%1$s], q{ 1811 return m_subInterfaces[%1$s]; 1812 }); 1813 }.format(i); 1814 } 1815 } 1816 1817 // generate route methods 1818 foreach (i, F; Info.RouteFunctions) { 1819 alias ParamNames = ParameterIdentifierTuple!F; 1820 static if (ParamNames.length == 0) enum pnames = ""; 1821 else enum pnames = ", " ~ [ParamNames].join(", "); 1822 1823 ret ~= q{ 1824 mixin CloneFunction!(Info.RouteFunctions[%1$s], q{ 1825 return executeClientMethod!(I, %1$s%2$s)(*m_intf, m_requestFilter, m_requestBodyFilter); 1826 }); 1827 }.format(i, pnames); 1828 } 1829 1830 // generate stubs for non-route functions 1831 static foreach (m; __traits(allMembers, I)) 1832 foreach (i, fun; MemberFunctionsTuple!(I, m)) 1833 static if (hasUDA!(fun, NoRouteAttribute)) 1834 ret ~= q{ 1835 mixin CloneFunction!(MemberFunctionsTuple!(I, "%s")[%s], q{ 1836 assert(false); 1837 }); 1838 }.format(m, i); 1839 1840 return ret; 1841 } 1842 1843 1844 private auto executeClientMethod(I, size_t ridx, ARGS...) 1845 (const ref RestInterface!I intf, scope void delegate(HTTPClientRequest) @safe request_filter, 1846 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter) 1847 { 1848 import vibe.internal.interfaceproxy : asInterface; 1849 import vibe.web.internal.rest.common : ParameterKind; 1850 import vibe.stream.operations : readAll; 1851 import vibe.textfilter.urlencode : filterURLEncode, urlEncode; 1852 1853 alias Info = RestInterface!I; 1854 alias Func = Info.RouteFunctions[ridx]; 1855 alias RT = ReturnType!Func; 1856 alias PTT = ParameterTypeTuple!Func; 1857 alias SerPolicyType = SerPolicyT!I.PolicyTemplate; 1858 enum sroute = Info.staticRoutes[ridx]; 1859 auto route = intf.routes[ridx]; 1860 auto settings = intf.settings; 1861 1862 InetHeaderMap headers; 1863 InetHeaderMap reqhdrs; 1864 InetHeaderMap opthdrs; 1865 HTTPStatus status_code; 1866 string status_phrase; 1867 1868 string url_prefix; 1869 1870 auto query = appender!string(); 1871 auto jsonBody = Json.emptyObject; 1872 string body_; 1873 1874 void addQueryParam(size_t i)(string name) 1875 { 1876 if (query.data.length) query.put('&'); 1877 query.filterURLEncode(name); 1878 query.put("="); 1879 static if (is(PT == Json)) 1880 query.filterURLEncode(ARGS[i].toString()); 1881 else 1882 // Note: CTFE triggers compiler bug here (think we are returning Json, not string). 1883 query.filterURLEncode(toRestString!SerPolicyType(ARGS[i])); 1884 } 1885 1886 foreach (i, PT; PTT) { 1887 enum sparam = sroute.parameters[i]; 1888 auto fieldname = route.parameters[i].fieldName; 1889 static if (sparam.kind == ParameterKind.query) { 1890 addQueryParam!i(fieldname); 1891 } else static if (sparam.kind == ParameterKind.wholeBody) { 1892 jsonBody = serializeWithPolicy!(JsonSerializer, SerPolicyType)(ARGS[i]); 1893 } else static if (sparam.kind == ParameterKind.body_) { 1894 jsonBody[fieldname] = serializeWithPolicy!(JsonSerializer, SerPolicyType)(ARGS[i]); 1895 } else static if (sparam.kind == ParameterKind.header) { 1896 // Don't send 'out' parameter, as they should be default init anyway and it might confuse some server 1897 static if (sparam.isIn) { 1898 static if (isInstanceOf!(Nullable, PT)) { 1899 if (!ARGS[i].isNull) 1900 headers[fieldname] = to!string(ARGS[i]); 1901 } else headers[fieldname] = to!string(ARGS[i]); 1902 } 1903 static if (sparam.isOut) { 1904 // Optional parameter 1905 static if (isInstanceOf!(Nullable, PT)) { 1906 opthdrs[fieldname] = null; 1907 } else { 1908 reqhdrs[fieldname] = null; 1909 } 1910 } 1911 } 1912 } 1913 1914 static if (sroute.method == HTTPMethod.GET) { 1915 assert(jsonBody == Json.emptyObject, "GET request trying to send body parameters."); 1916 } else { 1917 debug body_ = jsonBody.toPrettyString(); 1918 else body_ = jsonBody.toString(); 1919 } 1920 1921 string url; 1922 foreach (i, p; route.fullPathParts) { 1923 if (p.isParameter) { 1924 switch (p.text) { 1925 foreach (j, PT; PTT) { 1926 static if (sroute.parameters[j].name[0] == '_' || sroute.parameters[j].name == "id") { 1927 case sroute.parameters[j].name: 1928 url ~= urlEncode(toRestString(ARGS[j])); 1929 goto sbrk; 1930 } 1931 } 1932 default: url ~= ":" ~ p.text; break; 1933 } 1934 sbrk:; 1935 } else url ~= p.text; 1936 } 1937 1938 scope (exit) { 1939 foreach (i, PT; PTT) { 1940 enum sparam = sroute.parameters[i]; 1941 auto fieldname = route.parameters[i].fieldName; 1942 static if (sparam.isOut) { 1943 static if (sparam.kind == ParameterKind.header) { 1944 static if (isInstanceOf!(Nullable, PT)) { 1945 ARGS[i] = to!(TemplateArgsOf!PT)( 1946 opthdrs.get(fieldname, null)); 1947 } else { 1948 if (auto ptr = fieldname in reqhdrs) 1949 ARGS[i] = to!PT(*ptr); 1950 } 1951 } else static if (sparam.kind == ParameterKind.status) { 1952 static if (is(PT == HTTPStatus) || is(PT == int)) { 1953 ARGS[i] = status_code; 1954 } else static if (is(PT == string)) { 1955 ARGS[i] = status_phrase; 1956 } 1957 } 1958 } 1959 } 1960 } 1961 1962 static if (!is(RT == void)) {{ 1963 alias result_serializers = ResultSerializersT!Func; 1964 static if (is(result_serializers == AliasSeq!(DefaultSerializerT))) 1965 headers["Accept"] = settings.content_type; 1966 else 1967 headers["Accept"] = result_serializers[0].contentType; 1968 }} else { 1969 headers["Accept"] = settings.content_type; 1970 } 1971 1972 // Do not require a `Content-Type` header if no response is expected 1973 // https://github.com/vibe-d/vibe.d/issues/2521 1974 static if (!is(RT == void)) 1975 // Don't override it if set from the parameter 1976 if ("Content-Type" !in opthdrs) 1977 opthdrs["Content-Type"] = null; 1978 1979 auto ret = request(URL(intf.baseURL), request_filter, request_body_filter, 1980 sroute.method, url, headers, query.data, body_, reqhdrs, opthdrs, 1981 intf.settings.httpClientSettings); 1982 status_code = cast(HTTPStatus)ret.statusCode; 1983 status_phrase = ret.statusPhrase; 1984 1985 static if (is(RT == InputStream)) { 1986 return ret.bodyReader.asInterface!InputStream; 1987 } else static if (isInputStream!RT) { 1988 return RT(ret.bodyReader); 1989 } else static if (!is(RT == void)) { 1990 scope(exit) ret.dropBody(); 1991 1992 string content_type; 1993 if (const hdr = "Content-Type" in opthdrs) 1994 content_type = *hdr; 1995 if (!content_type.length) 1996 content_type = "application/octet-stream"; 1997 1998 alias result_serializers = ResultSerializersT!Func; 1999 immutable serializer_ind = get_matching_content_type!(result_serializers)(content_type); 2000 foreach (i, serializer; result_serializers) 2001 if (serializer_ind == i) { 2002 // TODO: The JSON deserialiation code requires a forward range, 2003 // but streamInputRange is currently just a bare input 2004 // range, so for now we need to read everything into a 2005 // buffer instead. 2006 //import vibe.stream.wrapper : streamInputRange; 2007 //auto rng = streamInputRange(ret.bodyReader); 2008 auto rng = ret.bodyReader.readAll(); 2009 return serializer.deserialize!(SerPolicyT!I.PolicyTemplate, RT)(rng); 2010 } 2011 2012 throw new Exception("Unrecognized content type: " ~ content_type); 2013 } else ret.dropBody(); 2014 } 2015 2016 /** 2017 * Perform a request to the interface using the given parameters. 2018 * 2019 * Params: 2020 * verb = Kind of request (See $(D HTTPMethod) enum). 2021 * name = Location to request. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 2022 * it will be '/rejectedsoftware/vibe.d/issues'. 2023 * hdrs = The headers to send. Some field might be overriden (such as Content-Length). However, Content-Type will NOT be overriden. 2024 * query = The $(B encoded) query string. For a request on https://github.com/rejectedsoftware/vibe.d/issues?q=author%3ASantaClaus, 2025 * it will be 'author%3ASantaClaus'. 2026 * 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 2027 * the generic type "application/json". 2028 * reqReturnHdrs = A map of required return headers. 2029 * To avoid returning unused headers, nothing is written 2030 * to this structure unless there's an (usually empty) 2031 * entry (= the key exists) with the same key. 2032 * If any key present in `reqReturnHdrs` is not present 2033 * in the response, an Exception is thrown. 2034 * optReturnHdrs = A map of optional return headers. 2035 * This behaves almost as exactly as reqReturnHdrs, 2036 * except that non-existent key in the response will 2037 * not cause it to throw, but rather to set this entry 2038 * to 'null'. 2039 * 2040 * Returns: 2041 * The Json object returned by the request 2042 */ 2043 private HTTPClientResponse request(URL base_url, 2044 scope void delegate(HTTPClientRequest) @safe request_filter, 2045 scope void delegate(HTTPClientRequest, scope InputStream) @safe request_body_filter, 2046 HTTPMethod verb, string path, const scope ref InetHeaderMap hdrs, string query, 2047 string body_, ref InetHeaderMap reqReturnHdrs, 2048 ref InetHeaderMap optReturnHdrs, in HTTPClientSettings http_settings) 2049 @safe { 2050 import std.uni : sicmp; 2051 import vibe.http.client : requestHTTP; 2052 import vibe.http.common : HTTPStatus, httpMethodString, httpStatusText; 2053 import vibe.stream.memory : createMemoryStream; 2054 import vibe.stream.operations; 2055 2056 URL url = base_url; 2057 url.pathString = path; 2058 2059 if (query.length) url.queryString = query; 2060 2061 scope reqdg = (scope HTTPClientRequest req) { 2062 req.method = verb; 2063 foreach (k, v; hdrs.byKeyValue) 2064 req.headers[k] = v; 2065 2066 if (request_body_filter) { 2067 scope str = createMemoryStream(() @trusted { return cast(ubyte[])body_; } (), false); 2068 request_body_filter(req, str); 2069 } 2070 2071 if (request_filter) request_filter(req); 2072 2073 if (body_ != "") 2074 req.writeBody(cast(const(ubyte)[])body_, hdrs.get("Content-Type", "application/json; charset=UTF-8")); 2075 }; 2076 2077 HTTPClientResponse client_res; 2078 if (http_settings) client_res = requestHTTP(url, reqdg, http_settings); 2079 else client_res = requestHTTP(url, reqdg); 2080 2081 logDebug( 2082 "REST call: %s %s -> %d, %s", 2083 httpMethodString(verb), 2084 url.toString(), 2085 client_res.statusCode, 2086 client_res.statusPhrase 2087 ); 2088 2089 // Get required headers - Don't throw yet 2090 string[] missingKeys; 2091 foreach (k, ref v; reqReturnHdrs.byKeyValue) 2092 if (auto ptr = k in client_res.headers) 2093 v = (*ptr).idup; 2094 else 2095 missingKeys ~= k; 2096 2097 // Get optional headers 2098 foreach (k, ref v; optReturnHdrs.byKeyValue) 2099 if (auto ptr = k in client_res.headers) 2100 v = (*ptr).idup; 2101 else 2102 v = null; 2103 if (missingKeys.length) 2104 throw new Exception( 2105 "REST interface mismatch: Missing required header field(s): " 2106 ~ missingKeys.to!string); 2107 2108 if (!isSuccessCode(cast(HTTPStatus)client_res.statusCode)) 2109 { 2110 import std.base64; 2111 2112 Json msg; 2113 auto ctypeR = client_res.contentType.splitter(";"); 2114 const ctype = ctypeR.empty ? string.init : ctypeR.front.strip; 2115 if (ctype.sicmp("application/json") == 0) 2116 msg = client_res.readJson(); 2117 else if (ctype.sicmp("text/plain") == 0) 2118 msg = Json(["statusMessage": Json(client_res.bodyReader.readAllUTF8())]); 2119 else { 2120 msg = Json(["statusMessage": Json(client_res.statusPhrase)]); 2121 if (client_res.contentType.length) 2122 msg["contentType"] = Json(client_res.contentType); 2123 ubyte[] data = client_res.bodyReader.readAll(); 2124 if (data.length) 2125 msg["data"] = Base64.encode(data); 2126 } 2127 throw new RestException(client_res.statusCode, msg); 2128 } 2129 2130 return client_res; 2131 } 2132 2133 private { 2134 import vibe.data.json; 2135 import std.conv : to; 2136 2137 string toRestString(alias SerPolicyType = DefaultPolicy, T)(T value) 2138 @safe { 2139 import std.array : Appender, appender; 2140 import std.uuid : UUID; 2141 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 2142 else static if (is(T == bool)) return value ? "true" : "false"; 2143 else static if (is(T : int)) return to!string(value); 2144 else static if (is(T : double)) return to!string(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 2145 else static if (is(string : T)) return value; 2146 else static if (__traits(compiles, value.toISOExtString)) return value.toISOExtString; 2147 else static if (__traits(compiles, value.toString)) return value.toString; 2148 else static if (is(T == UUID)) return value.toString(); 2149 else { 2150 auto ret = appender!string; 2151 serializeWithPolicy!(JsonStringSerializer!(Appender!string), SerPolicyType, T)(value, ret); 2152 return ret.data; 2153 } 2154 } 2155 2156 T fromRestString(T, alias SerPolicyType = DefaultPolicy)(string value) 2157 { 2158 import std.conv : ConvException; 2159 import std.uuid : UUID, UUIDParsingException; 2160 import vibe.http.status : HTTPStatus; 2161 try { 2162 static if (isInstanceOf!(Nullable, T)) return T(fromRestString!(typeof(T.init.get()))(value)); 2163 else static if (is(T == bool)) return value == "1" || value.to!T; 2164 else static if (is(T : int)) return to!T(value); 2165 else static if (is(T : double)) return to!T(value); // FIXME: formattedWrite(dst, "%.16g", json.get!double); 2166 else static if (is(string : T)) return value; 2167 else static if (__traits(compiles, T.fromISOExtString("hello"))) return T.fromISOExtString(value); 2168 else static if (__traits(compiles, T.fromString("hello"))) return T.fromString(value); 2169 else static if (is(T == UUID)) return UUID(value); 2170 else return deserializeWithPolicy!(JsonStringSerializer!string, SerPolicyType, T)(value); 2171 } catch (ConvException e) { 2172 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2173 } catch (JSONException e) { 2174 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2175 } catch (UUIDParsingException e) { 2176 throw new HTTPStatusException(HTTPStatus.badRequest, e.msg); 2177 } 2178 } 2179 2180 // Converting from invalid JSON string to aggregate should throw bad request 2181 unittest { 2182 void assertHTTPStatus(E)(lazy E expression, HTTPStatus expectedStatus, 2183 string file = __FILE__, size_t line = __LINE__) 2184 { 2185 import core.exception : AssertError; 2186 import std.format : format; 2187 2188 try 2189 expression(); 2190 catch (HTTPStatusException e) 2191 { 2192 if (e.status != expectedStatus) 2193 throw new AssertError(format("assertHTTPStatus failed: " ~ 2194 "status expected %d but was %d", expectedStatus, e.status), 2195 file, line); 2196 2197 return; 2198 } 2199 2200 throw new AssertError("assertHTTPStatus failed: No " ~ 2201 "'HTTPStatusException' exception was thrown", file, line); 2202 } 2203 2204 struct Foo { int bar; } 2205 assertHTTPStatus(fromRestString!(Foo)("foo"), HTTPStatus.badRequest); 2206 2207 enum Bar { foo, bar } 2208 assert(fromRestString!(Bar)("bar") == Bar.bar); 2209 assertHTTPStatus(fromRestString!(Bar)("foobarbaz"), HTTPStatus.badRequest); 2210 } 2211 } 2212 2213 private string generateModuleImports(I)() 2214 { 2215 if (!__ctfe) 2216 assert (false); 2217 2218 import vibe.internal.meta.codegen : getRequiredImports; 2219 import std.algorithm : map; 2220 import std.array : join; 2221 2222 auto modules = getRequiredImports!I(); 2223 return join(map!(a => "static import " ~ a ~ ";")(modules), "\n"); 2224 } 2225 2226 /*************************************************************************** 2227 2228 The client sends the list of allowed content types in the 'Allow' http header 2229 and the response will contain a 'Content-Type' header. This function will 2230 try to find the best matching @SerializationResult UDA based on the allowed 2231 content types. 2232 2233 Note: 2234 2235 Comment 1: if the request doesn't specify any allowed content types, then * / * 2236 is assumed 2237 2238 Comment 2: if there are no UDA's matching the client's allowed content types, -1 2239 is returned 2240 2241 Comment 3: if there are more than 1 matching UDA, for ONE specific client's allowed 2242 content type(and their priority is the same - see below), 2243 then the one specified earlier in the code gets chosen 2244 2245 Comment 4: accept-params(quality factor) and accept-extensions are ignored 2246 https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 2247 2248 Comment 5: matching the most specific content type without any wildcard has priority 2249 2250 Comment 6: the request's content type can be in the format of 2251 2252 $(UL 2253 $(LI major type / minor type) 2254 $(LI major type / *) 2255 $(LI * / *) 2256 ) 2257 2258 Params: 2259 T = compile time known ResultSerializer classes 2260 req_content_types_str = list of allowed content types for example: 2261 text/*;q=0.3, text/html;q=0.7, text/html;level=1 2262 2263 Returns: 2264 index of the result serializers in the T... AliasSeq if matching found, 2265 -1 otherwise 2266 2267 ***************************************************************************/ 2268 2269 package int get_matching_content_type (T...)(string req_content_types_str) pure @safe 2270 { 2271 if (!req_content_types_str.strip().length) 2272 req_content_types_str = "*/*"; 2273 struct ContentType 2274 { 2275 this (string major_type, string minor_type) 2276 { 2277 this.major_type = major_type; 2278 this.minor_type = minor_type; 2279 this.full_type = major_type ~ "/" ~ minor_type; 2280 this.star_num = this.full_type.count('*'); // serves as priority 2281 } 2282 string major_type; 2283 string minor_type; 2284 string full_type; 2285 ulong star_num; 2286 } 2287 2288 // processing ResultSerializers 2289 alias packed_UDAs = AliasSeq!(T); 2290 ContentType[] UDA_content_types; 2291 foreach (UDA; packed_UDAs) 2292 { 2293 auto ctype = UDA.contentType.splitter(';').front; 2294 auto content_type_split = ctype.toLower().split("/"); 2295 assert(content_type_split.length == 2); 2296 UDA_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2297 } 2298 2299 // processing request content typess 2300 ContentType[] req_content_types; 2301 foreach (content_type; req_content_types_str.toLower().split(",")) 2302 { 2303 immutable semicolon_pos = content_type.indexOf(';'); 2304 if (semicolon_pos != -1) 2305 content_type = content_type[0 .. semicolon_pos]; // quality factor ignored 2306 auto content_type_split = content_type.split("/"); 2307 if (content_type_split.length == 2) 2308 req_content_types ~= ContentType(content_type_split[0].strip(), content_type_split[1].strip()); 2309 } 2310 // sorting content types by matching preference 2311 req_content_types.sort!(( x, y) => (x.star_num < y.star_num)); 2312 2313 int res = -1; 2314 ulong min_star_num = ulong.max; 2315 foreach (UDA_ind, UDA_content_type; UDA_content_types) 2316 foreach (const ref content_type; req_content_types) 2317 if ( 2318 ( 2319 ( 2320 UDA_content_type.major_type == content_type.major_type && 2321 UDA_content_type.minor_type == content_type.minor_type 2322 ) || 2323 ( 2324 UDA_content_type.major_type == content_type.major_type && 2325 content_type.minor_type == "*" 2326 ) || 2327 ( 2328 content_type.major_type == "*" && content_type.minor_type == "*" 2329 ) 2330 ) && 2331 ( 2332 content_type.star_num < min_star_num 2333 ) 2334 ) 2335 { 2336 res = cast(int) UDA_ind; 2337 min_star_num = content_type.star_num; 2338 } 2339 return res; 2340 } 2341 2342 version(unittest) 2343 { 2344 import std.range.interfaces : OutputRange; 2345 import vibe.internal.interfaceproxy : InterfaceProxy; 2346 2347 void s (OutputRange!char, int){}; 2348 int d (InterfaceProxy!(InputStream)){return 1;} 2349 2350 int test1(); 2351 2352 @resultSerializer!(s,d,"text/plain") 2353 @resultSerializer!(s,d," aPPliCatIon / jsOn ") 2354 int test2(); 2355 } 2356 2357 unittest 2358 { 2359 alias res = ResultSerializersT!(test1); 2360 assert(res.length == 1); 2361 assert(res[0].contentType == "application/json; charset=UTF-8"); 2362 2363 assert(get_matching_content_type!(res)("application/json") == 0); 2364 assert(get_matching_content_type!(res)("application/*") == 0); 2365 assert(get_matching_content_type!(res)(" appliCAtIon / *") == 0); 2366 assert(get_matching_content_type!(res)("*/*") == 0); 2367 assert(get_matching_content_type!(res)("") == 0); 2368 assert(get_matching_content_type!(res)("application/blabla") == -1); 2369 2370 alias res2 = ResultSerializersT!(test2); 2371 assert(res2.length == 2); 2372 assert(res2[0].contentType == "text/plain"); 2373 assert(res2[1].contentType == " aPPliCatIon / jsOn "); 2374 2375 assert(get_matching_content_type!(res2)("text/plain, application/json") == 0); 2376 assert(get_matching_content_type!(res2)("text/*, application/json") == 1); 2377 assert(get_matching_content_type!(res2)("*/*, application/json") == 1); 2378 assert(get_matching_content_type!(res2)("*/*") == 0); 2379 assert(get_matching_content_type!(res2)("") == 0); 2380 assert(get_matching_content_type!(res2)("blabla/blabla, blublu/blublu") == -1); 2381 } 2382 2383 version(unittest) 2384 { 2385 private struct Aggregate { } 2386 private interface Interface 2387 { 2388 Aggregate[] foo(); 2389 } 2390 } 2391 2392 unittest 2393 { 2394 enum imports = generateModuleImports!Interface; 2395 static assert (imports == "static import vibe.web.rest;"); 2396 } 2397 2398 // Check that the interface is valid. Every checks on the correctness of the 2399 // interface should be put in checkRestInterface, which allows to have consistent 2400 // errors in the server and client. 2401 package string getInterfaceValidationError(I, bool support_webparam_attributes = true)() 2402 out (result) { assert((result is null) == !result.length); } 2403 do { 2404 import vibe.web.internal.rest.common : ParameterKind, WebParamUDATuple; 2405 import std.algorithm : strip; 2406 2407 // The hack parameter is to kill "Statement is not reachable" warnings. 2408 string validateMethod(alias Func)(bool hack = true) { 2409 import vibe.internal.meta.uda; 2410 import std.string : format; 2411 2412 static assert(is(FunctionTypeOf!Func), "Internal error"); 2413 2414 if (!__ctfe) 2415 assert(false, "Internal error"); 2416 2417 enum FuncId = (fullyQualifiedName!I~ "." ~ __traits(identifier, Func)); 2418 alias PT = ParameterTypeTuple!Func; 2419 static if (!__traits(compiles, ParameterIdentifierTuple!Func)) { 2420 if (hack) return "%s: A parameter has no name.".format(FuncId); 2421 alias PN = AliasSeq!("-DummyInvalid-"); 2422 } else 2423 alias PN = ParameterIdentifierTuple!Func; 2424 alias WPAT = UDATuple!(WebParamAttribute, Func); 2425 2426 // Check if there is no orphan UDATuple (e.g. typo while writing the name of the parameter). 2427 foreach (i, uda; WPAT) { 2428 // Note: static foreach gets unrolled, generating multiple nested sub-scope. 2429 // The spec / DMD doesn't like when you have the same symbol in those, 2430 // leading to wrong codegen / wrong template being reused. 2431 // That's why those templates need different names. 2432 // See DMD bug #9748. 2433 mixin(GenOrphan!(i).Decl); 2434 // template CmpOrphan(string name) { enum CmpOrphan = (uda.identifier == name); } 2435 static if (!anySatisfy!(mixin(GenOrphan!(i).Name), PN)) { 2436 if (hack) return "%s: No parameter '%s' (referenced by attribute @%sParam)" 2437 .format(FuncId, uda.identifier, uda.origin); 2438 } 2439 } 2440 2441 foreach (i, P; PT) { 2442 static if (!PN[i].length) 2443 if (hack) return "%s: Parameter %d has no name." 2444 .format(FuncId, i); 2445 // Check for multiple origins 2446 static if (WPAT.length) { 2447 // It's okay to reuse GenCmp, as the order of params won't change. 2448 // It should/might not be reinstantiated by the compiler. 2449 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2450 alias WPA = Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT); 2451 static if (WPA.length > 1) 2452 if (hack) return "%s: Parameter '%s' has multiple @*Param attributes on it." 2453 .format(FuncId, PN[i]); 2454 } 2455 } 2456 2457 // Check for misplaced out and non-const ref 2458 static if (support_webparam_attributes) { 2459 alias PSC = ParameterStorageClass; 2460 foreach (i, SC; ParameterStorageClassTuple!Func) { 2461 static if (SC & PSC.out_ || (SC & PSC.ref_ && !is(ConstOf!(PT[i]) == PT[i])) ) { 2462 mixin(GenCmp!("Loop", i, PN[i]).Decl); 2463 alias Attr = AliasSeq!( 2464 WebParamUDATuple!(Func, i), 2465 Filter!(mixin(GenCmp!("Loop", i, PN[i]).Name), WPAT), 2466 ); 2467 static if (Attr.length != 1) { 2468 if (hack) return "%s: Parameter '%s' cannot be %s" 2469 .format(FuncId, PN[i], SC & PSC.out_ ? "out" : "ref"); 2470 } else static if (Attr[0].origin != ParameterKind.header && Attr[0].origin != ParameterKind.status) { 2471 if (hack) return "%s: %s parameter '%s' cannot be %s" 2472 .format(FuncId, Attr[0].origin, PN[i], 2473 SC & PSC.out_ ? "out" : "ref"); 2474 } 2475 } 2476 } 2477 } 2478 2479 // Check for @path(":name") 2480 enum pathAttr = findFirstUDA!(PathAttribute, Func); 2481 static if (pathAttr.found) { 2482 static if (!pathAttr.value.length) { 2483 if (hack) 2484 return "%s: Path is null or empty".format(FuncId); 2485 } else { 2486 import std.algorithm : canFind, splitter; 2487 // splitter doesn't work with alias this ? 2488 auto str = pathAttr.value.data; 2489 if (str.canFind("//")) return "%s: Path '%s' contains empty entries.".format(FuncId, pathAttr.value); 2490 str = str.strip('/'); 2491 if (!str.length) return null; 2492 foreach (elem; str.splitter('/')) { 2493 assert(elem.length, "Empty path entry not caught yet!?"); 2494 2495 if (elem[0] == ':') { 2496 // typeof(PN) is void when length is 0. 2497 static if (!PN.length) { 2498 if (hack) 2499 return "%s: Path contains '%s', but no parameter '_%s' defined." 2500 .format(FuncId, elem, elem[1..$]); 2501 } else { 2502 if (![PN].canFind("_"~elem[1..$])) 2503 if (hack) return "%s: Path contains '%s', but no parameter '_%s' defined." 2504 .format(FuncId, elem, elem[1..$]); 2505 elem = elem[1..$]; 2506 } 2507 } 2508 } 2509 // TODO: Check for validity of the subpath. 2510 } 2511 } 2512 return null; 2513 } 2514 2515 if (!__ctfe) 2516 assert(false, "Internal error"); 2517 bool hack = true; 2518 foreach (method; __traits(allMembers, I)) { 2519 // WORKAROUND #1045 / @@BUG14375@@ 2520 static if (method.length != 0) 2521 foreach (overload; MemberFunctionsTuple!(I, method)) { 2522 static if (validateMethod!(overload)()) 2523 if (hack) return validateMethod!(overload)(); 2524 } 2525 } 2526 return null; 2527 } 2528 2529 // Test detection of user typos (e.g., if the attribute is on a parameter that doesn't exist). 2530 unittest { 2531 enum msg = "No parameter 'ath' (referenced by attribute @headerParam)"; 2532 2533 interface ITypo { 2534 @headerParam("ath", "Authorization") // mistyped parameter name 2535 string getResponse(string auth); 2536 } 2537 enum err = getInterfaceValidationError!ITypo; 2538 static assert(err !is null && stripTestIdent(err) == msg, 2539 "Expected validation error for getResponse, got: "~stripTestIdent(err)); 2540 } 2541 2542 // Multiple origin for a parameter 2543 unittest { 2544 enum msg = "Parameter 'arg1' has multiple @*Param attributes on it."; 2545 2546 interface IMultipleOrigin { 2547 @headerParam("arg1", "Authorization") @bodyParam("arg1", "Authorization") 2548 string getResponse(string arg1, int arg2); 2549 } 2550 enum err = getInterfaceValidationError!IMultipleOrigin; 2551 static assert(err !is null && stripTestIdent(err) == msg, err); 2552 } 2553 2554 // Missing parameter name 2555 unittest { 2556 enum msg = "Parameter 0 has no name."; 2557 2558 interface IMissingName1 { 2559 string getResponse(string = "troublemaker"); 2560 } 2561 interface IMissingName2 { 2562 string getResponse(string); 2563 } 2564 enum err1 = getInterfaceValidationError!IMissingName1; 2565 static assert(err1 !is null && stripTestIdent(err1) == msg, err1); 2566 enum err2 = getInterfaceValidationError!IMissingName2; 2567 static assert(err2 !is null && stripTestIdent(err2) == msg, err2); 2568 } 2569 2570 // Issue 949 2571 unittest { 2572 enum msg = "Path contains ':owner', but no parameter '_owner' defined."; 2573 2574 @path("/repos/") 2575 interface IGithubPR { 2576 @path(":owner/:repo/pulls") 2577 string getPullRequests(string owner, string repo); 2578 } 2579 enum err = getInterfaceValidationError!IGithubPR; 2580 static assert(err !is null && stripTestIdent(err) == msg, err); 2581 } 2582 2583 // Issue 1017 2584 unittest { 2585 interface TestSuccess { @path("/") void test(); } 2586 interface TestSuccess2 { @path("/test/") void test(); } 2587 interface TestFail { @path("//") void test(); } 2588 interface TestFail2 { @path("/test//it/") void test(); } 2589 static assert(getInterfaceValidationError!TestSuccess is null); 2590 static assert(getInterfaceValidationError!TestSuccess2 is null); 2591 static assert(stripTestIdent(getInterfaceValidationError!TestFail) 2592 == "Path '//' contains empty entries."); 2593 static assert(stripTestIdent(getInterfaceValidationError!TestFail2) 2594 == "Path '/test//it/' contains empty entries."); 2595 } 2596 2597 unittest { 2598 interface NullPath { @path(null) void test(); } 2599 interface ExplicitlyEmptyPath { @path("") void test(); } 2600 static assert(stripTestIdent(getInterfaceValidationError!NullPath) 2601 == "Path is null or empty"); 2602 static assert(stripTestIdent(getInterfaceValidationError!ExplicitlyEmptyPath) 2603 == "Path is null or empty"); 2604 2605 // Note: Implicitly empty path are valid: 2606 // interface ImplicitlyEmptyPath { void get(); } 2607 } 2608 2609 // Accept @headerParam ref / out parameters 2610 unittest { 2611 interface HeaderRef { 2612 @headerParam("auth", "auth") 2613 string getData(ref string auth); 2614 string getData2(@viaHeader("auth") ref string auth); 2615 } 2616 static assert(getInterfaceValidationError!HeaderRef is null, 2617 stripTestIdent(getInterfaceValidationError!HeaderRef)); 2618 2619 interface HeaderOut { 2620 @headerParam("auth", "auth") 2621 void getData(out string auth); 2622 void getData(@viaHeader("auth") out string auth); 2623 } 2624 static assert(getInterfaceValidationError!HeaderOut is null, 2625 stripTestIdent(getInterfaceValidationError!HeaderOut)); 2626 } 2627 2628 // Reject unattributed / @queryParam or @bodyParam ref / out parameters 2629 unittest { 2630 interface QueryRef { 2631 @queryParam("auth", "auth") 2632 string getData(ref string auth); 2633 } 2634 static assert(stripTestIdent(getInterfaceValidationError!QueryRef) 2635 == "query parameter 'auth' cannot be ref"); 2636 2637 interface QueryRefConst { 2638 @queryParam("auth", "auth") 2639 string getData(const ref string auth); 2640 } 2641 enum err1 = getInterfaceValidationError!QueryRefConst; 2642 static assert(err1 is null, err1); 2643 2644 interface QueryOut { 2645 @queryParam("auth", "auth") 2646 void getData(out string auth); 2647 } 2648 static assert(stripTestIdent(getInterfaceValidationError!QueryOut) 2649 == "query parameter 'auth' cannot be out"); 2650 2651 interface BodyRef { 2652 @bodyParam("auth", "auth") 2653 string getData(ref string auth); 2654 } 2655 static assert(stripTestIdent(getInterfaceValidationError!BodyRef) 2656 == "body_ parameter 'auth' cannot be ref",x); 2657 2658 interface BodyRefConst { 2659 @bodyParam("auth", "auth") 2660 string getData(const ref string auth); 2661 } 2662 enum err2 = getInterfaceValidationError!BodyRefConst; 2663 static assert(err2 is null, err2); 2664 2665 interface BodyOut { 2666 @bodyParam("auth", "auth") 2667 void getData(out string auth); 2668 } 2669 static assert(stripTestIdent(getInterfaceValidationError!BodyOut) 2670 == "body_ parameter 'auth' cannot be out"); 2671 2672 // There's also the possibility of someone using an out unnamed 2673 // parameter (don't ask me why), but this is catched as unnamed 2674 // parameter, so we don't need to check it here. 2675 } 2676 2677 private string stripTestIdent(string msg) 2678 @safe { 2679 import std.string; 2680 auto idx = msg.indexOf(": "); 2681 return idx >= 0 ? msg[idx+2 .. $] : msg; 2682 } 2683 2684 // Small helper for client code generation 2685 private string paramCTMap(string[string] params) 2686 @safe { 2687 import std.array : appender, join; 2688 if (!__ctfe) 2689 assert (false, "This helper is only supposed to be called for codegen in RestClientInterface."); 2690 auto app = appender!(string[]); 2691 foreach (key, val; params) { 2692 app ~= "\""~key~"\""; 2693 app ~= val; 2694 } 2695 return app.data.join(", "); 2696 } 2697 2698 package string stripTUnderscore(string name, RestInterfaceSettings settings) 2699 @safe { 2700 if ((settings is null || settings.stripTrailingUnderscore) 2701 && name.endsWith("_")) 2702 return name[0 .. $-1]; 2703 else return name; 2704 } 2705 2706 // Workarounds @@DMD:9748@@, and maybe more 2707 package template GenCmp(string name, int id, string cmpTo) { 2708 import std.string : format; 2709 import std.conv : to; 2710 enum Decl = q{ 2711 template %1$s(alias uda) { 2712 enum %1$s = (uda.identifier == "%2$s"); 2713 } 2714 }.format(Name, cmpTo); 2715 enum Name = name~to!string(id); 2716 } 2717 2718 // Ditto 2719 private template GenOrphan(int id) { 2720 import std.string : format; 2721 import std.conv : to; 2722 enum Decl = q{ 2723 template %1$s(string name) { 2724 enum %1$s = (uda.identifier == name); 2725 } 2726 }.format(Name); 2727 enum Name = "OrphanCheck"~to!string(id); 2728 } 2729 2730 // Workaround for issue #1045 / DMD bug 14375 2731 // Also, an example of policy-based design using this module. 2732 @safe unittest { 2733 import std.traits, std.typetuple; 2734 import vibe.internal.meta.codegen; 2735 import vibe.internal.meta.typetuple; 2736 import vibe.web.internal.rest.common : ParameterKind; 2737 2738 interface Policies { 2739 @headerParam("auth", "Authorization") 2740 string BasicAuth(string auth, ulong expiry) @safe; 2741 } 2742 2743 @path("/keys/") 2744 interface IKeys(alias AuthenticationPolicy = Policies.BasicAuth) { 2745 static assert(is(FunctionTypeOf!AuthenticationPolicy == function), 2746 "Policies needs to be functions"); 2747 @path("/") @method(HTTPMethod.POST) @safe 2748 mixin CloneFunctionDecl!(AuthenticationPolicy, true, "create"); 2749 } 2750 2751 class KeysImpl : IKeys!() { 2752 override: 2753 string create(string auth, ulong expiry) @safe { 2754 return "4242-4242"; 2755 } 2756 } 2757 2758 // Some sanity checks 2759 // Note: order is most likely implementation dependent. 2760 // Good thing we only have one frontend... 2761 alias WPA = WebParamAttribute; 2762 static assert(Compare!( 2763 Group!(__traits(getAttributes, IKeys!().create)), 2764 Group!(PathAttribute("/"), 2765 MethodAttribute(HTTPMethod.POST), 2766 WPA(ParameterKind.header, "auth", "Authorization")))); 2767 2768 void register() @safe { 2769 auto router = new URLRouter(); 2770 router.registerRestInterface(new KeysImpl()); 2771 } 2772 2773 void query() @safe { 2774 auto client = new RestInterfaceClient!(IKeys!())("http://127.0.0.1:8080"); 2775 assert(client.create("Hello", 0) == "4242-4242"); 2776 } 2777 } 2778 2779 @safe unittest { // @noRoute support in RestInterfaceClient 2780 interface I { 2781 void foo(); 2782 @noRoute int bar(void* someparam); 2783 } 2784 auto cli = new RestInterfaceClient!I("http://127.0.0.1/"); 2785 }