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 mir.conv;
17 import mir.math;
18 import mir.timestamp;
19 import std.algorithm;
20 import std.array;
21 import std.base64;
22 import std.container;
23 import std.exception;
24 import std.format;
25 import std..string;
26 
27 import dyaml.exception;
28 import dyaml.node;
29 import dyaml.serializer;
30 import dyaml.style;
31 
32 package:
33 ///Exception thrown on Representer errors.
34 class RepresenterException : YAMLException
35 {
36     mixin ExceptionCtors;
37 }
38 
39 /**
40  * Represents YAML nodes as scalar, sequence and mapping nodes ready for output.
41  */
42 Node representData(const Node data, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
43 {
44     Node result;
45     final switch(data.type)
46     {
47         case NodeType.null_:
48             result = representNull();
49             break;
50         case NodeType.merge:
51             break;
52         case NodeType.boolean:
53             result = representBool(data);
54             break;
55         case NodeType.integer:
56             result = representLong(data);
57             break;
58         case NodeType.decimal:
59             result = representReal(data);
60             break;
61         case NodeType.binary:
62             result = representBytes(data);
63             break;
64         case NodeType.timestamp:
65             result = representTimestamp(data);
66             break;
67         case NodeType..string:
68             result = representString(data);
69             break;
70         case NodeType.mapping:
71             result = representPairs(data, defaultScalarStyle, defaultCollectionStyle);
72             break;
73         case NodeType.sequence:
74             result = representNodes(data, defaultScalarStyle, defaultCollectionStyle);
75             break;
76     }
77 
78     final switch (result.nodeID)
79     {
80         case NodeID.scalar:
81             if (result.scalarStyle == ScalarStyle.invalid)
82             {
83                 result.scalarStyle = defaultScalarStyle;
84             }
85             break;
86         case NodeID.sequence, NodeID.mapping:
87             if (defaultCollectionStyle != CollectionStyle.invalid)
88             {
89                 result.collectionStyle = defaultCollectionStyle;
90             }
91         case NodeID.invalid:
92     }
93 
94 
95     //Override tag if specified.
96     if(data.tag_ !is null){result.tag_ = data.tag_;}
97 
98     //Remember style if this was loaded before.
99     if(data.scalarStyle != ScalarStyle.invalid)
100     {
101         result.scalarStyle = data.scalarStyle;
102     }
103     if(data.collectionStyle != CollectionStyle.invalid)
104     {
105         result.collectionStyle = data.collectionStyle;
106     }
107     return result;
108 }
109 
110 @safe unittest
111 {
112     // We don't emit yaml merge nodes.
113     assert(representData(Node(YAMLMerge()), ScalarStyle.invalid, CollectionStyle.invalid) == Node.init);
114 }
115 
116 @safe unittest
117 {
118     assert(representData(Node(null), ScalarStyle.invalid, CollectionStyle.invalid) == Node("null", "tag:yaml.org,2002:null"));
119 }
120 
121 @safe unittest
122 {
123     assert(representData(Node(cast(string)null), ScalarStyle.invalid, CollectionStyle.invalid) == Node("null", "tag:yaml.org,2002:null"));
124     assert(representData(Node("Hello world!"), ScalarStyle.invalid, CollectionStyle.invalid) == Node("Hello world!", "tag:yaml.org,2002:str"));
125 }
126 
127 @safe unittest
128 {
129     assert(representData(Node(64), ScalarStyle.invalid, CollectionStyle.invalid) == Node("64", "tag:yaml.org,2002:int"));
130 }
131 
132 @safe unittest
133 {
134     assert(representData(Node(true), ScalarStyle.invalid, CollectionStyle.invalid) == Node("true", "tag:yaml.org,2002:bool"));
135     assert(representData(Node(false), ScalarStyle.invalid, CollectionStyle.invalid) == Node("false", "tag:yaml.org,2002:bool"));
136 }
137 
138 @safe unittest
139 {
140     // Float comparison is pretty unreliable...
141     auto result = representData(Node(1.0), ScalarStyle.invalid, CollectionStyle.invalid);
142     assert(approxEqual(result.as!string.to!double, 1.0));
143     assert(result.tag == "tag:yaml.org,2002:float");
144 
145     assert(representData(Node(double.nan), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".nan", "tag:yaml.org,2002:float"));
146     assert(representData(Node(double.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".inf", "tag:yaml.org,2002:float"));
147     assert(representData(Node(-double.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node("-.inf", "tag:yaml.org,2002:float"));
148 }
149 
150 unittest
151 {
152     import mir.conv;
153     assert(representData(Node(Timestamp(2000, 3, 14, 12, 34, 56)), ScalarStyle.invalid, CollectionStyle.invalid) == Node("2000-03-14T12:34:56Z", "tag:yaml.org,2002:timestamp"));
154 }
155 
156 @safe unittest
157 {
158     assert(representData(Node(Node[].init, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:set"));
159     assert(representData(Node(Node[].init, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:seq"));
160     {
161         auto nodes = [
162             Node("a"),
163             Node("b"),
164             Node("c"),
165         ];
166         assert(representData(Node(nodes, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) ==
167             Node([
168                 Node.Pair(
169                     Node("a", "tag:yaml.org,2002:str"),
170                     Node("null", "tag:yaml.org,2002:null")
171                 ),
172                 Node.Pair(
173                     Node("b", "tag:yaml.org,2002:str"),
174                     Node("null", "tag:yaml.org,2002:null")
175                 ),
176                 Node.Pair(
177                     Node("c", "tag:yaml.org,2002:str"),
178                     Node("null", "tag:yaml.org,2002:null")
179                 )
180             ], "tag:yaml.org,2002:set"));
181     }
182     {
183         auto nodes = [
184             Node("a"),
185             Node("b"),
186             Node("c"),
187         ];
188         assert(representData(Node(nodes, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) ==
189             Node([
190                 Node("a", "tag:yaml.org,2002:str"),
191                 Node("b", "tag:yaml.org,2002:str"),
192                 Node("c", "tag:yaml.org,2002:str")
193             ], "tag:yaml.org,2002:seq"));
194     }
195 }
196 
197 @safe unittest
198 {
199     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:omap"));
200     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:pairs"));
201     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:map"));
202     {
203         auto nodes = [
204             Node.Pair("a", "b"),
205             Node.Pair("a", "c")
206         ];
207         assertThrown(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid));
208     }
209     // Yeah, this gets ugly really fast.
210     {
211         auto nodes = [
212             Node.Pair("a", "b"),
213             Node.Pair("a", "c")
214         ];
215         assert(representData(Node(nodes, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) ==
216             Node([
217                 Node(
218                     [Node.Pair(
219                         Node("a", "tag:yaml.org,2002:str"),
220                         Node("b", "tag:yaml.org,2002:str")
221                     )],
222                 "tag:yaml.org,2002:map"),
223                 Node(
224                     [Node.Pair(
225                         Node("a", "tag:yaml.org,2002:str"),
226                         Node("c", "tag:yaml.org,2002:str")
227                     )],
228                 "tag:yaml.org,2002:map"),
229         ], "tag:yaml.org,2002:pairs"));
230     }
231     {
232         auto nodes = [
233             Node.Pair("a", "b"),
234             Node.Pair("a", "c")
235         ];
236         assertThrown(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid));
237     }
238     {
239         auto nodes = [
240             Node.Pair("a", "b"),
241             Node.Pair("c", "d")
242         ];
243         assert(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) ==
244             Node([
245                 Node([
246                     Node.Pair(
247                         Node("a", "tag:yaml.org,2002:str"),
248                         Node("b", "tag:yaml.org,2002:str")
249                     )
250                 ], "tag:yaml.org,2002:map"),
251                 Node([
252                     Node.Pair(
253                         Node("c", "tag:yaml.org,2002:str"),
254                         Node("d", "tag:yaml.org,2002:str")
255                     )
256                 ], "tag:yaml.org,2002:map"
257             )], "tag:yaml.org,2002:omap"));
258     }
259     {
260         auto nodes = [
261             Node.Pair("a", "b"),
262             Node.Pair("c", "d")
263         ];
264         assert(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) ==
265             Node([
266                 Node.Pair(
267                     Node("a", "tag:yaml.org,2002:str"),
268                     Node("b", "tag:yaml.org,2002:str")
269                 ),
270                 Node.Pair(
271                     Node("c", "tag:yaml.org,2002:str"),
272                     Node("d", "tag:yaml.org,2002:str")
273                 ),
274             ], "tag:yaml.org,2002:map"));
275     }
276 }
277 
278 private:
279 
280 //Represent a _null _node as a _null YAML value.
281 Node representNull() @safe
282 {
283     return Node("null", "tag:yaml.org,2002:null");
284 }
285 
286 //Represent a string _node as a string scalar.
287 Node representString(const Node node) @safe
288 {
289     string value = node.as!string;
290     return value is null
291            ? Node("null", "tag:yaml.org,2002:null")
292            : Node(value, "tag:yaml.org,2002:str");
293 }
294 
295 //Represent a bytes _node as a binary scalar.
296 Node representBytes(const Node node) @safe
297 {
298     const ubyte[] value = node.as!(ubyte[]);
299     if(value is null){return Node("null", "tag:yaml.org,2002:null");}
300 
301     auto newNode = Node(Base64.encode(value).idup, "tag:yaml.org,2002:binary");
302     newNode.scalarStyle = ScalarStyle.literal;
303     return newNode;
304 }
305 
306 //Represent a bool _node as a bool scalar.
307 Node representBool(const Node node) @safe
308 {
309     return Node(node.as!bool ? "true" : "false", "tag:yaml.org,2002:bool");
310 }
311 
312 //Represent a long _node as an integer scalar.
313 Node representLong(const Node node) @safe
314 {
315     return Node(node.as!long.to!string, "tag:yaml.org,2002:int");
316 }
317 
318 //Represent a double _node as a floating point scalar.
319 Node representReal(const Node node) @safe
320 {
321     import mir.conv: to;
322     double f = node.as!double;
323     string value = f != f                    ? ".nan":
324                    f == double.infinity        ? ".inf":
325                    f == -1.0 * double.infinity ? "-.inf":
326                    f.to!string;
327     return Node(value, "tag:yaml.org,2002:float");
328 }
329 
330 //Represent a _node as a timestamp.
331 Node representTimestamp(const Node node) @safe
332 {
333     return Node(node.as!Timestamp.toISOExtString(), "tag:yaml.org,2002:timestamp");
334 }
335 
336 //Represent a sequence _node as sequence/set.
337 Node representNodes(const Node node, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
338 {
339     auto nodes = node.as!(Node[]);
340     if(node.tag_ == "tag:yaml.org,2002:set")
341     {
342         //YAML sets are mapping with null values.
343         Node.Pair[] pairs;
344         pairs.length = nodes.length;
345 
346         foreach(idx, key; nodes)
347         {
348             pairs[idx] = Node.Pair(key, Node("null", "tag:yaml.org,2002:null"));
349         }
350         Node.Pair[] value;
351         value.length = pairs.length;
352 
353         auto bestStyle = CollectionStyle.flow;
354         foreach(idx, pair; pairs)
355         {
356             value[idx] = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
357             if(value[idx].shouldUseBlockStyle)
358             {
359                 bestStyle = CollectionStyle.block;
360             }
361         }
362 
363         auto newNode = Node(value, node.tag_);
364         newNode.collectionStyle = bestStyle;
365         return newNode;
366     }
367     else
368     {
369         Node[] value;
370         value.length = nodes.length;
371 
372         auto bestStyle = CollectionStyle.flow;
373         foreach(idx, item; nodes)
374         {
375             value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
376             const isScalar = value[idx].nodeID == NodeID.scalar;
377             const s = value[idx].scalarStyle;
378             if(!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain))
379             {
380                 bestStyle = CollectionStyle.block;
381             }
382         }
383 
384         auto newNode = Node(value, "tag:yaml.org,2002:seq");
385         newNode.collectionStyle = bestStyle;
386         return newNode;
387     }
388 }
389 
390 bool shouldUseBlockStyle(const Node value) @safe
391 {
392     const isScalar = value.nodeID == NodeID.scalar;
393     const s = value.scalarStyle;
394     return (!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain));
395 }
396 bool shouldUseBlockStyle(const Node.Pair value) @safe
397 {
398     const keyScalar = value.key.nodeID == NodeID.scalar;
399     const valScalar = value.value.nodeID == NodeID.scalar;
400     const keyStyle = value.key.scalarStyle;
401     const valStyle = value.value.scalarStyle;
402     if(!keyScalar ||
403        (keyStyle != ScalarStyle.invalid && keyStyle != ScalarStyle.plain))
404     {
405         return true;
406     }
407     if(!valScalar ||
408        (valStyle != ScalarStyle.invalid && valStyle != ScalarStyle.plain))
409     {
410         return true;
411     }
412     return false;
413 }
414 
415 //Represent a mapping _node as map/ordered map/pairs.
416 Node representPairs(const Node node, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
417 {
418     auto pairs = node.as!(Node.Pair[]);
419 
420     bool hasDuplicates(const Node.Pair[] pairs) @safe
421     {
422         //TODO this should be replaced by something with deterministic memory allocation.
423         auto keys = redBlackTree!Node();
424         foreach(pair; pairs)
425         {
426             if(pair.key in keys){return true;}
427             keys.insert(pair.key);
428         }
429         return false;
430     }
431 
432     Node[] mapToSequence(const Node.Pair[] pairs) @safe
433     {
434         Node[] nodes;
435         nodes.length = pairs.length;
436         foreach(idx, pair; pairs)
437         {
438             Node.Pair value;
439 
440             auto bestStyle = value.shouldUseBlockStyle ? CollectionStyle.block : CollectionStyle.flow;
441             value = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
442 
443             auto newNode = Node([value], "tag:yaml.org,2002:map");
444             newNode.collectionStyle = bestStyle;
445             nodes[idx] = newNode;
446         }
447         return nodes;
448     }
449 
450     if(node.tag_ == "tag:yaml.org,2002:omap")
451     {
452         enforce(!hasDuplicates(pairs),
453                 new RepresenterException("Duplicate entry in an ordered map"));
454         auto sequence = mapToSequence(pairs);
455         Node[] value;
456         value.length = sequence.length;
457 
458         auto bestStyle = CollectionStyle.flow;
459         foreach(idx, item; sequence)
460         {
461             value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
462             if(value[idx].shouldUseBlockStyle)
463             {
464                 bestStyle = CollectionStyle.block;
465             }
466         }
467 
468         auto newNode = Node(value, node.tag_);
469         newNode.collectionStyle = bestStyle;
470         return newNode;
471     }
472     else if(node.tag_ == "tag:yaml.org,2002:pairs")
473     {
474         auto sequence = mapToSequence(pairs);
475         Node[] value;
476         value.length = sequence.length;
477 
478         auto bestStyle = CollectionStyle.flow;
479         foreach(idx, item; sequence)
480         {
481             value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
482             if(value[idx].shouldUseBlockStyle)
483             {
484                 bestStyle = CollectionStyle.block;
485             }
486         }
487 
488         auto newNode = Node(value, node.tag_);
489         newNode.collectionStyle = bestStyle;
490         return newNode;
491     }
492     else
493     {
494         enforce(!hasDuplicates(pairs),
495                 new RepresenterException("Duplicate entry in an unordered map"));
496         Node.Pair[] value;
497         value.length = pairs.length;
498 
499         auto bestStyle = CollectionStyle.flow;
500         foreach(idx, pair; pairs)
501         {
502             value[idx] = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
503             if(value[idx].shouldUseBlockStyle)
504             {
505                 bestStyle = CollectionStyle.block;
506             }
507         }
508 
509         auto newNode = Node(value, "tag:yaml.org,2002:map");
510         newNode.collectionStyle = bestStyle;
511         return newNode;
512     }
513 }