1 /** 2 Contains common functionality for the REST and WEB interface generators. 3 4 Copyright: © 2012-2017 Sönke Ludwig 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Михаил Страшун 7 */ 8 module vibe.web.common; 9 10 import vibe.http.common; 11 import vibe.http.server : HTTPServerRequest; 12 import vibe.data.json; 13 import vibe.internal.meta.uda : onlyAsUda, UDATuple; 14 15 import std.meta : AliasSeq; 16 static import std.utf; 17 static import std.string; 18 import std.traits : getUDAs, ReturnType; 19 import std.typecons : Nullable; 20 21 22 /** 23 Adjusts the naming convention for a given function name to the specified style. 24 25 The input name is assumed to be in lowerCamelCase (D-style) or PascalCase. Acronyms 26 (e.g. "HTML") should be written all caps 27 */ 28 string adjustMethodStyle(string name, MethodStyle style) 29 @safe { 30 if (!name.length) { 31 return ""; 32 } 33 34 import std.uni; 35 36 string separate(char separator, bool upper_case) 37 { 38 string ret; 39 size_t start = 0, i = 0; 40 while (i < name.length) { 41 // skip acronyms 42 while (i < name.length && (i+1 >= name.length || (name[i+1] >= 'A' && name[i+1] <= 'Z'))) { 43 std.utf.decode(name, i); 44 } 45 46 // skip the main (lowercase) part of a word 47 while (i < name.length && !(name[i] >= 'A' && name[i] <= 'Z')) { 48 std.utf.decode(name, i); 49 } 50 51 // add a single word 52 if( ret.length > 0 ) { 53 ret ~= separator; 54 } 55 ret ~= name[start .. i]; 56 57 // quick skip the capital and remember the start of the next word 58 start = i; 59 if (i < name.length) { 60 std.utf.decode(name, i); 61 } 62 } 63 if (start < name.length) { 64 ret ~= separator ~ name[start .. $]; 65 } 66 return upper_case ? std..string.toUpper(ret) : std..string.toLower(ret); 67 } 68 69 final switch(style) { 70 case MethodStyle.unaltered: 71 return name; 72 case MethodStyle.camelCase: 73 size_t i = 0; 74 foreach (idx, dchar ch; name) { 75 if (isUpper(ch)) { 76 i = idx; 77 } 78 else break; 79 } 80 if (i == 0) { 81 std.utf.decode(name, i); 82 return std..string.toLower(name[0 .. i]) ~ name[i .. $]; 83 } else { 84 std.utf.decode(name, i); 85 if (i < name.length) { 86 return std..string.toLower(name[0 .. i-1]) ~ name[i-1 .. $]; 87 } 88 else { 89 return std..string.toLower(name); 90 } 91 } 92 case MethodStyle.pascalCase: 93 size_t idx = 0; 94 std.utf.decode(name, idx); 95 return std..string.toUpper(name[0 .. idx]) ~ name[idx .. $]; 96 case MethodStyle.lowerCase: 97 return std..string.toLower(name); 98 case MethodStyle.upperCase: 99 return std..string.toUpper(name); 100 case MethodStyle.lowerUnderscored: return separate('_', false); 101 case MethodStyle.upperUnderscored: return separate('_', true); 102 case MethodStyle.lowerDashed: return separate('-', false); 103 case MethodStyle.upperDashed: return separate('-', true); 104 } 105 } 106 107 unittest 108 { 109 assert(adjustMethodStyle("methodNameTest", MethodStyle.unaltered) == "methodNameTest"); 110 assert(adjustMethodStyle("methodNameTest", MethodStyle.camelCase) == "methodNameTest"); 111 assert(adjustMethodStyle("methodNameTest", MethodStyle.pascalCase) == "MethodNameTest"); 112 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerCase) == "methodnametest"); 113 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperCase) == "METHODNAMETEST"); 114 assert(adjustMethodStyle("methodNameTest", MethodStyle.lowerUnderscored) == "method_name_test"); 115 assert(adjustMethodStyle("methodNameTest", MethodStyle.upperUnderscored) == "METHOD_NAME_TEST"); 116 assert(adjustMethodStyle("MethodNameTest", MethodStyle.unaltered) == "MethodNameTest"); 117 assert(adjustMethodStyle("MethodNameTest", MethodStyle.camelCase) == "methodNameTest"); 118 assert(adjustMethodStyle("MethodNameTest", MethodStyle.pascalCase) == "MethodNameTest"); 119 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerCase) == "methodnametest"); 120 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperCase) == "METHODNAMETEST"); 121 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerUnderscored) == "method_name_test"); 122 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperUnderscored) == "METHOD_NAME_TEST"); 123 assert(adjustMethodStyle("MethodNameTest", MethodStyle.lowerDashed) == "method-name-test"); 124 assert(adjustMethodStyle("MethodNameTest", MethodStyle.upperDashed) == "METHOD-NAME-TEST"); 125 assert(adjustMethodStyle("Q", MethodStyle.lowerUnderscored) == "q"); 126 assert(adjustMethodStyle("getHTML", MethodStyle.lowerUnderscored) == "get_html"); 127 assert(adjustMethodStyle("getHTMLEntity", MethodStyle.lowerUnderscored) == "get_html_entity"); 128 assert(adjustMethodStyle("ID", MethodStyle.lowerUnderscored) == "id"); 129 assert(adjustMethodStyle("ID", MethodStyle.pascalCase) == "ID"); 130 assert(adjustMethodStyle("ID", MethodStyle.camelCase) == "id"); 131 assert(adjustMethodStyle("IDTest", MethodStyle.lowerUnderscored) == "id_test"); 132 assert(adjustMethodStyle("IDTest", MethodStyle.pascalCase) == "IDTest"); 133 assert(adjustMethodStyle("IDTest", MethodStyle.camelCase) == "idTest"); 134 assert(adjustMethodStyle("anyA", MethodStyle.lowerUnderscored) == "any_a", adjustMethodStyle("anyA", MethodStyle.lowerUnderscored)); 135 } 136 137 138 /** 139 Determines the HTTP method and path for a given function symbol. 140 141 The final method and path are determined from the function name, as well as 142 any $(D @method) and $(D @path) attributes that may be applied to it. 143 144 This function is designed for CTFE usage and will assert at run time. 145 146 Returns: 147 A tuple of three elements is returned: 148 $(UL 149 $(LI flag "was UDA used to override path") 150 $(LI $(D HTTPMethod) extracted) 151 $(LI URL path extracted) 152 ) 153 */ 154 auto extractHTTPMethodAndName(alias Func, bool indexSpecialCase)() 155 { 156 if (!__ctfe) 157 assert(false); 158 159 struct HandlerMeta 160 { 161 bool hadPathUDA; 162 HTTPMethod method; 163 string url; 164 } 165 166 import vibe.internal.meta.uda : findFirstUDA; 167 import vibe.internal.meta.traits : isPropertySetter, 168 isPropertyGetter; 169 import std.algorithm : startsWith; 170 import std.typecons : Nullable; 171 172 immutable httpMethodPrefixes = [ 173 HTTPMethod.GET : [ "get", "query" ], 174 HTTPMethod.PUT : [ "put", "set" ], 175 HTTPMethod.PATCH : [ "update", "patch" ], 176 HTTPMethod.POST : [ "add", "create", "post" ], 177 HTTPMethod.DELETE : [ "remove", "erase", "delete" ], 178 ]; 179 180 enum name = __traits(identifier, Func); 181 alias T = typeof(&Func); 182 183 Nullable!HTTPMethod udmethod; 184 Nullable!string udurl; 185 186 // Cases may conflict and are listed in order of priority 187 188 // Workaround for Nullable incompetence 189 enum uda1 = findFirstUDA!(MethodAttribute, Func); 190 enum uda2 = findFirstUDA!(PathAttribute, Func); 191 192 static if (uda1.found) { 193 udmethod = uda1.value; 194 } 195 static if (uda2.found) { 196 udurl = uda2.value; 197 } 198 199 // Everything is overriden, no further analysis needed 200 if (!udmethod.isNull() && !udurl.isNull()) { 201 return HandlerMeta(true, udmethod.get(), udurl.get()); 202 } 203 204 // Anti-copy-paste delegate 205 typeof(return) udaOverride( HTTPMethod method, string url ){ 206 return HandlerMeta( 207 !udurl.isNull(), 208 udmethod.isNull() ? method : udmethod.get(), 209 udurl.isNull() ? url : udurl.get() 210 ); 211 } 212 213 if (isPropertyGetter!T) { 214 return udaOverride(HTTPMethod.GET, name); 215 } 216 else if(isPropertySetter!T) { 217 return udaOverride(HTTPMethod.PUT, name); 218 } 219 else { 220 foreach (method, prefixes; httpMethodPrefixes) { 221 foreach (prefix; prefixes) { 222 import std.uni : isLower; 223 if (name.startsWith(prefix) && (name.length == prefix.length || !name[prefix.length].isLower)) { 224 string tmp = name[prefix.length..$]; 225 return udaOverride(method, tmp.length ? tmp : "/"); 226 } 227 } 228 } 229 230 static if (indexSpecialCase && name == "index") { 231 return udaOverride(HTTPMethod.GET, "/"); 232 } else 233 return udaOverride(HTTPMethod.POST, name); 234 } 235 } 236 237 unittest 238 { 239 interface Sample 240 { 241 string getInfo(); 242 string updateDescription(); 243 244 @method(HTTPMethod.DELETE) 245 string putInfo(); 246 247 @path("matters") 248 string getMattersnot(); 249 250 @path("compound/path") @method(HTTPMethod.POST) 251 string mattersnot(); 252 253 string get(); 254 255 string posts(); 256 257 string patches(); 258 } 259 260 enum ret1 = extractHTTPMethodAndName!(Sample.getInfo, false,); 261 static assert (ret1.hadPathUDA == false); 262 static assert (ret1.method == HTTPMethod.GET); 263 static assert (ret1.url == "Info"); 264 enum ret2 = extractHTTPMethodAndName!(Sample.updateDescription, false); 265 static assert (ret2.hadPathUDA == false); 266 static assert (ret2.method == HTTPMethod.PATCH); 267 static assert (ret2.url == "Description"); 268 enum ret3 = extractHTTPMethodAndName!(Sample.putInfo, false); 269 static assert (ret3.hadPathUDA == false); 270 static assert (ret3.method == HTTPMethod.DELETE); 271 static assert (ret3.url == "Info"); 272 enum ret4 = extractHTTPMethodAndName!(Sample.getMattersnot, false); 273 static assert (ret4.hadPathUDA == true); 274 static assert (ret4.method == HTTPMethod.GET); 275 static assert (ret4.url == "matters"); 276 enum ret5 = extractHTTPMethodAndName!(Sample.mattersnot, false); 277 static assert (ret5.hadPathUDA == true); 278 static assert (ret5.method == HTTPMethod.POST); 279 static assert (ret5.url == "compound/path"); 280 enum ret6 = extractHTTPMethodAndName!(Sample.get, false); 281 static assert (ret6.hadPathUDA == false); 282 static assert (ret6.method == HTTPMethod.GET); 283 static assert (ret6.url == "/"); 284 enum ret7 = extractHTTPMethodAndName!(Sample.posts, false); 285 static assert(ret7.hadPathUDA == false); 286 static assert(ret7.method == HTTPMethod.POST); 287 static assert(ret7.url == "posts"); 288 enum ret8 = extractHTTPMethodAndName!(Sample.patches, false); 289 static assert(ret8.hadPathUDA == false); 290 static assert(ret8.method == HTTPMethod.POST); 291 static assert(ret8.url == "patches"); 292 } 293 294 295 /** 296 Attribute to define the content type for methods. 297 298 This currently applies only to methods returning an $(D InputStream) or 299 $(D ubyte[]). 300 */ 301 ContentTypeAttribute contentType(string data) 302 @safe { 303 if (!__ctfe) 304 assert(false, onlyAsUda!__FUNCTION__); 305 return ContentTypeAttribute(data); 306 } 307 308 309 /** 310 Attribute to force a specific HTTP method for an interface method. 311 312 The usual URL generation rules are still applied, so if there 313 are any "get", "query" or similar prefixes, they are filtered out. 314 */ 315 MethodAttribute method(HTTPMethod data) 316 @safe { 317 if (!__ctfe) 318 assert(false, onlyAsUda!__FUNCTION__); 319 return MethodAttribute(data); 320 } 321 322 /// 323 unittest { 324 interface IAPI 325 { 326 // Will be "POST /info" instead of default "GET /info" 327 @method(HTTPMethod.POST) string getInfo(); 328 } 329 } 330 331 332 /** 333 Attibute to force a specific URL path. 334 335 This attribute can be applied either to an interface itself, in which 336 case it defines the root path for all methods within it, 337 or on any function, in which case it defines the relative path 338 of this method. 339 Path are always relative, even path on interfaces, as you can 340 see in the example below. 341 342 See_Also: $(D rootPathFromName) for automatic name generation. 343 */ 344 PathAttribute path(string data) 345 @safe { 346 if (!__ctfe) 347 assert(false, onlyAsUda!__FUNCTION__); 348 return PathAttribute(data); 349 } 350 351 /// 352 @safe unittest { 353 @path("/foo") 354 interface IAPI 355 { 356 @path("info2") string getInfo() @safe; 357 } 358 359 class API : IAPI { 360 string getInfo() @safe { return "Hello, World!"; } 361 } 362 363 void test() 364 @safe { 365 import vibe.http.router; 366 import vibe.web.rest; 367 368 auto router = new URLRouter; 369 370 // Tie IAPI.getInfo to "GET /root/foo/info2" 371 router.registerRestInterface!IAPI(new API(), "/root/"); 372 373 // Or just to "GET /foo/info2" 374 router.registerRestInterface!IAPI(new API()); 375 376 // ... 377 } 378 } 379 380 381 /// Convenience alias to generate a name from the interface's name. 382 @property PathAttribute rootPathFromName() 383 @safe { 384 if (!__ctfe) 385 assert(false, onlyAsUda!__FUNCTION__); 386 return PathAttribute(""); 387 } 388 /// 389 @safe unittest 390 { 391 import vibe.http.router; 392 import vibe.web.rest; 393 394 @rootPathFromName 395 interface IAPI 396 { 397 int getFoo() @safe; 398 } 399 400 class API : IAPI 401 { 402 int getFoo() 403 { 404 return 42; 405 } 406 } 407 408 auto router = new URLRouter(); 409 registerRestInterface(router, new API()); 410 auto routes= router.getAllRoutes(); 411 412 assert(routes[0].pattern == "/iapi/foo" && routes[0].method == HTTPMethod.GET); 413 } 414 415 416 /** 417 Methods marked with this attribute will not be treated as web endpoints. 418 419 This attribute enables the definition of public methods that do not take 420 part in the interface genration process. 421 */ 422 @property NoRouteAttribute noRoute() 423 { 424 import vibe.web.common : onlyAsUda; 425 if (!__ctfe) 426 assert(false, onlyAsUda!__FUNCTION__); 427 return NoRouteAttribute.init; 428 } 429 430 /// 431 unittest { 432 interface IAPI { 433 // Accessible as "GET /info" 434 string getInfo(); 435 436 // Not accessible over HTTP 437 @noRoute 438 int getFoo(); 439 } 440 } 441 442 443 /** 444 Respresents a Rest error response 445 */ 446 class RestException : HTTPStatusException { 447 private { 448 Json m_jsonResult; 449 } 450 451 /// 452 this (int status, string result, string file = __FILE__, int line = __LINE__, 453 Throwable next = null) @safe 454 { 455 Json jsonResult = Json.emptyObject; 456 jsonResult["statusMessage"] = result; 457 this(status, jsonResult, file, line); 458 } 459 460 /// 461 this (int status, Json jsonResult, string file = __FILE__, int line = __LINE__, 462 Throwable next = null) @safe 463 { 464 if (jsonResult.type == Json.Type.object && jsonResult["statusMessage"].type == Json.Type..string) { 465 super(status, jsonResult["statusMessage"].get!string, file, line, next); 466 } 467 else { 468 super(status, httpStatusText(status) ~ " (" ~ jsonResult.toString() ~ ")", file, line, next); 469 } 470 471 m_jsonResult = jsonResult; 472 } 473 474 /// The result text reported to the client 475 @property inout(Json) jsonResult () inout nothrow pure @safe @nogc 476 { 477 return m_jsonResult; 478 } 479 } 480 481 /// private 482 package struct ContentTypeAttribute 483 { 484 string data; 485 alias data this; 486 } 487 488 /// private 489 package struct MethodAttribute 490 { 491 HTTPMethod data; 492 alias data this; 493 } 494 495 496 /** UDA for using a custom serializer for the method return value. 497 498 Instead of using the default serializer (JSON), this allows to define 499 custom serializers. Multiple serializers can be specified and will be 500 matched against the `Accept` header of the HTTP request. 501 502 Params: 503 serialize = An alias to a generic function taking an output range as 504 its first argument and the value to be serialized as its second 505 argument. The result of the serialization is written byte-wise into 506 the output range. 507 deserialize = An alias to a generic function taking a forward range 508 as its first argument and a reference to the value that is to be 509 deserialized. 510 content_type = The MIME type of the serialized representation. 511 */ 512 alias resultSerializer(alias serialize, alias deserialize, string content_type) 513 = ResultSerializer!(serialize, deserialize, content_type); 514 515 /// 516 unittest { 517 import std.bitmanip : bigEndianToNative, nativeToBigEndian; 518 519 interface MyRestInterface { 520 static struct Point { 521 int x, y; 522 } 523 524 static void serialize(R, T)(ref R output_range, const ref T value) 525 { 526 static assert(is(T == Point)); // only Point supported in this example 527 output_range.put(nativeToBigEndian(value.x)); 528 output_range.put(nativeToBigEndian(value.y)); 529 } 530 531 static T deserialize(T, R)(R input_range) 532 { 533 static assert(is(T == Point)); // only Point supported in this example 534 T ret; 535 ubyte[4] xbuf, ybuf; 536 input_range.takeExactly(4).copy(xbuf[]); 537 input_range.takeExactly(4).copy(ybuf[]); 538 ret.x = bigEndianToNative!int(xbuf); 539 ret.y = bigEndianToNative!int(ybuf); 540 return ret; 541 } 542 543 // serialize as binary data in network byte order 544 @resultSerializer!(serialize, deserialize, "application/binary") 545 Point getPoint(); 546 } 547 } 548 549 /// private 550 struct ResultSerializer(alias ST, alias DT, string ContentType) { 551 enum contentType = ContentType; 552 alias serialize = ST; 553 alias deserialize = DT; 554 } 555 556 557 package void defaultSerialize (alias P, T, RT) (ref RT output_range, const scope ref T value) 558 { 559 static struct R { 560 typeof(output_range) underlying; 561 void put(char ch) { underlying.put(ch); } 562 void put(const(char)[] ch) { underlying.put(cast(const(ubyte)[])ch); } 563 } 564 auto dst = R(output_range); 565 value.serializeWithPolicy!(JsonStringSerializer!R, P) (dst); 566 } 567 568 package T defaultDeserialize (alias P, T, R) (R input_range) 569 { 570 return deserializeWithPolicy!(JsonStringSerializer!(typeof(std..string.assumeUTF(input_range))), P, T) 571 (std..string.assumeUTF(input_range)); 572 } 573 574 package alias DefaultSerializerT = ResultSerializer!( 575 defaultSerialize, defaultDeserialize, "application/json; charset=UTF-8"); 576 577 578 /// Convenience template to get all the ResultSerializers for a function 579 package template ResultSerializersT(alias func) { 580 alias DefinedSerializers = getUDAs!(func, ResultSerializer); 581 static if (DefinedSerializers.length) 582 alias ResultSerializersT = DefinedSerializers; 583 else 584 alias ResultSerializersT = AliasSeq!(DefaultSerializerT); 585 } 586 587 /// 588 package template SerPolicyT (Iface) 589 { 590 static if (getUDAs!(Iface, SerPolicy).length) 591 { 592 alias SerPolicyT = getUDAs!(Iface, SerPolicy)[0]; 593 } 594 else 595 { 596 alias SerPolicyT = SerPolicy!DefaultPolicy; 597 } 598 } 599 600 /// 601 package struct SerPolicy (alias PolicyTemplatePar) 602 { 603 alias PolicyTemplate = PolicyTemplatePar; 604 } 605 606 /// 607 public alias serializationPolicy (Args...) = SerPolicy!(Args); 608 609 unittest 610 { 611 import vibe.data.serialization : Base64ArrayPolicy; 612 import std.array : appender; 613 import std.conv : to; 614 615 struct X 616 { 617 string name = "test"; 618 ubyte[] arr = [138, 245, 231, 234, 142, 132, 142]; 619 } 620 X x; 621 622 // Interface using Base64 array serialization 623 @serializationPolicy!(Base64ArrayPolicy) 624 interface ITestBase64 625 { 626 @safe X getTest(); 627 } 628 629 alias serPolicyFound = SerPolicyT!ITestBase64; 630 alias resultSerializerFound = ResultSerializersT!(ITestBase64.getTest)[0]; 631 632 // serialization test with base64 encoding 633 auto output = appender!string(); 634 635 resultSerializerFound.serialize!(serPolicyFound.PolicyTemplate)(output, x); 636 auto serialized = output.data; 637 assert(serialized == `{"name":"test","arr":"ivXn6o6Ejg=="}`, 638 "serialization is not correct, produced: " ~ serialized); 639 640 // deserialization test with base64 encoding 641 auto deserialized = serialized.deserializeWithPolicy!(JsonStringSerializer!string, serPolicyFound.PolicyTemplate, X)(); 642 assert(deserialized.name == "test", "deserialization of `name` is not correct, produced: " ~ deserialized.name); 643 assert(deserialized.arr == [138, 245, 231, 234, 142, 132, 142], 644 "deserialization of `arr` is not correct, produced: " ~ to!string(deserialized.arr)); 645 646 // Interface NOT using Base64 array serialization 647 interface ITestPlain 648 { 649 @safe X getTest(); 650 } 651 652 alias plainSerPolicyFound = SerPolicyT!ITestPlain; 653 alias plainResultSerializerFound = ResultSerializersT!(ITestPlain.getTest)[0]; 654 655 // serialization test without base64 encoding 656 output = appender!string(); 657 plainResultSerializerFound.serialize!(plainSerPolicyFound.PolicyTemplate)(output, x); 658 serialized = output.data; 659 assert(serialized == `{"name":"test","arr":[138,245,231,234,142,132,142]}`, 660 "serialization is not correct, produced: " ~ serialized); 661 662 // deserialization test without base64 encoding 663 deserialized = serialized.deserializeWithPolicy!(JsonStringSerializer!string, plainSerPolicyFound.PolicyTemplate, X)(); 664 assert(deserialized.name == "test", "deserialization of `name` is not correct, produced: " ~ deserialized.name); 665 assert(deserialized.arr == [138, 245, 231, 234, 142, 132, 142], 666 "deserialization of `arr` is not correct, produced: " ~ to!string(deserialized.arr)); 667 } 668 669 /** 670 * This struct contains the name of a route specified by the `path` function. 671 */ 672 struct PathAttribute 673 { 674 /// The specified path 675 string data; 676 alias data this; 677 } 678 679 /// private 680 package struct NoRouteAttribute {} 681 682 /** 683 * This struct contains a mapping between the name used by HTTP (field) 684 * and the parameter (identifier) name of the function. 685 */ 686 public struct WebParamAttribute { 687 import vibe.web.internal.rest.common : ParameterKind; 688 689 /// The type of the WebParamAttribute 690 ParameterKind origin; 691 /// Parameter name (function parameter name). 692 string identifier; 693 /// The meaning of this field depends on the origin. (HTTP request name) 694 string field; 695 } 696 697 698 /** 699 * Declare that a parameter will be transmitted to the API through the body. 700 * 701 * It will be serialized as part of a JSON object. 702 * The serialization format is currently not customizable. 703 * If no fieldname is given, the entire body is serialized into the object. 704 * 705 * There are currently two kinds of symbol to do this: `viaBody` and `bodyParam`. 706 * `viaBody` should be applied to the parameter itself, while `bodyParam` 707 * is applied to the function. 708 * `bodyParam` was introduced long before the D language for UDAs on parameters 709 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 710 * 711 * Params: 712 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 713 * field = The name of the field in the JSON object. 714 * 715 * ---- 716 * void ship(@viaBody("package") int pack); 717 * // The server will receive the following body for a call to ship(42): 718 * // { "package": 42 } 719 * ---- 720 */ 721 WebParamAttribute viaBody(string field = null) 722 @safe { 723 import vibe.web.internal.rest.common : ParameterKind; 724 if (!__ctfe) 725 assert(false, onlyAsUda!__FUNCTION__); 726 return WebParamAttribute(ParameterKind.body_, null, field); 727 } 728 729 /// Ditto 730 WebParamAttribute bodyParam(string identifier, string field) @safe 731 in { 732 assert(field.length > 0, "fieldname can't be empty."); 733 } 734 do 735 { 736 import vibe.web.internal.rest.common : ParameterKind; 737 if (!__ctfe) 738 assert(false, onlyAsUda!__FUNCTION__); 739 return WebParamAttribute(ParameterKind.body_, identifier, field); 740 } 741 742 /// ditto 743 WebParamAttribute bodyParam(string identifier) 744 @safe { 745 import vibe.web.internal.rest.common : ParameterKind; 746 if (!__ctfe) 747 assert(false, onlyAsUda!__FUNCTION__); 748 return WebParamAttribute(ParameterKind.body_, identifier, ""); 749 } 750 751 /** 752 * Declare that a parameter will be transmitted to the API through the headers. 753 * 754 * If the parameter is a string, or any scalar type (float, int, char[], ...), it will be send as a string. 755 * If it's an aggregate, it will be serialized as JSON. 756 * However, passing aggregate via header isn't a good practice and should be avoided for new production code. 757 * 758 * There are currently two kinds of symbol to do this: `viaHeader` and `headerParam`. 759 * `viaHeader` should be applied to the parameter itself, while `headerParam` 760 * is applied to the function. 761 * `headerParam` was introduced long before the D language for UDAs on parameters 762 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 763 * 764 * Params: 765 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 766 * field = The name of the header field to use (e.g: 'Accept', 'Content-Type'...). 767 * 768 * ---- 769 * // The server will receive the content of the "Authorization" header. 770 * void login(@viaHeader("Authorization") string auth); 771 * ---- 772 */ 773 WebParamAttribute viaHeader(string field) 774 @safe { 775 import vibe.web.internal.rest.common : ParameterKind; 776 if (!__ctfe) 777 assert(false, onlyAsUda!__FUNCTION__); 778 return WebParamAttribute(ParameterKind.header, null, field); 779 } 780 781 /// Ditto 782 WebParamAttribute headerParam(string identifier, string field) 783 @safe { 784 import vibe.web.internal.rest.common : ParameterKind; 785 if (!__ctfe) 786 assert(false, onlyAsUda!__FUNCTION__); 787 return WebParamAttribute(ParameterKind.header, identifier, field); 788 } 789 790 /** 791 * Declare that a parameter will be transmitted to the API through the query string. 792 * 793 * It will be serialized as part of a JSON object, and will go through URL serialization. 794 * The serialization format is not customizable. 795 * 796 * There are currently two kinds of symbol to do this: `viaQuery` and `queryParam`. 797 * `viaQuery` should be applied to the parameter itself, while `queryParam` 798 * is applied to the function. 799 * `queryParam` was introduced long before the D language for UDAs on parameters 800 * (introduced in DMD v2.082.0), and will be deprecated in a future release. 801 * 802 * Params: 803 * identifier = The name of the parameter to customize. A compiler error will be issued on mismatch. 804 * field = The field name to use. 805 * 806 * ---- 807 * // For a call to postData("D is awesome"), the server will receive the query: 808 * // POST /data?test=%22D is awesome%22 809 * void postData(@viaQuery("test") string data); 810 * ---- 811 */ 812 WebParamAttribute viaQuery(string field) 813 @safe { 814 import vibe.web.internal.rest.common : ParameterKind; 815 if (!__ctfe) 816 assert(false, onlyAsUda!__FUNCTION__); 817 return WebParamAttribute(ParameterKind.query, null, field); 818 } 819 820 /// Ditto 821 WebParamAttribute queryParam(string identifier, string field) 822 @safe { 823 import vibe.web.internal.rest.common : ParameterKind; 824 if (!__ctfe) 825 assert(false, onlyAsUda!__FUNCTION__); 826 return WebParamAttribute(ParameterKind.query, identifier, field); 827 } 828 829 /** 830 Determines the naming convention of an identifier. 831 */ 832 enum MethodStyle 833 { 834 /// Special value for free-style conventions 835 unaltered, 836 /// camelCaseNaming 837 camelCase, 838 /// PascalCaseNaming 839 pascalCase, 840 /// lowercasenaming 841 lowerCase, 842 /// UPPERCASENAMING 843 upperCase, 844 /// lower_case_naming 845 lowerUnderscored, 846 /// UPPER_CASE_NAMING 847 upperUnderscored, 848 /// lower-case-naming 849 lowerDashed, 850 /// UPPER-CASE-NAMING 851 upperDashed, 852 } 853 854 855 /// Speficies how D fields are mapped to form field names 856 enum NestedNameStyle { 857 underscore, /// Use underscores to separate fields and array indices 858 d /// Use native D style and separate fields by dots and put array indices into brackets 859 } 860 861 862 // concatenates two URL parts avoiding any duplicate slashes 863 // in resulting URL. `trailing` defines of result URL must 864 // end with slash 865 package string concatURL(string prefix, string url, bool trailing = false) 866 @safe { 867 import std.algorithm : startsWith, endsWith; 868 869 auto pre = prefix.endsWith("/"); 870 auto post = url.startsWith("/"); 871 872 if (!url.length) return trailing && !pre ? prefix ~ "/" : prefix; 873 874 auto suffix = trailing && !url.endsWith("/") ? "/" : null; 875 876 if (pre) { 877 // "/" is ASCII, so can just slice 878 if (post) return prefix ~ url[1 .. $] ~ suffix; 879 else return prefix ~ url ~ suffix; 880 } else { 881 if (post) return prefix ~ url ~ suffix; 882 else return prefix ~ "/" ~ url ~ suffix; 883 } 884 } 885 886 @safe unittest { 887 assert(concatURL("/test/", "/it/", false) == "/test/it/"); 888 assert(concatURL("/test", "it/", false) == "/test/it/"); 889 assert(concatURL("/test", "it", false) == "/test/it"); 890 assert(concatURL("/test", "", false) == "/test"); 891 assert(concatURL("/test/", "", false) == "/test/"); 892 assert(concatURL("/test/", "/it/", true) == "/test/it/"); 893 assert(concatURL("/test", "it/", true) == "/test/it/"); 894 assert(concatURL("/test", "it", true) == "/test/it/"); 895 assert(concatURL("/test", "", true) == "/test/"); 896 assert(concatURL("/test/", "", true) == "/test/"); 897 } 898 899 900 /// private 901 template isNullable(T) { 902 import std.traits; 903 enum isNullable = isInstanceOf!(Nullable, T); 904 } 905 906 static assert(isNullable!(Nullable!int)); 907 908 package struct ParamError { 909 string field; 910 string text; 911 } 912 913 package enum ParamResult { 914 ok, 915 skipped, 916 error 917 } 918 919 // maximum array index in the parameter fields. 920 private enum MAX_ARR_INDEX = 0xffff; 921 922 // handle the actual data inside the parameter 923 private ParamResult processFormParam(T)(scope string data, string fieldname, ref T dst, ref ParamError err) 924 { 925 static if (is(T == bool)) 926 { 927 // getting here means the item is present, set to true. 928 dst = true; 929 return ParamResult.ok; 930 } 931 else 932 { 933 if (!data.webConvTo(dst, err)) { 934 err.field = fieldname; 935 return ParamResult.error; 936 } 937 return ParamResult.ok; 938 } 939 } 940 941 // NOTE: dst is assumed to be uninitialized 942 package ParamResult readFormParamRec(T)(scope HTTPServerRequest req, ref T dst, string fieldname, bool required, NestedNameStyle style, ref ParamError err) 943 { 944 import std.traits; 945 import std.typecons; 946 import vibe.data.serialization; 947 import std.algorithm : startsWith; 948 949 static if (isStaticArray!T || (isDynamicArray!T && !isSomeString!(OriginalType!T))) { 950 alias EL = typeof(T.init[0]); 951 enum isSimpleElement = !(isDynamicArray!EL && !isSomeString!(OriginalType!EL)) && 952 !isStaticArray!EL && 953 !(is(EL == struct) && 954 !is(typeof(EL.fromString(string.init))) && 955 !is(typeof(EL.fromStringValidate(string.init, null))) && 956 !is(typeof(EL.fromISOExtString(string.init)))); 957 958 static if (isStaticArray!T) 959 { 960 bool[T.length] seen; 961 } 962 else 963 { 964 static assert(!is(EL == bool), 965 "Boolean arrays are not allowed, because their length cannot " ~ 966 "be uniquely determined. Use a static array instead."); 967 // array to check for duplicates 968 dst = T.init; 969 bool[] seen; 970 } 971 // process the items in the order they appear. 972 char indexSep = style == NestedNameStyle.d ? '[' : '_'; 973 const minLength = fieldname.length + (style == NestedNameStyle.d ? 2 : 1); 974 const indexTrailer = style == NestedNameStyle.d ? "]" : ""; 975 976 ParamResult processItems(DL)(DL dlist) 977 { 978 foreach (k, v; dlist.byKeyValue) 979 { 980 if (k.length < minLength) 981 // sanity check to prevent out of bounds 982 continue; 983 if (k.startsWith(fieldname) && k[fieldname.length] == indexSep) 984 { 985 // found a potential match 986 string key = k[fieldname.length + 1 .. $]; 987 size_t idx; 988 if (key == indexTrailer) 989 { 990 static if (isSimpleElement) 991 { 992 // this is a non-indexed array item. Find an empty slot, or expand the array 993 import std.algorithm : countUntil; 994 idx = seen[].countUntil(false); 995 static if (isStaticArray!T) 996 { 997 if (idx == size_t.max) 998 { 999 // ignore extras, and we know there are no more matches to come. 1000 break; 1001 } 1002 } 1003 else if (idx == size_t.max) 1004 { 1005 // append to the end. 1006 idx = dst.length; 1007 } 1008 } 1009 else 1010 { 1011 // not valid for non-simple elements. 1012 continue; 1013 } 1014 } 1015 else 1016 { 1017 import std.conv; 1018 idx = key.parse!size_t; 1019 static if (isStaticArray!T) 1020 { 1021 if (idx >= T.length) 1022 // keep existing behavior, ignore extras 1023 continue; 1024 } 1025 else if (idx > MAX_ARR_INDEX) 1026 { 1027 // Getting a bit large, we don't want to allow DOS attacks. 1028 err.field = k; 1029 err.text = "Maximum index exceeded"; 1030 return ParamResult.error; 1031 } 1032 static if (isSimpleElement) 1033 { 1034 if (key != indexTrailer) 1035 // this can't be a match, simple elements are parsed from 1036 // the string, there should be no more to the key. 1037 continue; 1038 } 1039 else 1040 { 1041 // ensure there's more than just the index trailer 1042 if (key.length == indexTrailer.length || !key.startsWith(indexTrailer)) 1043 // not a valid entry. ignore this entry to preserve existing behavior. 1044 continue; 1045 } 1046 } 1047 1048 static if (!isStaticArray!T) 1049 { 1050 // check to see if we need to expand the array 1051 if (dst.length <= idx) 1052 { 1053 dst.length = idx + 1; 1054 seen.length = idx + 1; 1055 } 1056 } 1057 1058 if (seen[idx]) 1059 { 1060 // don't store it twice 1061 continue; 1062 } 1063 seen[idx] = true; 1064 1065 static if (isSimpleElement) 1066 { 1067 auto result = processFormParam(v, k, dst[idx], err); 1068 } 1069 else 1070 { 1071 auto subFieldname = k[0 .. $ - key.length + indexTrailer.length]; 1072 auto result = readFormParamRec(req, dst[idx], subFieldname, true, style, err); 1073 } 1074 if (result != ParamResult.ok) 1075 return result; 1076 } 1077 } 1078 1079 return ParamResult.ok; 1080 } 1081 1082 if (processItems(req.form) == ParamResult.error) 1083 return ParamResult.error; 1084 if (processItems(req.query) == ParamResult.error) 1085 return ParamResult.error; 1086 1087 // make sure all static array items have been seen 1088 static if (isStaticArray!T) 1089 { 1090 import std.algorithm : countUntil; 1091 auto notSeen = seen[].countUntil(false); 1092 if (notSeen != -1) 1093 { 1094 err.field = style.getArrayFieldName(fieldname, notSeen); 1095 err.text = "Missing array form field entry."; 1096 return ParamResult.error; 1097 } 1098 } 1099 } else static if (isNullable!T) { 1100 typeof(dst.get()) el = void; 1101 auto r = readFormParamRec(req, el, fieldname, false, style, err); 1102 final switch (r) { 1103 case ParamResult.ok: dst.setVoid(el); break; 1104 case ParamResult.skipped: dst.setVoid(T.init); break; 1105 case ParamResult.error: return ParamResult.error; 1106 } 1107 } else static if (is(T == struct) && 1108 !is(typeof(T.fromString(string.init))) && 1109 !is(typeof(T.fromStringValidate(string.init, null))) && 1110 !is(typeof(T.fromISOExtString(string.init)))) 1111 { 1112 foreach (m; __traits(allMembers, T)) { 1113 auto r = readFormParamRec(req, __traits(getMember, dst, m), style.getMemberFieldName(fieldname, m), required, style, err); 1114 if (r != ParamResult.ok) 1115 return r; // FIXME: in case of errors the struct will be only partially initialized! All previous fields should be deinitialized first. 1116 } 1117 } else static if (is(T == bool)) { 1118 dst = (fieldname in req.form) !is null || (fieldname in req.query) !is null; 1119 } else if (auto pv = fieldname in req.form) { 1120 if (!(*pv).webConvTo(dst, err)) { 1121 err.field = fieldname; 1122 return ParamResult.error; 1123 } 1124 } else if (auto pv = fieldname in req.query) { 1125 if (!(*pv).webConvTo(dst, err)) { 1126 err.field = fieldname; 1127 return ParamResult.error; 1128 } 1129 } else if (required) { 1130 err.field = fieldname; 1131 err.text = "Missing form field."; 1132 return ParamResult.error; 1133 } 1134 else return ParamResult.skipped; 1135 1136 return ParamResult.ok; 1137 } 1138 1139 // test new array mechanisms 1140 unittest { 1141 import vibe.http.server; 1142 import vibe.inet.url; 1143 1144 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0=1&arr_2=2&arr_=3")); 1145 int[] arr; 1146 ParamError err; 1147 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1148 assert(result == ParamResult.ok); 1149 assert(arr == [1,3,2]); 1150 1151 // try with static array 1152 int[3] staticarr; 1153 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.underscore, err); 1154 assert(result == ParamResult.ok); 1155 assert(staticarr == [1,3,2]); 1156 1157 // d style 1158 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2]=1&arr[0]=2&arr[]=3")); 1159 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1160 assert(result == ParamResult.ok); 1161 assert(arr == [2,3,1]); 1162 1163 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.d, err); 1164 assert(result == ParamResult.ok); 1165 assert(staticarr == [2,3,1]); 1166 1167 // try nested arrays 1168 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2][]=1&arr[0][]=2&arr[1][]=3&arr[0][]=4")); 1169 int[][] arr2; 1170 result = req.readFormParamRec(arr2, "arr", false, NestedNameStyle.d, err); 1171 assert(result == ParamResult.ok); 1172 assert(arr2 == [[2,4],[3],[1]]); 1173 1174 int[][2] staticarr2; 1175 result = req.readFormParamRec(staticarr2, "arr", false, NestedNameStyle.d, err); 1176 assert(result == ParamResult.ok); 1177 assert(staticarr2 == [[2,4],[3]]); 1178 1179 // bug with key length 1180 req = createTestHTTPServerRequest(URL("http://localhost/route?arr=1")); 1181 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1182 assert(result == ParamResult.ok); 1183 assert(arr.length == 0); 1184 } 1185 1186 unittest { // complex array parameters 1187 import vibe.http.server; 1188 import vibe.inet.url; 1189 1190 static struct S { 1191 int a, b; 1192 } 1193 1194 S[] arr; 1195 ParamError err; 1196 1197 // d style 1198 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr[0].a=1&arr[0].b=2")); 1199 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1200 assert(result == ParamResult.ok); 1201 assert(arr == [S(1, 2)]); 1202 1203 // underscore style 1204 req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0_a=1&arr_0_b=2")); 1205 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1206 assert(result == ParamResult.ok); 1207 assert(arr == [S(1, 2)]); 1208 } 1209 1210 package bool webConvTo(T)(string str, ref T dst, ref ParamError err) 1211 nothrow { 1212 import std.conv; 1213 import std.exception; 1214 try { 1215 static if (is(typeof(T.fromStringValidate(str, &err.text)))) { 1216 static assert(is(typeof(T.fromStringValidate(str, &err.text)) == Nullable!T)); 1217 auto res = T.fromStringValidate(str, &err.text); 1218 if (res.isNull()) return false; 1219 dst.setVoid(res.get); 1220 } else static if (is(typeof(T.fromString(str)))) { 1221 static assert(is(typeof(T.fromString(str)) == T)); 1222 dst.setVoid(T.fromString(str)); 1223 } else static if (is(typeof(T.fromISOExtString(str)))) { 1224 static assert(is(typeof(T.fromISOExtString(str)) == T)); 1225 dst.setVoid(T.fromISOExtString(str)); 1226 } else { 1227 dst.setVoid(str.to!T()); 1228 } 1229 } catch (Exception e) { 1230 import vibe.core.log : logDebug; 1231 import std.encoding : sanitize; 1232 err.text = e.msg; 1233 debug try logDebug("Error converting web field: %s", e.toString().sanitize); 1234 catch (Exception) {} 1235 return false; 1236 } 1237 return true; 1238 } 1239 1240 // properly sets an uninitialized variable 1241 package void setVoid(T, U)(ref T dst, U value) 1242 { 1243 import std.traits; 1244 static if (hasElaborateAssign!T) { 1245 static if (is(T == U)) { 1246 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&value)[0 .. T.sizeof]; 1247 typeid(T).postblit(&dst); 1248 } else { 1249 static T init = T.init; 1250 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&init)[0 .. T.sizeof]; 1251 dst = value; 1252 } 1253 } else dst = value; 1254 } 1255 1256 unittest { 1257 static assert(!__traits(compiles, { bool[] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1258 static assert(__traits(compiles, { bool[2] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1259 1260 enum Test: string { a = "AAA", b="BBB" } 1261 static assert(__traits(compiles, { Test barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1262 } 1263 1264 private string getArrayFieldName(T)(NestedNameStyle style, string prefix, T index) 1265 { 1266 import std.format : format; 1267 final switch (style) { 1268 case NestedNameStyle.underscore: return format("%s_%s", prefix, index); 1269 case NestedNameStyle.d: return format("%s[%s]", prefix, index); 1270 } 1271 } 1272 1273 private string getMemberFieldName(NestedNameStyle style, string prefix, string member) 1274 @safe { 1275 import std.format : format; 1276 final switch (style) { 1277 case NestedNameStyle.underscore: return format("%s_%s", prefix, member); 1278 case NestedNameStyle.d: return format("%s.%s", prefix, member); 1279 } 1280 }