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 * Class that processes YAML mappings, sequences and scalars into nodes. This can be 9 * used to add custom data types. A tutorial can be found 10 * $(LINK2 ../tutorials/custom_types.html, here). 11 */ 12 module dyaml.constructor; 13 14 15 import std.array; 16 import std.algorithm; 17 import std.base64; 18 import std.container; 19 import std.conv; 20 import std.datetime; 21 import std.exception; 22 import std.stdio; 23 import std.regex; 24 import std.string; 25 import std.typecons; 26 import std.utf; 27 28 import dyaml.node; 29 import dyaml.exception; 30 import dyaml.tag; 31 import dyaml.style; 32 33 34 // Exception thrown at constructor errors. 35 package class ConstructorException : YAMLException 36 { 37 /// Construct a ConstructorException. 38 /// 39 /// Params: msg = Error message. 40 /// start = Start position of the error context. 41 /// end = End position of the error context. 42 this(string msg, Mark start, Mark end, string file = __FILE__, int line = __LINE__) 43 @safe pure nothrow 44 { 45 super(msg ~ "\nstart: " ~ start.toString() ~ "\nend: " ~ end.toString(), 46 file, line); 47 } 48 } 49 50 private alias ConstructorException Error; 51 52 /** Constructs YAML values. 53 * 54 * Each YAML scalar, sequence or mapping has a tag specifying its data type. 55 * Constructor uses user-specifyable functions to create a node of desired 56 * data type from a scalar, sequence or mapping. 57 * 58 * 59 * Each of these functions is associated with a tag, and can process either 60 * a scalar, a sequence, or a mapping. The constructor passes each value to 61 * the function with corresponding tag, which then returns the resulting value 62 * that can be stored in a node. 63 * 64 * If a tag is detected with no known constructor function, it is considered an error. 65 */ 66 final class Constructor 67 { 68 private: 69 // Constructor functions from scalars. 70 Node.Value delegate(ref Node)[Tag] fromScalar_; 71 // Constructor functions from sequences. 72 Node.Value delegate(ref Node)[Tag] fromSequence_; 73 // Constructor functions from mappings. 74 Node.Value delegate(ref Node)[Tag] fromMapping_; 75 76 public: 77 /// Construct a Constructor. 78 /// 79 /// If you don't want to support default YAML tags/data types, you can use 80 /// defaultConstructors to disable constructor functions for these. 81 /// 82 /// Params: defaultConstructors = Use constructors for default YAML tags? 83 this(const Flag!"useDefaultConstructors" defaultConstructors = Yes.useDefaultConstructors) 84 @safe nothrow 85 { 86 if(!defaultConstructors){return;} 87 88 addConstructorScalar("tag:yaml.org,2002:null", &constructNull); 89 addConstructorScalar("tag:yaml.org,2002:bool", &constructBool); 90 addConstructorScalar("tag:yaml.org,2002:int", &constructLong); 91 addConstructorScalar("tag:yaml.org,2002:float", &constructReal); 92 addConstructorScalar("tag:yaml.org,2002:binary", &constructBinary); 93 addConstructorScalar("tag:yaml.org,2002:timestamp", &constructTimestamp); 94 addConstructorScalar("tag:yaml.org,2002:str", &constructString); 95 96 ///In a mapping, the default value is kept as an entry with the '=' key. 97 addConstructorScalar("tag:yaml.org,2002:value", &constructString); 98 99 addConstructorSequence("tag:yaml.org,2002:omap", &constructOrderedMap); 100 addConstructorSequence("tag:yaml.org,2002:pairs", &constructPairs); 101 addConstructorMapping("tag:yaml.org,2002:set", &constructSet); 102 addConstructorSequence("tag:yaml.org,2002:seq", &constructSequence); 103 addConstructorMapping("tag:yaml.org,2002:map", &constructMap); 104 addConstructorScalar("tag:yaml.org,2002:merge", &constructMerge); 105 } 106 107 /// Destroy the constructor. 108 @nogc pure @safe nothrow ~this() 109 { 110 fromScalar_.destroy(); 111 fromScalar_ = null; 112 fromSequence_.destroy(); 113 fromSequence_ = null; 114 fromMapping_.destroy(); 115 fromMapping_ = null; 116 } 117 118 /** Add a constructor function from scalar. 119 * 120 * The function must take a reference to $(D Node) to construct from. 121 * The node contains a string for scalars, $(D Node[]) for sequences and 122 * $(D Node.Pair[]) for mappings. 123 * 124 * Any exception thrown by this function will be caught by D:YAML and 125 * its message will be added to a $(D YAMLException) that will also tell 126 * the user which type failed to construct, and position in the file. 127 * 128 * 129 * The value returned by this function will be stored in the resulting node. 130 * 131 * Only one constructor function can be set for one tag. 132 * 133 * 134 * Structs and classes must implement the $(D opCmp()) operator for D:YAML 135 * support. The signature of the operator that must be implemented 136 * is $(D const int opCmp(ref const MyStruct s)) for structs where 137 * $(I MyStruct) is the struct type, and $(D int opCmp(Object o)) for 138 * classes. Note that the class $(D opCmp()) should not alter the compared 139 * values - it is not const for compatibility reasons. 140 * 141 * Params: tag = Tag for the function to handle. 142 * ctor = Constructor function. 143 * 144 * Example: 145 * 146 * -------------------- 147 * import std.string; 148 * 149 * import dyaml.all; 150 * 151 * struct MyStruct 152 * { 153 * int x, y, z; 154 * 155 * //Any D:YAML type must have a custom opCmp operator. 156 * //This is used for ordering in mappings. 157 * const int opCmp(ref const MyStruct s) 158 * { 159 * if(x != s.x){return x - s.x;} 160 * if(y != s.y){return y - s.y;} 161 * if(z != s.z){return z - s.z;} 162 * return 0; 163 * } 164 * } 165 * 166 * MyStruct constructMyStructScalar(ref Node node) 167 * { 168 * //Guaranteed to be string as we construct from scalar. 169 * //!mystruct x:y:z 170 * auto parts = node.as!string().split(":"); 171 * // If this throws, the D:YAML will handle it and throw a YAMLException. 172 * return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2])); 173 * } 174 * 175 * void main() 176 * { 177 * auto loader = Loader("file.yaml"); 178 * auto constructor = new Constructor; 179 * constructor.addConstructorScalar("!mystruct", &constructMyStructScalar); 180 * loader.constructor = constructor; 181 * Node node = loader.load(); 182 * } 183 * -------------------- 184 */ 185 void addConstructorScalar(T)(const string tag, T function(ref Node) ctor) 186 @safe nothrow 187 { 188 const t = Tag(tag); 189 auto deleg = addConstructor!T(t, ctor); 190 (*delegates!string)[t] = deleg; 191 } 192 193 /** Add a constructor function from sequence. 194 * 195 * See_Also: addConstructorScalar 196 * 197 * Example: 198 * 199 * -------------------- 200 * import std.string; 201 * 202 * import dyaml.all; 203 * 204 * struct MyStruct 205 * { 206 * int x, y, z; 207 * 208 * //Any D:YAML type must have a custom opCmp operator. 209 * //This is used for ordering in mappings. 210 * const int opCmp(ref const MyStruct s) 211 * { 212 * if(x != s.x){return x - s.x;} 213 * if(y != s.y){return y - s.y;} 214 * if(z != s.z){return z - s.z;} 215 * return 0; 216 * } 217 * } 218 * 219 * MyStruct constructMyStructSequence(ref Node node) 220 * { 221 * //node is guaranteed to be sequence. 222 * //!mystruct [x, y, z] 223 * return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int); 224 * } 225 * 226 * void main() 227 * { 228 * auto loader = Loader("file.yaml"); 229 * auto constructor = new Constructor; 230 * constructor.addConstructorSequence("!mystruct", &constructMyStructSequence); 231 * loader.constructor = constructor; 232 * Node node = loader.load(); 233 * } 234 * -------------------- 235 */ 236 void addConstructorSequence(T)(const string tag, T function(ref Node) ctor) 237 @safe nothrow 238 { 239 const t = Tag(tag); 240 auto deleg = addConstructor!T(t, ctor); 241 (*delegates!(Node[]))[t] = deleg; 242 } 243 244 /** Add a constructor function from a mapping. 245 * 246 * See_Also: addConstructorScalar 247 * 248 * Example: 249 * 250 * -------------------- 251 * import std.string; 252 * 253 * import dyaml.all; 254 * 255 * struct MyStruct 256 * { 257 * int x, y, z; 258 * 259 * //Any D:YAML type must have a custom opCmp operator. 260 * //This is used for ordering in mappings. 261 * const int opCmp(ref const MyStruct s) 262 * { 263 * if(x != s.x){return x - s.x;} 264 * if(y != s.y){return y - s.y;} 265 * if(z != s.z){return z - s.z;} 266 * return 0; 267 * } 268 * } 269 * 270 * MyStruct constructMyStructMapping(ref Node node) 271 * { 272 * //node is guaranteed to be mapping. 273 * //!mystruct {"x": x, "y": y, "z": z} 274 * return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int); 275 * } 276 * 277 * void main() 278 * { 279 * auto loader = Loader("file.yaml"); 280 * auto constructor = new Constructor; 281 * constructor.addConstructorMapping("!mystruct", &constructMyStructMapping); 282 * loader.constructor = constructor; 283 * Node node = loader.load(); 284 * } 285 * -------------------- 286 */ 287 void addConstructorMapping(T)(const string tag, T function(ref Node) ctor) 288 @safe nothrow 289 { 290 const t = Tag(tag); 291 auto deleg = addConstructor!T(t, ctor); 292 (*delegates!(Node.Pair[]))[t] = deleg; 293 } 294 295 package: 296 /* 297 * Construct a node. 298 * 299 * Params: start = Start position of the node. 300 * end = End position of the node. 301 * tag = Tag (data type) of the node. 302 * value = Value to construct node from (string, nodes or pairs). 303 * style = Style of the node (scalar or collection style). 304 * 305 * Returns: Constructed node. 306 */ 307 Node node(T, U)(const Mark start, const Mark end, const Tag tag, 308 T value, U style) @trusted 309 if((is(T : string) || is(T == Node[]) || is(T == Node.Pair[])) && 310 (is(U : CollectionStyle) || is(U : ScalarStyle))) 311 { 312 enum type = is(T : string) ? "scalar" : 313 is(T == Node[]) ? "sequence" : 314 is(T == Node.Pair[]) ? "mapping" : 315 "ERROR"; 316 enforce((tag in *delegates!T) !is null, 317 new Error("No constructor function from " ~ type ~ 318 " for tag " ~ tag.get(), start, end)); 319 320 Node node = Node(value); 321 try 322 { 323 static if(is(U : ScalarStyle)) 324 { 325 return Node.rawNode((*delegates!T)[tag](node), start, tag, 326 style, CollectionStyle.Invalid); 327 } 328 else static if(is(U : CollectionStyle)) 329 { 330 return Node.rawNode((*delegates!T)[tag](node), start, tag, 331 ScalarStyle.Invalid, style); 332 } 333 else static assert(false); 334 } 335 catch(Exception e) 336 { 337 throw new Error("Error constructing " ~ typeid(T).toString() 338 ~ ":\n" ~ e.msg, start, end); 339 } 340 } 341 342 private: 343 /* 344 * Add a constructor function. 345 * 346 * Params: tag = Tag for the function to handle. 347 * ctor = Constructor function. 348 */ 349 auto addConstructor(T)(const Tag tag, T function(ref Node) ctor) 350 @safe nothrow 351 { 352 assert((tag in fromScalar_) is null && 353 (tag in fromSequence_) is null && 354 (tag in fromMapping_) is null, 355 "Constructor function for tag " ~ tag.get ~ " is already " ~ 356 "specified. Can't specify another one."); 357 358 359 return (ref Node n) 360 { 361 static if(Node.allowed!T){return Node.value(ctor(n));} 362 else {return Node.userValue(ctor(n));} 363 }; 364 } 365 366 //Get the array of constructor functions for scalar, sequence or mapping. 367 @property auto delegates(T)() @safe pure nothrow @nogc 368 { 369 static if(is(T : string)) {return &fromScalar_;} 370 else static if(is(T : Node[])) {return &fromSequence_;} 371 else static if(is(T : Node.Pair[])){return &fromMapping_;} 372 else static assert(false); 373 } 374 } 375 376 377 /// Construct a _null _node. 378 YAMLNull constructNull(ref Node node) @safe pure nothrow @nogc 379 { 380 return YAMLNull(); 381 } 382 383 /// Construct a merge _node - a _node that merges another _node into a mapping. 384 YAMLMerge constructMerge(ref Node node) @safe pure nothrow @nogc 385 { 386 return YAMLMerge(); 387 } 388 389 /// Construct a boolean _node. 390 bool constructBool(ref Node node) @safe 391 { 392 static yes = ["yes", "true", "on"]; 393 static no = ["no", "false", "off"]; 394 string value = node.as!string().toLower(); 395 if(yes.canFind(value)){return true;} 396 if(no.canFind(value)) {return false;} 397 throw new Exception("Unable to parse boolean value: " ~ value); 398 } 399 400 /// Construct an integer (long) _node. 401 long constructLong(ref Node node) 402 { 403 string value = node.as!string().replace("_", ""); 404 const char c = value[0]; 405 const long sign = c != '-' ? 1 : -1; 406 if(c == '-' || c == '+') 407 { 408 value = value[1 .. $]; 409 } 410 411 enforce(value != "", new Exception("Unable to parse float value: " ~ value)); 412 413 long result; 414 try 415 { 416 //Zero. 417 if(value == "0") {result = cast(long)0;} 418 //Binary. 419 else if(value.startsWith("0b")){result = sign * to!int(value[2 .. $], 2);} 420 //Hexadecimal. 421 else if(value.startsWith("0x")){result = sign * to!int(value[2 .. $], 16);} 422 //Octal. 423 else if(value[0] == '0') {result = sign * to!int(value, 8);} 424 //Sexagesimal. 425 else if(value.canFind(":")) 426 { 427 long val = 0; 428 long base = 1; 429 foreach_reverse(digit; value.split(":")) 430 { 431 val += to!long(digit) * base; 432 base *= 60; 433 } 434 result = sign * val; 435 } 436 //Decimal. 437 else{result = sign * to!long(value);} 438 } 439 catch(ConvException e) 440 { 441 throw new Exception("Unable to parse integer value: " ~ value); 442 } 443 444 return result; 445 } 446 unittest 447 { 448 long getLong(string str) 449 { 450 auto node = Node(str); 451 return constructLong(node); 452 } 453 454 string canonical = "685230"; 455 string decimal = "+685_230"; 456 string octal = "02472256"; 457 string hexadecimal = "0x_0A_74_AE"; 458 string binary = "0b1010_0111_0100_1010_1110"; 459 string sexagesimal = "190:20:30"; 460 461 assert(685230 == getLong(canonical)); 462 assert(685230 == getLong(decimal)); 463 assert(685230 == getLong(octal)); 464 assert(685230 == getLong(hexadecimal)); 465 assert(685230 == getLong(binary)); 466 assert(685230 == getLong(sexagesimal)); 467 } 468 469 /// Construct a floating point (real) _node. 470 real constructReal(ref Node node) 471 { 472 string value = node.as!string().replace("_", "").toLower(); 473 const char c = value[0]; 474 const real sign = c != '-' ? 1.0 : -1.0; 475 if(c == '-' || c == '+') 476 { 477 value = value[1 .. $]; 478 } 479 480 enforce(value != "" && value != "nan" && value != "inf" && value != "-inf", 481 new Exception("Unable to parse float value: " ~ value)); 482 483 real result; 484 try 485 { 486 //Infinity. 487 if (value == ".inf"){result = sign * real.infinity;} 488 //Not a Number. 489 else if(value == ".nan"){result = real.nan;} 490 //Sexagesimal. 491 else if(value.canFind(":")) 492 { 493 real val = 0.0; 494 real base = 1.0; 495 foreach_reverse(digit; value.split(":")) 496 { 497 val += to!real(digit) * base; 498 base *= 60.0; 499 } 500 result = sign * val; 501 } 502 //Plain floating point. 503 else{result = sign * to!real(value);} 504 } 505 catch(ConvException e) 506 { 507 throw new Exception("Unable to parse float value: \"" ~ value ~ "\""); 508 } 509 510 return result; 511 } 512 unittest 513 { 514 bool eq(real a, real b, real epsilon = 0.2) 515 { 516 return a >= (b - epsilon) && a <= (b + epsilon); 517 } 518 519 real getReal(string str) 520 { 521 auto node = Node(str); 522 return constructReal(node); 523 } 524 525 string canonical = "6.8523015e+5"; 526 string exponential = "685.230_15e+03"; 527 string fixed = "685_230.15"; 528 string sexagesimal = "190:20:30.15"; 529 string negativeInf = "-.inf"; 530 string NaN = ".NaN"; 531 532 assert(eq(685230.15, getReal(canonical))); 533 assert(eq(685230.15, getReal(exponential))); 534 assert(eq(685230.15, getReal(fixed))); 535 assert(eq(685230.15, getReal(sexagesimal))); 536 assert(eq(-real.infinity, getReal(negativeInf))); 537 assert(to!string(getReal(NaN)) == "nan"); 538 } 539 540 /// Construct a binary (base64) _node. 541 ubyte[] constructBinary(ref Node node) 542 { 543 import std.ascii : newline; 544 import std.array : array; 545 546 string value = node.as!string; 547 // For an unknown reason, this must be nested to work (compiler bug?). 548 try 549 { 550 return Base64.decode(value.representation.filter!(c => !newline.canFind(c)).array); 551 } 552 catch(Base64Exception e) 553 { 554 throw new Exception("Unable to decode base64 value: " ~ e.msg); 555 } 556 } 557 558 unittest 559 { 560 ubyte[] test = cast(ubyte[])"The Answer: 42"; 561 char[] buffer; 562 buffer.length = 256; 563 string input = cast(string)Base64.encode(test, buffer); 564 auto node = Node(input); 565 auto value = constructBinary(node); 566 assert(value == test); 567 assert(value == [84, 104, 101, 32, 65, 110, 115, 119, 101, 114, 58, 32, 52, 50]); 568 } 569 570 /// Construct a timestamp (SysTime) _node. 571 SysTime constructTimestamp(ref Node node) 572 { 573 string value = node.as!string; 574 575 auto YMDRegexp = regex("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)"); 576 auto HMSRegexp = regex("^[Tt \t]+([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(\\.[0-9]*)?"); 577 auto TZRegexp = regex("^[ \t]*Z|([-+][0-9][0-9]?)(:[0-9][0-9])?"); 578 579 try 580 { 581 // First, get year, month and day. 582 auto matches = match(value, YMDRegexp); 583 584 enforce(!matches.empty, 585 new Exception("Unable to parse timestamp value: " ~ value)); 586 587 auto captures = matches.front.captures; 588 const year = to!int(captures[1]); 589 const month = to!int(captures[2]); 590 const day = to!int(captures[3]); 591 592 // If available, get hour, minute, second and fraction, if present. 593 value = matches.front.post; 594 matches = match(value, HMSRegexp); 595 if(matches.empty) 596 { 597 return SysTime(DateTime(year, month, day), UTC()); 598 } 599 600 captures = matches.front.captures; 601 const hour = to!int(captures[1]); 602 const minute = to!int(captures[2]); 603 const second = to!int(captures[3]); 604 const hectonanosecond = cast(int)(to!real("0" ~ captures[4]) * 10000000); 605 606 // If available, get timezone. 607 value = matches.front.post; 608 matches = match(value, TZRegexp); 609 if(matches.empty || matches.front.captures[0] == "Z") 610 { 611 // No timezone. 612 return SysTime(DateTime(year, month, day, hour, minute, second), 613 hectonanosecond.dur!"hnsecs", UTC()); 614 } 615 616 // We have a timezone, so parse it. 617 captures = matches.front.captures; 618 int sign = 1; 619 int tzHours = 0; 620 if(!captures[1].empty) 621 { 622 if(captures[1][0] == '-') {sign = -1;} 623 tzHours = to!int(captures[1][1 .. $]); 624 } 625 const tzMinutes = (!captures[2].empty) ? to!int(captures[2][1 .. $]) : 0; 626 const tzOffset = dur!"minutes"(sign * (60 * tzHours + tzMinutes)); 627 628 return SysTime(DateTime(year, month, day, hour, minute, second), 629 hectonanosecond.dur!"hnsecs", 630 new immutable SimpleTimeZone(tzOffset)); 631 } 632 catch(ConvException e) 633 { 634 throw new Exception("Unable to parse timestamp value " ~ value ~ " : " ~ e.msg); 635 } 636 catch(DateTimeException e) 637 { 638 throw new Exception("Invalid timestamp value " ~ value ~ " : " ~ e.msg); 639 } 640 641 assert(false, "This code should never be reached"); 642 } 643 unittest 644 { 645 writeln("D:YAML construction timestamp unittest"); 646 647 string timestamp(string value) 648 { 649 auto node = Node(value); 650 return constructTimestamp(node).toISOString(); 651 } 652 653 string canonical = "2001-12-15T02:59:43.1Z"; 654 string iso8601 = "2001-12-14t21:59:43.10-05:00"; 655 string spaceSeparated = "2001-12-14 21:59:43.10 -5"; 656 string noTZ = "2001-12-15 2:59:43.10"; 657 string noFraction = "2001-12-15 2:59:43"; 658 string ymd = "2002-12-14"; 659 660 assert(timestamp(canonical) == "20011215T025943.1Z"); 661 //avoiding float conversion errors 662 assert(timestamp(iso8601) == "20011214T215943.0999999-05:00" || 663 timestamp(iso8601) == "20011214T215943.1-05:00"); 664 assert(timestamp(spaceSeparated) == "20011214T215943.0999999-05:00" || 665 timestamp(spaceSeparated) == "20011214T215943.1-05:00"); 666 assert(timestamp(noTZ) == "20011215T025943.0999999Z" || 667 timestamp(noTZ) == "20011215T025943.1Z"); 668 assert(timestamp(noFraction) == "20011215T025943Z"); 669 assert(timestamp(ymd) == "20021214T000000Z"); 670 } 671 672 /// Construct a string _node. 673 string constructString(ref Node node) 674 { 675 return node.as!string; 676 } 677 678 /// Convert a sequence of single-element mappings into a sequence of pairs. 679 Node.Pair[] getPairs(string type, Node[] nodes) 680 { 681 Node.Pair[] pairs; 682 683 foreach(ref node; nodes) 684 { 685 enforce(node.isMapping && node.length == 1, 686 new Exception("While constructing " ~ type ~ 687 ", expected a mapping with single element")); 688 689 pairs.assumeSafeAppend(); 690 pairs ~= node.as!(Node.Pair[]); 691 } 692 693 return pairs; 694 } 695 696 /// Construct an ordered map (ordered sequence of key:value pairs without duplicates) _node. 697 Node.Pair[] constructOrderedMap(ref Node node) 698 { 699 auto pairs = getPairs("ordered map", node.as!(Node[])); 700 701 //Detect duplicates. 702 //TODO this should be replaced by something with deterministic memory allocation. 703 auto keys = redBlackTree!Node(); 704 scope(exit){keys.destroy();} 705 foreach(ref pair; pairs) 706 { 707 enforce(!(pair.key in keys), 708 new Exception("Duplicate entry in an ordered map: " 709 ~ pair.key.debugString())); 710 keys.insert(pair.key); 711 } 712 return pairs; 713 } 714 unittest 715 { 716 writeln("D:YAML construction ordered map unittest"); 717 718 alias Node.Pair Pair; 719 720 Node[] alternateTypes(uint length) 721 { 722 Node[] pairs; 723 foreach(long i; 0 .. length) 724 { 725 auto pair = (i % 2) ? Pair(i.to!string, i) : Pair(i, i.to!string); 726 pairs.assumeSafeAppend(); 727 pairs ~= Node([pair]); 728 } 729 return pairs; 730 } 731 732 Node[] sameType(uint length) 733 { 734 Node[] pairs; 735 foreach(long i; 0 .. length) 736 { 737 auto pair = Pair(i.to!string, i); 738 pairs.assumeSafeAppend(); 739 pairs ~= Node([pair]); 740 } 741 return pairs; 742 } 743 744 bool hasDuplicates(Node[] nodes) 745 { 746 auto node = Node(nodes); 747 return null !is collectException(constructOrderedMap(node)); 748 } 749 750 assert(hasDuplicates(alternateTypes(8) ~ alternateTypes(2))); 751 assert(!hasDuplicates(alternateTypes(8))); 752 assert(hasDuplicates(sameType(64) ~ sameType(16))); 753 assert(hasDuplicates(alternateTypes(64) ~ alternateTypes(16))); 754 assert(!hasDuplicates(sameType(64))); 755 assert(!hasDuplicates(alternateTypes(64))); 756 } 757 758 /// Construct a pairs (ordered sequence of key: value pairs allowing duplicates) _node. 759 Node.Pair[] constructPairs(ref Node node) 760 { 761 return getPairs("pairs", node.as!(Node[])); 762 } 763 764 /// Construct a set _node. 765 Node[] constructSet(ref Node node) 766 { 767 auto pairs = node.as!(Node.Pair[]); 768 769 // In future, the map here should be replaced with something with deterministic 770 // memory allocation if possible. 771 // Detect duplicates. 772 ubyte[Node] map; 773 scope(exit){map.destroy();} 774 Node[] nodes; 775 foreach(ref pair; pairs) 776 { 777 enforce((pair.key in map) is null, new Exception("Duplicate entry in a set")); 778 map[pair.key] = 0; 779 nodes.assumeSafeAppend(); 780 nodes ~= pair.key; 781 } 782 783 return nodes; 784 } 785 unittest 786 { 787 writeln("D:YAML construction set unittest"); 788 789 Node.Pair[] set(uint length) 790 { 791 Node.Pair[] pairs; 792 foreach(long i; 0 .. length) 793 { 794 pairs.assumeSafeAppend(); 795 pairs ~= Node.Pair(i.to!string, YAMLNull()); 796 } 797 798 return pairs; 799 } 800 801 auto DuplicatesShort = set(8) ~ set(2); 802 auto noDuplicatesShort = set(8); 803 auto DuplicatesLong = set(64) ~ set(4); 804 auto noDuplicatesLong = set(64); 805 806 bool eq(Node.Pair[] a, Node[] b) 807 { 808 if(a.length != b.length){return false;} 809 foreach(i; 0 .. a.length) 810 { 811 if(a[i].key != b[i]) 812 { 813 return false; 814 } 815 } 816 return true; 817 } 818 819 auto nodeDuplicatesShort = Node(DuplicatesShort.dup); 820 auto nodeNoDuplicatesShort = Node(noDuplicatesShort.dup); 821 auto nodeDuplicatesLong = Node(DuplicatesLong.dup); 822 auto nodeNoDuplicatesLong = Node(noDuplicatesLong.dup); 823 824 assert(null !is collectException(constructSet(nodeDuplicatesShort))); 825 assert(null is collectException(constructSet(nodeNoDuplicatesShort))); 826 assert(null !is collectException(constructSet(nodeDuplicatesLong))); 827 assert(null is collectException(constructSet(nodeNoDuplicatesLong))); 828 } 829 830 /// Construct a sequence (array) _node. 831 Node[] constructSequence(ref Node node) 832 { 833 return node.as!(Node[]); 834 } 835 836 /// Construct an unordered map (unordered set of key:value _pairs without duplicates) _node. 837 Node.Pair[] constructMap(ref Node node) 838 { 839 auto pairs = node.as!(Node.Pair[]); 840 //Detect duplicates. 841 //TODO this should be replaced by something with deterministic memory allocation. 842 auto keys = redBlackTree!Node(); 843 scope(exit){keys.destroy();} 844 foreach(ref pair; pairs) 845 { 846 enforce(!(pair.key in keys), 847 new Exception("Duplicate entry in a map: " ~ pair.key.debugString())); 848 keys.insert(pair.key); 849 } 850 return pairs; 851 } 852 853 854 // Unittests 855 private: 856 857 import dyaml.loader; 858 859 struct MyStruct 860 { 861 int x, y, z; 862 863 int opCmp(ref const MyStruct s) const pure @safe nothrow 864 { 865 if(x != s.x){return x - s.x;} 866 if(y != s.y){return y - s.y;} 867 if(z != s.z){return z - s.z;} 868 return 0; 869 } 870 } 871 872 MyStruct constructMyStructScalar(ref Node node) 873 { 874 // Guaranteed to be string as we construct from scalar. 875 auto parts = node.as!string().split(":"); 876 return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2])); 877 } 878 879 MyStruct constructMyStructSequence(ref Node node) 880 { 881 // node is guaranteed to be sequence. 882 return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int); 883 } 884 885 MyStruct constructMyStructMapping(ref Node node) 886 { 887 // node is guaranteed to be mapping. 888 return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int); 889 } 890 891 unittest 892 { 893 char[] data = "!mystruct 1:2:3".dup; 894 auto loader = Loader(data); 895 auto constructor = new Constructor; 896 constructor.addConstructorScalar("!mystruct", &constructMyStructScalar); 897 loader.constructor = constructor; 898 Node node = loader.load(); 899 900 assert(node.as!MyStruct == MyStruct(1, 2, 3)); 901 } 902 903 unittest 904 { 905 char[] data = "!mystruct [1, 2, 3]".dup; 906 auto loader = Loader(data); 907 auto constructor = new Constructor; 908 constructor.addConstructorSequence("!mystruct", &constructMyStructSequence); 909 loader.constructor = constructor; 910 Node node = loader.load(); 911 912 assert(node.as!MyStruct == MyStruct(1, 2, 3)); 913 } 914 915 unittest 916 { 917 char[] data = "!mystruct {x: 1, y: 2, z: 3}".dup; 918 auto loader = Loader(data); 919 auto constructor = new Constructor; 920 constructor.addConstructorMapping("!mystruct", &constructMyStructMapping); 921 loader.constructor = constructor; 922 Node node = loader.load(); 923 924 assert(node.as!MyStruct == MyStruct(1, 2, 3)); 925 }