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 /// Node of a YAML document. Used to read YAML data once it's loaded, 8 /// and to prepare data to emit. 9 module dyaml.node; 10 11 import dyaml.event; 12 import dyaml.exception; 13 import dyaml.style; 14 import mir.conv: to; 15 import mir.format: text; 16 import mir.primitives; 17 import mir.timestamp; 18 import std.exception: assertThrown, assumeWontThrow, collectException; 19 import std.meta : AliasSeq; 20 import std.traits; 21 import std.typecons: Flag, Yes, No; 22 23 /// Exception thrown at node related errors. 24 class NodeException : YAMLException 25 { 26 package: 27 // Construct a NodeException. 28 // 29 // Params: msg = Error message. 30 // start = Start position of the node. 31 this(string msg, Mark start, string file = __FILE__, size_t line = __LINE__) 32 @safe 33 { 34 super(msg ~ "\nNode at: " ~ start.toString(), file, line); 35 } 36 } 37 38 // Node kinds. 39 enum NodeID : ubyte 40 { 41 scalar, 42 sequence, 43 mapping, 44 invalid 45 } 46 47 /// Null YAML type. Used in nodes with _null values. 48 alias YAMLNull = typeof(null); 49 50 // Merge YAML type, used to support "tag:yaml.org,2002:merge". 51 package struct YAMLMerge{} 52 53 // Key-value pair of YAML nodes, used in mappings. 54 private struct Pair 55 { 56 public: 57 /// Key node. 58 Node key; 59 /// Value node. 60 Node value; 61 62 /// Construct a Pair from two values. Will be converted to Nodes if needed. 63 this(K, V)(K key, V value) 64 { 65 static if(is(Unqual!K == Node)){this.key = key;} 66 else {this.key = Node(key);} 67 static if(is(Unqual!V == Node)){this.value = value;} 68 else {this.value = Node(value);} 69 } 70 71 /// Equality test with another Pair. 72 bool opEquals(const ref Pair rhs) const @safe 73 { 74 return key == rhs.key && value == rhs.value; 75 } 76 77 // Comparison with another Pair. 78 int opCmp(ref const(Pair) rhs) const @safe 79 { 80 const keyCmp = key.opCmp(rhs.key); 81 return keyCmp != 0 ? keyCmp 82 : value.opCmp(rhs.value); 83 } 84 } 85 86 alias NodeType = Node.Value.Kind; 87 88 /** YAML node. 89 * 90 * This is a pseudo-dynamic type that can store any YAML value, including a 91 * sequence or mapping of nodes. You can get data from a Node directly or 92 * iterate over it if it's a collection. 93 */ 94 struct Node 95 { 96 import std.meta: staticIndexOf; 97 import mir.algebraic: TaggedVariant, visit, tryVisit, optionalVisit; 98 import mir.serde: serdeIgnore; 99 100 @serdeIgnore: 101 public: 102 alias Pair = .Pair; 103 104 105 106 107 108 109 110 111 112 113 114 115 package: 116 // YAML value type. 117 alias Value = TaggedVariant!( 118 [ 119 "null_", 120 "merge", 121 "boolean", 122 "integer", 123 "decimal", 124 "binary", 125 "timestamp", 126 "string", 127 "mapping", 128 "sequence", 129 ], 130 typeof(null), 131 YAMLMerge, 132 bool, 133 long, 134 double, 135 ubyte[], 136 Timestamp, 137 string, 138 Node.Pair[], 139 Node[], 140 ); 141 142 // Can Value hold this type naturally? 143 enum allowed(T) = isIntegral!T || 144 isFloatingPoint!T || 145 isSomeString!T || 146 is(Unqual!T == bool) || 147 staticIndexOf!(T, Value.AllowedTypes) >= 0 || 148 __traits(compiles, Timestamp(T.init)) || 149 is(T == Value); 150 151 // Stored value. 152 Value value_; 153 // Start position of the node. 154 Mark startMark_; 155 156 // Tag of the node. 157 string tag_; 158 // Node scalar style. Used to remember style this node was loaded with. 159 ScalarStyle scalarStyle = ScalarStyle.invalid; 160 // Node collection style. Used to remember style this node was loaded with. 161 CollectionStyle collectionStyle = CollectionStyle.invalid; 162 163 public: 164 /** Construct a Node from a value. 165 * 166 * Any type except for Node can be stored in a Node, but default YAML 167 * types (integers, floats, strings, timestamps, etc.) will be stored 168 * more efficiently. To create a node representing a null value, 169 * construct it from YAMLNull. 170 * 171 * If value is a node, its value will be copied directly. The tag and 172 * other information attached to the original node will be discarded. 173 * 174 * If value is an array of nodes or pairs, it is stored directly. 175 * Otherwise, every value in the array is converted to a node, and 176 * those nodes are stored. 177 * 178 * Note that to emit any non-default types you store 179 * in a node, you need a Representer to represent them in YAML - 180 * otherwise emitting will fail. 181 * 182 * Params: value = Value to store in the node. 183 * tag = Overrides tag of the node when emitted, regardless 184 * of tag determined by Representer. Representer uses 185 * this to determine YAML data type when a D data type 186 * maps to multiple different YAML data types. Tag must 187 * be in full form, e.g. "tag:yaml.org,2002:int", not 188 * a shortcut, like "!!int". 189 */ 190 this(T)(T value, const string tag = null) @safe 191 if (allowed!T || isArray!T || isAssociativeArray!T || is(Unqual!T == Node) || castableToNode!T) 192 { 193 tag_ = tag; 194 195 //Unlike with assignment, we're just copying the value. 196 static if (is(Unqual!T == Node)) 197 { 198 setValue(value.value_); 199 } 200 else static if(isSomeString!T) 201 { 202 setValue(value.to!string); 203 } 204 else static if(is(Unqual!T == bool)) 205 { 206 setValue(cast(bool)value); 207 } 208 else static if(isIntegral!T) 209 { 210 setValue(cast(long)value); 211 } 212 else static if(isFloatingPoint!T) 213 { 214 setValue(cast(double)value); 215 } 216 else static if (isArray!T) 217 { 218 alias ElementT = Unqual!(ElementType!T); 219 // Construction from raw node or pair array. 220 static if(is(ElementT == Node) || is(ElementT == Node.Pair)) 221 { 222 setValue(value); 223 } 224 // Need to handle byte buffers separately. 225 else static if(is(ElementT == byte) || is(ElementT == ubyte)) 226 { 227 setValue(cast(ubyte[]) value); 228 } 229 else 230 { 231 Node[] nodes; 232 foreach(ref v; value) 233 { 234 nodes ~= Node(v); 235 } 236 setValue(nodes); 237 } 238 } 239 else static if (isAssociativeArray!T) 240 { 241 Node.Pair[] pairs; 242 foreach(k, ref v; value) 243 { 244 pairs ~= Pair(k, v); 245 } 246 setValue(pairs); 247 } 248 // User defined type. 249 else 250 { 251 setValue(value); 252 } 253 } 254 /// Construct a scalar node 255 @safe unittest 256 { 257 // Integer 258 { 259 auto node = Node(5); 260 } 261 // String 262 { 263 auto node = Node("Hello world!"); 264 } 265 // Floating point 266 { 267 auto node = Node(5.0f); 268 } 269 // Boolean 270 { 271 auto node = Node(true); 272 } 273 // Time 274 { 275 auto node = Node(Timestamp(2005, 6, 15, 20, 0, 0)); 276 } 277 // Integer, dumped as a string 278 { 279 auto node = Node(5, "tag:yaml.org,2002:str"); 280 } 281 } 282 /// Construct a sequence node 283 @safe unittest 284 { 285 // Will be emitted as a sequence (default for arrays) 286 { 287 auto seq = Node([1, 2, 3, 4, 5]); 288 } 289 // Will be emitted as a set (overridden tag) 290 { 291 auto set = Node([1, 2, 3, 4, 5], "tag:yaml.org,2002:set"); 292 } 293 // Can also store arrays of arrays 294 { 295 auto node = Node([[1,2], [3,4]]); 296 } 297 } 298 /// Construct a mapping node 299 @safe unittest 300 { 301 // Will be emitted as an unordered mapping (default for mappings) 302 auto map = Node([1 : "a", 2 : "b"]); 303 // Will be emitted as an ordered map (overridden tag) 304 auto omap = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:omap"); 305 // Will be emitted as pairs (overridden tag) 306 auto pairs = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:pairs"); 307 } 308 @safe unittest 309 { 310 { 311 auto node = Node(42); 312 assert(node.nodeID == NodeID.scalar); 313 assert(node.as!int == 42 && node.as!float == 42.0f && node.as!string == "42"); 314 } 315 316 { 317 auto node = Node("string"); 318 assert(node.as!string == "string"); 319 } 320 } 321 @safe unittest 322 { 323 with(Node([1, 2, 3])) 324 { 325 assert(nodeID == NodeID.sequence); 326 assert(length == 3); 327 assert(opIndex(2).as!int == 3); 328 } 329 330 } 331 @safe unittest 332 { 333 int[string] aa; 334 aa["1"] = 1; 335 aa["2"] = 2; 336 with(Node(aa)) 337 { 338 assert(nodeID == NodeID.mapping); 339 assert(length == 2); 340 assert(opIndex("2").as!int == 2); 341 } 342 } 343 @safe unittest 344 { 345 auto node = Node(Node(4, "tag:yaml.org,2002:str")); 346 assert(node == 4); 347 assert(node.tag_ == ""); 348 } 349 350 /** Construct a node from arrays of _keys and _values. 351 * 352 * Constructs a mapping node with key-value pairs from 353 * _keys and _values, keeping their order. Useful when order 354 * is important (ordered maps, pairs). 355 * 356 * 357 * keys and values must have equal length. 358 * 359 * 360 * If _keys and/or _values are nodes, they are stored directly/ 361 * Otherwise they are converted to nodes and then stored. 362 * 363 * Params: keys = Keys of the mapping, from first to last pair. 364 * values = Values of the mapping, from first to last pair. 365 * tag = Overrides tag of the node when emitted, regardless 366 * of tag determined by Representer. Representer uses 367 * this to determine YAML data type when a D data type 368 * maps to multiple different YAML data types. 369 * This is used to differentiate between YAML unordered 370 * mappings ("!!map"), ordered mappings ("!!omap"), and 371 * pairs ("!!pairs") which are all internally 372 * represented as an array of node pairs. Tag must be 373 * in full form, e.g. "tag:yaml.org,2002:omap", not a 374 * shortcut, like "!!omap". 375 * 376 */ 377 this(K, V)(K[] keys, V[] values, const string tag = null) 378 if(!(isSomeString!(K[]) || isSomeString!(V[]))) 379 in(keys.length == values.length, 380 "Lengths of keys and values arrays to construct " ~ 381 "a YAML node from don't match") 382 { 383 tag_ = tag; 384 385 Node.Pair[] pairs; 386 foreach(i; 0 .. keys.length){pairs ~= Pair(keys[i], values[i]);} 387 setValue(pairs); 388 } 389 /// 390 @safe unittest 391 { 392 // Will be emitted as an unordered mapping (default for mappings) 393 auto map = Node([1, 2], ["a", "b"]); 394 // Will be emitted as an ordered map (overridden tag) 395 auto omap = Node([1, 2], ["a", "b"], "tag:yaml.org,2002:omap"); 396 // Will be emitted as pairs (overriden tag) 397 auto pairs = Node([1, 2], ["a", "b"], "tag:yaml.org,2002:pairs"); 398 } 399 @safe unittest 400 { 401 with(Node(["1", "2"], [1, 2])) 402 { 403 assert(nodeID == NodeID.mapping); 404 assert(length == 2); 405 assert(opIndex("2").as!int == 2); 406 } 407 408 } 409 410 /// Return tag of the node. 411 @property string tag() const @safe nothrow 412 { 413 return tag_; 414 } 415 416 /// Return the start position of the node. 417 @property Mark startMark() const @safe pure nothrow 418 { 419 return startMark_; 420 } 421 422 /** Equality test. 423 * 424 * If T is Node, recursively compares all subnodes. 425 * This might be quite expensive if testing entire documents. 426 * 427 * If T is not Node, gets a value of type T from the node and tests 428 * equality with that. 429 * 430 * To test equality with a null YAML value, use YAMLNull. 431 * 432 * Params: rhs = Variable to test equality with. 433 * 434 * Returns: true if equal, false otherwise. 435 */ 436 bool opEquals(const Node rhs) const @safe 437 { 438 return opCmp(rhs) == 0; 439 } 440 bool opEquals(T)(const auto ref T rhs) const 441 { 442 try 443 { 444 auto stored = get!(T, No.stringConversion); 445 // NaNs aren't normally equal to each other, but we'll pretend they are. 446 static if(isFloatingPoint!T) 447 { 448 return rhs == stored || (rhs != rhs && stored != stored); 449 } 450 else 451 { 452 return rhs == stored; 453 } 454 } 455 catch(NodeException e) 456 { 457 return false; 458 } 459 } 460 /// 461 @safe unittest 462 { 463 auto node = Node(42); 464 465 assert(node == 42); 466 assert(node != "42"); 467 assert(node != "43"); 468 469 auto node2 = Node(null); 470 assert(node2 == null); 471 472 const node3 = Node(42); 473 assert(node3 == 42); 474 } 475 476 /// Shortcut for get(). 477 alias as = get; 478 479 /** Get the value of the node as specified type. 480 * 481 * If the specifed type does not match type in the node, 482 * conversion is attempted. The stringConversion template 483 * parameter can be used to disable conversion from non-string 484 * types to strings. 485 * 486 * Numeric values are range checked, throwing if out of range of 487 * requested type. 488 * 489 * Timestamps are stored as mir.timestamp.Timestamp. 490 * Binary values are decoded and stored as ubyte[]. 491 * 492 * To get a null value, use get!YAMLNull . This is to 493 * prevent getting null values for types such as strings or classes. 494 * 495 * $(BR)$(B Mapping default values:) 496 * 497 * $(PBR 498 * The '=' key can be used to denote the default value of a mapping. 499 * This can be used when a node is scalar in early versions of a program, 500 * but is replaced by a mapping later. Even if the node is a mapping, the 501 * get method can be used as if it was a scalar if it has a default value. 502 * This way, new YAML files where the node is a mapping can still be read 503 * by old versions of the program, which expect the node to be a scalar. 504 * ) 505 * 506 * Returns: Value of the node as specified type. 507 * 508 * Throws: NodeException if unable to convert to specified type, or if 509 * the value is out of range of requested type. 510 */ 511 inout(T) get(T, Flag!"stringConversion" stringConversion = Yes.stringConversion)() inout 512 if (allowed!(Unqual!T) || hasNodeConstructor!(inout(Unqual!T)) || (!hasIndirections!(Unqual!T) && hasNodeConstructor!(Unqual!T))) 513 { 514 static if(!allowed!(Unqual!T)) 515 { 516 static if (hasSimpleNodeConstructor!(Unqual!T) || hasSimpleNodeConstructor!(inout(Unqual!T))) 517 { 518 alias params = AliasSeq!(this); 519 } 520 else static if (hasExpandedNodeConstructor!(Unqual!T) || hasExpandedNodeConstructor!(inout(Unqual!T))) 521 { 522 alias params = AliasSeq!(this, tag_); 523 } 524 else 525 { 526 static assert(0, "Unknown Node constructor?"); 527 } 528 529 static if (is(T == class)) 530 { 531 return new inout T(params); 532 } 533 else static if (is(T == struct)) 534 { 535 return T(params); 536 } 537 else 538 { 539 static assert(0, "Unhandled user type"); 540 } 541 } 542 else 543 static if (is(typeof(Timestamp.init.opCast!(Unqual!T)))) 544 { 545 return value_.trustedGet!Timestamp.opCast!(Unqual!T); 546 } 547 else 548 { 549 550 // If we're getting from a mapping and we're not getting Node.Pair[], 551 // we're getting the default value. 552 static if (staticIndexOf!(Unqual!T, Value.AllowedTypes) >= 0) 553 { 554 if (_is!(Unqual!T)) 555 return value_.trustedGet!(Unqual!T); 556 } 557 558 if(nodeID == NodeID.mapping){return this["="].get!( T, stringConversion);} 559 560 static if(isSomeString!T) 561 { 562 static if(!stringConversion) 563 { 564 if (type != NodeType..string) 565 throw new NodeException(text("Node stores unexpected type: ", type, ". Expected: ", typeid(T)), startMark_); 566 return to!T(getValue!string); 567 } 568 else 569 { 570 // Try to convert to string. 571 try 572 { 573 return coerceValue!T(); 574 } 575 catch(Exception e) 576 { 577 throw new NodeException("Unable to convert node value to string", startMark_); 578 } 579 } 580 } 581 else static if(isFloatingPoint!T) 582 { 583 final switch (type) 584 { 585 case NodeType.integer: 586 return to!T(getValue!long); 587 case NodeType.decimal: 588 return to!T(getValue!double); 589 case NodeType.binary: 590 case NodeType..string: 591 case NodeType.boolean: 592 case NodeType.null_: 593 case NodeType.merge: 594 case NodeType.timestamp: 595 case NodeType.mapping: 596 case NodeType.sequence: 597 throw new NodeException(text("Node stores unexpected type: ", type, ". Expected: ", typeid(T)), startMark_); 598 } 599 } 600 else static if(isIntegral!T) 601 { 602 if (type != NodeType.integer) 603 throw new NodeException(text("Node stores unexpected type: ", type, ". Expected: ", typeid(T)), startMark_); 604 immutable temp = getValue!long; 605 if(!(temp >= T.min && temp <= T.max)) 606 throw new NodeException(text("Integer value of type ", typeid(T), " out of range. Value: ", temp), startMark_); 607 return temp.to!T; 608 } 609 else 610 throw new NodeException(text("Node stores unexpected type: ", type, ". Expected: ", typeid(T)), startMark_); 611 } 612 } 613 /// ditto 614 T get(T)() const 615 if (hasIndirections!(Unqual!T) && hasNodeConstructor!(Unqual!T) && (!hasNodeConstructor!(inout(Unqual!T)))) 616 { 617 static if (hasSimpleNodeConstructor!T) 618 { 619 alias params = AliasSeq!(this); 620 } 621 else static if (hasExpandedNodeConstructor!T) 622 { 623 alias params = AliasSeq!(this, tag_); 624 } 625 else 626 { 627 static assert(0, "Unknown Node constructor?"); 628 } 629 static if (is(T == class)) 630 { 631 return new T(params); 632 } 633 else static if (is(T == struct)) 634 { 635 return T(params); 636 } 637 else 638 { 639 static assert(0, "Unhandled user type"); 640 } 641 } 642 /// Automatic type conversion 643 @safe unittest 644 { 645 auto node = Node(42); 646 647 assert(node.get!int == 42); 648 assert(node.get!string == "42"); 649 assert(node.get!double == 42.0); 650 } 651 /// Scalar node to struct and vice versa 652 @safe unittest 653 { 654 import std.array: appender; 655 import mir.format: text; 656 import dyaml.dumper : dumper; 657 import dyaml.loader : Loader; 658 static struct MyStruct 659 { 660 int x, y, z; 661 662 this(int x, int y, int z) @safe 663 { 664 this.x = x; 665 this.y = y; 666 this.z = z; 667 } 668 669 this(Node node) @safe 670 { 671 import std..string: split; 672 auto parts = node.as!string().split(":"); 673 x = parts[0].to!int; 674 y = parts[1].to!int; 675 z = parts[2].to!int; 676 } 677 678 Node opCast(T: Node)() @safe 679 { 680 //Using custom scalar format, x:y:z. 681 //Representing as a scalar, with custom tag to specify this data type. 682 return Node(text!":"(x, y, z), "!mystruct.tag"); 683 } 684 } 685 686 auto app = appender!(char[]); 687 688 // Dump struct to yaml document 689 dumper().dump(app, Node(MyStruct(1,2,3))); 690 691 // Read yaml document back as a MyStruct 692 auto loader = Loader.fromString(app.data); 693 Node node = loader.load(); 694 assert(node.as!MyStruct == MyStruct(1,2,3)); 695 } 696 /// Sequence node to struct and vice versa 697 @safe unittest 698 { 699 import std.array: Appender; 700 import dyaml.dumper : dumper; 701 import dyaml.loader : Loader; 702 static struct MyStruct 703 { 704 int x, y, z; 705 706 this(int x, int y, int z) @safe 707 { 708 this.x = x; 709 this.y = y; 710 this.z = z; 711 } 712 713 this(Node node) @safe 714 { 715 x = node[0].as!int; 716 y = node[1].as!int; 717 z = node[2].as!int; 718 } 719 720 Node opCast(T: Node)() 721 { 722 return Node([x, y, z], "!mystruct.tag"); 723 } 724 } 725 726 auto appender = new Appender!string; 727 728 // Dump struct to yaml document 729 dumper().dump(appender, Node(MyStruct(1,2,3))); 730 731 // Read yaml document back as a MyStruct 732 auto loader = Loader.fromString(appender.data); 733 Node node = loader.load(); 734 assert(node.as!MyStruct == MyStruct(1,2,3)); 735 } 736 /// Mapping node to struct and vice versa 737 @safe unittest 738 { 739 import std.array: Appender; 740 import dyaml.dumper : dumper; 741 import dyaml.loader : Loader; 742 static struct MyStruct 743 { 744 int x, y, z; 745 746 Node opCast(T: Node)() 747 { 748 auto pairs = [Node.Pair("x", x), 749 Node.Pair("y", y), 750 Node.Pair("z", z)]; 751 return Node(pairs, "!mystruct.tag"); 752 } 753 754 this(int x, int y, int z) 755 { 756 this.x = x; 757 this.y = y; 758 this.z = z; 759 } 760 761 this(Node node) @safe 762 { 763 x = node["x"].as!int; 764 y = node["y"].as!int; 765 z = node["z"].as!int; 766 } 767 } 768 769 auto appender = new Appender!string; 770 771 // Dump struct to yaml document 772 dumper().dump(appender, Node(MyStruct(1,2,3))); 773 774 // Read yaml document back as a MyStruct 775 auto loader = Loader.fromString(appender.data); 776 Node node = loader.load(); 777 assert(node.as!MyStruct == MyStruct(1,2,3)); 778 } 779 /// Classes can be used too 780 @system unittest { 781 import std.array: Appender; 782 import dyaml.dumper : dumper; 783 import dyaml.loader : Loader; 784 785 static class MyClass 786 { 787 int x, y, z; 788 789 this(int x, int y, int z) 790 { 791 this.x = x; 792 this.y = y; 793 this.z = z; 794 } 795 796 this(Node node) @safe inout 797 { 798 import std..string: split; 799 auto parts = node.as!string().split(":"); 800 x = parts[0].to!int; 801 y = parts[1].to!int; 802 z = parts[2].to!int; 803 } 804 805 ///Useful for Node.as!string. 806 override string toString() 807 { 808 return text("MyClass(", x,", ", y,", ", z,")"); 809 } 810 811 Node opCast(T: Node)() @safe 812 { 813 //Using custom scalar format, x:y:z. 814 auto scalar = text!":"(x, y, z); 815 //Representing as a scalar, with custom tag to specify this data type. 816 return Node(scalar, "!myclass.tag"); 817 } 818 override bool opEquals(Object o) 819 { 820 if (auto other = cast(MyClass)o) 821 { 822 return (other.x == x) && (other.y == y) && (other.z == z); 823 } 824 return false; 825 } 826 } 827 auto appender = new Appender!string; 828 829 // Dump class to yaml document 830 dumper().dump(appender, Node(new MyClass(1,2,3))); 831 832 // Read yaml document back as a MyClass 833 auto loader = Loader.fromString(appender.data); 834 Node node = loader.load(); 835 assert(node.as!MyClass == new MyClass(1,2,3)); 836 } 837 // Make sure custom tags and styles are kept. 838 @safe unittest 839 { 840 static struct MyStruct 841 { 842 Node opCast(T: Node)() 843 { 844 auto node = Node("hi", "!mystruct.tag"); 845 node.setStyle(ScalarStyle.doubleQuoted); 846 return node; 847 } 848 } 849 850 auto node = Node(MyStruct.init); 851 assert(node.tag == "!mystruct.tag"); 852 assert(node.scalarStyle == ScalarStyle.doubleQuoted); 853 } 854 // ditto, but for collection style 855 @safe unittest 856 { 857 static struct MyStruct 858 { 859 Node opCast(T: Node)() 860 { 861 auto node = Node(["hi"], "!mystruct.tag"); 862 node.setStyle(CollectionStyle.flow); 863 return node; 864 } 865 } 866 867 auto node = Node(MyStruct.init); 868 assert(node.tag == "!mystruct.tag"); 869 assert(node.collectionStyle == CollectionStyle.flow); 870 } 871 @safe unittest 872 { 873 assertThrown!NodeException(Node("42").get!int); 874 assertThrown!NodeException(Node("42").get!double); 875 assertThrown!NodeException(Node(long.max).get!ushort); 876 Node(null).get!YAMLNull; 877 } 878 @safe unittest 879 { 880 const node = Node(42); 881 assert(node.get!int == 42); 882 assert(node.get!string == "42"); 883 assert(node.get!double == 42.0); 884 885 immutable node2 = Node(42); 886 assert(node2.get!int == 42); 887 assert(node2.get!(const int) == 42); 888 assert(node2.get!(immutable int) == 42); 889 assert(node2.get!string == "42"); 890 assert(node2.get!(const string) == "42"); 891 assert(node2.get!(immutable string) == "42"); 892 assert(node2.get!double == 42.0); 893 assert(node2.get!(const double) == 42.0); 894 assert(node2.get!(immutable double) == 42.0); 895 } 896 897 /** If this is a collection, return its _length. 898 * 899 * Otherwise, throw NodeException. 900 * 901 * Returns: Number of elements in a sequence or key-value pairs in a mapping. 902 * 903 * Throws: NodeException if this is not a sequence nor a mapping. 904 */ 905 @property size_t length() const @safe 906 { 907 final switch(nodeID) 908 { 909 case NodeID.sequence: 910 return getValue!(Node[]).length; 911 case NodeID.mapping: 912 return getValue!(Pair[]).length; 913 case NodeID.scalar: 914 case NodeID.invalid: 915 throw new NodeException(text("Trying to get length of a ", nodeID, " node"), startMark_); 916 } 917 } 918 @safe unittest 919 { 920 auto node = Node([1,2,3]); 921 assert(node.length == 3); 922 const cNode = Node([1,2,3]); 923 assert(cNode.length == 3); 924 immutable iNode = Node([1,2,3]); 925 assert(iNode.length == 3); 926 } 927 928 /** Get the element at specified index. 929 * 930 * If the node is a sequence, index must be integral. 931 * 932 * 933 * If the node is a mapping, return the value corresponding to the first 934 * key equal to index. containsKey() can be used to determine if a mapping 935 * has a specific key. 936 * 937 * To get element at a null index, use YAMLNull for index. 938 * 939 * Params: index = Index to use. 940 * 941 * Returns: Value corresponding to the index. 942 * 943 * Throws: NodeException if the index could not be found, 944 * non-integral index is used with a sequence or the node is 945 * not a collection. 946 */ 947 ref inout(Node) opIndex(T)(T index) inout @safe 948 { 949 final switch (nodeID) 950 { 951 case NodeID.sequence: 952 checkSequenceIndex(index); 953 static if(isIntegral!T) 954 { 955 return getValue!(Node[])[index]; 956 } 957 else 958 { 959 assert(false, "Only integers may index sequence nodes"); 960 } 961 case NodeID.mapping: 962 auto idx = findPair(index); 963 if(idx >= 0) 964 { 965 return getValue!(Pair[])[idx].value; 966 } 967 968 string msg = "Mapping index not found" ~ (isSomeString!T ? ": " ~ to!string(index) : ""); 969 throw new NodeException(msg, startMark_); 970 case NodeID.scalar: 971 case NodeID.invalid: 972 throw new NodeException(text("Trying to index a ", nodeID, " node"), startMark_); 973 } 974 } 975 /// 976 @safe unittest 977 { 978 Node narray = Node([11, 12, 13, 14]); 979 Node nmap = Node(["11", "12", "13", "14"], [11, 12, 13, 14]); 980 981 assert(narray[0].as!int == 11); 982 assert(null !is collectException(narray[42])); 983 assert(nmap["11"].as!int == 11); 984 assert(nmap["14"].as!int == 14); 985 } 986 @safe unittest 987 { 988 Node narray = Node([11, 12, 13, 14]); 989 Node nmap = Node(["11", "12", "13", "14"], [11, 12, 13, 14]); 990 991 assert(narray[0].as!int == 11); 992 assert(null !is collectException(narray[42])); 993 assert(nmap["11"].as!int == 11); 994 assert(nmap["14"].as!int == 14); 995 assert(null !is collectException(nmap["42"])); 996 997 narray.add(null); 998 nmap.add(null, "Nothing"); 999 assert(narray[4].as!YAMLNull == null); 1000 assert(nmap[null].as!string == "Nothing"); 1001 1002 assertThrown!NodeException(nmap[11]); 1003 assertThrown!NodeException(nmap[14]); 1004 } 1005 1006 /** Determine if a collection contains specified value. 1007 * 1008 * If the node is a sequence, check if it contains the specified value. 1009 * If it's a mapping, check if it has a value that matches specified value. 1010 * 1011 * Params: rhs = Item to look for. Use YAMLNull to check for a null value. 1012 * 1013 * Returns: true if rhs was found, false otherwise. 1014 * 1015 * Throws: NodeException if the node is not a collection. 1016 */ 1017 bool contains(T)(T rhs) const 1018 { 1019 return contains_!(T, No.key, "contains")(rhs); 1020 } 1021 @safe unittest 1022 { 1023 auto mNode = Node(["1", "2", "3"]); 1024 assert(mNode.contains("2")); 1025 const cNode = Node(["1", "2", "3"]); 1026 assert(cNode.contains("2")); 1027 immutable iNode = Node(["1", "2", "3"]); 1028 assert(iNode.contains("2")); 1029 } 1030 1031 1032 /** Determine if a mapping contains specified key. 1033 * 1034 * Params: rhs = Key to look for. Use YAMLNull to check for a null key. 1035 * 1036 * Returns: true if rhs was found, false otherwise. 1037 * 1038 * Throws: NodeException if the node is not a mapping. 1039 */ 1040 bool containsKey(T)(T rhs) const 1041 { 1042 return contains_!(T, Yes.key, "containsKey")(rhs); 1043 } 1044 1045 // Unittest for contains() and containsKey(). 1046 @safe unittest 1047 { 1048 auto seq = Node([1, 2, 3, 4, 5]); 1049 assert(seq.contains(3)); 1050 assert(seq.contains(5)); 1051 assert(!seq.contains("5")); 1052 assert(!seq.contains(6)); 1053 assert(!seq.contains(float.nan)); 1054 assertThrown!NodeException(seq.containsKey(5)); 1055 1056 auto seq2 = Node(["1", "2"]); 1057 assert(seq2.contains("1")); 1058 assert(!seq2.contains(1)); 1059 1060 auto map = Node(["1", "2", "3", "4"], [1, 2, 3, 4]); 1061 assert(map.contains(1)); 1062 assert(!map.contains("1")); 1063 assert(!map.contains(5)); 1064 assert(!map.contains(float.nan)); 1065 assert(map.containsKey("1")); 1066 assert(map.containsKey("4")); 1067 assert(!map.containsKey(1)); 1068 assert(!map.containsKey("5")); 1069 1070 assert(!seq.contains(null)); 1071 assert(!map.contains(null)); 1072 assert(!map.containsKey(null)); 1073 seq.add(null); 1074 map.add("Nothing", null); 1075 assert(seq.contains(null)); 1076 assert(map.contains(null)); 1077 assert(!map.containsKey(null)); 1078 map.add(null, "Nothing"); 1079 assert(map.containsKey(null)); 1080 1081 auto map2 = Node([1, 2, 3, 4], [1, 2, 3, 4]); 1082 assert(!map2.contains("1")); 1083 assert(map2.contains(1)); 1084 assert(!map2.containsKey("1")); 1085 assert(map2.containsKey(1)); 1086 1087 // scalar 1088 assertThrown!NodeException(Node(1).contains(4)); 1089 assertThrown!NodeException(Node(1).containsKey(4)); 1090 1091 auto mapNan = Node([1.0, 2, double.nan], [1, double.nan, 5]); 1092 1093 assert(mapNan.contains(double.nan)); 1094 assert(mapNan.containsKey(double.nan)); 1095 } 1096 1097 /// Assignment (shallow copy) by value. 1098 void opAssign()(auto ref Node rhs) 1099 { 1100 assumeWontThrow(setValue(rhs.value_)); 1101 startMark_ = rhs.startMark_; 1102 tag_ = rhs.tag_; 1103 scalarStyle = rhs.scalarStyle; 1104 collectionStyle = rhs.collectionStyle; 1105 } 1106 // Unittest for opAssign(). 1107 @safe unittest 1108 { 1109 auto seq = Node([1, 2, 3, 4, 5]); 1110 auto assigned = seq; 1111 assert(seq == assigned, 1112 "Node.opAssign() doesn't produce an equivalent copy"); 1113 } 1114 1115 /** Set element at specified index in a collection. 1116 * 1117 * This method can only be called on collection nodes. 1118 * 1119 * If the node is a sequence, index must be integral. 1120 * 1121 * If the node is a mapping, sets the _value corresponding to the first 1122 * key matching index (including conversion, so e.g. "42" matches 42). 1123 * 1124 * If the node is a mapping and no key matches index, a new key-value 1125 * pair is added to the mapping. In sequences the index must be in 1126 * range. This ensures behavior siilar to D arrays and associative 1127 * arrays. 1128 * 1129 * To set element at a null index, use YAMLNull for index. 1130 * 1131 * Params: 1132 * value = Value to assign. 1133 * index = Index of the value to set. 1134 * 1135 * Throws: NodeException if the node is not a collection, index is out 1136 * of range or if a non-integral index is used on a sequence node. 1137 */ 1138 void opIndexAssign(K, V)(V value, K index) 1139 { 1140 final switch (nodeID) 1141 { 1142 case NodeID.sequence: 1143 checkSequenceIndex(index); 1144 static if(isIntegral!K || is(Unqual!K == bool)) 1145 { 1146 auto nodes = getValue!(Node[]); 1147 static if(is(Unqual!V == Node)){nodes[index] = value;} 1148 else {nodes[index] = Node(value);} 1149 setValue(nodes); 1150 return; 1151 } 1152 assert(false, "Only integers may index sequence nodes"); 1153 case NodeID.mapping: 1154 const idx = findPair(index); 1155 if(idx < 0){add(index, value);} 1156 else 1157 { 1158 auto pairs = as!(Node.Pair[])(); 1159 static if(is(Unqual!V == Node)){pairs[idx].value = value;} 1160 else {pairs[idx].value = Node(value);} 1161 setValue(pairs); 1162 } 1163 return; 1164 case NodeID.scalar: 1165 case NodeID.invalid: 1166 throw new NodeException(text("Trying to index a ", nodeID, " node"), startMark_); 1167 } 1168 } 1169 @safe unittest 1170 { 1171 with(Node([1, 2, 3, 4, 3])) 1172 { 1173 opIndexAssign(42, 3); 1174 assert(length == 5); 1175 assert(opIndex(3).as!int == 42); 1176 1177 opIndexAssign(null, 0); 1178 assert(opIndex(0) == null); 1179 } 1180 with(Node(["1", "2", "3"], [4, 5, 6])) 1181 { 1182 opIndexAssign(42, "3"); 1183 opIndexAssign(123, 456); 1184 assert(length == 4); 1185 assert(opIndex("3").as!int == 42); 1186 assert(opIndex(456).as!int == 123); 1187 1188 opIndexAssign(43, 3); 1189 //3 and "3" should be different 1190 assert(length == 5); 1191 assert(opIndex("3").as!int == 42); 1192 assert(opIndex(3).as!int == 43); 1193 1194 opIndexAssign(null, "2"); 1195 assert(opIndex("2") == null); 1196 } 1197 } 1198 1199 /** Return a range object iterating over a sequence, getting each 1200 * element as T. 1201 * 1202 * If T is Node, simply iterate over the nodes in the sequence. 1203 * Otherwise, convert each node to T during iteration. 1204 * 1205 * Throws: NodeException if the node is not a sequence or an element 1206 * could not be converted to specified type. 1207 */ 1208 template sequence(T = Node) 1209 { 1210 auto sequence(this This)() 1211 { 1212 import mir.ndslice.slice: sliced; 1213 import mir.ndslice.topology: map; 1214 if (nodeID != NodeID.sequence) 1215 throw new NodeException(text("Trying to 'sequence'-iterate over a ", nodeID, " node"), startMark_); 1216 static if (is(T == Node)) 1217 return get!(Node[]).sliced; 1218 else 1219 return get!(Node[]).map!((ref elem) => elem.as!T); 1220 } 1221 } 1222 @safe unittest 1223 { 1224 import mir.ndslice.topology: map; 1225 Node n1 = Node([1, 2, 3, 4]); 1226 int[int] array; 1227 Node n2 = Node(array); 1228 const n3 = Node([1, 2, 3, 4]); 1229 1230 auto r = n1.sequence!int.map!(x => x * 10); 1231 assert(r == [10, 20, 30, 40]); 1232 1233 assertThrown(n2.sequence); 1234 1235 auto r2 = n3.sequence!int.map!(x => x * 10); 1236 assert(r2 == [10, 20, 30, 40]); 1237 } 1238 1239 /** Return a range object iterating over mapping's pairs. 1240 * 1241 * Throws: NodeException if the node is not a mapping. 1242 * 1243 */ 1244 template mapping() 1245 { 1246 auto mapping(this This)() 1247 { 1248 import mir.ndslice.slice: sliced; 1249 if (nodeID != NodeID.mapping) 1250 throw new NodeException(text("Trying to 'mapping'-iterate over a ", nodeID, " node"), startMark_); 1251 return get!(Node.Pair[]).sliced; 1252 } 1253 } 1254 1255 @safe unittest 1256 { 1257 int[int] array; 1258 Node n = Node(array); 1259 n[1] = "foo"; 1260 n[2] = "bar"; 1261 n[3] = "baz"; 1262 1263 string[int] test; 1264 foreach (pair; n.mapping) 1265 test[pair.key.as!int] = pair.value.as!string; 1266 1267 assert(test[1] == "foo"); 1268 assert(test[2] == "bar"); 1269 assert(test[3] == "baz"); 1270 1271 int[int] constArray = [1: 2, 3: 4]; 1272 const x = Node(constArray); 1273 foreach (pair; x.mapping) 1274 assert(pair.value == constArray[pair.key.as!int]); 1275 } 1276 1277 /** Return a range object iterating over mapping's keys. 1278 * 1279 * If K is Node, simply iterate over the keys in the mapping. 1280 * Otherwise, convert each key to T during iteration. 1281 * 1282 * Throws: NodeException if the nodes is not a mapping or an element 1283 * could not be converted to specified type. 1284 */ 1285 auto mappingKeys(K = Node)() const 1286 { 1287 import mir.ndslice.topology: map; 1288 if (nodeID != NodeID.mapping) 1289 throw new NodeException(text("Trying to 'mappingKeys'-iterate over a ", nodeID, " node"), startMark_); 1290 static if (is(Unqual!K == Node)) 1291 return mapping.map!(pair => pair.key); 1292 else 1293 return mapping.map!(pair => pair.key.as!K); 1294 } 1295 @safe unittest 1296 { 1297 int[int] array; 1298 Node m1 = Node(array); 1299 m1["foo"] = 2; 1300 m1["bar"] = 3; 1301 1302 assert(m1.mappingKeys == ["foo", "bar"] || m1.mappingKeys == ["bar", "foo"]); 1303 1304 const cm1 = Node(["foo": 2, "bar": 3]); 1305 1306 assert(cm1.mappingKeys == ["foo", "bar"] || cm1.mappingKeys == ["bar", "foo"]); 1307 } 1308 1309 /** Return a range object iterating over mapping's values. 1310 * 1311 * If V is Node, simply iterate over the values in the mapping. 1312 * Otherwise, convert each key to V during iteration. 1313 * 1314 * Throws: NodeException if the nodes is not a mapping or an element 1315 * could not be converted to specified type. 1316 */ 1317 auto mappingValues(V = Node)() const 1318 { 1319 import mir.ndslice.topology: map; 1320 if (nodeID != NodeID.mapping) 1321 throw new NodeException(text("Trying to 'mappingValues'-iterate over a ", nodeID, " node"), startMark_); 1322 static if (is(Unqual!V == Node)) 1323 return mapping.map!(pair => pair.value); 1324 else 1325 return mapping.map!(pair => pair.value.as!V); 1326 } 1327 @safe unittest 1328 { 1329 int[int] array; 1330 Node m1 = Node(array); 1331 m1["foo"] = 2; 1332 m1["bar"] = 3; 1333 1334 assert(m1.mappingValues == [2, 3] || m1.mappingValues == [3, 2]); 1335 1336 const cm1 = Node(["foo": 2, "bar": 3]); 1337 1338 assert(cm1.mappingValues == [2, 3] || cm1.mappingValues == [3, 2]); 1339 } 1340 1341 1342 /** Foreach over a sequence, getting each element as T. 1343 * 1344 * If T is Node, simply iterate over the nodes in the sequence. 1345 * Otherwise, convert each node to T during iteration. 1346 * 1347 * Throws: NodeException if the node is not a sequence or an 1348 * element could not be converted to specified type. 1349 */ 1350 int opApply(D)(D dg) if (isDelegate!D && (Parameters!D.length == 1)) 1351 { 1352 if (nodeID != NodeID.sequence) 1353 throw new NodeException(text("Trying to sequence-foreach over a ", nodeID, " node"), startMark_); 1354 1355 int result; 1356 foreach(ref node; get!(Node[])) 1357 { 1358 static if(is(Unqual!(Parameters!D[0]) == Node)) 1359 { 1360 result = dg(node); 1361 } 1362 else 1363 { 1364 Parameters!D[0] temp = node.as!(Parameters!D[0]); 1365 result = dg(temp); 1366 } 1367 if(result){break;} 1368 } 1369 return result; 1370 } 1371 /// ditto 1372 int opApply(D)(D dg) const if (isDelegate!D && (Parameters!D.length == 1)) 1373 { 1374 if (nodeID != NodeID.sequence) 1375 throw new NodeException(text("Trying to sequence-foreach over a ", nodeID, " node"), startMark_); 1376 1377 int result; 1378 foreach(ref node; get!(Node[])) 1379 { 1380 static if(is(Unqual!(Parameters!D[0]) == Node)) 1381 { 1382 result = dg(node); 1383 } 1384 else 1385 { 1386 Parameters!D[0] temp = node.as!(Parameters!D[0]); 1387 result = dg(temp); 1388 } 1389 if(result){break;} 1390 } 1391 return result; 1392 } 1393 @safe unittest 1394 { 1395 Node n1 = Node(11); 1396 Node n2 = Node(12); 1397 Node n3 = Node(13); 1398 Node n4 = Node(14); 1399 Node narray = Node([n1, n2, n3, n4]); 1400 const cNArray = narray; 1401 1402 int[] array, array2, array3; 1403 foreach(int value; narray) 1404 { 1405 array ~= value; 1406 } 1407 foreach(Node node; narray) 1408 { 1409 array2 ~= node.as!int; 1410 } 1411 foreach (const Node node; cNArray) 1412 { 1413 array3 ~= node.as!int; 1414 } 1415 assert(array == [11, 12, 13, 14]); 1416 assert(array2 == [11, 12, 13, 14]); 1417 assert(array3 == [11, 12, 13, 14]); 1418 } 1419 @safe unittest 1420 { 1421 string[] testStrs = ["1", "2", "3"]; 1422 auto node1 = Node(testStrs); 1423 int i = 0; 1424 foreach (string elem; node1) 1425 { 1426 assert(elem == testStrs[i]); 1427 i++; 1428 } 1429 const node2 = Node(testStrs); 1430 i = 0; 1431 foreach (string elem; node2) 1432 { 1433 assert(elem == testStrs[i]); 1434 i++; 1435 } 1436 immutable node3 = Node(testStrs); 1437 i = 0; 1438 foreach (string elem; node3) 1439 { 1440 assert(elem == testStrs[i]); 1441 i++; 1442 } 1443 } 1444 @safe unittest 1445 { 1446 auto node = Node(["a":1, "b":2, "c":3]); 1447 const cNode = node; 1448 assertThrown({foreach (Node n; node) {}}()); 1449 assertThrown({foreach (const Node n; cNode) {}}()); 1450 } 1451 1452 /** Foreach over a mapping, getting each key/value as K/V. 1453 * 1454 * If the K and/or V is Node, simply iterate over the nodes in the mapping. 1455 * Otherwise, convert each key/value to T during iteration. 1456 * 1457 * Throws: NodeException if the node is not a mapping or an 1458 * element could not be converted to specified type. 1459 */ 1460 int opApply(DG)(DG dg) if (isDelegate!DG && (Parameters!DG.length == 2)) 1461 { 1462 alias K = Parameters!DG[0]; 1463 alias V = Parameters!DG[1]; 1464 if (nodeID != NodeID.mapping) 1465 throw new NodeException(text("Trying to mapping-foreach over a ", nodeID, " node"), startMark_); 1466 1467 int result; 1468 foreach(ref pair; get!(Node.Pair[])) 1469 { 1470 static if(is(Unqual!K == Node) && is(Unqual!V == Node)) 1471 { 1472 result = dg(pair.key, pair.value); 1473 } 1474 else static if(is(Unqual!K == Node)) 1475 { 1476 V tempValue = pair.value.as!V; 1477 result = dg(pair.key, tempValue); 1478 } 1479 else static if(is(Unqual!V == Node)) 1480 { 1481 K tempKey = pair.key.as!K; 1482 result = dg(tempKey, pair.value); 1483 } 1484 else 1485 { 1486 K tempKey = pair.key.as!K; 1487 V tempValue = pair.value.as!V; 1488 result = dg(tempKey, tempValue); 1489 } 1490 1491 if(result){break;} 1492 } 1493 return result; 1494 } 1495 /// ditto 1496 int opApply(DG)(DG dg) const if (isDelegate!DG && (Parameters!DG.length == 2)) 1497 { 1498 alias K = Parameters!DG[0]; 1499 alias V = Parameters!DG[1]; 1500 if (nodeID != NodeID.mapping) 1501 throw new NodeException(text("Trying to mapping-foreach over a ", nodeID, " node"), startMark_); 1502 1503 int result; 1504 foreach(ref pair; get!(Node.Pair[])) 1505 { 1506 static if(is(Unqual!K == Node) && is(Unqual!V == Node)) 1507 { 1508 result = dg(pair.key, pair.value); 1509 } 1510 else static if(is(Unqual!K == Node)) 1511 { 1512 V tempValue = pair.value.as!V; 1513 result = dg(pair.key, tempValue); 1514 } 1515 else static if(is(Unqual!V == Node)) 1516 { 1517 K tempKey = pair.key.as!K; 1518 result = dg(tempKey, pair.value); 1519 } 1520 else 1521 { 1522 K tempKey = pair.key.as!K; 1523 V tempValue = pair.value.as!V; 1524 result = dg(tempKey, tempValue); 1525 } 1526 1527 if(result){break;} 1528 } 1529 return result; 1530 } 1531 @safe unittest 1532 { 1533 Node n1 = Node(cast(long)11); 1534 Node n2 = Node(cast(long)12); 1535 Node n3 = Node(cast(long)13); 1536 Node n4 = Node(cast(long)14); 1537 1538 Node k1 = Node("11"); 1539 Node k2 = Node("12"); 1540 Node k3 = Node("13"); 1541 Node k4 = Node("14"); 1542 1543 Node nmap1 = Node([Pair(k1, n1), 1544 Pair(k2, n2), 1545 Pair(k3, n3), 1546 Pair(k4, n4)]); 1547 1548 int[string] expected = ["11" : 11, 1549 "12" : 12, 1550 "13" : 13, 1551 "14" : 14]; 1552 int[string] array; 1553 foreach(string key, int value; nmap1) 1554 { 1555 array[key] = value; 1556 } 1557 assert(array == expected); 1558 1559 Node nmap2 = Node([Pair(k1, Node(cast(long)5)), 1560 Pair(k2, Node(true)), 1561 Pair(k3, Node(cast(double)1.0)), 1562 Pair(k4, Node("yarly"))]); 1563 1564 foreach(string key, Node value; nmap2) 1565 { 1566 switch(key) 1567 { 1568 case "11": assert(value.as!int == 5 ); break; 1569 case "12": assert(value.as!bool == true ); break; 1570 case "13": assert(value.as!float == 1.0 ); break; 1571 case "14": assert(value.as!string == "yarly"); break; 1572 default: assert(false); 1573 } 1574 } 1575 const nmap3 = nmap2; 1576 1577 foreach(const Node key, const Node value; nmap3) 1578 { 1579 switch(key.as!string) 1580 { 1581 case "11": assert(value.as!int == 5 ); break; 1582 case "12": assert(value.as!bool == true ); break; 1583 case "13": assert(value.as!float == 1.0 ); break; 1584 case "14": assert(value.as!string == "yarly"); break; 1585 default: assert(false); 1586 } 1587 } 1588 } 1589 @safe unittest 1590 { 1591 string[int] testStrs = [0: "1", 1: "2", 2: "3"]; 1592 auto node1 = Node(testStrs); 1593 foreach (const int i, string elem; node1) 1594 { 1595 assert(elem == testStrs[i]); 1596 } 1597 const node2 = Node(testStrs); 1598 foreach (const int i, string elem; node2) 1599 { 1600 assert(elem == testStrs[i]); 1601 } 1602 immutable node3 = Node(testStrs); 1603 foreach (const int i, string elem; node3) 1604 { 1605 assert(elem == testStrs[i]); 1606 } 1607 } 1608 @safe unittest 1609 { 1610 auto node = Node(["a", "b", "c"]); 1611 const cNode = node; 1612 assertThrown({foreach (Node a, Node b; node) {}}()); 1613 assertThrown({foreach (const Node a, const Node b; cNode) {}}()); 1614 } 1615 1616 /** Add an element to a sequence. 1617 * 1618 * This method can only be called on sequence nodes. 1619 * 1620 * If value is a node, it is copied to the sequence directly. Otherwise 1621 * value is converted to a node and then stored in the sequence. 1622 * 1623 * $(P When emitting, all values in the sequence will be emitted. When 1624 * using the !!set tag, the user needs to ensure that all elements in 1625 * the sequence are unique, otherwise $(B invalid) YAML code will be 1626 * emitted.) 1627 * 1628 * Params: value = Value to _add to the sequence. 1629 */ 1630 void add(T)(T value) 1631 { 1632 if (value_.isNull) 1633 { 1634 setValue(Node[].init); 1635 } 1636 if (nodeID != NodeID.sequence) 1637 throw new NodeException(text("Trying to add an element to a ", nodeID, " node"), startMark_); 1638 1639 auto nodes = get!(Node[])(); 1640 static if(is(Unqual!T == Node)){nodes ~= value;} 1641 else {nodes ~= Node(value);} 1642 setValue(nodes); 1643 } 1644 @safe unittest 1645 { 1646 with(Node([1, 2, 3, 4])) 1647 { 1648 add(5.0f); 1649 assert(opIndex(4).as!float == 5.0f); 1650 } 1651 with(Node()) 1652 { 1653 add(5.0f); 1654 assert(opIndex(0).as!float == 5.0f); 1655 } 1656 with(Node(5.0f)) 1657 { 1658 assertThrown!NodeException(add(5.0f)); 1659 } 1660 with(Node([5.0f : true])) 1661 { 1662 assertThrown!NodeException(add(5.0f)); 1663 } 1664 } 1665 1666 /** Add a key-value pair to a mapping. 1667 * 1668 * This method can only be called on mapping nodes. 1669 * 1670 * If key and/or value is a node, it is copied to the mapping directly. 1671 * Otherwise it is converted to a node and then stored in the mapping. 1672 * 1673 * $(P It is possible for the same key to be present more than once in a 1674 * mapping. When emitting, all key-value pairs will be emitted. 1675 * This is useful with the "!!pairs" tag, but will result in 1676 * $(B invalid) YAML with "!!map" and "!!omap" tags.) 1677 * 1678 * Params: key = Key to _add. 1679 * value = Value to _add. 1680 */ 1681 void add(K, V)(K key, V value) 1682 { 1683 if (value_.isNull) 1684 { 1685 setValue(Node.Pair[].init); 1686 } 1687 if (nodeID != NodeID.mapping) 1688 throw new NodeException(text("Trying to add a key-value pair to a ", nodeID, " node"), startMark_); 1689 1690 auto pairs = get!(Node.Pair[])(); 1691 pairs ~= Pair(key, value); 1692 setValue(pairs); 1693 } 1694 @safe unittest 1695 { 1696 with(Node([1, 2], [3, 4])) 1697 { 1698 add(5, "6"); 1699 assert(opIndex(5).as!string == "6"); 1700 } 1701 with(Node()) 1702 { 1703 add(5, "6"); 1704 assert(opIndex(5).as!string == "6"); 1705 } 1706 with(Node(5.0f)) 1707 { 1708 assertThrown!NodeException(add(5, "6")); 1709 } 1710 with(Node([5.0f])) 1711 { 1712 assertThrown!NodeException(add(5, "6")); 1713 } 1714 } 1715 1716 /** Determine whether a key is in a mapping, and access its value. 1717 * 1718 * This method can only be called on mapping nodes. 1719 * 1720 * Params: key = Key to search for. 1721 * 1722 * Returns: A pointer to the value (as a Node) corresponding to key, 1723 * or null if not found. 1724 * 1725 * Note: Any modification to the node can invalidate the returned 1726 * pointer. 1727 * 1728 * See_Also: contains 1729 */ 1730 inout(Node*) opBinaryRight(string op, K)(K key) inout 1731 if (op == "in") 1732 { 1733 if (nodeID != NodeID.mapping) 1734 throw new NodeException(text("Trying to use 'in' on a ", nodeID, " node"), startMark_); 1735 1736 auto idx = findPair(key); 1737 if(idx < 0) 1738 { 1739 return null; 1740 } 1741 else 1742 { 1743 return &(get!(Node.Pair[])[idx].value); 1744 } 1745 } 1746 @safe unittest 1747 { 1748 auto mapping = Node(["foo", "baz"], ["bar", "qux"]); 1749 assert("bad" !in mapping && ("bad" in mapping) is null); 1750 Node* foo = "foo" in mapping; 1751 assert(foo !is null); 1752 assert(*foo == Node("bar")); 1753 assert(foo.get!string == "bar"); 1754 *foo = Node("newfoo"); 1755 assert(mapping["foo"] == Node("newfoo")); 1756 } 1757 @safe unittest 1758 { 1759 auto mNode = Node(["a": 2]); 1760 assert("a" in mNode); 1761 const cNode = Node(["a": 2]); 1762 assert("a" in cNode); 1763 immutable iNode = Node(["a": 2]); 1764 assert("a" in iNode); 1765 } 1766 1767 /** Remove first (if any) occurence of a value in a collection. 1768 * 1769 * This method can only be called on collection nodes. 1770 * 1771 * If the node is a sequence, the first node matching value is removed. 1772 * If the node is a mapping, the first key-value pair where _value 1773 * matches specified value is removed. 1774 * 1775 * Params: rhs = Value to _remove. 1776 * 1777 * Throws: NodeException if the node is not a collection. 1778 */ 1779 void remove(T)(T rhs) 1780 { 1781 remove_!(T, No.key, "remove")(rhs); 1782 } 1783 @safe unittest 1784 { 1785 with(Node([1, 2, 3, 4, 3])) 1786 { 1787 remove(3); 1788 assert(length == 4); 1789 assert(opIndex(2).as!int == 4); 1790 assert(opIndex(3).as!int == 3); 1791 1792 add(null); 1793 assert(length == 5); 1794 remove(null); 1795 assert(length == 4); 1796 } 1797 with(Node(["1", "2", "3"], [4, 5, 6])) 1798 { 1799 remove(4); 1800 assert(length == 2); 1801 add("nullkey", null); 1802 assert(length == 3); 1803 remove(null); 1804 assert(length == 2); 1805 } 1806 } 1807 1808 /** Remove element at the specified index of a collection. 1809 * 1810 * This method can only be called on collection nodes. 1811 * 1812 * If the node is a sequence, index must be integral. 1813 * 1814 * If the node is a mapping, remove the first key-value pair where 1815 * key matches index. 1816 * 1817 * If the node is a mapping and no key matches index, nothing is removed 1818 * and no exception is thrown. This ensures behavior siilar to D arrays 1819 * and associative arrays. 1820 * 1821 * Params: index = Index to remove at. 1822 * 1823 * Throws: NodeException if the node is not a collection, index is out 1824 * of range or if a non-integral index is used on a sequence node. 1825 */ 1826 void removeAt(T)(T index) 1827 { 1828 remove_!(T, Yes.key, "removeAt")(index); 1829 } 1830 @safe unittest 1831 { 1832 with(Node([1, 2, 3, 4, 3])) 1833 { 1834 removeAt(3); 1835 assertThrown!NodeException(removeAt("3")); 1836 assert(length == 4); 1837 assert(opIndex(3).as!int == 3); 1838 } 1839 with(Node(["1", "2", "3"], [4, 5, 6])) 1840 { 1841 // no integer 2 key, so don't remove anything 1842 removeAt(2); 1843 assert(length == 3); 1844 removeAt("2"); 1845 assert(length == 2); 1846 add(null, "nullval"); 1847 assert(length == 3); 1848 removeAt(null); 1849 assert(length == 2); 1850 } 1851 } 1852 1853 /// Compare with another _node. 1854 int opCmp(const ref Node rhs) const @safe 1855 { 1856 // Compare tags - if equal or both null, we need to compare further. 1857 const tagCmp = (tag_ is null) ? (rhs.tag_ is null) ? 0 : -1 1858 : (rhs.tag_ is null) ? 1 : __cmp(tag_, rhs.tag_); 1859 if(tagCmp != 0){return tagCmp;} 1860 1861 static int cmp(T1, T2)(T1 a, T2 b) 1862 { 1863 return a > b ? 1 : 1864 a < b ? -1 : 1865 0; 1866 } 1867 1868 // Compare validity: if both valid, we have to compare further. 1869 1870 const typeCmp = cmp(type, rhs.type); 1871 if(typeCmp != 0){return typeCmp;} 1872 1873 static int compareCollections(T)(const ref Node lhs, const ref Node rhs) 1874 { 1875 const c1 = lhs.getValue!T; 1876 const c2 = rhs.getValue!T; 1877 if(c1 is c2){return 0;} 1878 if(c1.length != c2.length) 1879 { 1880 return cmp(c1.length, c2.length); 1881 } 1882 // Equal lengths, compare items. 1883 foreach(i; 0 .. c1.length) 1884 { 1885 const itemCmp = c1[i].opCmp(c2[i]); 1886 if(itemCmp != 0){return itemCmp;} 1887 } 1888 return 0; 1889 } 1890 1891 final switch(type) 1892 { 1893 case NodeType..string: 1894 return __cmp(getValue!string, 1895 rhs.getValue!string); 1896 case NodeType.integer: 1897 return cmp(getValue!long, rhs.getValue!long); 1898 case NodeType.boolean: 1899 const b1 = getValue!bool; 1900 const b2 = rhs.getValue!bool; 1901 return b1 ? b2 ? 0 : 1 1902 : b2 ? -1 : 0; 1903 case NodeType.binary: 1904 const b1 = getValue!(ubyte[]); 1905 const b2 = rhs.getValue!(ubyte[]); 1906 return __cmp(b1, b2); 1907 case NodeType.null_: 1908 return 0; 1909 case NodeType.decimal: 1910 const r1 = getValue!double; 1911 const r2 = rhs.getValue!double; 1912 if(r1 != r1) 1913 { 1914 return r2 != r2 ? 0 : -1; 1915 } 1916 if(r2 != r2) 1917 { 1918 return 1; 1919 } 1920 // Fuzzy equality. 1921 if(r1 <= r2 + double.epsilon && r1 >= r2 - double.epsilon) 1922 { 1923 return 0; 1924 } 1925 return cmp(r1, r2); 1926 case NodeType.timestamp: 1927 const t1 = getValue!Timestamp; 1928 const t2 = rhs.getValue!Timestamp; 1929 return cmp(t1, t2); 1930 case NodeType.mapping: 1931 return compareCollections!(Pair[])(this, rhs); 1932 case NodeType.sequence: 1933 return compareCollections!(Node[])(this, rhs); 1934 case NodeType.merge: 1935 assert(false, "Cannot compare merge nodes"); 1936 } 1937 } 1938 1939 // Ensure opCmp is symmetric for collections 1940 @safe unittest 1941 { 1942 auto node1 = Node( 1943 [ 1944 Node("New York Yankees", "tag:yaml.org,2002:str"), 1945 Node("Atlanta Braves", "tag:yaml.org,2002:str") 1946 ], "tag:yaml.org,2002:seq" 1947 ); 1948 auto node2 = Node( 1949 [ 1950 Node("Detroit Tigers", "tag:yaml.org,2002:str"), 1951 Node("Chicago cubs", "tag:yaml.org,2002:str") 1952 ], "tag:yaml.org,2002:seq" 1953 ); 1954 assert(node1 > node2); 1955 assert(node2 < node1); 1956 } 1957 1958 // Compute hash of the node. 1959 hash_t toHash() nothrow const @trusted 1960 { 1961 const valueHash = value_.toHash(); 1962 1963 return tag_ is null ? valueHash : tag_.hashOf(valueHash); 1964 } 1965 @safe unittest 1966 { 1967 assert(Node(42).toHash() != Node(41).toHash()); 1968 assert(Node(42).toHash() != Node(42, "some-tag").toHash()); 1969 } 1970 1971 /// Get type of the node value. 1972 @property NodeType type() const @safe pure nothrow 1973 { 1974 return value_.kind; 1975 } 1976 1977 /// Get the kind of node this is. 1978 @property NodeID nodeID() const @safe nothrow 1979 { 1980 final switch (type) 1981 { 1982 case NodeType.sequence: 1983 return NodeID.sequence; 1984 case NodeType.mapping: 1985 return NodeID.mapping; 1986 case NodeType.boolean: 1987 case NodeType.integer: 1988 case NodeType.binary: 1989 case NodeType..string: 1990 case NodeType.timestamp: 1991 case NodeType.null_: 1992 case NodeType.merge: 1993 case NodeType.decimal: 1994 return NodeID.scalar; 1995 } 1996 } 1997 package: 1998 1999 // Get a string representation of the node tree. Used for debugging. 2000 // 2001 // Params: level = Level of the node in the tree. 2002 // 2003 // Returns: String representing the node tree. 2004 @property string debugString(uint level = 0) const @safe 2005 { 2006 string indent; 2007 foreach(i; 0 .. level){indent ~= " ";} 2008 2009 final switch (nodeID) 2010 { 2011 case NodeID.invalid: 2012 return indent ~ "invalid"; 2013 case NodeID.sequence: 2014 string result = indent ~ "sequence:\n"; 2015 foreach(ref node; get!(Node[])) 2016 { 2017 result ~= node.debugString(level + 1); 2018 } 2019 return result; 2020 case NodeID.mapping: 2021 string result = indent ~ "mapping:\n"; 2022 foreach(ref pair; get!(Node.Pair[])) 2023 { 2024 result ~= indent ~ " pair\n"; 2025 result ~= pair.key.debugString(level + 2); 2026 result ~= pair.value.debugString(level + 2); 2027 } 2028 return result; 2029 case NodeID.scalar: 2030 return text(indent, "scalar(", (convertsTo!string ? get!string : type.to!string), ")\n"); 2031 } 2032 } 2033 2034 2035 public: 2036 2037 // Determine if the value can be converted to specified type. 2038 @property bool convertsTo(T)() const 2039 { 2040 static if (staticIndexOf!(T, Value.AllowedTypes) >= 0) 2041 if (value_._is!T) 2042 return true; 2043 2044 // Every type allowed in Value should be convertible to string. 2045 static if(isSomeString!T) {return true;} 2046 else static if(isFloatingPoint!T){return type == NodeType.integer || type == NodeType.decimal;} 2047 else static if(isIntegral!T) {return type == NodeType.integer;} 2048 else static if(is(Unqual!T==bool)){return type == NodeType.boolean;} 2049 else {return false;} 2050 } 2051 /** 2052 * Sets the style of this node when dumped. 2053 * 2054 * Params: style = Any valid style. 2055 */ 2056 void setStyle(CollectionStyle style) @safe 2057 { 2058 if (nodeID != NodeID.mapping && nodeID != NodeID.sequence) 2059 throw new NodeException("Cannot set collection style for non-collection nodes", startMark_); 2060 collectionStyle = style; 2061 } 2062 /// Ditto 2063 void setStyle(ScalarStyle style) @safe 2064 { 2065 if (nodeID != NodeID.scalar) 2066 throw new NodeException("Cannot set scalar style for non-scalar nodes", startMark_); 2067 scalarStyle = style; 2068 } 2069 /// 2070 @safe unittest 2071 { 2072 import std.array: Appender; 2073 import dyaml.dumper; 2074 auto stream = new Appender!string(); 2075 auto node = Node([1, 2, 3, 4, 5]); 2076 node.setStyle(CollectionStyle.block); 2077 2078 auto dumper = dumper(); 2079 dumper.dump(stream, node); 2080 } 2081 /// 2082 @safe unittest 2083 { 2084 import std.array: Appender; 2085 import dyaml.dumper; 2086 auto stream = new Appender!string(); 2087 auto node = Node(4); 2088 node.setStyle(ScalarStyle.literal); 2089 2090 auto dumper = dumper(); 2091 dumper.dump(stream, node); 2092 } 2093 @safe unittest 2094 { 2095 assertThrown!NodeException(Node(4).setStyle(CollectionStyle.block)); 2096 assertThrown!NodeException(Node([4]).setStyle(ScalarStyle.literal)); 2097 } 2098 @safe unittest 2099 { 2100 import std.array: Appender; 2101 import dyaml.dumper; 2102 { 2103 auto stream = new Appender!string(); 2104 auto node = Node([1, 2, 3, 4, 5]); 2105 node.setStyle(CollectionStyle.block); 2106 auto dumper = dumper(); 2107 dumper.explicitEnd = false; 2108 dumper.explicitStart = false; 2109 dumper.YAMLVersion = null; 2110 dumper.dump(stream, node); 2111 2112 //Block style should start with a hyphen. 2113 assert(stream.data[0] == '-'); 2114 } 2115 { 2116 auto stream = new Appender!string(); 2117 auto node = Node([1, 2, 3, 4, 5]); 2118 node.setStyle(CollectionStyle.flow); 2119 auto dumper = dumper(); 2120 dumper.explicitEnd = false; 2121 dumper.explicitStart = false; 2122 dumper.YAMLVersion = null; 2123 dumper.dump(stream, node); 2124 2125 //Flow style should start with a bracket. 2126 assert(stream.data[0] == '['); 2127 } 2128 { 2129 auto stream = new Appender!string(); 2130 auto node = Node(1); 2131 node.setStyle(ScalarStyle.singleQuoted); 2132 auto dumper = dumper(); 2133 dumper.explicitEnd = false; 2134 dumper.explicitStart = false; 2135 dumper.YAMLVersion = null; 2136 dumper.dump(stream, node); 2137 2138 assert(stream.data == "!!int '1'\n"); 2139 } 2140 { 2141 auto stream = new Appender!string(); 2142 auto node = Node(1); 2143 node.setStyle(ScalarStyle.doubleQuoted); 2144 auto dumper = dumper(); 2145 dumper.explicitEnd = false; 2146 dumper.explicitStart = false; 2147 dumper.YAMLVersion = null; 2148 dumper.dump(stream, node); 2149 2150 assert(stream.data == "!!int \"1\"\n"); 2151 } 2152 } 2153 2154 /++ 2155 Determine if the value stored by the node is of specified type. 2156 This only works for default YAML types, not for user defined types. 2157 +/ 2158 @property bool _is(T)() const 2159 { 2160 return value_._is!T; 2161 } 2162 2163 private: 2164 2165 // Implementation of contains() and containsKey(). 2166 bool contains_(T, Flag!"key" key, string func)(T rhs) const 2167 { 2168 final switch (nodeID) 2169 { 2170 case NodeID.mapping: 2171 return findPair!(T, key)(rhs) >= 0; 2172 case NodeID.sequence: 2173 static if(!key) 2174 { 2175 foreach(ref node; getValue!(Node[])) 2176 { 2177 if(node == rhs){return true;} 2178 } 2179 return false; 2180 } 2181 else 2182 { 2183 throw new NodeException(text("Trying to use ", func, "() on a ", nodeID, " node"), startMark_); 2184 } 2185 case NodeID.scalar: 2186 case NodeID.invalid: 2187 throw new NodeException(text("Trying to use ", func, "() on a ", nodeID, " node"), startMark_); 2188 } 2189 2190 } 2191 2192 // Implementation of remove() and removeAt() 2193 void remove_(T, Flag!"key" key, string func)(T rhs) 2194 { 2195 static void removeElem(E, I)(ref Node node, I index) 2196 { 2197 auto elems = node.getValue!(E[]); 2198 foreach(i; cast(size_t)index + 1 .. elems.length) 2199 elems[i - 1] = elems[i]; 2200 elems.length = elems.length - 1; 2201 node.setValue(elems); 2202 } 2203 2204 final switch (nodeID) 2205 { 2206 case NodeID.mapping: 2207 const index = findPair!(T, key)(rhs); 2208 if(index >= 0){removeElem!Pair(this, index);} 2209 break; 2210 case NodeID.sequence: 2211 static long getIndex(ref Node node, ref T rhs) 2212 { 2213 foreach(idx, ref elem; node.get!(Node[])) 2214 { 2215 if(elem.convertsTo!T && elem.as!(T, No.stringConversion) == rhs) 2216 { 2217 return idx; 2218 } 2219 } 2220 return -1; 2221 } 2222 2223 const index = select!key(rhs, getIndex(this, rhs)); 2224 2225 // This throws if the index is not integral. 2226 checkSequenceIndex(index); 2227 2228 static if(isIntegral!(typeof(index))){removeElem!Node(this, index); break; } 2229 else {assert(false, "Non-integral sequence index");} 2230 case NodeID.scalar: 2231 case NodeID.invalid: 2232 throw new NodeException(text("Trying to " ~ func ~ "() from a ", nodeID, " node"), startMark_); 2233 } 2234 } 2235 2236 // Get index of pair with key (or value, if key is false) matching index. 2237 // Cannot be inferred @safe due to https://issues.dlang.org/show_bug.cgi?id=16528 2238 sizediff_t findPair(T, Flag!"key" key = Yes.key)(const ref T index) const @safe 2239 { 2240 const pairs = getValue!(Pair[])(); 2241 const(Node)* node; 2242 foreach(idx, ref const(Pair) pair; pairs) 2243 { 2244 static if(key){node = &pair.key;} 2245 else {node = &pair.value;} 2246 2247 static if (staticIndexOf!(T, Value.AllowedTypes) >= 0) 2248 const isT = node._is!T; 2249 else 2250 enum isT = false; 2251 const bool typeMatch = isFloatingPoint!T && (node.type == NodeType.integer || node.type == NodeType.decimal) || 2252 isIntegral!T && node.type == NodeType.integer || 2253 is(Unqual!T==bool) && node.type == NodeType.boolean || 2254 isSomeString!T && node.type == NodeType..string || 2255 isT; 2256 if(typeMatch && *node == index) 2257 { 2258 return idx; 2259 } 2260 } 2261 return -1; 2262 } 2263 2264 // Check if index is integral and in range. 2265 void checkSequenceIndex(T)(T index) const 2266 { 2267 assert(nodeID == NodeID.sequence, "checkSequenceIndex() called on a non-sequence node"); 2268 2269 static if(!isIntegral!T) 2270 { 2271 throw new NodeException("Indexing a sequence with a non-integral type.", startMark_); 2272 } 2273 else 2274 { 2275 if (!(index >= 0 && index < getValue!(Node[]).length)) 2276 throw new NodeException(text("Sequence index out of range: ", index), startMark_); 2277 } 2278 } 2279 // Safe wrapper for getting a value out of the variant. 2280 inout(T) getValue(T)() @trusted inout 2281 { 2282 return value_.get!T; 2283 } 2284 // Safe wrapper for coercing a value out of the variant. 2285 inout(T) coerceValue(T)() @trusted inout 2286 { 2287 import mir.conv: to; 2288 return value_.tryVisit!(to!T); 2289 } 2290 // Safe wrapper for setting a value for the variant. 2291 void setValue(T)(T value) @trusted 2292 { 2293 static if (allowed!T) 2294 { 2295 value_ = Value(value); 2296 } 2297 else 2298 { 2299 auto tmpNode = cast(Node)value; 2300 tag_ = tmpNode.tag; 2301 scalarStyle = tmpNode.scalarStyle; 2302 collectionStyle = tmpNode.collectionStyle; 2303 value_ = tmpNode.value_; 2304 } 2305 } 2306 2307 /// Serialization support for mir-ion and asdf 2308 public void serialize(S)(ref S serializer) const @safe 2309 { 2310 value_.visit!( 2311 (YAMLNull v) { 2312 serializer.putValue(null); 2313 }, 2314 (YAMLMerge v) { 2315 serializer.putValue("tag:yaml.org,2002:merge"); 2316 }, 2317 (bool v) { 2318 serializer.putValue(v); 2319 }, 2320 (long v) { 2321 serializer.putValue(v); 2322 }, 2323 (double v) { 2324 serializer.putValue(v); 2325 }, 2326 (const(ubyte)[] v) { 2327 import mir.lob: Blob; 2328 serializer.putValue(Blob(v)); 2329 }, 2330 (Timestamp v) { 2331 serializer.putValue(v); 2332 }, 2333 (string v) { 2334 serializer.putValue(v); 2335 }, 2336 (const(Node.Pair)[] v) { 2337 auto state = serializer.structBegin(v.length); 2338 foreach (ref e; v) 2339 { 2340 serializer.putKey(e.key.get!string); 2341 e.value.serialize(serializer); 2342 } 2343 serializer.structEnd(state); 2344 }, 2345 (const(Node)[] v) { 2346 auto state = serializer.listBegin(v.length); 2347 foreach (ref e; v) 2348 { 2349 serializer.elemBegin; 2350 e.serialize(serializer); 2351 } 2352 serializer.listEnd(state); 2353 }, 2354 ); 2355 } 2356 2357 /// Deserialization support for mir-ion 2358 public auto deserializeFromIon(IonDescribedValue)(scope const(char[])[] symbolTable, IonDescribedValue value) @trusted { 2359 import mir.ndslice.topology: map; 2360 import mir.array.allocation: array; 2361 return deserializeFromIon(symbolTable.map!idup.array, value); 2362 } 2363 2364 /// ditto 2365 public auto deserializeFromIon(IonDescribedValue)(const(string)[] symbolTable, IonDescribedValue value) @trusted { 2366 import mir.ion.exception; 2367 import mir.ion.type_code; 2368 import mir.ion.value; 2369 import mir.lob; 2370 import mir.timestamp; 2371 2372 if (value == null) { 2373 // Ion has typed null values 2374 value_ = YAMLNull.init; 2375 return null; 2376 } 2377 2378 final switch (value.descriptor.type) { 2379 2380 case IonTypeCode.null_: 2381 assert(0); // handled above 2382 case IonTypeCode.bool_: 2383 value_ = value.trustedGet!bool; 2384 return null; 2385 case IonTypeCode.uInt: 2386 case IonTypeCode.nInt: 2387 value_ = value.trustedGet!IonInt.get!long; 2388 return null; 2389 case IonTypeCode.float_: 2390 value_ = value.trustedGet!IonFloat.get!double; 2391 return null; 2392 case IonTypeCode.decimal: 2393 value_ = value.trustedGet!IonDecimal.get!double; 2394 return null; 2395 case IonTypeCode.timestamp: 2396 value_ = value.trustedGet!IonTimestamp.get!Timestamp; 2397 return null; 2398 case IonTypeCode.symbol: { 2399 auto symbolId = value.trustedGet!IonSymbolID.get; 2400 if (symbolId >= symbolTable.length) 2401 return IonErrorCode.symbolIdIsTooLargeForTheCurrentSymbolTable.ionException; 2402 value_ = symbolTable[symbolId]; 2403 return null; 2404 } 2405 case IonTypeCode..string: 2406 value_ = value.trustedGet!(const(char)[]).idup; 2407 return null; 2408 case IonTypeCode.clob: 2409 throw new IonException("Mir Ion: CLOBs aren't supported in DYAML"); 2410 case IonTypeCode.blob: // as ubyte[] 2411 value_ = value.trustedGet!Blob.data.dup; 2412 return null; 2413 case IonTypeCode.list: { 2414 auto list = value.trustedGet!IonList; 2415 auto ret = new Node[list.walkLength]; 2416 size_t i; 2417 foreach (IonDescribedValue elem; list) { 2418 Node val; 2419 if (auto exc = val.deserializeFromIon(symbolTable, elem)) 2420 return exc; 2421 ret[i++] = val; 2422 } 2423 value_ = ret; 2424 return null; 2425 } 2426 case IonTypeCode.sexp: { 2427 // TODO: use special types for SEXP if needed. 2428 auto sexp = value.trustedGet!IonSexp; 2429 auto ret = new Node[sexp.walkLength]; 2430 size_t i; 2431 foreach (IonDescribedValue elem; sexp) { 2432 Node val; 2433 if (auto exc = val.deserializeFromIon(symbolTable, elem)) 2434 return exc; 2435 ret[i++] = val; 2436 } 2437 value_ = ret; 2438 return null; 2439 } 2440 case IonTypeCode.struct_: { 2441 auto obj = value.trustedGet!IonStruct; 2442 auto ret = new Pair[obj.walkLength]; 2443 size_t i; 2444 foreach (size_t symbolId, IonDescribedValue elem; obj) { 2445 if (symbolId >= symbolTable.length) 2446 return IonErrorCode.symbolIdIsTooLargeForTheCurrentSymbolTable.ionException; 2447 auto key = symbolTable[symbolId]; 2448 Node val; 2449 if (auto exc = val.deserializeFromIon(symbolTable, elem)) 2450 return exc; 2451 ret[i++] = Pair(key, val); 2452 } 2453 value_ = ret; 2454 return null; 2455 } 2456 case IonTypeCode.annotations: { 2457 IonAnnotations annotations; 2458 value.trustedGet!IonAnnotationWrapper.unwrap(annotations, value); 2459 Node[] annotationValues; 2460 annotationValues.reserve(1); 2461 foreach (size_t symbolId; annotations) { 2462 if (symbolId >= symbolTable.length) 2463 return IonErrorCode.symbolIdIsTooLargeForTheCurrentSymbolTable.ionException; 2464 annotationValues ~= symbolTable[symbolId].Node; 2465 } 2466 Node val; 2467 if (auto exc = val.deserializeFromIon(symbolTable, value)) 2468 return exc; 2469 value_ = [Pair(Node(annotationValues), val)]; 2470 return null; 2471 } 2472 } 2473 } 2474 } 2475 2476 package: 2477 // Merge pairs into an array of pairs based on merge rules in the YAML spec. 2478 // 2479 // Any new pair will only be added if there is not already a pair 2480 // with the same key. 2481 // 2482 // Params: pairs = Appender managing the array of pairs to merge into. 2483 // toMerge = Pairs to merge. 2484 void merge(Appender)(ref Appender pairs, Node.Pair[] toMerge) @safe 2485 { 2486 import mir.algorithm.iteration: all; 2487 foreach(ref pair; toMerge) 2488 if(pairs.data.all!(p => p.key != pair.key)) 2489 pairs.put(pair); 2490 } 2491 2492 enum hasNodeConstructor(T) = hasSimpleNodeConstructor!T || hasExpandedNodeConstructor!T; 2493 template hasSimpleNodeConstructor(T) 2494 { 2495 static if (is(T == struct)) 2496 { 2497 enum hasSimpleNodeConstructor = is(typeof(T(Node.init))); 2498 } 2499 else static if (is(T == class)) 2500 { 2501 enum hasSimpleNodeConstructor = is(typeof(new T(Node.init))); 2502 } 2503 else enum hasSimpleNodeConstructor = false; 2504 } 2505 template hasExpandedNodeConstructor(T) 2506 { 2507 static if (is(T == struct)) 2508 { 2509 enum hasExpandedNodeConstructor = is(typeof(T(Node.init, ""))); 2510 } 2511 else static if (is(T == class)) 2512 { 2513 enum hasExpandedNodeConstructor = is(typeof(new T(Node.init, ""))); 2514 } 2515 else enum hasExpandedNodeConstructor = false; 2516 } 2517 enum castableToNode(T) = (is(T == struct) || is(T == class)) && is(typeof(T.opCast!Node()) : Node); 2518 2519 @safe unittest 2520 { 2521 import dyaml : Loader, Node; 2522 2523 static struct Foo 2524 { 2525 string[] bars; 2526 2527 this(const Node node) 2528 { 2529 foreach(value; node["bars"].sequence) 2530 { 2531 bars ~= value.as!string; 2532 } 2533 } 2534 } 2535 2536 Loader.fromString(`{ bars: ["a", "b"] }`) 2537 .load 2538 .as!(Foo); 2539 } 2540 @safe unittest 2541 { 2542 import dyaml : Loader, Node; 2543 import std : split, to; 2544 2545 static class MyClass 2546 { 2547 int x, y, z; 2548 2549 this(Node node) 2550 { 2551 auto parts = node.as!string().split(":"); 2552 x = parts[0].to!int; 2553 y = parts[1].to!int; 2554 z = parts[2].to!int; 2555 } 2556 } 2557 2558 auto loader = Loader.fromString(`"1:2:3"`); 2559 Node node = loader.load(); 2560 auto mc = node.get!MyClass; 2561 } 2562 @safe unittest 2563 { 2564 import dyaml : Loader, Node; 2565 import std : split, to; 2566 2567 static class MyClass 2568 { 2569 int x, y, z; 2570 2571 this(Node node) 2572 { 2573 auto parts = node.as!string().split(":"); 2574 x = parts[0].to!int; 2575 y = parts[1].to!int; 2576 z = parts[2].to!int; 2577 } 2578 } 2579 2580 auto loader = Loader.fromString(`"1:2:3"`); 2581 const node = loader.load(); 2582 auto mc = node.get!MyClass; 2583 }