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 }