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  * Composes nodes from YAML events provided by parser.
9  * Code based on PyYAML: http://www.pyyaml.org
10  */
11 module dyaml.composer;
12 
13 import core.memory;
14 
15 import std.algorithm;
16 import std.array;
17 import std.conv;
18 import std.exception;
19 import std.format;
20 import std.range;
21 import std.typecons;
22 
23 import dyaml.constructor;
24 import dyaml.event;
25 import dyaml.exception;
26 import dyaml.node;
27 import dyaml.parser;
28 import dyaml.resolver;
29 
30 
31 package:
32 
33 ///Composes YAML documents from events provided by a Parser.
34 struct Composer
35 {
36     private:
37         ///Parser providing YAML events.
38         Parser parser_;
39         ///Resolver resolving tags (data types).
40         Resolver resolver_;
41         ///Nodes associated with anchors. Used by YAML aliases.
42         Node[string] anchors_;
43 
44         ///Used to reduce allocations when creating pair arrays.
45         ///
46         ///We need one appender for each nesting level that involves
47         ///a pair array, as the inner levels are processed as a
48         ///part of the outer levels. Used as a stack.
49         Appender!(Node.Pair[])[] pairAppenders_;
50         ///Used to reduce allocations when creating node arrays.
51         ///
52         ///We need one appender for each nesting level that involves
53         ///a node array, as the inner levels are processed as a
54         ///part of the outer levels. Used as a stack.
55         Appender!(Node[])[] nodeAppenders_;
56 
57     public:
58         /**
59          * Construct a composer.
60          *
61          * Params:  parser      = Parser to provide YAML events.
62          *          resolver    = Resolver to resolve tags (data types).
63          */
64         this(Parser parser, Resolver resolver) @safe nothrow
65         {
66             parser_ = parser;
67             resolver_ = resolver;
68         }
69 
70         /**
71          * Determine if there are any nodes left.
72          *
73          * Must be called before loading as it handles the stream start event.
74          */
75         bool checkNode() @safe
76         {
77             // If next event is stream start, skip it
78             parser_.skipOver!"a.id == b"(EventID.streamStart);
79 
80             //True if there are more documents available.
81             return parser_.front.id != EventID.streamEnd;
82         }
83 
84         ///Get a YAML document as a node (the root of the document).
85         Node getNode() @safe
86         {
87             //Get the root node of the next document.
88             assert(parser_.front.id != EventID.streamEnd,
89                    "Trying to get a node from Composer when there is no node to " ~
90                    "get. use checkNode() to determine if there is a node.");
91 
92             return composeDocument();
93         }
94 
95         /// Set file name.
96         ref inout(string) name() inout @safe return pure nothrow @nogc
97         {
98             return parser_.name;
99         }
100         /// Get a mark from the current reader position
101         Mark mark() const @safe pure nothrow @nogc
102         {
103             return parser_.mark;
104         }
105 
106         /// Get resolver
107         ref Resolver resolver() @safe return pure nothrow @nogc {
108             return resolver_;
109         }
110 
111     private:
112 
113         void skipExpected(const EventID id) @safe
114         {
115             const foundExpected = parser_.skipOver!"a.id == b"(id);
116             assert(foundExpected, text("Expected ", id, " not found."));
117         }
118         ///Ensure that appenders for specified nesting levels exist.
119         ///
120         ///Params:  pairAppenderLevel = Current level in the pair appender stack.
121         ///         nodeAppenderLevel = Current level the node appender stack.
122         void ensureAppendersExist(const uint pairAppenderLevel, const uint nodeAppenderLevel)
123             @safe
124         {
125             while(pairAppenders_.length <= pairAppenderLevel)
126             {
127                 pairAppenders_ ~= appender!(Node.Pair[])();
128             }
129             while(nodeAppenders_.length <= nodeAppenderLevel)
130             {
131                 nodeAppenders_ ~= appender!(Node[])();
132             }
133         }
134 
135         ///Compose a YAML document and return its root node.
136         Node composeDocument() @safe
137         {
138             skipExpected(EventID.documentStart);
139 
140             //Compose the root node.
141             Node node = composeNode(0, 0);
142 
143             skipExpected(EventID.documentEnd);
144 
145             anchors_.destroy();
146             return node;
147         }
148 
149         /// Compose a node.
150         ///
151         /// Params: pairAppenderLevel = Current level of the pair appender stack.
152         ///         nodeAppenderLevel = Current level of the node appender stack.
153         Node composeNode(const uint pairAppenderLevel, const uint nodeAppenderLevel) @safe
154         {
155             if(parser_.front.id == EventID.alias_)
156             {
157                 const event = parser_.front;
158                 parser_.popFront();
159                 const anchor = event.anchor;
160                 enforce((anchor in anchors_) !is null,
161                         new ComposerException("Found undefined alias: " ~ anchor,
162                                               event.startMark));
163 
164                 //If the node referenced by the anchor is uninitialized,
165                 //it's not finished, i.e. we're currently composing it
166                 //and trying to use it recursively here.
167                 enforce(anchors_[anchor] != Node(),
168                         new ComposerException(text("Found recursive alias: ", anchor),
169                               event.startMark, "defined here", anchors_[anchor].startMark));
170 
171                 return anchors_[anchor];
172             }
173 
174             const event = parser_.front;
175             const anchor = event.anchor;
176             if((anchor !is null) && (anchor in anchors_) !is null)
177             {
178                 throw new ComposerException(text("Found duplicate anchor: ", anchor),
179                     event.startMark, "defined here", anchors_[anchor].startMark);
180             }
181 
182             Node result;
183             //Associate the anchor, if any, with an uninitialized node.
184             //used to detect duplicate and recursive anchors.
185             if(anchor !is null)
186             {
187                 Node tempNode;
188                 tempNode.startMark_ = event.startMark;
189                 anchors_[anchor] = tempNode;
190             }
191 
192             switch (parser_.front.id)
193             {
194                 case EventID.scalar:
195                     result = composeScalarNode();
196                     break;
197                 case EventID.sequenceStart:
198                     result = composeSequenceNode(pairAppenderLevel, nodeAppenderLevel);
199                     break;
200                 case EventID.mappingStart:
201                     result = composeMappingNode(pairAppenderLevel, nodeAppenderLevel);
202                     break;
203                 default: assert(false, "This code should never be reached");
204             }
205 
206             if(anchor !is null)
207             {
208                 anchors_[anchor] = result;
209             }
210             return result;
211         }
212 
213         ///Compose a scalar node.
214         Node composeScalarNode() @safe
215         {
216             const event = parser_.front;
217             parser_.popFront();
218             const tag = resolver_.resolve(NodeID.scalar, event.tag, event.value,
219                                           event.implicit);
220 
221             Node node = constructNode(event.startMark, event.endMark, tag,
222                                           event.value);
223             node.scalarStyle = event.scalarStyle;
224 
225             return node;
226         }
227 
228         /// Compose a sequence node.
229         ///
230         /// Params: pairAppenderLevel = Current level of the pair appender stack.
231         ///         nodeAppenderLevel = Current level of the node appender stack.
232         Node composeSequenceNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
233             @safe
234         {
235             ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
236             auto nodeAppender = &(nodeAppenders_[nodeAppenderLevel]);
237 
238             const startEvent = parser_.front;
239             parser_.popFront();
240             const tag = resolver_.resolve(NodeID.sequence, startEvent.tag, null,
241                                           startEvent.implicit);
242 
243             while(parser_.front.id != EventID.sequenceEnd)
244             {
245                 nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1));
246             }
247 
248             Node node = constructNode(startEvent.startMark, parser_.front.endMark,
249                                           tag, nodeAppender.data.dup);
250             node.collectionStyle = startEvent.collectionStyle;
251             parser_.popFront();
252             nodeAppender.clear();
253 
254             return node;
255         }
256 
257         /**
258          * Flatten a node, merging it with nodes referenced through YAMLMerge data type.
259          *
260          * Node must be a mapping or a sequence of mappings.
261          *
262          * Params:  root              = Node to flatten.
263          *          startMark         = Start position of the node.
264          *          endMark           = End position of the node.
265          *          pairAppenderLevel = Current level of the pair appender stack.
266          *          nodeAppenderLevel = Current level of the node appender stack.
267          *
268          * Returns: Flattened mapping as pairs.
269          */
270         Node.Pair[] flatten(ref Node root, const Mark startMark, const Mark endMark,
271                             const uint pairAppenderLevel, const uint nodeAppenderLevel) @safe
272         {
273             void error(Node node)
274             {
275                 //this is Composer, but the code is related to Constructor.
276                 throw new ConstructorException("While constructing a mapping, " ~
277                    "expected a mapping or a list of " ~
278                    "mappings for merging, but found: " ~
279                    text(node.type),
280                    endMark, "mapping started here", startMark);
281             }
282 
283             ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
284             auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
285 
286             final switch (root.nodeID)
287             {
288                 case NodeID.mapping:
289                     Node[] toMerge;
290                     toMerge.reserve(root.length);
291                     foreach (ref Node key, ref Node value; root)
292                     {
293                         if(key.type == NodeType.merge)
294                         {
295                             toMerge ~= value;
296                         }
297                         else
298                         {
299                             auto temp = Node.Pair(key, value);
300                             pairAppender.put(temp);
301                         }
302                     }
303                     foreach (node; toMerge)
304                     {
305                         pairAppender.put(flatten(node, startMark, endMark,
306                                                      pairAppenderLevel + 1, nodeAppenderLevel));
307                     }
308                     break;
309                 case NodeID.sequence:
310                     foreach (ref Node node; root)
311                     {
312                         if (node.nodeID != NodeID.mapping)
313                         {
314                             error(node);
315                         }
316                         pairAppender.put(flatten(node, startMark, endMark,
317                                                      pairAppenderLevel + 1, nodeAppenderLevel));
318                     }
319                     break;
320                 case NodeID.scalar:
321                 case NodeID.invalid:
322                     error(root);
323                     break;
324             }
325 
326             auto flattened = pairAppender.data.dup;
327             pairAppender.clear();
328 
329             return flattened;
330         }
331 
332         /// Compose a mapping node.
333         ///
334         /// Params: pairAppenderLevel = Current level of the pair appender stack.
335         ///         nodeAppenderLevel = Current level of the node appender stack.
336         Node composeMappingNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
337             @safe
338         {
339             ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
340             const startEvent = parser_.front;
341             parser_.popFront();
342             const tag = resolver_.resolve(NodeID.mapping, startEvent.tag, null,
343                                           startEvent.implicit);
344             auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
345 
346             Tuple!(Node, Mark)[] toMerge;
347             while(parser_.front.id != EventID.mappingEnd)
348             {
349                 auto pair = Node.Pair(composeNode(pairAppenderLevel + 1, nodeAppenderLevel),
350                                       composeNode(pairAppenderLevel + 1, nodeAppenderLevel));
351 
352                 //Need to flatten and merge the node referred by YAMLMerge.
353                 if(pair.key.type == NodeType.merge)
354                 {
355                     toMerge ~= tuple(pair.value, cast(Mark)parser_.front.endMark);
356                 }
357                 //Not YAMLMerge, just add the pair.
358                 else
359                 {
360                     pairAppender.put(pair);
361                 }
362             }
363             foreach(node; toMerge)
364             {
365                 merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1],
366                                              pairAppenderLevel + 1, nodeAppenderLevel));
367             }
368 
369             auto sorted = pairAppender.data.dup.sort!((x,y) => x.key > y.key);
370             if (sorted.length)
371             {
372                 foreach (index, const ref value; sorted[0 .. $ - 1].enumerate)
373                     if (value.key == sorted[index + 1].key)
374                     {
375                         throw new ComposerException(
376                             text("Key '", value.key.get!string, "' appears multiple times in mapping"),
377                                 sorted[index + 1].key.startMark, "defined here", value.key.startMark);
378                     }
379             }
380 
381             Node node = constructNode(startEvent.startMark, parser_.front.endMark,
382                                           tag, pairAppender.data.dup);
383             node.collectionStyle = startEvent.collectionStyle;
384             parser_.popFront();
385 
386             pairAppender.clear();
387             return node;
388         }
389 }
390 
391 // Provide good error message on multiple keys (which JSON supports)
392 @safe unittest
393 {
394     import dyaml.loader : Loader;
395 
396     const str = `{
397     "comment": "This is a common technique",
398     "name": "foobar",
399     "comment": "To write down comments pre-JSON5"
400 }`;
401 
402     const exc = collectException!LoaderException(Loader.fromString(str).load());
403     assert(exc);
404     assert(exc.message() ==
405        "Unable to load <unknown>: Key 'comment' appears multiple times in mapping\n" ~
406        "<unknown>:4,5\ndefined here: <unknown>:2,5");
407 }
408 
409 // Provide good error message on duplicate anchors
410 @safe unittest
411 {
412     import dyaml.loader : Loader;
413 
414     const str = `{
415     a: &anchor b,
416     b: &anchor c,
417 }`;
418 
419     const exc = collectException!LoaderException(Loader.fromString(str).load());
420     assert(exc);
421     assert(exc.message() ==
422        "Unable to load <unknown>: Found duplicate anchor: anchor\n" ~
423        "<unknown>:3,8\ndefined here: <unknown>:2,8");
424 }
425 
426 // Provide good error message on missing alias
427 @safe unittest
428 {
429     import dyaml.loader : Loader;
430 
431     const str = `{
432     a: *anchor,
433 }`;
434 
435     const exc = collectException!LoaderException(Loader.fromString(str).load());
436     assert(exc);
437     assert(exc.message() ==
438        "Unable to load <unknown>: Found undefined alias: anchor\n" ~
439        "<unknown>:2,8");
440 }
441 
442 // Provide good error message on recursive alias
443 @safe unittest
444 {
445     import dyaml.loader : Loader;
446 
447     const str = `a: &anchor {
448     b: *anchor
449 }`;
450 
451     const exc = collectException!LoaderException(Loader.fromString(str).load());
452     assert(exc);
453     assert(exc.message() ==
454        "Unable to load <unknown>: Found recursive alias: anchor\n" ~
455        "<unknown>:2,8\ndefined here: <unknown>:1,4");
456 }
457 
458 // Provide good error message on failed merges
459 @safe unittest
460 {
461     import dyaml.loader : Loader;
462 
463     const str = `a: &anchor 3
464 b: { <<: *anchor }`;
465 
466     const exc = collectException!LoaderException(Loader.fromString(str).load());
467     assert(exc);
468     assert(exc.message() ==
469        "Unable to load <unknown>: While constructing a mapping, expected a mapping or a list of mappings for merging, but found: integer\n" ~
470        "<unknown>:2,19\nmapping started here: <unknown>:2,4");
471 }