1 /++ 2 Implements RFC 6570 (URI Template) 3 4 Standards: https://tools.ietf.org/html/rfc6570 5 6 Examples: 7 --- 8 import uritemplate; 9 10 string uriTemplate = "https://cool.webfreak.org/{user}/profile{?fields*}"; 11 12 assert(uriTemplate.expandTemplateURIString([ 13 "user": URIVariable("bob"), 14 "fields": URIVariable([ 15 "address", "name", "birthday" 16 ]) 17 ]) == "https://cool.webfreak.org/bob/profile?fields=address&fields=name&fields=birthday"); 18 19 assert(uriTemplate.expandTemplateURIString([ 20 "user": URIVariable(["bob!", "Straße"]), 21 "fields": URIVariable([ 22 "address", "name", "birthday" 23 ]) 24 ]) == "https://cool.webfreak.org/bob%21,Stra%C3%9Fe/profile?fields=address&fields=name&fields=birthday"); 25 --- 26 +/ 27 module uritemplate; 28 29 import std.algorithm; 30 import std.array; 31 import std.ascii; 32 import std.conv; 33 import std.range; 34 import std..string; 35 import std.typecons; 36 import std.utf; 37 38 @safe: 39 40 unittest 41 { 42 // documentation unittest 43 string uriTemplate = "https://cool.webfreak.org/{user}/profile{?fields*}"; 44 assert(uriTemplate.expandTemplateURIString([ 45 "user": URIVariable("bob"), 46 "fields": URIVariable([ 47 "address", "name", "birthday" 48 ]) 49 ]) == "https://cool.webfreak.org/bob/profile?fields=address&fields=name&fields=birthday"); 50 assert(uriTemplate.expandTemplateURIString([ 51 "user": URIVariable(["bob!", "Straße"]), 52 "fields": URIVariable([ 53 "address", "name", "birthday" 54 ]) 55 ]) == "https://cool.webfreak.org/bob%21,Stra%C3%9Fe/profile?fields=address&fields=name&fields=birthday"); 56 } 57 58 /// Exception thrown on malformed URI templates 59 class TemplateURIFormatException : Exception 60 { 61 /// Index at which point in the input URI template this error occurred. 62 size_t index; 63 64 /// 65 this(size_t index, string msg, string file = __FILE__, size_t line = __LINE__, 66 Throwable nextInChain = null) pure nothrow @nogc @safe 67 { 68 this.index = index; 69 super(msg, file, line, nextInChain); 70 } 71 } 72 73 /// Tagged union for template URI variables. 74 /// Implementation tags the length of the arrays for the type. 75 struct URIVariable 76 { 77 /// Describes the possible types a variable can have. 78 enum Type 79 { 80 /// A single string value. 81 value, 82 /// A string array. 83 array, 84 /// A string -> string associative array. 85 map 86 } 87 88 enum undefined = URIVariable.init; 89 90 enum TagBitLength = 2; 91 enum TagBitMask = ~((cast(size_t)-1) << TagBitLength); 92 93 // pointer to a map or pointer to the array .ptr 94 void* ptr; 95 96 // tagged length (tag in least significant `TagBitLength` bits) 97 size_t rawLength; 98 99 /// Initializes this variable to a string value. 100 this(string value) @safe 101 { 102 this.value = value; 103 } 104 105 /// Initializes this variable to an array value. All items are assumed to 106 /// be defined, so null strings are equivalent to empty strings. 107 this(string[] array) @safe 108 { 109 this.array = array; 110 } 111 112 /// Initializes this variable to a map value. All items are assumed to be 113 /// defined, so null strings are equivalent to empty strings. 114 this(Tuple!(string, string)[] map) @safe 115 { 116 this.map = map; 117 } 118 119 /// ditto 120 this(string[string] map) @safe 121 { 122 this.map = map; 123 } 124 125 /// Returns: whether this variable is undefined or not. 126 /// A variable is undefined if it is the empty list or unset. Empty string 127 /// is not considered undefined. 128 bool isUndefined() const @property @safe 129 { 130 final switch (type) 131 { 132 case Type.value: 133 return ptr is null && length == 0; 134 case Type.array: 135 case Type.map: 136 return length == 0; 137 } 138 } 139 140 /// Clears this variable back to being undefined. 141 void clear() @safe 142 { 143 ptr = null; 144 rawLength = 0; 145 } 146 147 /// Returns: whether this variable is a single value, array or map. 148 /// Note that if `isUndefined` is true this is of type string 149 /// anyway, potentially violating RFC 6570 section 2.3 if not 150 /// checked. 151 Type type() const @property @trusted 152 { 153 return cast(Type)(rawLength & TagBitMask); 154 } 155 156 /// The length of a value, count of an array or count of a map. 157 size_t length() const @property @safe 158 { 159 return rawLength >> TagBitLength; 160 } 161 162 /// Returns: true if this is an empty list or empty string. 163 bool isEmpty() const @property @safe 164 { 165 return (rawLength & ~TagBitMask) == 0; 166 } 167 168 /// Returns: the single value this variable is pointing to. 169 string value() const @property @trusted 170 in(type == Type.value, 171 "Attempted to access variable of type " ~ type.to!string ~ " as singular value.") 172 { 173 return (cast(immutable(char)*) ptr)[0 .. length]; 174 } 175 176 /// Sets the type of this variable to value and sets the data. 177 /// Params: 178 /// value = The (new) value to set this variable to. 179 /// Returns: The given value. 180 string value(string value) @property @trusted 181 { 182 ptr = cast(void*) value.ptr; 183 if (ptr is null) 184 ptr = cast(void*) someEmptyString.ptr; // make sure we are not undefined 185 rawLength = (value.length << TagBitLength) | Type.value; 186 return value; 187 } 188 189 /// Returns: the string array this variable is pointing to. 190 inout(string[]) array() inout @property @trusted 191 in(type == Type.array, "Attempted to access variable of type " ~ type.to!string ~ " as array.") 192 { 193 return (cast(inout(string)*) ptr)[0 .. length]; 194 } 195 196 /// Sets the type of this variable to array and sets the data. 197 /// Params: 198 /// array = The (new) array to set this variable to. 199 /// Returns: The given array. 200 string[] array(string[] array) @property @trusted 201 { 202 ptr = array.ptr; 203 rawLength = (array.length << TagBitLength) | Type.array; 204 return array; 205 } 206 207 /// Returns: the associative array this variable is pointing to. 208 inout(Tuple!(string, string)[]) map() inout @property @trusted 209 in(type == Type.map, "Attempted to access variable of type " ~ type.to!string ~ " as map.") 210 { 211 return (cast(inout(Tuple!(string, string))*) ptr)[0 .. length]; 212 } 213 214 /// Sets the type of this variable to map and sets the data. 215 /// Params: 216 /// map = The (new) map to set this variable to. 217 /// Returns: The given map. 218 Tuple!(string, string)[] map(Tuple!(string, string)[] map) @property @trusted 219 { 220 ptr = map.ptr; 221 rawLength = (map.length << TagBitLength) | Type.map; 222 return map; 223 } 224 225 /// ditto 226 Tuple!(string, string)[] map(string[string] map) @property @trusted 227 { 228 auto t = map.byKeyValue.map!(a => tuple(a.key, a.value)).array; 229 ptr = t.ptr; 230 rawLength = (t.length << TagBitLength) | Type.map; 231 return t; 232 } 233 } 234 235 /// 236 unittest 237 { 238 URIVariable value = URIVariable("hello"); 239 URIVariable array = URIVariable(["hello", "world"]); 240 URIVariable map = URIVariable([tuple("foo", "bar")]); 241 // AA converts to tuple list at unspecified order 242 URIVariable mapAA = URIVariable(["foo": "baz"]); 243 244 assert(!value.isUndefined); 245 assert(!value.isEmpty); 246 247 assert(!array.isUndefined); 248 assert(!array.isEmpty); 249 250 assert(!map.isUndefined); 251 assert(!map.isEmpty); 252 253 assert(!mapAA.isUndefined); 254 assert(!mapAA.isEmpty); 255 256 assert(value.type == URIVariable.Type.value); 257 assert(array.type == URIVariable.Type.array); 258 assert(map.type == URIVariable.Type.map); 259 assert(mapAA.type == URIVariable.Type.map); 260 261 assert(value.value == "hello"); 262 assert(array.array == ["hello", "world"]); 263 assert(map.map == [tuple("foo", "bar")]); 264 assert(mapAA.map == [tuple("foo", "baz")]); 265 266 URIVariable undefined; 267 assert(undefined.isUndefined); 268 assert(undefined.isEmpty); 269 270 URIVariable empty = URIVariable(cast(string) null); 271 assert(!empty.isUndefined); 272 assert(empty.isEmpty); 273 274 URIVariable emptyList = URIVariable(cast(string[]) null); 275 assert(emptyList.isUndefined); 276 assert(emptyList.isEmpty); 277 278 value.clear(); 279 assert(value.isUndefined); 280 assert(value.isEmpty); 281 } 282 283 /// Callback to resolve a URIVariable by a given variable name. 284 /// The variable name is always resolved to a simple string and the returned 285 /// value should not be encoded or otherwise processed for the URI. 286 /// To conform to RFC 6570 the callback MUST always produce the same variables 287 /// for any given variable name. The values MUST be determined before template 288 /// expansion. 289 alias VariableCallback = const(URIVariable) delegate(string variableName) @safe; 290 291 /// Expands a URI template as defined in RFC 6570. 292 /// Params: 293 /// templateUri = Given URI template as defined in RFC 6570. 294 /// resolveVariable = Callback delegate to resolve a variable name 295 /// (parameter) to the variable value. (return value) May 296 /// be called more than once per variable name. 297 /// See $(REF VariableCallback). Values are percent encoded 298 /// by this function and must not be encoded by the 299 /// callback function. 300 /// 301 /// Note that all values returned by this function MUST be 302 /// formed prior to template expansion in order to comply 303 /// with RFC 6570. Therefore to ensure compatibility a 304 /// given variable name must always evaluate to the same 305 /// value every time. 306 /// strict = Validate entire string strictly according to RFC 6570. 307 /// Does NOT perform unicode code-point validation. 308 /// Performs character-by-character checks for exact 309 /// grammar checks. 310 /// Returns: The expanded URI (GC-allocated) or if there is no template in the 311 /// URI, the templateUri parameter as given. 312 /// Throws: $(LREF TemplateURIFormatException) on attempted use of a reserved 313 /// operator, invalid variable specifications or strict issues. 314 string expandTemplateURIString(scope return string templateUri, 315 scope VariableCallback resolveVariable, bool strict = false) 316 { 317 if (templateUri.indexOf('{') == -1) 318 { 319 if (strict) 320 validateTemplateURILiteral(templateUri, 0); 321 return templateUri; 322 } 323 324 auto ret = appender!string; 325 326 ptrdiff_t last; 327 while (true) 328 { 329 ptrdiff_t start = templateUri.indexOf('{', last); 330 if (start == -1) 331 break; 332 333 if (strict) 334 validateTemplateURILiteral(templateUri[last .. start], last); 335 336 ret ~= templateUri[last .. start]; 337 338 last = templateUri.indexOf('}', start); 339 if (last == -1) 340 throw new TemplateURIFormatException(start, 341 "Missing closing brace for template parameter"); 342 last++; 343 344 auto templateString = templateUri[start + 1 .. last - 1]; 345 ret ~= expandTemplateURIVariable(templateString, resolveVariable, strict, start + 1); 346 } 347 348 if (strict) 349 validateTemplateURILiteral(templateUri[last .. $], last); 350 351 ret ~= templateUri[last .. $]; 352 353 return ret.data; 354 } 355 356 /// ditto 357 string expandTemplateURIString(string templateUri, scope const URIVariable[string] variables, bool strict = false) 358 { 359 return expandTemplateURIString(templateUri, 360 delegate(string k) @safe => variables.get(k, URIVariable.init), strict); 361 } 362 363 /// ditto 364 string expandTemplateURIString(string templateUri, scope const string[string] variables, bool strict = false) 365 { 366 return expandTemplateURIString(templateUri, delegate(string k) @safe { 367 if (auto v = k in variables) 368 return URIVariable(*v); 369 else 370 return URIVariable.init; 371 }, strict); 372 } 373 374 private static immutable nullDelegate = delegate(string k) @safe => URIVariable.init; 375 /// ditto 376 string expandTemplateURIString(string templateUri, typeof(null) variables, bool strict = false) 377 { 378 return expandTemplateURIString(templateUri, nullDelegate, strict); 379 } 380 381 /// 382 @safe unittest 383 { 384 assert(expandTemplateURIString(`/notifications{?since,all,participating}`, 385 delegate(string k) => URIVariable.init) == "/notifications"); 386 387 assert(expandTemplateURIString(`/notifications{?since,all,participating}`, 388 null) == "/notifications"); 389 390 assert(expandTemplateURIString(`/notifications{?since,all,participating}`, 391 ["all": "1"]) == "/notifications?all=1"); 392 393 assert(expandTemplateURIString(`/notifications{?since,all,participating}`, 394 ["all": "1", "participating": "1"]) == "/notifications?all=1&participating=1"); 395 } 396 397 /// Expands a variable template part of a URI template as defined in RFC 6570. 398 /// Params: 399 /// templateString = The template variable definition without surrounding 400 /// braces. 401 /// resolveVariable = Callback delegate to resolve a variable name 402 /// (parameter) to the variable value. (return value) May 403 /// be called more than once per variable name. 404 /// See $(REF VariableCallback). Values are percent encoded 405 /// by this function and must not be encoded by the 406 /// callback function. 407 /// 408 /// Note that all values returned by this function MUST be 409 /// formed prior to template expansion in order to comply 410 /// with RFC 6570. Therefore to ensure compatibility a 411 /// given variable name must always evaluate to the same 412 /// value every time. 413 /// strict = Validate entire string strictly according to RFC 6570. 414 /// Does NOT perform unicode code-point validation. 415 /// Performs character-by-character checks for exact 416 /// grammar checks. 417 /// index = Byte index to offset malformed URI format exceptions. 418 /// Returns: the resolved variable value (properly URI encoded) 419 /// Throws: $(LREF TemplateURIFormatException) on attempted use of a reserved 420 /// operator, invalid variable specifications or strict issues. 421 string expandTemplateURIVariable(string templateString, 422 VariableCallback resolveVariable, bool strict = false, size_t index = 0) 423 { 424 if (!templateString.length) 425 throw new TemplateURIFormatException(index, "Empty template string"); 426 427 char op = templateString[0]; 428 switch (op) 429 { 430 case '+': 431 case '#': 432 case '.': 433 case '/': 434 case ';': 435 case '?': 436 case '&': 437 return serializeVariables(templateString[1 .. $], resolveVariable, op, strict, index); 438 case '=': 439 case ',': 440 case '!': 441 case '@': 442 case '|': 443 throw new TemplateURIFormatException(index, 444 "Attempted use of a reserved URI template variable operator"); 445 default: 446 if (strict && !(op.isAlphaNum || op == '_' || op == '%')) 447 throw new TemplateURIFormatException(index, 448 "Attempted use of an unknown URI template variable operator"); 449 return serializeVariables(templateString, resolveVariable, char.init, strict, index); 450 } 451 } 452 453 /// ditto 454 string expandTemplateURIVariable(string templateString, 455 URIVariable[string] variables, bool strict = false, size_t index = 0) 456 { 457 return expandTemplateURIVariable(templateString, 458 delegate(string k) @safe => variables.get(k, URIVariable.init), strict, index); 459 } 460 461 /// ditto 462 string expandTemplateURIVariable(string templateString, string[string] variables, 463 bool strict = false, size_t index = 0) 464 { 465 return expandTemplateURIVariable(templateString, delegate(string k) @safe { 466 if (auto v = k in variables) 467 return URIVariable(*v); 468 else 469 return URIVariable.init; 470 }, strict, index); 471 } 472 473 /// ditto 474 string expandTemplateURIVariable(string templateString, typeof(null) variables, 475 bool strict = false, size_t index = 0) 476 { 477 return expandTemplateURIVariable(templateString, 478 delegate(string k) @safe => URIVariable.init, strict, index); 479 } 480 481 /// 482 @safe unittest 483 { 484 assert(expandTemplateURIVariable(`?since,all,participating`, 485 delegate(string k) @safe => URIVariable.init) == ""); 486 487 assert(expandTemplateURIVariable(`?since,all,participating`, null) == ""); 488 489 assert(expandTemplateURIVariable(`?since,all,participating`, ["all": "1"]) == "?all=1"); 490 491 assert(expandTemplateURIVariable(`?since,all,participating`, [ 492 "all": URIVariable("1"), 493 "participating": URIVariable("1") 494 ]) == "?all=1&participating=1"); 495 } 496 497 /// Checks if the given literal parameter is valid according to RFC 6570 498 /// section 2.1. 499 /// Returns: `false` if invalid with errorIndex set to the first character 500 /// breaking the validity. Potentially end-of-string index for 501 /// unterminated percent-encoded characters. 502 bool isValidTemplateURILiteral(string literal, out size_t errorIndex) 503 { 504 int inPercent = 0; 505 foreach (i, char c; literal) 506 { 507 if (inPercent) 508 { 509 if (!c.isHexDigit) 510 { 511 errorIndex = i; 512 return false; 513 } 514 inPercent--; 515 } 516 else 517 { 518 switch (c) 519 { 520 //dfmt off 521 case 0: .. case 0x20: 522 //dfmt on 523 case '"': 524 case '\'': 525 case '<': 526 case '>': 527 case '\\': 528 case '^': 529 case '`': 530 case '{': 531 case '|': 532 case '}': 533 errorIndex = i; 534 return false; 535 case '%': 536 inPercent = 2; 537 break; 538 default: 539 break; 540 } 541 } 542 } 543 errorIndex = literal.length; 544 return !inPercent; 545 } 546 547 /// 548 @safe unittest 549 { 550 size_t error; 551 552 assert(isValidTemplateURILiteral("hello", error)); 553 554 assert(!isValidTemplateURILiteral("hello world", error)); 555 assert(error == 5); 556 } 557 558 /// Checks if the given literal parameter is valid according to RFC 6570 559 /// section 2.1. 560 /// Throws: $(LREF TemplateURIFormatException) in case of malformed characters. 561 void validateTemplateURILiteral(string literal, size_t index) 562 { 563 size_t offset; 564 if (!isValidTemplateURILiteral(literal, offset)) 565 throw new TemplateURIFormatException(index + offset, "Malformed template URI literal"); 566 } 567 568 /// Checks if the given literal parameter is valid according to RFC 6570 569 /// section 2.3. 570 /// Returns: `false` if invalid with errorIndex set to the first character 571 /// breaking the validity. Potentially end-of-string index for 572 /// unterminated percent-encoded characters. 573 bool isValidTemplateURIVariableName(string varname, out size_t errorIndex) 574 { 575 bool allowDot; 576 int inPercent = 0; 577 foreach (i, char c; varname) 578 { 579 if (inPercent) 580 { 581 if (!c.isHexDigit) 582 { 583 errorIndex = i; 584 return false; 585 } 586 inPercent--; 587 } 588 else 589 { 590 if (c == '%') 591 { 592 inPercent = 2; 593 allowDot = true; 594 } 595 else if (c == '.') 596 { 597 if (!allowDot) 598 { 599 errorIndex = i; 600 return false; 601 } 602 allowDot = false; 603 } 604 else if (!c.isAlphaNum && c != '_') 605 { 606 errorIndex = i; 607 return false; 608 } 609 else 610 { 611 allowDot = true; 612 } 613 } 614 } 615 errorIndex = varname.length; 616 return !inPercent && allowDot; 617 } 618 619 /// Checks if the given literal parameter is valid according to RFC 6570 620 /// section 2.3. 621 /// Throws: $(LREF TemplateURIFormatException) in case of malformed characters. 622 void validateTemplateURIVariableName(string varname, size_t index) 623 { 624 size_t offset; 625 if (!isValidTemplateURIVariableName(varname, offset)) 626 throw new TemplateURIFormatException(index + offset, "Malformed template URI variable name"); 627 } 628 629 private: 630 631 static immutable string someEmptyString = "\0"; 632 633 struct VariableRef 634 { 635 string name; 636 ushort maxLengthCodepoints; 637 bool explode; 638 639 this(string spec, bool strict = false, size_t index = 0) 640 { 641 if (!spec.length) 642 throw new TemplateURIFormatException(index, "Empty variable name definition"); 643 644 if (spec.endsWith('*')) 645 { 646 explode = true; 647 name = spec[0 .. $ - 1]; 648 } 649 else 650 { 651 auto colon = spec.indexOf(':'); 652 if (colon == -1) 653 { 654 name = spec; 655 } 656 else 657 { 658 name = spec[0 .. colon]; 659 auto maxLenSpec = spec[colon + 1 .. $]; 660 if (maxLenSpec.length > 4) 661 throw new TemplateURIFormatException(index + colon, 662 "Variable max-length too large, must be at most 4 characters"); 663 if (maxLenSpec.length == 0) 664 throw new TemplateURIFormatException(index + colon, 665 "Variable max-length after colon is missing"); 666 if (maxLenSpec[0] < '1' || maxLenSpec[0] > '9') 667 throw new TemplateURIFormatException(index + colon + 1, 668 "Variable max-length invalid starting character"); 669 if (!maxLenSpec[1 .. $].all!isDigit) 670 throw new TemplateURIFormatException(index + colon + 1, 671 "Variable max-length invalid number characters"); 672 maxLengthCodepoints = maxLenSpec.to!ushort; 673 } 674 } 675 676 if (strict) 677 validateTemplateURIVariableName(name, index); 678 } 679 } 680 681 /// Trims the given value to the maximum length or returns the string as-is if 682 /// maxLength is 0. Doesn't perform any percent-decoding, so the trim must be 683 /// performed before percent encoding the variable. 684 T[] trimVariableToLength(T)(T[] value, ushort maxLength) 685 { 686 assert(maxLength <= 9999, "maxlength too long (can only input at most 9999)"); 687 if (maxLength == 0) 688 return value; 689 690 size_t index = 0; 691 while (maxLength-- && index < value.length) 692 decode(value, index); 693 return value[0 .. index]; 694 } 695 696 unittest 697 { 698 assert(trimVariableToLength("hello", 0) == "hello"); 699 assert(trimVariableToLength("hello", 1) == "h"); 700 assert(trimVariableToLength("hello", 9999) == "hello"); 701 702 assert(trimVariableToLength("あああ", 3) == "あああ"); 703 assert(trimVariableToLength("あああ", 2) == "ああ"); 704 assert(trimVariableToLength("あああ", 1) == "あ"); 705 assert(trimVariableToLength("aあああ", 2) == "aあ"); 706 } 707 708 /// Returns: a range of $(LREF VariableRef) 709 auto splitVariables(T)(T[] definition, bool strict = false, size_t index = 0) 710 { 711 if (!definition.length) 712 throw new TemplateURIFormatException(index, "Empty variable name definition"); 713 714 return definition.splitter(',').map!(delegate(a) { 715 auto v = VariableRef(a, strict, index); 716 index += a.length + 1; 717 return v; 718 }); 719 } 720 721 string serializeVariables(string spec, VariableCallback resolveVariable, 722 char variableType, bool strict, size_t index) 723 { 724 auto variables = splitVariables(spec, strict, index); 725 auto ret = appender!string; 726 auto sep = getListSeparatorForType(variableType); 727 728 bool first = true; 729 foreach (variable; variables) 730 { 731 auto value = resolveVariable(variable.name); 732 if (value.isUndefined) 733 continue; 734 735 if (first) 736 { 737 ret ~= getStringStartForType(variableType); 738 first = false; 739 } 740 else 741 { 742 ret ~= sep; 743 } 744 745 ret ~= serializeVariable(variable, value, variableType); 746 } 747 748 return ret.data; 749 } 750 751 /// Converts a given variable to its string form, percent encoded. 752 string serializeVariable(scope const VariableRef variable, scope const URIVariable value, char variableType = char.init) 753 { 754 const string ifemp = variableType == '?' || variableType == '&' ? "=" : ""; 755 const bool named = variableType == '?' || variableType == '&' || variableType == ';'; 756 757 final switch (value.type) 758 { 759 case URIVariable.Type.value: 760 if (named) 761 { 762 if (value.isEmpty) 763 return variable.name.encodeLiteral ~ ifemp; 764 else 765 return variable.name.encodeLiteral ~ '=' ~ trimVariableToLength(value.value, 766 variable.maxLengthCodepoints).encodeByType(variableType); 767 } 768 else 769 { 770 return trimVariableToLength(value.value, variable.maxLengthCodepoints).encodeByType( 771 variableType); 772 } 773 case URIVariable.Type.array: 774 case URIVariable.Type.map: 775 auto ret = appender!string; 776 auto sep = getListSeparatorForType(variableType); 777 if (variable.explode) 778 { 779 if (named) 780 { 781 if (value.type == URIVariable.Type.array) 782 { 783 bool first = true; 784 foreach (v; value.array) 785 { 786 if (first) 787 first = false; 788 else 789 ret ~= sep; 790 791 ret ~= variable.name.encodeLiteral; 792 if (v.length) 793 { 794 ret ~= '='; 795 ret ~= v.encodeByType(variableType); 796 } 797 else 798 { 799 ret ~= ifemp; 800 } 801 } 802 } 803 else 804 { 805 bool first = true; 806 foreach (t; value.map) 807 { 808 auto k = t[0]; 809 auto v = t[1]; 810 if (first) 811 first = false; 812 else 813 ret ~= sep; 814 815 ret ~= k.encodeLiteral; 816 if (v.length) 817 { 818 ret ~= '='; 819 ret ~= v.encodeByType(variableType); 820 } 821 else 822 { 823 ret ~= ifemp; 824 } 825 } 826 } 827 } 828 else 829 { 830 if (value.type == URIVariable.Type.array) 831 return value.array.map!(a => a.encodeByType(variableType)).join(sep); 832 else 833 return value.map.map!(a => a[0].encodeByType( 834 variableType) ~ '=' ~ a[1].encodeByType(variableType)).join(sep); 835 } 836 } 837 else 838 { 839 if (named) 840 { 841 ret ~= variable.name.encodeLiteral; 842 // can't have empty values here because empty arrays are considered undefined 843 ret ~= '='; 844 } 845 846 if (value.type == URIVariable.Type.array) 847 { 848 bool first = true; 849 foreach (v; value.array) 850 { 851 if (first) 852 first = false; 853 else 854 ret ~= ','; 855 856 ret ~= v.encodeByType(variableType); 857 } 858 } 859 else 860 { 861 bool first = true; 862 foreach (t; value.map) 863 { 864 auto k = t[0]; 865 auto v = t[1]; 866 if (first) 867 first = false; 868 else 869 ret ~= ','; 870 871 ret ~= k.encodeByType(variableType); 872 ret ~= ','; 873 ret ~= v.encodeByType(variableType); 874 } 875 } 876 } 877 return ret.data; 878 } 879 } 880 881 string getListSeparatorForType(char variableType = char.init) 882 { 883 switch (variableType) 884 { 885 case '.': 886 return "."; 887 case '/': 888 return "/"; 889 case ';': 890 return ";"; 891 case '?': 892 case '&': 893 return "&"; 894 case char.init: 895 case ' ': 896 case '+': 897 case '#': 898 default: 899 return ","; 900 } 901 } 902 903 string getStringStartForType(char variableType = char.init) 904 { 905 switch (variableType) 906 { 907 case '.': 908 return "."; 909 case '/': 910 return "/"; 911 case ';': 912 return ";"; 913 case '?': 914 return "?"; 915 case '&': 916 return "&"; 917 case '#': 918 return "#"; 919 case char.init: 920 case '+': 921 default: 922 return ""; 923 } 924 } 925 926 string encodeByType(string text, char variableType = char.init) 927 { 928 switch (variableType) 929 { 930 case '+': 931 case '#': 932 return text.encodeLiteral; 933 default: 934 return text.encodeUnreserved; 935 } 936 } 937 938 string encodeLiteral(string text) 939 { 940 auto ret = appender!string(); 941 ret.reserve(text.length); 942 size_t index; 943 while (index < text.length) 944 { 945 dchar c = decode(text, index); 946 if (c == '%' && index + 2 <= text.length && text[index].isHexDigit 947 && text[index + 1].isHexDigit) 948 { 949 ret ~= text[index - 1 .. index + 2]; 950 index += 2; 951 } 952 else if (c.isAlphaNum || c.among!('-', '.', '_', '~', ':', '/', '?', 953 '#', '[', ']', '@', '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=')) 954 { 955 ret ~= c; 956 } 957 else 958 { 959 char[4] bytes; 960 const len = encode(bytes, c); 961 foreach (b; bytes[0 .. len]) 962 ret ~= encodeCharByte(b)[]; 963 } 964 } 965 return ret.data; 966 } 967 968 string encodeUnreserved(string text) 969 { 970 auto ret = appender!string(); 971 ret.reserve(text.length); 972 size_t index; 973 while (index < text.length) 974 { 975 dchar c = decode(text, index); 976 if (c.isAlphaNum || c.among!('-', '.', '_', '~')) 977 { 978 ret ~= c; 979 } 980 else 981 { 982 char[4] bytes; 983 const len = encode(bytes, c); 984 foreach (b; bytes[0 .. len]) 985 ret ~= encodeCharByte(b)[]; 986 } 987 } 988 return ret.data; 989 } 990 991 char[3] encodeCharByte(char c) 992 { 993 return ['%', hexDigits[c >> 4], hexDigits[c & 0xF]]; 994 } 995 996 version (unittest) 997 { 998 private string testExpand(T)(string templateString, T variables) 999 { 1000 const lax = expandTemplateURIString(templateString, variables, false); 1001 const strict = expandTemplateURIString(templateString, variables, true); 1002 assert(lax == strict); 1003 return strict; 1004 } 1005 } 1006 1007 // test level 1 examples 1008 @safe unittest 1009 { 1010 string[string] variables = ["var" : "value", "hello" : "Hello World!"]; 1011 1012 assert(testExpand("https://github.com", variables) == "https://github.com"); 1013 1014 assert(testExpand("{var}", variables) == "value"); 1015 assert(testExpand("{hello}", variables) == "Hello%20World%21"); 1016 } 1017 1018 // test level 2 examples 1019 @safe unittest 1020 { 1021 string[string] variables = [ 1022 "var" : "value", "hello" : "Hello World!", "path" : "/foo/bar", 1023 "empty" : "" 1024 ]; 1025 1026 assert(testExpand("{+var}", variables) == "value"); 1027 assert(testExpand("{+hello}", variables) == "Hello%20World!"); 1028 assert(testExpand("{+path}/here", variables) == "/foo/bar/here"); 1029 assert(testExpand("here?ref={+path}", variables) == "here?ref=/foo/bar"); 1030 1031 assert(testExpand("?{var,empty}", variables) == "?value,"); 1032 assert(testExpand("?{var,undef}", variables) == "?value"); 1033 assert(testExpand("?{undef,var}", variables) == "?value"); 1034 } 1035 1036 // test level 3 examples 1037 @safe unittest 1038 { 1039 string[string] variables = [ 1040 "var" : "value", "hello" : "Hello World!", "path" : "/foo/bar", 1041 "empty" : "", "x" : "1024", "y" : "768" 1042 ]; 1043 1044 assert(testExpand("map?{x,y}", variables) == "map?1024,768"); 1045 assert(testExpand("{x,hello,y}", variables) == "1024,Hello%20World%21,768"); 1046 1047 assert(testExpand("{+x,hello,y}", variables) == "1024,Hello%20World!,768"); 1048 assert(testExpand("{+path,x}/here", variables) == "/foo/bar,1024/here"); 1049 1050 assert(testExpand("{#x,hello,y}", variables) == "#1024,Hello%20World!,768"); 1051 assert(testExpand("{#path,x}/here", variables) == "#/foo/bar,1024/here"); 1052 1053 assert(testExpand("X{.var}", variables) == "X.value"); 1054 assert(testExpand("X{.x,y}", variables) == "X.1024.768"); 1055 1056 assert(testExpand("{/var}", variables) == "/value"); 1057 assert(testExpand("{/var,x}/here", variables) == "/value/1024/here"); 1058 1059 assert(testExpand("{;x,y}", variables) == ";x=1024;y=768"); 1060 assert(testExpand("{;x,y,empty}", variables) == ";x=1024;y=768;empty"); 1061 1062 assert(testExpand("{?x,y}", variables) == "?x=1024&y=768"); 1063 assert(testExpand("{?x,y,empty}", variables) == "?x=1024&y=768&empty="); 1064 1065 assert(testExpand("?fixed=yes{&x}", variables) == "?fixed=yes&x=1024"); 1066 assert(testExpand("{&x,y,empty}", variables) == "&x=1024&y=768&empty="); 1067 } 1068 1069 // test level 4 examples 1070 @safe unittest 1071 { 1072 URIVariable[string] variables = [ 1073 "var" : URIVariable("value"), "hello" : URIVariable("Hello World!"), 1074 "path" : URIVariable("/foo/bar"), 1075 "list" : URIVariable(["red", "green", "blue"]), 1076 "keys" : URIVariable([ 1077 tuple("semi", ";"), tuple("dot", "."), tuple("comma", ",") 1078 ]), 1079 ]; 1080 1081 assert(testExpand("{var:3}", variables) == "val"); 1082 assert(testExpand("{var:30}", variables) == "value"); 1083 assert(testExpand("{list}", variables) == "red,green,blue"); 1084 assert(testExpand("{list*}", variables) == "red,green,blue"); 1085 assert(testExpand("{keys}", variables) == "semi,%3B,dot,.,comma,%2C"); 1086 assert(testExpand("{keys*}", variables) == "semi=%3B,dot=.,comma=%2C"); 1087 1088 assert(testExpand("{+path:6}/here", variables) == "/foo/b/here"); 1089 assert(testExpand("{+list}", variables) == "red,green,blue"); 1090 assert(testExpand("{+list*}", variables) == "red,green,blue"); 1091 assert(testExpand("{+keys}", variables) == "semi,;,dot,.,comma,,"); 1092 assert(testExpand("{+keys*}", variables) == "semi=;,dot=.,comma=,"); 1093 1094 assert(testExpand("{#path:6}/here", variables) == "#/foo/b/here"); 1095 assert(testExpand("{#list}", variables) == "#red,green,blue"); 1096 assert(testExpand("{#list*}", variables) == "#red,green,blue"); 1097 assert(testExpand("{#keys}", variables) == "#semi,;,dot,.,comma,,"); 1098 assert(testExpand("{#keys*}", variables) == "#semi=;,dot=.,comma=,"); 1099 1100 assert(testExpand("X{.var:3}", variables) == "X.val"); 1101 assert(testExpand("X{.list}", variables) == "X.red,green,blue"); 1102 assert(testExpand("X{.list*}", variables) == "X.red.green.blue"); 1103 assert(testExpand("X{.keys}", variables) == "X.semi,%3B,dot,.,comma,%2C"); 1104 assert(testExpand("X{.keys*}", variables) == "X.semi=%3B.dot=..comma=%2C"); 1105 1106 assert(testExpand("{/var:1,var}", variables) == "/v/value"); 1107 assert(testExpand("{/list}", variables) == "/red,green,blue"); 1108 assert(testExpand("{/list*}", variables) == "/red/green/blue"); 1109 assert(testExpand("{/list*,path:4}", variables) == "/red/green/blue/%2Ffoo"); 1110 assert(testExpand("{/keys}", variables) == "/semi,%3B,dot,.,comma,%2C"); 1111 assert(testExpand("{/keys*}", variables) == "/semi=%3B/dot=./comma=%2C"); 1112 1113 assert(testExpand("{;hello:5}", variables) == ";hello=Hello"); 1114 assert(testExpand("{;list}", variables) == ";list=red,green,blue"); 1115 assert(testExpand("{;list*}", variables) == ";list=red;list=green;list=blue"); 1116 assert(testExpand("{;keys}", variables) == ";keys=semi,%3B,dot,.,comma,%2C"); 1117 assert(testExpand("{;keys*}", variables) == ";semi=%3B;dot=.;comma=%2C"); 1118 1119 assert(testExpand("{?var:3}", variables) == "?var=val"); 1120 assert(testExpand("{?list}", variables) == "?list=red,green,blue"); 1121 assert(testExpand("{?list*}", variables) == "?list=red&list=green&list=blue"); 1122 assert(testExpand("{?keys}", variables) == "?keys=semi,%3B,dot,.,comma,%2C"); 1123 assert(testExpand("{?keys*}", variables) == "?semi=%3B&dot=.&comma=%2C"); 1124 1125 assert(testExpand("{&var:3}", variables) == "&var=val"); 1126 assert(testExpand("{&list}", variables) == "&list=red,green,blue"); 1127 assert(testExpand("{&list*}", variables) == "&list=red&list=green&list=blue"); 1128 assert(testExpand("{&keys}", variables) == "&keys=semi,%3B,dot,.,comma,%2C"); 1129 assert(testExpand("{&keys*}", variables) == "&semi=%3B&dot=.&comma=%2C"); 1130 } 1131 1132 // strict tests 1133 @safe unittest 1134 { 1135 import std.exception : assertThrown; 1136 1137 URIVariable[string] variables = [ 1138 "%2atest" : URIVariable("ok"), "emptylist" : URIVariable([""]), 1139 "emptymap" : URIVariable([tuple("foo", "")]) 1140 ]; 1141 1142 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{b", variables, false)); 1143 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{b", variables, true)); 1144 1145 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{}", variables, false)); 1146 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{}", variables, true)); 1147 1148 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{=test}", variables, false)); 1149 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{=test}", variables, true)); 1150 1151 // unknown operator 1152 assert(expandTemplateURIString("/a{'test}", variables, false) == "/a"); 1153 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{'test}", variables, true)); 1154 assert(expandTemplateURIString("/a{%2test}", variables, false) == "/a"); 1155 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{%2test}", variables, true)); 1156 assert(expandTemplateURIString("/a{?%2atest}", variables, false) == "/a?%2atest=ok"); 1157 assert(expandTemplateURIString("/a{?%2atest}", variables, true) == "/a?%2atest=ok"); 1158 1159 assert(expandTemplateURIString("/a%", variables, false) == "/a%"); 1160 assert(expandTemplateURIString("/a%az", variables, false) == "/a%az"); 1161 assert(expandTemplateURIString("/a%afb", variables, false) == "/a%afb"); 1162 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a%", variables, true)); 1163 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a%az", variables, true)); 1164 assert(expandTemplateURIString("/a%afb", variables, true) == "/a%afb"); 1165 1166 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?}", variables, true)); 1167 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a.}", variables, true)); 1168 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?.a}", variables, true)); 1169 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a$}", variables, true)); 1170 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{??}", variables, true)); 1171 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a:}", variables, true)); 1172 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a:10000}", 1173 variables, true)); 1174 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a:999a}", variables, true)); 1175 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a:a999}", variables, true)); 1176 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a:0999}", variables, true)); 1177 assertThrown!TemplateURIFormatException(expandTemplateURIString("/a{?a,,b}", variables, true)); 1178 1179 assert(testExpand("{&emptylist}", variables) == "&emptylist="); 1180 assert(testExpand("{&emptylist*}", variables) == "&emptylist="); 1181 assert(testExpand("{&emptymap}", variables) == "&emptymap=foo,"); 1182 assert(testExpand("{&emptymap*}", variables) == "&foo="); 1183 } 1184 1185 // full examples 1186 unittest 1187 { 1188 URIVariable[string] variables = [ 1189 "count" : URIVariable(["one", "two", "three"]), 1190 "dom" : URIVariable(["example", "com"]), "dub" : URIVariable("me/too"), 1191 "hello" : URIVariable("Hello World!"), "half" : URIVariable("50%"), 1192 "var" : URIVariable("value"), "who" : URIVariable("fred"), 1193 "base" : URIVariable("http://example.com/home/"), 1194 "path" : URIVariable("/foo/bar"), 1195 "list" : URIVariable(["red", "green", "blue"]), 1196 "keys" : URIVariable([ 1197 tuple("semi", ";"), tuple("dot", "."), tuple("comma", ",") 1198 ]), "v" : URIVariable("6"), "x" : URIVariable("1024"), 1199 "y" : URIVariable("768"), "empty" : URIVariable(""), 1200 "empty_keys" : URIVariable(cast(Tuple!(string, string)[])[]), 1201 ]; 1202 1203 // section 3.2.1 1204 assert(testExpand("{count}", variables) == "one,two,three"); 1205 assert(testExpand("{count*}", variables) == "one,two,three"); 1206 assert(testExpand("{/count}", variables) == "/one,two,three"); 1207 assert(testExpand("{/count*}", variables) == "/one/two/three"); 1208 assert(testExpand("{;count}", variables) == ";count=one,two,three"); 1209 assert(testExpand("{;count*}", variables) == ";count=one;count=two;count=three"); 1210 assert(testExpand("{?count}", variables) == "?count=one,two,three"); 1211 assert(testExpand("{?count*}", variables) == "?count=one&count=two&count=three"); 1212 assert(testExpand("{&count*}", variables) == "&count=one&count=two&count=three"); 1213 1214 // section 3.2.2 1215 assert(testExpand("{var}", variables) == "value"); 1216 assert(testExpand("{hello}", variables) == "Hello%20World%21"); 1217 assert(testExpand("{half}", variables) == "50%25"); 1218 assert(testExpand("O{empty}X", variables) == "OX"); 1219 assert(testExpand("O{undef}X", variables) == "OX"); 1220 assert(testExpand("{x,y}", variables) == "1024,768"); 1221 assert(testExpand("{x,hello,y}", variables) == "1024,Hello%20World%21,768"); 1222 assert(testExpand("?{x,empty}", variables) == "?1024,"); 1223 assert(testExpand("?{x,undef}", variables) == "?1024"); 1224 assert(testExpand("?{undef,y}", variables) == "?768"); 1225 assert(testExpand("{var:3}", variables) == "val"); 1226 assert(testExpand("{var:30}", variables) == "value"); 1227 assert(testExpand("{list}", variables) == "red,green,blue"); 1228 assert(testExpand("{list*}", variables) == "red,green,blue"); 1229 assert(testExpand("{keys}", variables) == "semi,%3B,dot,.,comma,%2C"); 1230 assert(testExpand("{keys*}", variables) == "semi=%3B,dot=.,comma=%2C"); 1231 1232 // section 3.2.3 1233 assert(testExpand("{+var}", variables) == "value"); 1234 assert(testExpand("{+hello}", variables) == "Hello%20World!"); 1235 assert(testExpand("{+half}", variables) == "50%25"); 1236 1237 assert(testExpand("{base}index", variables) == "http%3A%2F%2Fexample.com%2Fhome%2Findex"); 1238 assert(testExpand("{+base}index", variables) == "http://example.com/home/index"); 1239 assert(testExpand("O{+empty}X", variables) == "OX"); 1240 assert(testExpand("O{+undef}X", variables) == "OX"); 1241 1242 assert(testExpand("{+path}/here", variables) == "/foo/bar/here"); 1243 assert(testExpand("here?ref={+path}", variables) == "here?ref=/foo/bar"); 1244 assert(testExpand("up{+path}{var}/here", variables) == "up/foo/barvalue/here"); 1245 assert(testExpand("{+x,hello,y}", variables) == "1024,Hello%20World!,768"); 1246 assert(testExpand("{+path,x}/here", variables) == "/foo/bar,1024/here"); 1247 1248 assert(testExpand("{+path:6}/here", variables) == "/foo/b/here"); 1249 assert(testExpand("{+list}", variables) == "red,green,blue"); 1250 assert(testExpand("{+list*}", variables) == "red,green,blue"); 1251 assert(testExpand("{+keys}", variables) == "semi,;,dot,.,comma,,"); 1252 assert(testExpand("{+keys*}", variables) == "semi=;,dot=.,comma=,"); 1253 1254 // section 3.2.4 1255 assert(testExpand("{#var}", variables) == "#value"); 1256 assert(testExpand("{#hello}", variables) == "#Hello%20World!"); 1257 assert(testExpand("{#half}", variables) == "#50%25"); 1258 assert(testExpand("foo{#empty}", variables) == "foo#"); 1259 assert(testExpand("foo{#undef}", variables) == "foo"); 1260 assert(testExpand("{#x,hello,y}", variables) == "#1024,Hello%20World!,768"); 1261 assert(testExpand("{#path,x}/here", variables) == "#/foo/bar,1024/here"); 1262 assert(testExpand("{#path:6}/here", variables) == "#/foo/b/here"); 1263 assert(testExpand("{#list}", variables) == "#red,green,blue"); 1264 assert(testExpand("{#list*}", variables) == "#red,green,blue"); 1265 assert(testExpand("{#keys}", variables) == "#semi,;,dot,.,comma,,"); 1266 assert(testExpand("{#keys*}", variables) == "#semi=;,dot=.,comma=,"); 1267 1268 // section 3.2.5 1269 assert(testExpand("{.who}", variables) == ".fred"); 1270 assert(testExpand("{.who,who}", variables) == ".fred.fred"); 1271 assert(testExpand("{.half,who}", variables) == ".50%25.fred"); 1272 assert(testExpand("www{.dom*}", variables) == "www.example.com"); 1273 assert(testExpand("X{.var}", variables) == "X.value"); 1274 assert(testExpand("X{.empty}", variables) == "X."); 1275 assert(testExpand("X{.undef}", variables) == "X"); 1276 assert(testExpand("X{.var:3}", variables) == "X.val"); 1277 assert(testExpand("X{.list}", variables) == "X.red,green,blue"); 1278 assert(testExpand("X{.list*}", variables) == "X.red.green.blue"); 1279 assert(testExpand("X{.keys}", variables) == "X.semi,%3B,dot,.,comma,%2C"); 1280 assert(testExpand("X{.keys*}", variables) == "X.semi=%3B.dot=..comma=%2C"); 1281 assert(testExpand("X{.empty_keys}", variables) == "X"); 1282 assert(testExpand("X{.empty_keys*}", variables) == "X"); 1283 1284 // section 3.2.6 1285 assert(testExpand("{/who}", variables) == "/fred"); 1286 assert(testExpand("{/who,who}", variables) == "/fred/fred"); 1287 assert(testExpand("{/half,who}", variables) == "/50%25/fred"); 1288 assert(testExpand("{/who,dub}", variables) == "/fred/me%2Ftoo"); 1289 assert(testExpand("{/var}", variables) == "/value"); 1290 assert(testExpand("{/var,empty}", variables) == "/value/"); 1291 assert(testExpand("{/var,undef}", variables) == "/value"); 1292 assert(testExpand("{/var,x}/here", variables) == "/value/1024/here"); 1293 assert(testExpand("{/var:1,var}", variables) == "/v/value"); 1294 assert(testExpand("{/list}", variables) == "/red,green,blue"); 1295 assert(testExpand("{/list*}", variables) == "/red/green/blue"); 1296 assert(testExpand("{/list*,path:4}", variables) == "/red/green/blue/%2Ffoo"); 1297 assert(testExpand("{/keys}", variables) == "/semi,%3B,dot,.,comma,%2C"); 1298 assert(testExpand("{/keys*}", variables) == "/semi=%3B/dot=./comma=%2C"); 1299 1300 // section 3.2.7 1301 assert(testExpand("{;who}", variables) == ";who=fred"); 1302 assert(testExpand("{;half}", variables) == ";half=50%25"); 1303 assert(testExpand("{;empty}", variables) == ";empty"); 1304 assert(testExpand("{;v,empty,who}", variables) == ";v=6;empty;who=fred"); 1305 assert(testExpand("{;v,bar,who}", variables) == ";v=6;who=fred"); 1306 assert(testExpand("{;x,y}", variables) == ";x=1024;y=768"); 1307 assert(testExpand("{;x,y,empty}", variables) == ";x=1024;y=768;empty"); 1308 assert(testExpand("{;x,y,undef}", variables) == ";x=1024;y=768"); 1309 assert(testExpand("{;hello:5}", variables) == ";hello=Hello"); 1310 assert(testExpand("{;list}", variables) == ";list=red,green,blue"); 1311 assert(testExpand("{;list*}", variables) == ";list=red;list=green;list=blue"); 1312 assert(testExpand("{;keys}", variables) == ";keys=semi,%3B,dot,.,comma,%2C"); 1313 assert(testExpand("{;keys*}", variables) == ";semi=%3B;dot=.;comma=%2C"); 1314 1315 // section 3.2.8 1316 assert(testExpand("{?who}", variables) == "?who=fred"); 1317 assert(testExpand("{?half}", variables) == "?half=50%25"); 1318 assert(testExpand("{?x,y}", variables) == "?x=1024&y=768"); 1319 assert(testExpand("{?x,y,empty}", variables) == "?x=1024&y=768&empty="); 1320 assert(testExpand("{?x,y,undef}", variables) == "?x=1024&y=768"); 1321 assert(testExpand("{?var:3}", variables) == "?var=val"); 1322 assert(testExpand("{?list}", variables) == "?list=red,green,blue"); 1323 assert(testExpand("{?list*}", variables) == "?list=red&list=green&list=blue"); 1324 assert(testExpand("{?keys}", variables) == "?keys=semi,%3B,dot,.,comma,%2C"); 1325 assert(testExpand("{?keys*}", variables) == "?semi=%3B&dot=.&comma=%2C"); 1326 1327 // section 3.2.9 1328 assert(testExpand("{&who}", variables) == "&who=fred"); 1329 assert(testExpand("{&half}", variables) == "&half=50%25"); 1330 assert(testExpand("?fixed=yes{&x}", variables) == "?fixed=yes&x=1024"); 1331 assert(testExpand("{&x,y,empty}", variables) == "&x=1024&y=768&empty="); 1332 assert(testExpand("{&x,y,undef}", variables) == "&x=1024&y=768"); 1333 1334 assert(testExpand("{&var:3}", variables) == "&var=val"); 1335 assert(testExpand("{&list}", variables) == "&list=red,green,blue"); 1336 assert(testExpand("{&list*}", variables) == "&list=red&list=green&list=blue"); 1337 assert(testExpand("{&keys}", variables) == "&keys=semi,%3B,dot,.,comma,%2C"); 1338 assert(testExpand("{&keys*}", variables) == "&semi=%3B&dot=.&comma=%2C"); 1339 }