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