1 
2 //          Copyright Ferdinand Majerech 2011.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 /**
8  * Class that processes YAML mappings, sequences and scalars into nodes.
9  * This can be used to add custom data types. A tutorial can be found
10  * $(LINK2 https://dlang-community.github.io/D-YAML/, here).
11  */
12 module dyaml.constructor;
13 
14 
15 import std.array;
16 import std.algorithm;
17 import std.base64;
18 import std.container;
19 import std.conv;
20 import std.datetime;
21 import std.exception;
22 import std.regex;
23 import std..string;
24 import std.typecons;
25 import std.utf;
26 
27 import dyaml.node;
28 import dyaml.exception;
29 import dyaml.style;
30 
31 
32 // Exception thrown at constructor errors.
33 package class ConstructorException : YAMLException
34 {
35     /// Construct a ConstructorException.
36     ///
37     /// Params:  msg   = Error message.
38     ///          start = Start position of the error context.
39     ///          end   = End position of the error context.
40     this(string msg, Mark start, Mark end, string file = __FILE__, int line = __LINE__)
41         @safe pure nothrow
42     {
43         super(msg ~ "\nstart: " ~ start.toString() ~ "\nend: " ~ end.toString(),
44               file, line);
45     }
46 }
47 
48 /** Constructs YAML values.
49  *
50  * Each YAML scalar, sequence or mapping has a tag specifying its data type.
51  * Constructor uses user-specifyable functions to create a node of desired
52  * data type from a scalar, sequence or mapping.
53  *
54  *
55  * Each of these functions is associated with a tag, and can process either
56  * a scalar, a sequence, or a mapping. The constructor passes each value to
57  * the function with corresponding tag, which then returns the resulting value
58  * that can be stored in a node.
59  *
60  * If a tag is detected with no known constructor function, it is considered an error.
61  */
62 final class Constructor
63 {
64     private:
65         // Constructor functions from scalars.
66         Node delegate(ref Node) @safe[string] fromScalar_;
67         // Constructor functions from sequences.
68         Node delegate(ref Node) @safe[string] fromSequence_;
69         // Constructor functions from mappings.
70         Node delegate(ref Node) @safe[string] fromMapping_;
71 
72     public:
73         /// Construct a Constructor.
74         ///
75         /// If you don't want to support default YAML tags/data types, you can use
76         /// defaultConstructors to disable constructor functions for these.
77         ///
78         /// Params:  defaultConstructors = Use constructors for default YAML tags?
79         this(const Flag!"useDefaultConstructors" defaultConstructors = Yes.useDefaultConstructors)
80             @safe nothrow
81         {
82             if(!defaultConstructors){return;}
83 
84             addConstructorScalar("tag:yaml.org,2002:null",      &constructNull);
85             addConstructorScalar("tag:yaml.org,2002:bool",      &constructBool);
86             addConstructorScalar("tag:yaml.org,2002:int",       &constructLong);
87             addConstructorScalar("tag:yaml.org,2002:float",     &constructReal);
88             addConstructorScalar("tag:yaml.org,2002:binary",    &constructBinary);
89             addConstructorScalar("tag:yaml.org,2002:timestamp", &constructTimestamp);
90             addConstructorScalar("tag:yaml.org,2002:str",       &constructString);
91 
92             ///In a mapping, the default value is kept as an entry with the '=' key.
93             addConstructorScalar("tag:yaml.org,2002:value",     &constructString);
94 
95             addConstructorSequence("tag:yaml.org,2002:omap",    &constructOrderedMap);
96             addConstructorSequence("tag:yaml.org,2002:pairs",   &constructPairs);
97             addConstructorMapping("tag:yaml.org,2002:set",      &constructSet);
98             addConstructorSequence("tag:yaml.org,2002:seq",     &constructSequence);
99             addConstructorMapping("tag:yaml.org,2002:map",      &constructMap);
100             addConstructorScalar("tag:yaml.org,2002:merge",     &constructMerge);
101         }
102 
103         /** Add a constructor function from scalar.
104          *
105          * The function must take a reference to $(D Node) to construct from.
106          * The node contains a string for scalars, $(D Node[]) for sequences and
107          * $(D Node.Pair[]) for mappings.
108          *
109          * Any exception thrown by this function will be caught by D:YAML and
110          * its message will be added to a $(D YAMLException) that will also tell
111          * the user which type failed to construct, and position in the file.
112          *
113          *
114          * The value returned by this function will be stored in the resulting node.
115          *
116          * Only one constructor function can be set for one tag.
117          *
118          *
119          * Structs and classes must implement the $(D opCmp()) operator for D:YAML
120          * support. The signature of the operator that must be implemented
121          * is $(D const int opCmp(ref const MyStruct s)) for structs where
122          * $(I MyStruct) is the struct type, and $(D int opCmp(Object o)) for
123          * classes. Note that the class $(D opCmp()) should not alter the compared
124          * values - it is not const for compatibility reasons.
125          *
126          * Params:  tag  = Tag for the function to handle.
127          *          ctor = Constructor function.
128          */
129         void addConstructorScalar(T)(const string tag, T function(ref Node) @safe ctor)
130         {
131             const t = tag;
132             const deleg = addConstructor!T(t, ctor);
133             (*delegates!string)[t] = deleg;
134         }
135         ///
136         @safe unittest
137         {
138             static struct MyStruct
139             {
140                 int x, y, z;
141 
142                 //Any D:YAML type must have a custom opCmp operator.
143                 //This is used for ordering in mappings.
144                 int opCmp(ref const MyStruct s) const
145                 {
146                     if(x != s.x){return x - s.x;}
147                     if(y != s.y){return y - s.y;}
148                     if(z != s.z){return z - s.z;}
149                     return 0;
150                 }
151             }
152 
153             static MyStruct constructMyStructScalar(ref Node node) @safe
154             {
155                 //Guaranteed to be string as we construct from scalar.
156                 //!mystruct x:y:z
157                 auto parts = node.as!string().split(":");
158                 // If this throws, the D:YAML will handle it and throw a YAMLException.
159                 return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2]));
160             }
161 
162             import dyaml.loader : Loader;
163             auto loader = Loader.fromString("!mystruct 12:34:56");
164             auto constructor = new Constructor;
165             constructor.addConstructorScalar("!mystruct", &constructMyStructScalar);
166             loader.constructor = constructor;
167             Node node = loader.load();
168             assert(node.get!MyStruct == MyStruct(12, 34, 56));
169         }
170 
171         /** Add a constructor function from sequence.
172          *
173          * See_Also:    addConstructorScalar
174          */
175         void addConstructorSequence(T)(const string tag, T function(ref Node) @safe ctor)
176         {
177             const t = tag;
178             const deleg = addConstructor!T(t, ctor);
179             (*delegates!(Node[]))[t] = deleg;
180         }
181         ///
182         @safe unittest
183         {
184             static struct MyStruct
185             {
186                 int x, y, z;
187 
188                 //Any D:YAML type must have a custom opCmp operator.
189                 //This is used for ordering in mappings.
190                 int opCmp(ref const MyStruct s) const
191                 {
192                     if(x != s.x){return x - s.x;}
193                     if(y != s.y){return y - s.y;}
194                     if(z != s.z){return z - s.z;}
195                     return 0;
196                 }
197             }
198 
199             static MyStruct constructMyStructSequence(ref Node node) @safe
200             {
201                 //node is guaranteed to be sequence.
202                 //!mystruct [x, y, z]
203                 return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int);
204             }
205 
206             import dyaml.loader : Loader;
207             auto loader = Loader.fromString("!mystruct [1,2,3]");
208             auto constructor = new Constructor;
209             constructor.addConstructorSequence("!mystruct", &constructMyStructSequence);
210             loader.constructor = constructor;
211             Node node = loader.load();
212             assert(node.get!MyStruct == MyStruct(1, 2, 3));
213          }
214 
215         /** Add a constructor function from a mapping.
216          *
217          * See_Also:    addConstructorScalar
218          */
219         void addConstructorMapping(T)(const string tag, T function(ref Node) @safe ctor)
220         {
221             const t = tag;
222             const deleg = addConstructor!T(t, ctor);
223             (*delegates!(Node.Pair[]))[t] = deleg;
224         }
225         ///
226         @safe unittest {
227             static struct MyStruct
228             {
229                 int x, y, z;
230 
231                 //Any D:YAML type must have a custom opCmp operator.
232                 //This is used for ordering in mappings.
233                 int opCmp(ref const MyStruct s) const
234                 {
235                     if(x != s.x){return x - s.x;}
236                     if(y != s.y){return y - s.y;}
237                     if(z != s.z){return z - s.z;}
238                     return 0;
239                 }
240             }
241 
242             static MyStruct constructMyStructMapping(ref Node node) @safe
243             {
244                 //node is guaranteed to be mapping.
245                 //!mystruct {"x": x, "y": y, "z": z}
246                 return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int);
247             }
248 
249             import dyaml.loader : Loader;
250             auto loader = Loader.fromString(`!mystruct {"x": 11, "y": 22, "z": 33}`);
251             auto constructor = new Constructor;
252             constructor.addConstructorMapping("!mystruct", &constructMyStructMapping);
253             loader.constructor = constructor;
254             Node node = loader.load();
255             assert(node.get!MyStruct == MyStruct(11, 22, 33));
256         }
257 
258     package:
259         /*
260          * Construct a node.
261          *
262          * Params:  start = Start position of the node.
263          *          end   = End position of the node.
264          *          tag   = Tag (data type) of the node.
265          *          value = Value to construct node from (string, nodes or pairs).
266          *          style = Style of the node (scalar or collection style).
267          *
268          * Returns: Constructed node.
269          */
270         Node node(T, U)(const Mark start, const Mark end, const string tag,
271                         T value, U style) @safe
272             if((is(T : string) || is(T == Node[]) || is(T == Node.Pair[])) &&
273                (is(U : CollectionStyle) || is(U : ScalarStyle)))
274         {
275             enum type = is(T : string)       ? "scalar"   :
276                         is(T == Node[])      ? "sequence" :
277                         is(T == Node.Pair[]) ? "mapping"  :
278                                                "ERROR";
279             enforce((tag in *delegates!T) !is null,
280                     new ConstructorException("No constructor function from " ~ type ~
281                               " for tag " ~ tag, start, end));
282 
283             Node node = Node(value);
284             try
285             {
286                 static if(is(U : ScalarStyle))
287                 {
288                     auto newNode = (*delegates!T)[tag](node);
289                     newNode.startMark_ = start;
290                     newNode.scalarStyle = style;
291                     return newNode;
292                 }
293                 else static if(is(U : CollectionStyle))
294                 {
295                     auto newNode = (*delegates!T)[tag](node);
296                     newNode.startMark_ = start;
297                     newNode.collectionStyle = style;
298                     return newNode;
299                 }
300                 else static assert(false);
301             }
302             catch(Exception e)
303             {
304                 throw new ConstructorException("Error constructing " ~ typeid(T).toString()
305                                 ~ ":\n" ~ e.msg, start, end);
306             }
307         }
308 
309     private:
310         /*
311          * Add a constructor function.
312          *
313          * Params:  tag  = Tag for the function to handle.
314          *          ctor = Constructor function.
315          */
316         auto addConstructor(T)(const string tag, T function(ref Node) @safe ctor)
317         {
318             assert((tag in fromScalar_) is null &&
319                    (tag in fromSequence_) is null &&
320                    (tag in fromMapping_) is null,
321                    "Constructor function for tag " ~ tag ~ " is already " ~
322                    "specified. Can't specify another one.");
323 
324 
325             return (ref Node n) @safe
326             {
327                 return Node(ctor(n), tag);
328             };
329         }
330 
331         //Get the array of constructor functions for scalar, sequence or mapping.
332         @property auto delegates(T)()
333         {
334             static if(is(T : string))          {return &fromScalar_;}
335             else static if(is(T : Node[]))     {return &fromSequence_;}
336             else static if(is(T : Node.Pair[])){return &fromMapping_;}
337             else static assert(false);
338         }
339 }
340 
341 ///Construct a struct from a scalar
342 @safe unittest
343 {
344     static struct MyStruct
345     {
346         int x, y, z;
347 
348         int opCmp(ref const MyStruct s) const pure @safe nothrow
349         {
350             if(x != s.x){return x - s.x;}
351             if(y != s.y){return y - s.y;}
352             if(z != s.z){return z - s.z;}
353             return 0;
354         }
355     }
356 
357     static MyStruct constructMyStructScalar(ref Node node) @safe
358     {
359         // Guaranteed to be string as we construct from scalar.
360         auto parts = node.as!string().split(":");
361         return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2]));
362     }
363 
364     import dyaml.loader : Loader;
365     string data = "!mystruct 1:2:3";
366     auto loader = Loader.fromString(data);
367     auto constructor = new Constructor;
368     constructor.addConstructorScalar("!mystruct", &constructMyStructScalar);
369     loader.constructor = constructor;
370     Node node = loader.load();
371 
372     assert(node.as!MyStruct == MyStruct(1, 2, 3));
373 }
374 ///Construct a struct from a sequence
375 @safe unittest
376 {
377     static struct MyStruct
378     {
379         int x, y, z;
380 
381         int opCmp(ref const MyStruct s) const pure @safe nothrow
382         {
383             if(x != s.x){return x - s.x;}
384             if(y != s.y){return y - s.y;}
385             if(z != s.z){return z - s.z;}
386             return 0;
387         }
388     }
389     static MyStruct constructMyStructSequence(ref Node node) @safe
390     {
391         // node is guaranteed to be sequence.
392         return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int);
393     }
394 
395     import dyaml.loader : Loader;
396     string data = "!mystruct [1, 2, 3]";
397     auto loader = Loader.fromString(data);
398     auto constructor = new Constructor;
399     constructor.addConstructorSequence("!mystruct", &constructMyStructSequence);
400     loader.constructor = constructor;
401     Node node = loader.load();
402 
403     assert(node.as!MyStruct == MyStruct(1, 2, 3));
404 }
405 ///Construct a struct from a mapping
406 @safe unittest
407 {
408     static struct MyStruct
409     {
410         int x, y, z;
411 
412         int opCmp(ref const MyStruct s) const pure @safe nothrow
413         {
414             if(x != s.x){return x - s.x;}
415             if(y != s.y){return y - s.y;}
416             if(z != s.z){return z - s.z;}
417             return 0;
418         }
419     }
420     static MyStruct constructMyStructMapping(ref Node node) @safe
421     {
422         // node is guaranteed to be mapping.
423         return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int);
424     }
425 
426     import dyaml.loader : Loader;
427     string data = "!mystruct {x: 1, y: 2, z: 3}";
428     auto loader = Loader.fromString(data);
429     auto constructor = new Constructor;
430     constructor.addConstructorMapping("!mystruct", &constructMyStructMapping);
431     loader.constructor = constructor;
432     Node node = loader.load();
433 
434     assert(node.as!MyStruct == MyStruct(1, 2, 3));
435 }
436 
437 /// Construct a _null _node.
438 YAMLNull constructNull(ref Node node) @safe pure nothrow @nogc
439 {
440     return YAMLNull();
441 }
442 
443 /// Construct a merge _node - a _node that merges another _node into a mapping.
444 YAMLMerge constructMerge(ref Node node) @safe pure nothrow @nogc
445 {
446     return YAMLMerge();
447 }
448 
449 /// Construct a boolean _node.
450 bool constructBool(ref Node node) @safe
451 {
452     static yes = ["yes", "true", "on"];
453     static no = ["no", "false", "off"];
454     string value = node.as!string().toLower();
455     if(yes.canFind(value)){return true;}
456     if(no.canFind(value)) {return false;}
457     throw new Exception("Unable to parse boolean value: " ~ value);
458 }
459 
460 /// Construct an integer (long) _node.
461 long constructLong(ref Node node) @safe
462 {
463     string value = node.as!string().replace("_", "");
464     const char c = value[0];
465     const long sign = c != '-' ? 1 : -1;
466     if(c == '-' || c == '+')
467     {
468         value = value[1 .. $];
469     }
470 
471     enforce(value != "", new Exception("Unable to parse float value: " ~ value));
472 
473     long result;
474     try
475     {
476         //Zero.
477         if(value == "0")               {result = cast(long)0;}
478         //Binary.
479         else if(value.startsWith("0b")){result = sign * to!int(value[2 .. $], 2);}
480         //Hexadecimal.
481         else if(value.startsWith("0x")){result = sign * to!int(value[2 .. $], 16);}
482         //Octal.
483         else if(value[0] == '0')       {result = sign * to!int(value, 8);}
484         //Sexagesimal.
485         else if(value.canFind(":"))
486         {
487             long val;
488             long base = 1;
489             foreach_reverse(digit; value.split(":"))
490             {
491                 val += to!long(digit) * base;
492                 base *= 60;
493             }
494             result = sign * val;
495         }
496         //Decimal.
497         else{result = sign * to!long(value);}
498     }
499     catch(ConvException e)
500     {
501         throw new Exception("Unable to parse integer value: " ~ value);
502     }
503 
504     return result;
505 }
506 @safe unittest
507 {
508     long getLong(string str) @safe
509     {
510         auto node = Node(str);
511         return constructLong(node);
512     }
513 
514     string canonical   = "685230";
515     string decimal     = "+685_230";
516     string octal       = "02472256";
517     string hexadecimal = "0x_0A_74_AE";
518     string binary      = "0b1010_0111_0100_1010_1110";
519     string sexagesimal = "190:20:30";
520 
521     assert(685230 == getLong(canonical));
522     assert(685230 == getLong(decimal));
523     assert(685230 == getLong(octal));
524     assert(685230 == getLong(hexadecimal));
525     assert(685230 == getLong(binary));
526     assert(685230 == getLong(sexagesimal));
527 }
528 
529 /// Construct a floating point (real) _node.
530 real constructReal(ref Node node) @safe
531 {
532     string value = node.as!string().replace("_", "").toLower();
533     const char c = value[0];
534     const real sign = c != '-' ? 1.0 : -1.0;
535     if(c == '-' || c == '+')
536     {
537         value = value[1 .. $];
538     }
539 
540     enforce(value != "" && value != "nan" && value != "inf" && value != "-inf",
541             new Exception("Unable to parse float value: " ~ value));
542 
543     real result;
544     try
545     {
546         //Infinity.
547         if     (value == ".inf"){result = sign * real.infinity;}
548         //Not a Number.
549         else if(value == ".nan"){result = real.nan;}
550         //Sexagesimal.
551         else if(value.canFind(":"))
552         {
553             real val = 0.0;
554             real base = 1.0;
555             foreach_reverse(digit; value.split(":"))
556             {
557                 val += to!real(digit) * base;
558                 base *= 60.0;
559             }
560             result = sign * val;
561         }
562         //Plain floating point.
563         else{result = sign * to!real(value);}
564     }
565     catch(ConvException e)
566     {
567         throw new Exception("Unable to parse float value: \"" ~ value ~ "\"");
568     }
569 
570     return result;
571 }
572 @safe unittest
573 {
574     bool eq(real a, real b, real epsilon = 0.2) @safe
575     {
576         return a >= (b - epsilon) && a <= (b + epsilon);
577     }
578 
579     real getReal(string str) @safe
580     {
581         auto node = Node(str);
582         return constructReal(node);
583     }
584 
585     string canonical   = "6.8523015e+5";
586     string exponential = "685.230_15e+03";
587     string fixed       = "685_230.15";
588     string sexagesimal = "190:20:30.15";
589     string negativeInf = "-.inf";
590     string NaN         = ".NaN";
591 
592     assert(eq(685230.15, getReal(canonical)));
593     assert(eq(685230.15, getReal(exponential)));
594     assert(eq(685230.15, getReal(fixed)));
595     assert(eq(685230.15, getReal(sexagesimal)));
596     assert(eq(-real.infinity, getReal(negativeInf)));
597     assert(to!string(getReal(NaN)) == "nan");
598 }
599 
600 /// Construct a binary (base64) _node.
601 ubyte[] constructBinary(ref Node node) @safe
602 {
603     import std.ascii : newline;
604     import std.array : array;
605 
606     string value = node.as!string;
607     // For an unknown reason, this must be nested to work (compiler bug?).
608     try
609     {
610         return Base64.decode(value.representation.filter!(c => !newline.canFind(c)).array);
611     }
612     catch(Base64Exception e)
613     {
614         throw new Exception("Unable to decode base64 value: " ~ e.msg);
615     }
616 }
617 
618 @safe unittest
619 {
620     auto test = "The Answer: 42".representation;
621     char[] buffer;
622     buffer.length = 256;
623     string input = Base64.encode(test, buffer).idup;
624     auto node = Node(input);
625     const value = constructBinary(node);
626     assert(value == test);
627     assert(value == [84, 104, 101, 32, 65, 110, 115, 119, 101, 114, 58, 32, 52, 50]);
628 }
629 
630 /// Construct a timestamp (SysTime) _node.
631 SysTime constructTimestamp(ref Node node) @safe
632 {
633     string value = node.as!string;
634 
635     auto YMDRegexp = regex("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)");
636     auto HMSRegexp = regex("^[Tt \t]+([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(\\.[0-9]*)?");
637     auto TZRegexp  = regex("^[ \t]*Z|([-+][0-9][0-9]?)(:[0-9][0-9])?");
638 
639     try
640     {
641         // First, get year, month and day.
642         auto matches = match(value, YMDRegexp);
643 
644         enforce(!matches.empty,
645                 new Exception("Unable to parse timestamp value: " ~ value));
646 
647         auto captures = matches.front.captures;
648         const year  = to!int(captures[1]);
649         const month = to!int(captures[2]);
650         const day   = to!int(captures[3]);
651 
652         // If available, get hour, minute, second and fraction, if present.
653         value = matches.front.post;
654         matches  = match(value, HMSRegexp);
655         if(matches.empty)
656         {
657             return SysTime(DateTime(year, month, day), UTC());
658         }
659 
660         captures = matches.front.captures;
661         const hour            = to!int(captures[1]);
662         const minute          = to!int(captures[2]);
663         const second          = to!int(captures[3]);
664         const hectonanosecond = cast(int)(to!real("0" ~ captures[4]) * 10_000_000);
665 
666         // If available, get timezone.
667         value = matches.front.post;
668         matches = match(value, TZRegexp);
669         if(matches.empty || matches.front.captures[0] == "Z")
670         {
671             // No timezone.
672             return SysTime(DateTime(year, month, day, hour, minute, second),
673                            hectonanosecond.dur!"hnsecs", UTC());
674         }
675 
676         // We have a timezone, so parse it.
677         captures = matches.front.captures;
678         int sign    = 1;
679         int tzHours;
680         if(!captures[1].empty)
681         {
682             if(captures[1][0] == '-') {sign = -1;}
683             tzHours   = to!int(captures[1][1 .. $]);
684         }
685         const tzMinutes = (!captures[2].empty) ? to!int(captures[2][1 .. $]) : 0;
686         const tzOffset  = dur!"minutes"(sign * (60 * tzHours + tzMinutes));
687 
688         return SysTime(DateTime(year, month, day, hour, minute, second),
689                        hectonanosecond.dur!"hnsecs",
690                        new immutable SimpleTimeZone(tzOffset));
691     }
692     catch(ConvException e)
693     {
694         throw new Exception("Unable to parse timestamp value " ~ value ~ " : " ~ e.msg);
695     }
696     catch(DateTimeException e)
697     {
698         throw new Exception("Invalid timestamp value " ~ value ~ " : " ~ e.msg);
699     }
700 
701     assert(false, "This code should never be reached");
702 }
703 @safe unittest
704 {
705     string timestamp(string value)
706     {
707         auto node = Node(value);
708         return constructTimestamp(node).toISOString();
709     }
710 
711     string canonical      = "2001-12-15T02:59:43.1Z";
712     string iso8601        = "2001-12-14t21:59:43.10-05:00";
713     string spaceSeparated = "2001-12-14 21:59:43.10 -5";
714     string noTZ           = "2001-12-15 2:59:43.10";
715     string noFraction     = "2001-12-15 2:59:43";
716     string ymd            = "2002-12-14";
717 
718     assert(timestamp(canonical)      == "20011215T025943.1Z");
719     //avoiding float conversion errors
720     assert(timestamp(iso8601)        == "20011214T215943.0999999-05:00" ||
721            timestamp(iso8601)        == "20011214T215943.1-05:00");
722     assert(timestamp(spaceSeparated) == "20011214T215943.0999999-05:00" ||
723            timestamp(spaceSeparated) == "20011214T215943.1-05:00");
724     assert(timestamp(noTZ)           == "20011215T025943.0999999Z" ||
725            timestamp(noTZ)           == "20011215T025943.1Z");
726     assert(timestamp(noFraction)     == "20011215T025943Z");
727     assert(timestamp(ymd)            == "20021214T000000Z");
728 }
729 
730 /// Construct a string _node.
731 string constructString(ref Node node) @safe
732 {
733     return node.as!string;
734 }
735 
736 /// Convert a sequence of single-element mappings into a sequence of pairs.
737 Node.Pair[] getPairs(string type, Node[] nodes) @safe
738 {
739     Node.Pair[] pairs;
740     pairs.reserve(nodes.length);
741     foreach(ref node; nodes)
742     {
743         enforce(node.isMapping && node.length == 1,
744                 new Exception("While constructing " ~ type ~
745                               ", expected a mapping with single element"));
746 
747         pairs ~= node.as!(Node.Pair[]);
748     }
749 
750     return pairs;
751 }
752 
753 /// Construct an ordered map (ordered sequence of key:value pairs without duplicates) _node.
754 Node.Pair[] constructOrderedMap(ref Node node) @safe
755 {
756     auto pairs = getPairs("ordered map", node.as!(Node[]));
757 
758     //Detect duplicates.
759     //TODO this should be replaced by something with deterministic memory allocation.
760     auto keys = redBlackTree!Node();
761     foreach(ref pair; pairs)
762     {
763         enforce(!(pair.key in keys),
764                 new Exception("Duplicate entry in an ordered map: "
765                               ~ pair.key.debugString()));
766         keys.insert(pair.key);
767     }
768     return pairs;
769 }
770 @safe unittest
771 {
772     Node[] alternateTypes(uint length) @safe
773     {
774         Node[] pairs;
775         foreach(long i; 0 .. length)
776         {
777             auto pair = (i % 2) ? Node.Pair(i.to!string, i) : Node.Pair(i, i.to!string);
778             pairs ~= Node([pair]);
779         }
780         return pairs;
781     }
782 
783     Node[] sameType(uint length) @safe
784     {
785         Node[] pairs;
786         foreach(long i; 0 .. length)
787         {
788             auto pair = Node.Pair(i.to!string, i);
789             pairs ~= Node([pair]);
790         }
791         return pairs;
792     }
793 
794     bool hasDuplicates(Node[] nodes) @safe
795     {
796         auto node = Node(nodes);
797         return null !is collectException(constructOrderedMap(node));
798     }
799 
800     assert(hasDuplicates(alternateTypes(8) ~ alternateTypes(2)));
801     assert(!hasDuplicates(alternateTypes(8)));
802     assert(hasDuplicates(sameType(64) ~ sameType(16)));
803     assert(hasDuplicates(alternateTypes(64) ~ alternateTypes(16)));
804     assert(!hasDuplicates(sameType(64)));
805     assert(!hasDuplicates(alternateTypes(64)));
806 }
807 
808 /// Construct a pairs (ordered sequence of key: value pairs allowing duplicates) _node.
809 Node.Pair[] constructPairs(ref Node node) @safe
810 {
811     return getPairs("pairs", node.as!(Node[]));
812 }
813 
814 /// Construct a set _node.
815 Node[] constructSet(ref Node node) @safe
816 {
817     auto pairs = node.as!(Node.Pair[]);
818 
819     // In future, the map here should be replaced with something with deterministic
820     // memory allocation if possible.
821     // Detect duplicates.
822     ubyte[Node] map;
823     Node[] nodes;
824     nodes.reserve(pairs.length);
825     foreach(ref pair; pairs)
826     {
827         enforce((pair.key in map) is null, new Exception("Duplicate entry in a set"));
828         map[pair.key] = 0;
829         nodes ~= pair.key;
830     }
831 
832     return nodes;
833 }
834 @safe unittest
835 {
836     Node.Pair[] set(uint length) @safe
837     {
838         Node.Pair[] pairs;
839         foreach(long i; 0 .. length)
840         {
841             pairs ~= Node.Pair(i.to!string, YAMLNull());
842         }
843 
844         return pairs;
845     }
846 
847     auto DuplicatesShort   = set(8) ~ set(2);
848     auto noDuplicatesShort = set(8);
849     auto DuplicatesLong    = set(64) ~ set(4);
850     auto noDuplicatesLong  = set(64);
851 
852     bool eq(Node.Pair[] a, Node[] b)
853     {
854         if(a.length != b.length){return false;}
855         foreach(i; 0 .. a.length)
856         {
857             if(a[i].key != b[i])
858             {
859                 return false;
860             }
861         }
862         return true;
863     }
864 
865     auto nodeDuplicatesShort   = Node(DuplicatesShort.dup);
866     auto nodeNoDuplicatesShort = Node(noDuplicatesShort.dup);
867     auto nodeDuplicatesLong    = Node(DuplicatesLong.dup);
868     auto nodeNoDuplicatesLong  = Node(noDuplicatesLong.dup);
869 
870     assert(null !is collectException(constructSet(nodeDuplicatesShort)));
871     assert(null is  collectException(constructSet(nodeNoDuplicatesShort)));
872     assert(null !is collectException(constructSet(nodeDuplicatesLong)));
873     assert(null is  collectException(constructSet(nodeNoDuplicatesLong)));
874 }
875 
876 /// Construct a sequence (array) _node.
877 Node[] constructSequence(ref Node node) @safe
878 {
879     return node.as!(Node[]);
880 }
881 
882 /// Construct an unordered map (unordered set of key:value _pairs without duplicates) _node.
883 Node.Pair[] constructMap(ref Node node) @safe
884 {
885     auto pairs = node.as!(Node.Pair[]);
886     //Detect duplicates.
887     //TODO this should be replaced by something with deterministic memory allocation.
888     auto keys = redBlackTree!Node();
889     foreach(ref pair; pairs)
890     {
891         enforce(!(pair.key in keys),
892                 new Exception("Duplicate entry in a map: " ~ pair.key.debugString()));
893         keys.insert(pair.key);
894     }
895     return pairs;
896 }