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 (getUDAs!(func, ResultSerializer).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 deprecated 854 Unaltered = unaltered, 855 deprecated 856 CamelCase = camelCase, 857 deprecated 858 PascalCase = pascalCase, 859 deprecated 860 LowerCase = lowerCase, 861 deprecated 862 UpperCase = upperCase, 863 deprecated 864 LowerUnderscored = lowerUnderscored, 865 deprecated 866 UpperUnderscored = upperUnderscored, 867 } 868 869 870 /// Speficies how D fields are mapped to form field names 871 enum NestedNameStyle { 872 underscore, /// Use underscores to separate fields and array indices 873 d /// Use native D style and separate fields by dots and put array indices into brackets 874 } 875 876 877 // concatenates two URL parts avoiding any duplicate slashes 878 // in resulting URL. `trailing` defines of result URL must 879 // end with slash 880 package string concatURL(string prefix, string url, bool trailing = false) 881 @safe { 882 import std.algorithm : startsWith, endsWith; 883 884 auto pre = prefix.endsWith("/"); 885 auto post = url.startsWith("/"); 886 887 if (!url.length) return trailing && !pre ? prefix ~ "/" : prefix; 888 889 auto suffix = trailing && !url.endsWith("/") ? "/" : null; 890 891 if (pre) { 892 // "/" is ASCII, so can just slice 893 if (post) return prefix ~ url[1 .. $] ~ suffix; 894 else return prefix ~ url ~ suffix; 895 } else { 896 if (post) return prefix ~ url ~ suffix; 897 else return prefix ~ "/" ~ url ~ suffix; 898 } 899 } 900 901 @safe unittest { 902 assert(concatURL("/test/", "/it/", false) == "/test/it/"); 903 assert(concatURL("/test", "it/", false) == "/test/it/"); 904 assert(concatURL("/test", "it", false) == "/test/it"); 905 assert(concatURL("/test", "", false) == "/test"); 906 assert(concatURL("/test/", "", false) == "/test/"); 907 assert(concatURL("/test/", "/it/", true) == "/test/it/"); 908 assert(concatURL("/test", "it/", true) == "/test/it/"); 909 assert(concatURL("/test", "it", true) == "/test/it/"); 910 assert(concatURL("/test", "", true) == "/test/"); 911 assert(concatURL("/test/", "", true) == "/test/"); 912 } 913 914 915 /// private 916 template isNullable(T) { 917 import std.traits; 918 enum isNullable = isInstanceOf!(Nullable, T); 919 } 920 921 static assert(isNullable!(Nullable!int)); 922 923 package struct ParamError { 924 string field; 925 string text; 926 } 927 928 package enum ParamResult { 929 ok, 930 skipped, 931 error 932 } 933 934 // maximum array index in the parameter fields. 935 private enum MAX_ARR_INDEX = 0xffff; 936 937 // handle the actual data inside the parameter 938 private ParamResult processFormParam(T)(scope string data, string fieldname, ref T dst, ref ParamError err) 939 { 940 static if (is(T == bool)) 941 { 942 // getting here means the item is present, set to true. 943 dst = true; 944 return ParamResult.ok; 945 } 946 else 947 { 948 if (!data.webConvTo(dst, err)) { 949 err.field = fieldname; 950 return ParamResult.error; 951 } 952 return ParamResult.ok; 953 } 954 } 955 956 // NOTE: dst is assumed to be uninitialized 957 package ParamResult readFormParamRec(T)(scope HTTPServerRequest req, ref T dst, string fieldname, bool required, NestedNameStyle style, ref ParamError err) 958 { 959 import std.traits; 960 import std.typecons; 961 import vibe.data.serialization; 962 import std.algorithm : startsWith; 963 964 static if (isStaticArray!T || (isDynamicArray!T && !isSomeString!(OriginalType!T))) { 965 alias EL = typeof(T.init[0]); 966 enum isSimpleElement = !(isDynamicArray!EL && !isSomeString!(OriginalType!EL)) && 967 !isStaticArray!EL && 968 !(is(EL == struct) && 969 !is(typeof(EL.fromString(string.init))) && 970 !is(typeof(EL.fromStringValidate(string.init, null))) && 971 !is(typeof(EL.fromISOExtString(string.init)))); 972 973 static if (isStaticArray!T) 974 { 975 bool[T.length] seen; 976 } 977 else 978 { 979 static assert(!is(EL == bool), 980 "Boolean arrays are not allowed, because their length cannot " ~ 981 "be uniquely determined. Use a static array instead."); 982 // array to check for duplicates 983 dst = T.init; 984 bool[] seen; 985 } 986 // process the items in the order they appear. 987 char indexSep = style == NestedNameStyle.d ? '[' : '_'; 988 const minLength = fieldname.length + (style == NestedNameStyle.d ? 2 : 1); 989 const indexTrailer = style == NestedNameStyle.d ? "]" : ""; 990 991 ParamResult processItems(DL)(DL dlist) 992 { 993 foreach (k, v; dlist.byKeyValue) 994 { 995 if (k.length < minLength) 996 // sanity check to prevent out of bounds 997 continue; 998 if (k.startsWith(fieldname) && k[fieldname.length] == indexSep) 999 { 1000 // found a potential match 1001 string key = k[fieldname.length + 1 .. $]; 1002 size_t idx; 1003 if (key == indexTrailer) 1004 { 1005 static if (isSimpleElement) 1006 { 1007 // this is a non-indexed array item. Find an empty slot, or expand the array 1008 import std.algorithm : countUntil; 1009 idx = seen[].countUntil(false); 1010 static if (isStaticArray!T) 1011 { 1012 if (idx == size_t.max) 1013 { 1014 // ignore extras, and we know there are no more matches to come. 1015 break; 1016 } 1017 } 1018 else if (idx == size_t.max) 1019 { 1020 // append to the end. 1021 idx = dst.length; 1022 } 1023 } 1024 else 1025 { 1026 // not valid for non-simple elements. 1027 continue; 1028 } 1029 } 1030 else 1031 { 1032 import std.conv; 1033 idx = key.parse!size_t; 1034 static if (isStaticArray!T) 1035 { 1036 if (idx >= T.length) 1037 // keep existing behavior, ignore extras 1038 continue; 1039 } 1040 else if (idx > MAX_ARR_INDEX) 1041 { 1042 // Getting a bit large, we don't want to allow DOS attacks. 1043 err.field = k; 1044 err.text = "Maximum index exceeded"; 1045 return ParamResult.error; 1046 } 1047 static if (isSimpleElement) 1048 { 1049 if (key != indexTrailer) 1050 // this can't be a match, simple elements are parsed from 1051 // the string, there should be no more to the key. 1052 continue; 1053 } 1054 else 1055 { 1056 // ensure there's more than just the index trailer 1057 if (key.length == indexTrailer.length || !key.startsWith(indexTrailer)) 1058 // not a valid entry. ignore this entry to preserve existing behavior. 1059 continue; 1060 } 1061 } 1062 1063 static if (!isStaticArray!T) 1064 { 1065 // check to see if we need to expand the array 1066 if (dst.length <= idx) 1067 { 1068 dst.length = idx + 1; 1069 seen.length = idx + 1; 1070 } 1071 } 1072 1073 if (seen[idx]) 1074 { 1075 // don't store it twice 1076 continue; 1077 } 1078 seen[idx] = true; 1079 1080 static if (isSimpleElement) 1081 { 1082 auto result = processFormParam(v, k, dst[idx], err); 1083 } 1084 else 1085 { 1086 auto subFieldname = k[0 .. $ - key.length + indexTrailer.length]; 1087 auto result = readFormParamRec(req, dst[idx], subFieldname, true, style, err); 1088 } 1089 if (result != ParamResult.ok) 1090 return result; 1091 } 1092 } 1093 1094 return ParamResult.ok; 1095 } 1096 1097 if (processItems(req.form) == ParamResult.error) 1098 return ParamResult.error; 1099 if (processItems(req.query) == ParamResult.error) 1100 return ParamResult.error; 1101 1102 // make sure all static array items have been seen 1103 static if (isStaticArray!T) 1104 { 1105 import std.algorithm : countUntil; 1106 auto notSeen = seen[].countUntil(false); 1107 if (notSeen != -1) 1108 { 1109 err.field = style.getArrayFieldName(fieldname, notSeen); 1110 err.text = "Missing array form field entry."; 1111 return ParamResult.error; 1112 } 1113 } 1114 } else static if (isNullable!T) { 1115 typeof(dst.get()) el = void; 1116 auto r = readFormParamRec(req, el, fieldname, false, style, err); 1117 final switch (r) { 1118 case ParamResult.ok: dst.setVoid(el); break; 1119 case ParamResult.skipped: dst.setVoid(T.init); break; 1120 case ParamResult.error: return ParamResult.error; 1121 } 1122 } else static if (is(T == struct) && 1123 !is(typeof(T.fromString(string.init))) && 1124 !is(typeof(T.fromStringValidate(string.init, null))) && 1125 !is(typeof(T.fromISOExtString(string.init)))) 1126 { 1127 foreach (m; __traits(allMembers, T)) { 1128 auto r = readFormParamRec(req, __traits(getMember, dst, m), style.getMemberFieldName(fieldname, m), required, style, err); 1129 if (r != ParamResult.ok) 1130 return r; // FIXME: in case of errors the struct will be only partially initialized! All previous fields should be deinitialized first. 1131 } 1132 } else static if (is(T == bool)) { 1133 dst = (fieldname in req.form) !is null || (fieldname in req.query) !is null; 1134 } else if (auto pv = fieldname in req.form) { 1135 if (!(*pv).webConvTo(dst, err)) { 1136 err.field = fieldname; 1137 return ParamResult.error; 1138 } 1139 } else if (auto pv = fieldname in req.query) { 1140 if (!(*pv).webConvTo(dst, err)) { 1141 err.field = fieldname; 1142 return ParamResult.error; 1143 } 1144 } else if (required) { 1145 err.field = fieldname; 1146 err.text = "Missing form field."; 1147 return ParamResult.error; 1148 } 1149 else return ParamResult.skipped; 1150 1151 return ParamResult.ok; 1152 } 1153 1154 // test new array mechanisms 1155 unittest { 1156 import vibe.http.server; 1157 import vibe.inet.url; 1158 1159 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0=1&arr_2=2&arr_=3")); 1160 int[] arr; 1161 ParamError err; 1162 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1163 assert(result == ParamResult.ok); 1164 assert(arr == [1,3,2]); 1165 1166 // try with static array 1167 int[3] staticarr; 1168 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.underscore, err); 1169 assert(result == ParamResult.ok); 1170 assert(staticarr == [1,3,2]); 1171 1172 // d style 1173 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2]=1&arr[0]=2&arr[]=3")); 1174 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1175 assert(result == ParamResult.ok); 1176 assert(arr == [2,3,1]); 1177 1178 result = req.readFormParamRec(staticarr, "arr", false, NestedNameStyle.d, err); 1179 assert(result == ParamResult.ok); 1180 assert(staticarr == [2,3,1]); 1181 1182 // try nested arrays 1183 req = createTestHTTPServerRequest(URL("http://localhost/route?arr[2][]=1&arr[0][]=2&arr[1][]=3&arr[0][]=4")); 1184 int[][] arr2; 1185 result = req.readFormParamRec(arr2, "arr", false, NestedNameStyle.d, err); 1186 assert(result == ParamResult.ok); 1187 assert(arr2 == [[2,4],[3],[1]]); 1188 1189 int[][2] staticarr2; 1190 result = req.readFormParamRec(staticarr2, "arr", false, NestedNameStyle.d, err); 1191 assert(result == ParamResult.ok); 1192 assert(staticarr2 == [[2,4],[3]]); 1193 1194 // bug with key length 1195 req = createTestHTTPServerRequest(URL("http://localhost/route?arr=1")); 1196 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1197 assert(result == ParamResult.ok); 1198 assert(arr.length == 0); 1199 } 1200 1201 unittest { // complex array parameters 1202 import vibe.http.server; 1203 import vibe.inet.url; 1204 1205 static struct S { 1206 int a, b; 1207 } 1208 1209 S[] arr; 1210 ParamError err; 1211 1212 // d style 1213 auto req = createTestHTTPServerRequest(URL("http://localhost/route?arr[0].a=1&arr[0].b=2")); 1214 auto result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.d, err); 1215 assert(result == ParamResult.ok); 1216 assert(arr == [S(1, 2)]); 1217 1218 // underscore style 1219 req = createTestHTTPServerRequest(URL("http://localhost/route?arr_0_a=1&arr_0_b=2")); 1220 result = req.readFormParamRec(arr, "arr", false, NestedNameStyle.underscore, err); 1221 assert(result == ParamResult.ok); 1222 assert(arr == [S(1, 2)]); 1223 } 1224 1225 package bool webConvTo(T)(string str, ref T dst, ref ParamError err) 1226 nothrow { 1227 import std.conv; 1228 import std.exception; 1229 try { 1230 static if (is(typeof(T.fromStringValidate(str, &err.text)))) { 1231 static assert(is(typeof(T.fromStringValidate(str, &err.text)) == Nullable!T)); 1232 auto res = T.fromStringValidate(str, &err.text); 1233 if (res.isNull()) return false; 1234 dst.setVoid(res.get); 1235 } else static if (is(typeof(T.fromString(str)))) { 1236 static assert(is(typeof(T.fromString(str)) == T)); 1237 dst.setVoid(T.fromString(str)); 1238 } else static if (is(typeof(T.fromISOExtString(str)))) { 1239 static assert(is(typeof(T.fromISOExtString(str)) == T)); 1240 dst.setVoid(T.fromISOExtString(str)); 1241 } else { 1242 dst.setVoid(str.to!T()); 1243 } 1244 } catch (Exception e) { 1245 import vibe.core.log : logDebug; 1246 import std.encoding : sanitize; 1247 err.text = e.msg; 1248 debug try logDebug("Error converting web field: %s", e.toString().sanitize); 1249 catch (Exception) {} 1250 return false; 1251 } 1252 return true; 1253 } 1254 1255 // properly sets an uninitialized variable 1256 package void setVoid(T, U)(ref T dst, U value) 1257 { 1258 import std.traits; 1259 static if (hasElaborateAssign!T) { 1260 static if (is(T == U)) { 1261 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&value)[0 .. T.sizeof]; 1262 typeid(T).postblit(&dst); 1263 } else { 1264 static T init = T.init; 1265 (cast(ubyte*)&dst)[0 .. T.sizeof] = (cast(ubyte*)&init)[0 .. T.sizeof]; 1266 dst = value; 1267 } 1268 } else dst = value; 1269 } 1270 1271 unittest { 1272 static assert(!__traits(compiles, { bool[] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1273 static assert(__traits(compiles, { bool[2] barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1274 1275 enum Test: string { a = "AAA", b="BBB" } 1276 static assert(__traits(compiles, { Test barr; ParamError err;readFormParamRec(null, barr, "f", true, NestedNameStyle.d, err); })); 1277 } 1278 1279 private string getArrayFieldName(T)(NestedNameStyle style, string prefix, T index) 1280 { 1281 import std.format : format; 1282 final switch (style) { 1283 case NestedNameStyle.underscore: return format("%s_%s", prefix, index); 1284 case NestedNameStyle.d: return format("%s[%s]", prefix, index); 1285 } 1286 } 1287 1288 private string getMemberFieldName(NestedNameStyle style, string prefix, string member) 1289 @safe { 1290 import std.format : format; 1291 final switch (style) { 1292 case NestedNameStyle.underscore: return format("%s_%s", prefix, member); 1293 case NestedNameStyle.d: return format("%s.%s", prefix, member); 1294 } 1295 }