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  * YAML node _representer. Prepares YAML nodes for output. A tutorial can be 
9  * found $(LINK2 ../tutorials/custom_types.html, here).    
10  *
11  * Code based on $(LINK2 http://www.pyyaml.org, PyYAML).
12  */
13 module dyaml.representer;
14 
15 
16 import std.algorithm;
17 import std.array;
18 import std.base64;
19 import std.container;
20 import std.conv;
21 import std.datetime;
22 import std.exception;
23 import std.format;
24 import std.math;
25 import std.typecons;
26 import std.string;
27 
28 import dyaml.exception;
29 import dyaml.node;
30 import dyaml.serializer;
31 import dyaml.style;
32 import dyaml.tag;
33 
34 
35 ///Exception thrown on Representer errors.
36 class RepresenterException : YAMLException
37 {
38     mixin ExceptionCtors;
39 }
40 
41 /**
42  * Represents YAML nodes as scalar, sequence and mapping nodes ready for output.
43  *
44  * This class is used to add support for dumping of custom data types.
45  *
46  * It can also override default node formatting styles for output.
47  */
48 final class Representer
49 {
50     private:
51         // Representer functions indexed by types.
52         Node function(ref Node, Representer)[TypeInfo] representers_;
53         // Default style for scalar nodes.
54         ScalarStyle defaultScalarStyle_ = ScalarStyle.Invalid;
55         // Default style for collection nodes.
56         CollectionStyle defaultCollectionStyle_ = CollectionStyle.Invalid;
57 
58     public:
59         @disable bool opEquals(ref Representer);
60         @disable int opCmp(ref Representer);
61 
62         /**
63          * Construct a Representer.
64          * 
65          * Params:  useDefaultRepresenters = Use default representer functions
66          *                                   for default YAML types? This can be
67          *                                   disabled to use custom representer
68          *                                   functions for default types.
69          */
70         this(const Flag!"useDefaultRepresenters" useDefaultRepresenters = Yes.useDefaultRepresenters) 
71             @safe pure
72         {
73             if(!useDefaultRepresenters){return;}
74             addRepresenter!YAMLNull(&representNull);
75             addRepresenter!string(&representString);
76             addRepresenter!(ubyte[])(&representBytes);
77             addRepresenter!bool(&representBool);
78             addRepresenter!long(&representLong);
79             addRepresenter!real(&representReal);
80             addRepresenter!(Node[])(&representNodes);
81             addRepresenter!(Node.Pair[])(&representPairs);
82             addRepresenter!SysTime(&representSysTime);
83         }
84 
85         ///Destroy the Representer.
86         pure @safe nothrow ~this()
87         {
88             representers_.destroy();
89             representers_ = null;
90         }
91 
92         ///Set default _style for scalars. If style is $(D ScalarStyle.Invalid), the _style is chosen automatically.
93         @property void defaultScalarStyle(ScalarStyle style) pure @safe nothrow
94         {
95             defaultScalarStyle_ = style;
96         }
97 
98         ///Set default _style for collections. If style is $(D CollectionStyle.Invalid), the _style is chosen automatically. 
99         @property void defaultCollectionStyle(CollectionStyle style) pure @safe nothrow
100         {
101             defaultCollectionStyle_ = style;
102         }
103 
104         /**
105          * Add a function to represent nodes with a specific data type.
106          *
107          * The representer function takes references to a $(D Node) storing the data
108          * type and to the $(D Representer). It returns the represented node and may
109          * throw a $(D RepresenterException). See the example for more information.
110          * 
111          *
112          * Only one function may be specified for one data type. Default data 
113          * types already have representer functions unless disabled in the
114          * $(D Representer) constructor.
115          *
116          *
117          * Structs and classes must implement the $(D opCmp()) operator for D:YAML 
118          * support. The signature of the operator that must be implemented 
119          * is $(D const int opCmp(ref const MyStruct s)) for structs where 
120          * $(I MyStruct) is the struct type, and $(D int opCmp(Object o)) for 
121          * classes. Note that the class $(D opCmp()) should not alter the compared
122          * values - it is not const for compatibility reasons.
123          *
124          * Params:  representer = Representer function to add.
125          *
126          * Examples:
127          *
128          * Representing a simple struct:
129          * --------------------
130          * import std.string;
131          *
132          * import dyaml.all;
133          *
134          * struct MyStruct
135          * {
136          *     int x, y, z;
137          *
138          *     //Any D:YAML type must have a custom opCmp operator.
139          *     //This is used for ordering in mappings.
140          *     const int opCmp(ref const MyStruct s)
141          *     {
142          *         if(x != s.x){return x - s.x;}
143          *         if(y != s.y){return y - s.y;}
144          *         if(z != s.z){return z - s.z;}
145          *         return 0;
146          *     }
147          * }
148          *
149          * Node representMyStruct(ref Node node, Representer representer)
150          * { 
151          *     //The node is guaranteed to be MyStruct as we add representer for MyStruct.
152          *     auto value = node.as!MyStruct;
153          *     //Using custom scalar format, x:y:z.
154          *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
155          *     //Representing as a scalar, with custom tag to specify this data type.
156          *     return representer.representScalar("!mystruct.tag", scalar);
157          * }
158          *
159          * void main()
160          * {
161          *     auto dumper = Dumper("file.yaml");
162          *     auto representer = new Representer;
163          *     representer.addRepresenter!MyStruct(&representMyStruct);
164          *     dumper.representer = representer;
165          *     dumper.dump(Node(MyStruct(1,2,3)));
166          * }
167          * --------------------
168          *
169          * Representing a class:
170          * --------------------
171          * import std.string;
172          *
173          * import dyaml.all;
174          *
175          * class MyClass
176          * {
177          *     int x, y, z;
178          *
179          *     this(int x, int y, int z)
180          *     {
181          *         this.x = x; 
182          *         this.y = y; 
183          *         this.z = z;
184          *     }
185          *
186          *     //Any D:YAML type must have a custom opCmp operator.
187          *     //This is used for ordering in mappings.
188          *     override int opCmp(Object o)
189          *     {
190          *         MyClass s = cast(MyClass)o;
191          *         if(s is null){return -1;}
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          *     ///Useful for Node.as!string .
199          *     override string toString()
200          *     {
201          *         return format("MyClass(%s, %s, %s)", x, y, z);
202          *     }
203          * }
204          *
205          * //Same as representMyStruct.
206          * Node representMyClass(ref Node node, Representer representer)
207          * { 
208          *     //The node is guaranteed to be MyClass as we add representer for MyClass.
209          *     auto value = node.as!MyClass;
210          *     //Using custom scalar format, x:y:z.
211          *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
212          *     //Representing as a scalar, with custom tag to specify this data type.
213          *     return representer.representScalar("!myclass.tag", scalar);
214          * }
215          *
216          * void main()
217          * {
218          *     auto dumper = Dumper("file.yaml");
219          *     auto representer = new Representer;
220          *     representer.addRepresenter!MyClass(&representMyClass);
221          *     dumper.representer = representer;
222          *     dumper.dump(Node(new MyClass(1,2,3)));
223          * }
224          * --------------------
225          */
226         void addRepresenter(T)(Node function(ref Node, Representer) representer) 
227             @trusted pure
228         {
229             assert((typeid(T) in representers_) is null, 
230                    "Representer function for data type " ~ T.stringof ~
231                    " already specified. Can't specify another one");
232             representers_[typeid(T)] = representer;
233         }
234 
235         //If profiling shows a bottleneck on tag construction in these 3 methods,
236         //we'll need to take Tag directly and have string based wrappers for 
237         //user code.
238 
239         /**
240          * Represent a _scalar with specified _tag.
241          *
242          * This is used by representer functions that produce scalars.
243          *
244          * Params:  tag    = Tag of the _scalar.
245          *          scalar = Scalar value.
246          *          style  = Style of the _scalar. If invalid, default _style will be used.
247          *                   If the node was loaded before, previous _style will always be used.
248          *
249          * Returns: The represented node.
250          *
251          * Example:
252          * --------------------
253          * struct MyStruct
254          * {
255          *     int x, y, z;
256          *
257          *     //Any D:YAML type must have a custom opCmp operator.
258          *     //This is used for ordering in mappings.
259          *     const int opCmp(ref const MyStruct s)
260          *     {
261          *         if(x != s.x){return x - s.x;}
262          *         if(y != s.y){return y - s.y;}
263          *         if(z != s.z){return z - s.z;}
264          *         return 0;
265          *     }        
266          * }
267          *
268          * Node representMyStruct(ref Node node, Representer representer)
269          * { 
270          *     auto value = node.as!MyStruct;
271          *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
272          *     return representer.representScalar("!mystruct.tag", scalar);
273          * }
274          * --------------------
275          */
276         Node representScalar(string tag, string scalar, 
277                              ScalarStyle style = ScalarStyle.Invalid) @trusted
278         {
279             if(style == ScalarStyle.Invalid){style = defaultScalarStyle_;}
280             return Node.rawNode(Node.Value(scalar), Mark(), Tag(tag), style,
281                                 CollectionStyle.Invalid);
282         }
283 
284         /**
285          * Represent a _sequence with specified _tag, representing children first.
286          *
287          * This is used by representer functions that produce sequences.
288          *
289          * Params:  tag      = Tag of the _sequence.
290          *          sequence = Sequence of nodes.
291          *          style    = Style of the _sequence. If invalid, default _style will be used.
292          *                     If the node was loaded before, previous _style will always be used.
293          *
294          * Returns: The represented node.
295          *
296          * Throws:  $(D RepresenterException) if a child could not be represented.
297          *
298          * Example:
299          * --------------------
300          * struct MyStruct
301          * {
302          *     int x, y, z;
303          *
304          *     //Any D:YAML type must have a custom opCmp operator.
305          *     //This is used for ordering in mappings.
306          *     const int opCmp(ref const MyStruct s)
307          *     {
308          *         if(x != s.x){return x - s.x;}
309          *         if(y != s.y){return y - s.y;}
310          *         if(z != s.z){return z - s.z;}
311          *         return 0;
312          *     }        
313          * }
314          *
315          * Node representMyStruct(ref Node node, Representer representer)
316          * { 
317          *     auto value = node.as!MyStruct;
318          *     auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
319          *     //use flow style
320          *     return representer.representSequence("!mystruct.tag", nodes,
321          *                                          CollectionStyle.Flow);
322          * }
323          * --------------------
324          */
325         Node representSequence(string tag, Node[] sequence, 
326                                CollectionStyle style = CollectionStyle.Invalid) @trusted
327         {
328             Node[] value;
329             value.length = sequence.length;
330 
331             auto bestStyle = CollectionStyle.Flow;
332             foreach(idx, ref item; sequence)
333             {
334                 value[idx] = representData(item);
335                 const isScalar = value[idx].isScalar;
336                 const s = value[idx].scalarStyle;
337                 if(!isScalar || (s != ScalarStyle.Invalid && s != ScalarStyle.Plain))
338                 {
339                     bestStyle = CollectionStyle.Block;
340                 }
341             }
342 
343             if(style == CollectionStyle.Invalid)
344             {
345                 style = defaultCollectionStyle_ != CollectionStyle.Invalid 
346                         ? defaultCollectionStyle_
347                         : bestStyle;
348             }
349             return Node.rawNode(Node.Value(value), Mark(), Tag(tag), 
350                                 ScalarStyle.Invalid, style);
351         }
352 
353         /**
354          * Represent a mapping with specified _tag, representing children first.
355          *
356          * This is used by representer functions that produce mappings.
357          *
358          * Params:  tag   = Tag of the mapping.
359          *          pairs = Key-value _pairs of the mapping.
360          *          style = Style of the mapping. If invalid, default _style will be used.
361          *                  If the node was loaded before, previous _style will always be used.
362          *
363          * Returns: The represented node.
364          *
365          * Throws:  $(D RepresenterException) if a child could not be represented.
366          *
367          * Example:
368          * --------------------
369          * struct MyStruct
370          * {
371          *     int x, y, z;
372          *
373          *     //Any D:YAML type must have a custom opCmp operator.
374          *     //This is used for ordering in mappings.
375          *     const int opCmp(ref const MyStruct s)
376          *     {
377          *         if(x != s.x){return x - s.x;}
378          *         if(y != s.y){return y - s.y;}
379          *         if(z != s.z){return z - s.z;}
380          *         return 0;
381          *     }        
382          * }
383          *
384          * Node representMyStruct(ref Node node, Representer representer)
385          * { 
386          *     auto value = node.as!MyStruct;
387          *     auto pairs = [Node.Pair("x", value.x), 
388          *                   Node.Pair("y", value.y), 
389          *                   Node.Pair("z", value.z)];
390          *     return representer.representMapping("!mystruct.tag", pairs);
391          * }
392          * --------------------
393          */
394         Node representMapping(string tag, Node.Pair[] pairs,
395                               CollectionStyle style = CollectionStyle.Invalid) @trusted
396         {
397             Node.Pair[] value;
398             value.length = pairs.length;
399 
400             auto bestStyle = CollectionStyle.Flow;
401             foreach(idx, ref pair; pairs)
402             {
403                 value[idx] = Node.Pair(representData(pair.key), representData(pair.value));
404                 const keyScalar = value[idx].key.isScalar;
405                 const valScalar = value[idx].value.isScalar;
406                 const keyStyle = value[idx].key.scalarStyle;
407                 const valStyle = value[idx].value.scalarStyle;
408                 if(!keyScalar ||
409                    (keyStyle != ScalarStyle.Invalid && keyStyle != ScalarStyle.Plain))
410                 {
411                     bestStyle = CollectionStyle.Block;
412                 }
413                 if(!valScalar ||
414                    (valStyle != ScalarStyle.Invalid && valStyle != ScalarStyle.Plain))
415                 {
416                     bestStyle = CollectionStyle.Block;
417                 }
418             }
419 
420             if(style == CollectionStyle.Invalid)
421             {
422                 style = defaultCollectionStyle_ != CollectionStyle.Invalid 
423                         ? defaultCollectionStyle_
424                         : bestStyle;
425             }
426             return Node.rawNode(Node.Value(value), Mark(), Tag(tag), 
427                                 ScalarStyle.Invalid, style);
428         }
429 
430     package:
431         //Represent a node based on its type, and return the represented result.
432         Node representData(ref Node data) @system
433         {
434             //User types are wrapped in YAMLObject.
435             auto type = data.isUserType ? data.as!YAMLObject.type : data.type;
436 
437             enforce((type in representers_) !is null,
438                     new RepresenterException("No representer function for type " 
439                                              ~ type.toString() ~ " , cannot represent."));
440             Node result = representers_[type](data, this);
441 
442             //Override tag if specified.
443             if(!data.tag_.isNull()){result.tag_ = data.tag_;}
444 
445             //Remember style if this was loaded before.
446             if(data.scalarStyle != ScalarStyle.Invalid)
447             {
448                 result.scalarStyle = data.scalarStyle;
449             }
450             if(data.collectionStyle != CollectionStyle.Invalid)
451             {
452                 result.collectionStyle = data.collectionStyle;
453             }
454             return result;
455         }
456 
457         //Represent a node, serializing with specified Serializer.
458         void represent(ref Serializer serializer, ref Node node) @trusted
459         {
460             auto data = representData(node);
461             serializer.serialize(data);
462         }
463 }
464 
465 
466 ///Represent a _null _node as a _null YAML value.
467 Node representNull(ref Node node, Representer representer) @safe
468 {
469     return representer.representScalar("tag:yaml.org,2002:null", "null");
470 }
471 
472 ///Represent a string _node as a string scalar.
473 Node representString(ref Node node, Representer representer) @safe
474 {
475     string value = node.as!string;
476     return value is null 
477            ? representNull(node, representer) 
478            : representer.representScalar("tag:yaml.org,2002:str", value);
479 }
480 
481 ///Represent a bytes _node as a binary scalar.
482 Node representBytes(ref Node node, Representer representer) @system
483 {
484     const ubyte[] value = node.as!(ubyte[]);
485     if(value is null){return representNull(node, representer);}
486     return representer.representScalar("tag:yaml.org,2002:binary", 
487                                        cast(string)Base64.encode(value),
488                                        ScalarStyle.Literal);
489 }
490 
491 ///Represent a bool _node as a bool scalar.
492 Node representBool(ref Node node, Representer representer) @safe
493 {
494     return representer.representScalar("tag:yaml.org,2002:bool", 
495                                        node.as!bool ? "true" : "false");
496 }
497 
498 ///Represent a long _node as an integer scalar.
499 Node representLong(ref Node node, Representer representer) @system
500 {
501     return representer.representScalar("tag:yaml.org,2002:int", 
502                                        to!string(node.as!long));
503 }
504 
505 ///Represent a real _node as a floating point scalar.
506 Node representReal(ref Node node, Representer representer) @system
507 {
508     real f = node.as!real;
509     string value = isNaN(f)                  ? ".nan":
510                    f == real.infinity        ? ".inf":
511                    f == -1.0 * real.infinity ? "-.inf":
512                    {auto a = appender!string();
513                     formattedWrite(a, "%12f", f);
514                     return a.data.strip();}();
515 
516     return representer.representScalar("tag:yaml.org,2002:float", value);
517 }
518 
519 ///Represent a SysTime _node as a timestamp.
520 Node representSysTime(ref Node node, Representer representer) @system
521 {
522     return representer.representScalar("tag:yaml.org,2002:timestamp", 
523                                        node.as!SysTime.toISOExtString());
524 }
525 
526 ///Represent a sequence _node as sequence/set.
527 Node representNodes(ref Node node, Representer representer) @safe
528 {
529     auto nodes = node.as!(Node[]);
530     if(node.tag_ == Tag("tag:yaml.org,2002:set"))
531     {
532         ///YAML sets are mapping with null values.
533         Node.Pair[] pairs;
534         pairs.length = nodes.length;
535         Node dummy;
536         foreach(idx, ref key; nodes)
537         {
538             pairs[idx] = Node.Pair(key, representNull(dummy, representer));
539         }
540         return representer.representMapping(node.tag_.get, pairs);
541     }
542     else
543     {
544         return representer.representSequence("tag:yaml.org,2002:seq", nodes);
545     }
546 }
547 
548 ///Represent a mapping _node as map/ordered map/pairs.
549 Node representPairs(ref Node node, Representer representer) @system
550 {
551     auto pairs = node.as!(Node.Pair[]);
552 
553     bool hasDuplicates(Node.Pair[] pairs)
554     {
555         //TODO this should be replaced by something with deterministic memory allocation.
556         auto keys = redBlackTree!Node();
557         scope(exit){keys.destroy();}
558         foreach(ref pair; pairs)
559         {
560             if(pair.key in keys){return true;}
561             keys.insert(pair.key);
562         }
563         return false;
564     }
565 
566     Node[] mapToSequence(Node.Pair[] pairs)
567     {
568         Node[] nodes;
569         nodes.length = pairs.length;
570         foreach(idx, ref pair; pairs)
571         {
572             nodes[idx] = representer.representMapping("tag:yaml.org,2002:map", [pair]);
573         }
574         return nodes;
575     }
576 
577     if(node.tag_ == Tag("tag:yaml.org,2002:omap"))
578     {
579         enforce(!hasDuplicates(pairs),
580                 new RepresenterException("Duplicate entry in an ordered map"));
581         return representer.representSequence(node.tag_.get, mapToSequence(pairs));
582     }
583     else if(node.tag_ == Tag("tag:yaml.org,2002:pairs"))
584     {
585         return representer.representSequence(node.tag_.get, mapToSequence(pairs));
586     }
587     else
588     {
589         enforce(!hasDuplicates(pairs),
590                 new RepresenterException("Duplicate entry in an unordered map"));
591         return representer.representMapping("tag:yaml.org,2002:map", pairs);
592     }
593 }
594 
595 //Unittests
596 //These should really all be encapsulated in unittests.
597 private:
598 
599 import dyaml.dumper;
600 
601 struct MyStruct
602 {
603     int x, y, z;
604 
605     int opCmp(ref const MyStruct s) const pure @safe nothrow
606     {
607         if(x != s.x){return x - s.x;}
608         if(y != s.y){return y - s.y;}
609         if(z != s.z){return z - s.z;}
610         return 0;
611     }        
612 }
613 
614 Node representMyStruct(ref Node node, Representer representer) @system
615 { 
616     //The node is guaranteed to be MyStruct as we add representer for MyStruct.
617     auto value = node.as!MyStruct;
618     //Using custom scalar format, x:y:z.
619     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
620     //Representing as a scalar, with custom tag to specify this data type.
621     return representer.representScalar("!mystruct.tag", scalar);
622 }
623 
624 Node representMyStructSeq(ref Node node, Representer representer) @safe
625 { 
626     auto value = node.as!MyStruct;
627     auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
628     return representer.representSequence("!mystruct.tag", nodes);
629 }
630 
631 Node representMyStructMap(ref Node node, Representer representer) @safe
632 { 
633     auto value = node.as!MyStruct;
634     auto pairs = [Node.Pair("x", value.x), 
635                   Node.Pair("y", value.y), 
636                   Node.Pair("z", value.z)];
637     return representer.representMapping("!mystruct.tag", pairs);
638 }
639 
640 class MyClass
641 {
642     int x, y, z;
643 
644     this(int x, int y, int z) pure @safe nothrow
645     {
646         this.x = x; 
647         this.y = y; 
648         this.z = z;
649     }
650     
651     override int opCmp(Object o) pure @safe nothrow
652     {
653         MyClass s = cast(MyClass)o;
654         if(s is null){return -1;}
655         if(x != s.x){return x - s.x;}
656         if(y != s.y){return y - s.y;}
657         if(z != s.z){return z - s.z;}
658         return 0;
659     }
660 
661     ///Useful for Node.as!string .
662     override string toString() @trusted
663     {
664         return format("MyClass(%s, %s, %s)", x, y, z);
665     }
666 }
667 
668 //Same as representMyStruct.
669 Node representMyClass(ref Node node, Representer representer) @system
670 { 
671     //The node is guaranteed to be MyClass as we add representer for MyClass.
672     auto value = node.as!MyClass;
673     //Using custom scalar format, x:y:z.
674     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
675     //Representing as a scalar, with custom tag to specify this data type.
676     return representer.representScalar("!myclass.tag", scalar);
677 }
678 
679 import dyaml.stream;
680 
681 unittest
682 {
683     foreach(r; [&representMyStruct, 
684                 &representMyStructSeq, 
685                 &representMyStructMap])
686     {
687         auto dumper = Dumper(new YMemoryStream());
688         auto representer = new Representer;
689         representer.addRepresenter!MyStruct(r);
690         dumper.representer = representer;
691         dumper.dump(Node(MyStruct(1,2,3)));
692     }
693 }
694 
695 unittest
696 {
697     auto dumper = Dumper(new YMemoryStream());
698     auto representer = new Representer;
699     representer.addRepresenter!MyClass(&representMyClass);
700     dumper.representer = representer;
701     dumper.dump(Node(new MyClass(1,2,3)));
702 }