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