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