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 }