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