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             break;
95         case NodeID.invalid:
96             break;
97     }
98 
99 
100     //Override tag if specified.
101     if(data.tag_ !is null){result.tag_ = data.tag_;}
102 
103     //Remember style if this was loaded before.
104     if(data.scalarStyle != ScalarStyle.invalid)
105     {
106         result.scalarStyle = data.scalarStyle;
107     }
108     if(data.collectionStyle != CollectionStyle.invalid)
109     {
110         result.collectionStyle = data.collectionStyle;
111     }
112     return result;
113 }
114 
115 @safe unittest
116 {
117     // We don't emit yaml merge nodes.
118     assert(representData(Node(YAMLMerge()), ScalarStyle.invalid, CollectionStyle.invalid) == Node.init);
119 }
120 
121 @safe unittest
122 {
123     assert(representData(Node(YAMLNull()), ScalarStyle.invalid, CollectionStyle.invalid) == Node("null", "tag:yaml.org,2002:null"));
124 }
125 
126 @safe unittest
127 {
128     assert(representData(Node(cast(string)null), ScalarStyle.invalid, CollectionStyle.invalid) == Node("", "tag:yaml.org,2002:str"));
129     assert(representData(Node("Hello world!"), ScalarStyle.invalid, CollectionStyle.invalid) == Node("Hello world!", "tag:yaml.org,2002:str"));
130 }
131 
132 @safe unittest
133 {
134     assert(representData(Node(64), ScalarStyle.invalid, CollectionStyle.invalid) == Node("64", "tag:yaml.org,2002:int"));
135 }
136 
137 @safe unittest
138 {
139     assert(representData(Node(true), ScalarStyle.invalid, CollectionStyle.invalid) == Node("true", "tag:yaml.org,2002:bool"));
140     assert(representData(Node(false), ScalarStyle.invalid, CollectionStyle.invalid) == Node("false", "tag:yaml.org,2002:bool"));
141 }
142 
143 @safe unittest
144 {
145     // Float comparison is pretty unreliable...
146     auto result = representData(Node(1.0), ScalarStyle.invalid, CollectionStyle.invalid);
147     assert(isClose(result.as!string.to!real, 1.0));
148     assert(result.tag == "tag:yaml.org,2002:float");
149 
150     assert(representData(Node(real.nan), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".nan", "tag:yaml.org,2002:float"));
151     assert(representData(Node(real.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".inf", "tag:yaml.org,2002:float"));
152     assert(representData(Node(-real.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node("-.inf", "tag:yaml.org,2002:float"));
153 }
154 
155 @safe unittest
156 {
157     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"));
158 }
159 
160 @safe unittest
161 {
162     assert(representData(Node(Node[].init, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:set"));
163     assert(representData(Node(Node[].init, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:seq"));
164     {
165         auto nodes = [
166             Node("a"),
167             Node("b"),
168             Node("c"),
169         ];
170         assert(representData(Node(nodes, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) ==
171             Node([
172                 Node.Pair(
173                     Node("a", "tag:yaml.org,2002:str"),
174                     Node("null", "tag:yaml.org,2002:null")
175                 ),
176                 Node.Pair(
177                     Node("b", "tag:yaml.org,2002:str"),
178                     Node("null", "tag:yaml.org,2002:null")
179                 ),
180                 Node.Pair(
181                     Node("c", "tag:yaml.org,2002:str"),
182                     Node("null", "tag:yaml.org,2002:null")
183                 )
184             ], "tag:yaml.org,2002:set"));
185     }
186     {
187         auto nodes = [
188             Node("a"),
189             Node("b"),
190             Node("c"),
191         ];
192         assert(representData(Node(nodes, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) ==
193             Node([
194                 Node("a", "tag:yaml.org,2002:str"),
195                 Node("b", "tag:yaml.org,2002:str"),
196                 Node("c", "tag:yaml.org,2002:str")
197             ], "tag:yaml.org,2002:seq"));
198     }
199 }
200 
201 @safe unittest
202 {
203     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:omap"));
204     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:pairs"));
205     assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:map"));
206     {
207         auto nodes = [
208             Node.Pair("a", "b"),
209             Node.Pair("a", "c")
210         ];
211         assertThrown(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid));
212     }
213     // Yeah, this gets ugly really fast.
214     {
215         auto nodes = [
216             Node.Pair("a", "b"),
217             Node.Pair("a", "c")
218         ];
219         assert(representData(Node(nodes, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) ==
220             Node([
221                 Node(
222                     [Node.Pair(
223                         Node("a", "tag:yaml.org,2002:str"),
224                         Node("b", "tag:yaml.org,2002:str")
225                     )],
226                 "tag:yaml.org,2002:map"),
227                 Node(
228                     [Node.Pair(
229                         Node("a", "tag:yaml.org,2002:str"),
230                         Node("c", "tag:yaml.org,2002:str")
231                     )],
232                 "tag:yaml.org,2002:map"),
233         ], "tag:yaml.org,2002:pairs"));
234     }
235     {
236         auto nodes = [
237             Node.Pair("a", "b"),
238             Node.Pair("a", "c")
239         ];
240         assertThrown(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid));
241     }
242     {
243         auto nodes = [
244             Node.Pair("a", "b"),
245             Node.Pair("c", "d")
246         ];
247         assert(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) ==
248             Node([
249                 Node([
250                     Node.Pair(
251                         Node("a", "tag:yaml.org,2002:str"),
252                         Node("b", "tag:yaml.org,2002:str")
253                     )
254                 ], "tag:yaml.org,2002:map"),
255                 Node([
256                     Node.Pair(
257                         Node("c", "tag:yaml.org,2002:str"),
258                         Node("d", "tag:yaml.org,2002:str")
259                     )
260                 ], "tag:yaml.org,2002:map"
261             )], "tag:yaml.org,2002:omap"));
262     }
263     {
264         auto nodes = [
265             Node.Pair("a", "b"),
266             Node.Pair("c", "d")
267         ];
268         assert(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) ==
269             Node([
270                 Node.Pair(
271                     Node("a", "tag:yaml.org,2002:str"),
272                     Node("b", "tag:yaml.org,2002:str")
273                 ),
274                 Node.Pair(
275                     Node("c", "tag:yaml.org,2002:str"),
276                     Node("d", "tag:yaml.org,2002:str")
277                 ),
278             ], "tag:yaml.org,2002:map"));
279     }
280 }
281 
282 private:
283 
284 //Represent a _null _node as a _null YAML value.
285 Node representNull() @safe
286 {
287     return Node("null", "tag:yaml.org,2002:null");
288 }
289 
290 //Represent a string _node as a string scalar.
291 Node representString(const Node node) @safe
292 {
293     string value = node.as!string;
294     return 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 }