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