// Copyright Ferdinand Majerech 2011. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) /// Node of a YAML document. Used to read YAML data once it's loaded, /// and to prepare data to emit. module dyaml.node; import std.algorithm; import std.array; import std.conv; import std.datetime; import std.exception; import std.math; import std.range; import std.string; import std.traits; import std.typecons; import std.variant; import dyaml.event; import dyaml.exception; import dyaml.style; /// Exception thrown at node related errors. class NodeException : YAMLException { package: // Construct a NodeException. // // Params: msg = Error message. // start = Start position of the node. this(string msg, Mark start, string file = __FILE__, int line = __LINE__) @safe { super(msg ~ "\nNode at: " ~ start.toString(), file, line); } } // Node kinds. enum NodeID : ubyte { scalar, sequence, mapping, invalid } /// Null YAML type. Used in nodes with _null values. struct YAMLNull { /// Used for string conversion. string toString() const pure @safe nothrow {return "null";} } // Merge YAML type, used to support "tag:yaml.org,2002:merge". package struct YAMLMerge{} // Key-value pair of YAML nodes, used in mappings. private struct Pair { public: /// Key node. Node key; /// Value node. Node value; /// Construct a Pair from two values. Will be converted to Nodes if needed. this(K, V)(K key, V value) { static if(is(Unqual!K == Node)){this.key = key;} else {this.key = Node(key);} static if(is(Unqual!V == Node)){this.value = value;} else {this.value = Node(value);} } /// Equality test with another Pair. bool opEquals(const ref Pair rhs) const @safe { return key == rhs.key && value == rhs.value; } // Comparison with another Pair. int opCmp(ref const(Pair) rhs) const @safe { const keyCmp = key.opCmp(rhs.key); return keyCmp != 0 ? keyCmp : value.opCmp(rhs.value); } } enum NodeType { null_, merge, boolean, integer, decimal, binary, timestamp, string, mapping, sequence, invalid } /** YAML node. * * This is a pseudo-dynamic type that can store any YAML value, including a * sequence or mapping of nodes. You can get data from a Node directly or * iterate over it if it's a collection. */ struct Node { public: alias Pair = .Pair; package: // YAML value type. alias Value = Algebraic!(YAMLNull, YAMLMerge, bool, long, real, ubyte[], SysTime, string, Node.Pair[], Node[]); // Can Value hold this type naturally? enum allowed(T) = isIntegral!T || isFloatingPoint!T || isSomeString!T || is(Unqual!T == bool) || Value.allowed!T; // Stored value. Value value_; // Start position of the node. Mark startMark_; // Tag of the node. string tag_; // Node scalar style. Used to remember style this node was loaded with. ScalarStyle scalarStyle = ScalarStyle.invalid; // Node collection style. Used to remember style this node was loaded with. CollectionStyle collectionStyle = CollectionStyle.invalid; public: /** Construct a Node from a value. * * Any type except for Node can be stored in a Node, but default YAML * types (integers, floats, strings, timestamps, etc.) will be stored * more efficiently. To create a node representing a null value, * construct it from YAMLNull. * * If value is a node, its value will be copied directly. The tag and * other information attached to the original node will be discarded. * * If value is an array of nodes or pairs, it is stored directly. * Otherwise, every value in the array is converted to a node, and * those nodes are stored. * * Note that to emit any non-default types you store * in a node, you need a Representer to represent them in YAML - * otherwise emitting will fail. * * Params: value = Value to store in the node. * tag = Overrides tag of the node when emitted, regardless * of tag determined by Representer. Representer uses * this to determine YAML data type when a D data type * maps to multiple different YAML data types. Tag must * be in full form, e.g. "tag:yaml.org,2002:int", not * a shortcut, like "!!int". */ this(T)(T value, const string tag = null) @safe if (allowed!T || isArray!T || isAssociativeArray!T || is(Unqual!T == Node) || castableToNode!T) { tag_ = tag; //Unlike with assignment, we're just copying the value. static if (is(Unqual!T == Node)) { setValue(value.value_); } else static if(isSomeString!T) { setValue(value.to!string); } else static if(is(Unqual!T == bool)) { setValue(cast(bool)value); } else static if(isIntegral!T) { setValue(cast(long)value); } else static if(isFloatingPoint!T) { setValue(cast(real)value); } else static if (isArray!T) { alias ElementT = Unqual!(ElementType!T); // Construction from raw node or pair array. static if(is(ElementT == Node) || is(ElementT == Node.Pair)) { setValue(value); } // Need to handle byte buffers separately. else static if(is(ElementT == byte) || is(ElementT == ubyte)) { setValue(cast(ubyte[]) value); } else { Node[] nodes; foreach(ref v; value) { nodes ~= Node(v); } setValue(nodes); } } else static if (isAssociativeArray!T) { Node.Pair[] pairs; foreach(k, ref v; value) { pairs ~= Pair(k, v); } setValue(pairs); } // User defined type. else { setValue(value); } } /// Construct a scalar node @safe unittest { // Integer { auto node = Node(5); } // String { auto node = Node("Hello world!"); } // Floating point { auto node = Node(5.0f); } // Boolean { auto node = Node(true); } // Time { auto node = Node(SysTime(DateTime(2005, 06, 15, 20, 00, 00), UTC())); } // Integer, dumped as a string { auto node = Node(5, "tag:yaml.org,2002:str"); } } /// Construct a sequence node @safe unittest { // Will be emitted as a sequence (default for arrays) { auto seq = Node([1, 2, 3, 4, 5]); } // Will be emitted as a set (overridden tag) { auto set = Node([1, 2, 3, 4, 5], "tag:yaml.org,2002:set"); } // Can also store arrays of arrays { auto node = Node([[1,2], [3,4]]); } } /// Construct a mapping node @safe unittest { // Will be emitted as an unordered mapping (default for mappings) auto map = Node([1 : "a", 2 : "b"]); // Will be emitted as an ordered map (overridden tag) auto omap = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:omap"); // Will be emitted as pairs (overridden tag) auto pairs = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:pairs"); } @safe unittest { { auto node = Node(42); assert(node.nodeID == NodeID.scalar); assert(node.as!int == 42 && node.as!float == 42.0f && node.as!string == "42"); } { auto node = Node("string"); assert(node.as!string == "string"); } } @safe unittest { with(Node([1, 2, 3])) { assert(nodeID == NodeID.sequence); assert(length == 3); assert(opIndex(2).as!int == 3); } } @safe unittest { int[string] aa; aa["1"] = 1; aa["2"] = 2; with(Node(aa)) { assert(nodeID == NodeID.mapping); assert(length == 2); assert(opIndex("2").as!int == 2); } } @safe unittest { auto node = Node(Node(4, "tag:yaml.org,2002:str")); assert(node == 4); assert(node.tag_ == ""); } /** Construct a node from arrays of _keys and _values. * * Constructs a mapping node with key-value pairs from * _keys and _values, keeping their order. Useful when order * is important (ordered maps, pairs). * * * keys and values must have equal length. * * * If _keys and/or _values are nodes, they are stored directly/ * Otherwise they are converted to nodes and then stored. * * Params: keys = Keys of the mapping, from first to last pair. * values = Values of the mapping, from first to last pair. * tag = Overrides tag of the node when emitted, regardless * of tag determined by Representer. Representer uses * this to determine YAML data type when a D data type * maps to multiple different YAML data types. * This is used to differentiate between YAML unordered * mappings ("!!map"), ordered mappings ("!!omap"), and * pairs ("!!pairs") which are all internally * represented as an array of node pairs. Tag must be * in full form, e.g. "tag:yaml.org,2002:omap", not a * shortcut, like "!!omap". * */ this(K, V)(K[] keys, V[] values, const string tag = null) if(!(isSomeString!(K[]) || isSomeString!(V[]))) in(keys.length == values.length, "Lengths of keys and values arrays to construct " ~ "a YAML node from don't match") { tag_ = tag; Node.Pair[] pairs; foreach(i; 0 .. keys.length){pairs ~= Pair(keys[i], values[i]);} setValue(pairs); } /// @safe unittest { // Will be emitted as an unordered mapping (default for mappings) auto map = Node([1, 2], ["a", "b"]); // Will be emitted as an ordered map (overridden tag) auto omap = Node([1, 2], ["a", "b"], "tag:yaml.org,2002:omap"); // Will be emitted as pairs (overriden tag) auto pairs = Node([1, 2], ["a", "b"], "tag:yaml.org,2002:pairs"); } @safe unittest { with(Node(["1", "2"], [1, 2])) { assert(nodeID == NodeID.mapping); assert(length == 2); assert(opIndex("2").as!int == 2); } } /// Is this node valid (initialized)? @property bool isValid() const @safe pure nothrow { return value_.hasValue; } /// Return tag of the node. @property string tag() const @safe nothrow { return tag_; } /** Equality test. * * If T is Node, recursively compares all subnodes. * This might be quite expensive if testing entire documents. * * If T is not Node, gets a value of type T from the node and tests * equality with that. * * To test equality with a null YAML value, use YAMLNull. * * Params: rhs = Variable to test equality with. * * Returns: true if equal, false otherwise. */ bool opEquals(const Node rhs) const @safe { return opCmp(rhs) == 0; } bool opEquals(T)(const auto ref T rhs) const { try { auto stored = get!(T, No.stringConversion); // NaNs aren't normally equal to each other, but we'll pretend they are. static if(isFloatingPoint!T) { return rhs == stored || (isNaN(rhs) && isNaN(stored)); } else { return rhs == stored; } } catch(NodeException e) { return false; } } /// @safe unittest { auto node = Node(42); assert(node == 42); assert(node != "42"); assert(node != "43"); auto node2 = Node(YAMLNull()); assert(node2 == YAMLNull()); const node3 = Node(42); assert(node3 == 42); } /// Shortcut for get(). alias as = get; /** Get the value of the node as specified type. * * If the specifed type does not match type in the node, * conversion is attempted. The stringConversion template * parameter can be used to disable conversion from non-string * types to strings. * * Numeric values are range checked, throwing if out of range of * requested type. * * Timestamps are stored as std.datetime.SysTime. * Binary values are decoded and stored as ubyte[]. * * To get a null value, use get!YAMLNull . This is to * prevent getting null values for types such as strings or classes. * * $(BR)$(B Mapping default values:) * * $(PBR * The '=' key can be used to denote the default value of a mapping. * This can be used when a node is scalar in early versions of a program, * but is replaced by a mapping later. Even if the node is a mapping, the * get method can be used as if it was a scalar if it has a default value. * This way, new YAML files where the node is a mapping can still be read * by old versions of the program, which expect the node to be a scalar. * ) * * Returns: Value of the node as specified type. * * Throws: NodeException if unable to convert to specified type, or if * the value is out of range of requested type. */ inout(T) get(T, Flag!"stringConversion" stringConversion = Yes.stringConversion)() inout if (allowed!(Unqual!T) || hasNodeConstructor!(Unqual!T)) { if(isType!(Unqual!T)){return getValue!T;} static if(!allowed!(Unqual!T)) { static if (hasSimpleNodeConstructor!T) { alias params = AliasSeq!(this); } else static if (hasExpandedNodeConstructor!T) { alias params = AliasSeq!(this, tag_); } else { static assert(0, "Unknown Node constructor?"); } static if (is(T == class)) { return new inout T(params); } else static if (is(T == struct)) { return T(params); } else { static assert(0, "Unhandled user type"); } } else { // If we're getting from a mapping and we're not getting Node.Pair[], // we're getting the default value. if(nodeID == NodeID.mapping){return this["="].get!( T, stringConversion);} static if(isSomeString!T) { static if(!stringConversion) { enforce(type == NodeType.string, new NodeException( "Node stores unexpected type: " ~ text(type) ~ ". Expected: " ~ typeid(T).toString(), startMark_)); return to!T(getValue!string); } else { // Try to convert to string. try { return coerceValue!T(); } catch(VariantException e) { throw new NodeException("Unable to convert node value to string", startMark_); } } } else static if(isFloatingPoint!T) { final switch (type) { case NodeType.integer: return to!T(getValue!long); case NodeType.decimal: return to!T(getValue!real); case NodeType.binary: case NodeType.string: case NodeType.boolean: case NodeType.null_: case NodeType.merge: case NodeType.invalid: case NodeType.timestamp: case NodeType.mapping: case NodeType.sequence: throw new NodeException("Node stores unexpected type: " ~ text(type) ~ ". Expected: " ~ typeid(T).toString, startMark_); } } else static if(isIntegral!T) { enforce(type == NodeType.integer, new NodeException("Node stores unexpected type: " ~ text(type) ~ ". Expected: " ~ typeid(T).toString, startMark_)); immutable temp = getValue!long; enforce(temp >= T.min && temp <= T.max, new NodeException("Integer value of type " ~ typeid(T).toString() ~ " out of range. Value: " ~ to!string(temp), startMark_)); return temp.to!T; } else throw new NodeException("Node stores unexpected type: " ~ text(type) ~ ". Expected: " ~ typeid(T).toString, startMark_); } } /// Automatic type conversion @safe unittest { auto node = Node(42); assert(node.get!int == 42); assert(node.get!string == "42"); assert(node.get!double == 42.0); } /// Scalar node to struct and vice versa @safe unittest { import dyaml.dumper : dumper; import dyaml.loader : Loader; static struct MyStruct { int x, y, z; this(int x, int y, int z) @safe { this.x = x; this.y = y; this.z = z; } this(Node node) @safe { auto parts = node.as!string().split(":"); x = parts[0].to!int; y = parts[1].to!int; z = parts[2].to!int; } Node opCast(T: Node)() @safe { //Using custom scalar format, x:y:z. auto scalar = format("%s:%s:%s", x, y, z); //Representing as a scalar, with custom tag to specify this data type. return Node(scalar, "!mystruct.tag"); } } auto appender = new Appender!string; // Dump struct to yaml document dumper().dump(appender, Node(MyStruct(1,2,3))); // Read yaml document back as a MyStruct auto loader = Loader.fromString(appender.data); Node node = loader.load(); assert(node.as!MyStruct == MyStruct(1,2,3)); } /// Sequence node to struct and vice versa @safe unittest { import dyaml.dumper : dumper; import dyaml.loader : Loader; static struct MyStruct { int x, y, z; this(int x, int y, int z) @safe { this.x = x; this.y = y; this.z = z; } this(Node node) @safe { x = node[0].as!int; y = node[1].as!int; z = node[2].as!int; } Node opCast(T: Node)() { return Node([x, y, z], "!mystruct.tag"); } } auto appender = new Appender!string; // Dump struct to yaml document dumper().dump(appender, Node(MyStruct(1,2,3))); // Read yaml document back as a MyStruct auto loader = Loader.fromString(appender.data); Node node = loader.load(); assert(node.as!MyStruct == MyStruct(1,2,3)); } /// Mapping node to struct and vice versa @safe unittest { import dyaml.dumper : dumper; import dyaml.loader : Loader; static struct MyStruct { int x, y, z; Node opCast(T: Node)() { auto pairs = [Node.Pair("x", x), Node.Pair("y", y), Node.Pair("z", z)]; return Node(pairs, "!mystruct.tag"); } this(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } this(Node node) @safe { x = node["x"].as!int; y = node["y"].as!int; z = node["z"].as!int; } } auto appender = new Appender!string; // Dump struct to yaml document dumper().dump(appender, Node(MyStruct(1,2,3))); // Read yaml document back as a MyStruct auto loader = Loader.fromString(appender.data); Node node = loader.load(); assert(node.as!MyStruct == MyStruct(1,2,3)); } /// Classes can be used too @system unittest { import dyaml.dumper : dumper; import dyaml.loader : Loader; static class MyClass { int x, y, z; this(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } this(Node node) @safe inout { auto parts = node.as!string().split(":"); x = parts[0].to!int; y = parts[1].to!int; z = parts[2].to!int; } ///Useful for Node.as!string. override string toString() { return format("MyClass(%s, %s, %s)", x, y, z); } Node opCast(T: Node)() @safe { //Using custom scalar format, x:y:z. auto scalar = format("%s:%s:%s", x, y, z); //Representing as a scalar, with custom tag to specify this data type. return Node(scalar, "!myclass.tag"); } override bool opEquals(Object o) { if (auto other = cast(MyClass)o) { return (other.x == x) && (other.y == y) && (other.z == z); } return false; } } auto appender = new Appender!string; // Dump class to yaml document dumper().dump(appender, Node(new MyClass(1,2,3))); // Read yaml document back as a MyClass auto loader = Loader.fromString(appender.data); Node node = loader.load(); assert(node.as!MyClass == new MyClass(1,2,3)); } // Make sure custom tags and styles are kept. @safe unittest { static struct MyStruct { Node opCast(T: Node)() { auto node = Node("hi", "!mystruct.tag"); node.setStyle(ScalarStyle.doubleQuoted); return node; } } auto node = Node(MyStruct.init); assert(node.tag == "!mystruct.tag"); assert(node.scalarStyle == ScalarStyle.doubleQuoted); } // ditto, but for collection style @safe unittest { static struct MyStruct { Node opCast(T: Node)() { auto node = Node(["hi"], "!mystruct.tag"); node.setStyle(CollectionStyle.flow); return node; } } auto node = Node(MyStruct.init); assert(node.tag == "!mystruct.tag"); assert(node.collectionStyle == CollectionStyle.flow); } @safe unittest { assertThrown!NodeException(Node("42").get!int); assertThrown!NodeException(Node("42").get!double); assertThrown!NodeException(Node(long.max).get!ushort); Node(YAMLNull()).get!YAMLNull; } @safe unittest { const node = Node(42); assert(node.get!int == 42); assert(node.get!string == "42"); assert(node.get!double == 42.0); immutable node2 = Node(42); assert(node2.get!int == 42); assert(node2.get!(const int) == 42); assert(node2.get!(immutable int) == 42); assert(node2.get!string == "42"); assert(node2.get!(const string) == "42"); assert(node2.get!(immutable string) == "42"); assert(node2.get!double == 42.0); assert(node2.get!(const double) == 42.0); assert(node2.get!(immutable double) == 42.0); } /** If this is a collection, return its _length. * * Otherwise, throw NodeException. * * Returns: Number of elements in a sequence or key-value pairs in a mapping. * * Throws: NodeException if this is not a sequence nor a mapping. */ @property size_t length() const @safe { final switch(nodeID) { case NodeID.sequence: return getValue!(Node[]).length; case NodeID.mapping: return getValue!(Pair[]).length; case NodeID.scalar: case NodeID.invalid: throw new NodeException("Trying to get length of a " ~ nodeTypeString ~ " node", startMark_); } } @safe unittest { auto node = Node([1,2,3]); assert(node.length == 3); const cNode = Node([1,2,3]); assert(cNode.length == 3); immutable iNode = Node([1,2,3]); assert(iNode.length == 3); } /** Get the element at specified index. * * If the node is a sequence, index must be integral. * * * If the node is a mapping, return the value corresponding to the first * key equal to index. containsKey() can be used to determine if a mapping * has a specific key. * * To get element at a null index, use YAMLNull for index. * * Params: index = Index to use. * * Returns: Value corresponding to the index. * * Throws: NodeException if the index could not be found, * non-integral index is used with a sequence or the node is * not a collection. */ ref inout(Node) opIndex(T)(T index) inout @safe { final switch (nodeID) { case NodeID.sequence: checkSequenceIndex(index); static if(isIntegral!T) { return getValue!(Node[])[index]; } else { assert(false, "Only integers may index sequence nodes"); } case NodeID.mapping: auto idx = findPair(index); if(idx >= 0) { return getValue!(Pair[])[idx].value; } string msg = "Mapping index not found" ~ (isSomeString!T ? ": " ~ to!string(index) : ""); throw new NodeException(msg, startMark_); case NodeID.scalar: case NodeID.invalid: throw new NodeException("Trying to index a " ~ nodeTypeString ~ " node", startMark_); } } /// @safe unittest { Node narray = Node([11, 12, 13, 14]); Node nmap = Node(["11", "12", "13", "14"], [11, 12, 13, 14]); assert(narray[0].as!int == 11); assert(null !is collectException(narray[42])); assert(nmap["11"].as!int == 11); assert(nmap["14"].as!int == 14); } @safe unittest { Node narray = Node([11, 12, 13, 14]); Node nmap = Node(["11", "12", "13", "14"], [11, 12, 13, 14]); assert(narray[0].as!int == 11); assert(null !is collectException(narray[42])); assert(nmap["11"].as!int == 11); assert(nmap["14"].as!int == 14); assert(null !is collectException(nmap["42"])); narray.add(YAMLNull()); nmap.add(YAMLNull(), "Nothing"); assert(narray[4].as!YAMLNull == YAMLNull()); assert(nmap[YAMLNull()].as!string == "Nothing"); assertThrown!NodeException(nmap[11]); assertThrown!NodeException(nmap[14]); } /** Determine if a collection contains specified value. * * If the node is a sequence, check if it contains the specified value. * If it's a mapping, check if it has a value that matches specified value. * * Params: rhs = Item to look for. Use YAMLNull to check for a null value. * * Returns: true if rhs was found, false otherwise. * * Throws: NodeException if the node is not a collection. */ bool contains(T)(T rhs) const { return contains_!(T, No.key, "contains")(rhs); } @safe unittest { auto mNode = Node(["1", "2", "3"]); assert(mNode.contains("2")); const cNode = Node(["1", "2", "3"]); assert(cNode.contains("2")); immutable iNode = Node(["1", "2", "3"]); assert(iNode.contains("2")); } /** Determine if a mapping contains specified key. * * Params: rhs = Key to look for. Use YAMLNull to check for a null key. * * Returns: true if rhs was found, false otherwise. * * Throws: NodeException if the node is not a mapping. */ bool containsKey(T)(T rhs) const { return contains_!(T, Yes.key, "containsKey")(rhs); } // Unittest for contains() and containsKey(). @safe unittest { auto seq = Node([1, 2, 3, 4, 5]); assert(seq.contains(3)); assert(seq.contains(5)); assert(!seq.contains("5")); assert(!seq.contains(6)); assert(!seq.contains(float.nan)); assertThrown!NodeException(seq.containsKey(5)); auto seq2 = Node(["1", "2"]); assert(seq2.contains("1")); assert(!seq2.contains(1)); auto map = Node(["1", "2", "3", "4"], [1, 2, 3, 4]); assert(map.contains(1)); assert(!map.contains("1")); assert(!map.contains(5)); assert(!map.contains(float.nan)); assert(map.containsKey("1")); assert(map.containsKey("4")); assert(!map.containsKey(1)); assert(!map.containsKey("5")); assert(!seq.contains(YAMLNull())); assert(!map.contains(YAMLNull())); assert(!map.containsKey(YAMLNull())); seq.add(YAMLNull()); map.add("Nothing", YAMLNull()); assert(seq.contains(YAMLNull())); assert(map.contains(YAMLNull())); assert(!map.containsKey(YAMLNull())); map.add(YAMLNull(), "Nothing"); assert(map.containsKey(YAMLNull())); auto map2 = Node([1, 2, 3, 4], [1, 2, 3, 4]); assert(!map2.contains("1")); assert(map2.contains(1)); assert(!map2.containsKey("1")); assert(map2.containsKey(1)); // scalar assertThrown!NodeException(Node(1).contains(4)); assertThrown!NodeException(Node(1).containsKey(4)); auto mapNan = Node([1.0, 2, double.nan], [1, double.nan, 5]); assert(mapNan.contains(double.nan)); assert(mapNan.containsKey(double.nan)); } /// Assignment (shallow copy) by value. void opAssign()(auto ref Node rhs) { assumeWontThrow(setValue(rhs.value_)); startMark_ = rhs.startMark_; tag_ = rhs.tag_; scalarStyle = rhs.scalarStyle; collectionStyle = rhs.collectionStyle; } // Unittest for opAssign(). @safe unittest { auto seq = Node([1, 2, 3, 4, 5]); auto assigned = seq; assert(seq == assigned, "Node.opAssign() doesn't produce an equivalent copy"); } /** Set element at specified index in a collection. * * This method can only be called on collection nodes. * * If the node is a sequence, index must be integral. * * If the node is a mapping, sets the _value corresponding to the first * key matching index (including conversion, so e.g. "42" matches 42). * * If the node is a mapping and no key matches index, a new key-value * pair is added to the mapping. In sequences the index must be in * range. This ensures behavior siilar to D arrays and associative * arrays. * * To set element at a null index, use YAMLNull for index. * * Params: * value = Value to assign. * index = Index of the value to set. * * Throws: NodeException if the node is not a collection, index is out * of range or if a non-integral index is used on a sequence node. */ void opIndexAssign(K, V)(V value, K index) { final switch (nodeID) { case NodeID.sequence: checkSequenceIndex(index); static if(isIntegral!K || is(Unqual!K == bool)) { auto nodes = getValue!(Node[]); static if(is(Unqual!V == Node)){nodes[index] = value;} else {nodes[index] = Node(value);} setValue(nodes); return; } assert(false, "Only integers may index sequence nodes"); case NodeID.mapping: const idx = findPair(index); if(idx < 0){add(index, value);} else { auto pairs = as!(Node.Pair[])(); static if(is(Unqual!V == Node)){pairs[idx].value = value;} else {pairs[idx].value = Node(value);} setValue(pairs); } return; case NodeID.scalar: case NodeID.invalid: throw new NodeException("Trying to index a " ~ nodeTypeString ~ " node", startMark_); } } @safe unittest { with(Node([1, 2, 3, 4, 3])) { opIndexAssign(42, 3); assert(length == 5); assert(opIndex(3).as!int == 42); opIndexAssign(YAMLNull(), 0); assert(opIndex(0) == YAMLNull()); } with(Node(["1", "2", "3"], [4, 5, 6])) { opIndexAssign(42, "3"); opIndexAssign(123, 456); assert(length == 4); assert(opIndex("3").as!int == 42); assert(opIndex(456).as!int == 123); opIndexAssign(43, 3); //3 and "3" should be different assert(length == 5); assert(opIndex("3").as!int == 42); assert(opIndex(3).as!int == 43); opIndexAssign(YAMLNull(), "2"); assert(opIndex("2") == YAMLNull()); } } /** Return a range object iterating over a sequence, getting each * element as T. * * If T is Node, simply iterate over the nodes in the sequence. * Otherwise, convert each node to T during iteration. * * Throws: NodeException if the node is not a sequence or an element * could not be converted to specified type. */ template sequence(T = Node) { struct Range(N) { N subnodes; size_t position; this(N nodes) { subnodes = nodes; position = 0; } /* Input range functionality. */ bool empty() const @property { return position >= subnodes.length; } void popFront() { enforce(!empty, "Attempted to popFront an empty sequence"); position++; } T front() const @property { enforce(!empty, "Attempted to take the front of an empty sequence"); static if (is(Unqual!T == Node)) return subnodes[position]; else return subnodes[position].as!T; } /* Forward range functionality. */ Range save() { return this; } /* Bidirectional range functionality. */ void popBack() { enforce(!empty, "Attempted to popBack an empty sequence"); subnodes = subnodes[0 .. $ - 1]; } T back() { enforce(!empty, "Attempted to take the back of an empty sequence"); static if (is(Unqual!T == Node)) return subnodes[$ - 1]; else return subnodes[$ - 1].as!T; } /* Random-access range functionality. */ size_t length() const @property { return subnodes.length; } T opIndex(size_t index) { static if (is(Unqual!T == Node)) return subnodes[index]; else return subnodes[index].as!T; } static assert(isInputRange!Range); static assert(isForwardRange!Range); static assert(isBidirectionalRange!Range); static assert(isRandomAccessRange!Range); } auto sequence() { enforce(nodeID == NodeID.sequence, new NodeException("Trying to 'sequence'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); return Range!(Node[])(get!(Node[])); } auto sequence() const { enforce(nodeID == NodeID.sequence, new NodeException("Trying to 'sequence'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); return Range!(const(Node)[])(get!(Node[])); } } @safe unittest { Node n1 = Node([1, 2, 3, 4]); int[int] array; Node n2 = Node(array); const n3 = Node([1, 2, 3, 4]); auto r = n1.sequence!int.map!(x => x * 10); assert(r.equal([10, 20, 30, 40])); assertThrown(n2.sequence); auto r2 = n3.sequence!int.map!(x => x * 10); assert(r2.equal([10, 20, 30, 40])); } /** Return a range object iterating over mapping's pairs. * * Throws: NodeException if the node is not a mapping. * */ template mapping() { struct Range(T) { T pairs; size_t position; this(T pairs) @safe { this.pairs = pairs; position = 0; } /* Input range functionality. */ bool empty() @safe { return position >= pairs.length; } void popFront() @safe { enforce(!empty, "Attempted to popFront an empty mapping"); position++; } auto front() @safe { enforce(!empty, "Attempted to take the front of an empty mapping"); return pairs[position]; } /* Forward range functionality. */ Range save() @safe { return this; } /* Bidirectional range functionality. */ void popBack() @safe { enforce(!empty, "Attempted to popBack an empty mapping"); pairs = pairs[0 .. $ - 1]; } auto back() @safe { enforce(!empty, "Attempted to take the back of an empty mapping"); return pairs[$ - 1]; } /* Random-access range functionality. */ size_t length() const @property @safe { return pairs.length; } auto opIndex(size_t index) @safe { return pairs[index]; } static assert(isInputRange!Range); static assert(isForwardRange!Range); static assert(isBidirectionalRange!Range); static assert(isRandomAccessRange!Range); } auto mapping() { enforce(nodeID == NodeID.mapping, new NodeException("Trying to 'mapping'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); return Range!(Node.Pair[])(get!(Node.Pair[])); } auto mapping() const { enforce(nodeID == NodeID.mapping, new NodeException("Trying to 'mapping'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); return Range!(const(Node.Pair)[])(get!(Node.Pair[])); } } @safe unittest { int[int] array; Node n = Node(array); n[1] = "foo"; n[2] = "bar"; n[3] = "baz"; string[int] test; foreach (pair; n.mapping) test[pair.key.as!int] = pair.value.as!string; assert(test[1] == "foo"); assert(test[2] == "bar"); assert(test[3] == "baz"); int[int] constArray = [1: 2, 3: 4]; const x = Node(constArray); foreach (pair; x.mapping) assert(pair.value == constArray[pair.key.as!int]); } /** Return a range object iterating over mapping's keys. * * If K is Node, simply iterate over the keys in the mapping. * Otherwise, convert each key to T during iteration. * * Throws: NodeException if the nodes is not a mapping or an element * could not be converted to specified type. */ auto mappingKeys(K = Node)() const { enforce(nodeID == NodeID.mapping, new NodeException("Trying to 'mappingKeys'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); static if (is(Unqual!K == Node)) return mapping.map!(pair => pair.key); else return mapping.map!(pair => pair.key.as!K); } @safe unittest { int[int] array; Node m1 = Node(array); m1["foo"] = 2; m1["bar"] = 3; assert(m1.mappingKeys.equal(["foo", "bar"]) || m1.mappingKeys.equal(["bar", "foo"])); const cm1 = Node(["foo": 2, "bar": 3]); assert(cm1.mappingKeys.equal(["foo", "bar"]) || cm1.mappingKeys.equal(["bar", "foo"])); } /** Return a range object iterating over mapping's values. * * If V is Node, simply iterate over the values in the mapping. * Otherwise, convert each key to V during iteration. * * Throws: NodeException if the nodes is not a mapping or an element * could not be converted to specified type. */ auto mappingValues(V = Node)() const { enforce(nodeID == NodeID.mapping, new NodeException("Trying to 'mappingValues'-iterate over a " ~ nodeTypeString ~ " node", startMark_)); static if (is(Unqual!V == Node)) return mapping.map!(pair => pair.value); else return mapping.map!(pair => pair.value.as!V); } @safe unittest { int[int] array; Node m1 = Node(array); m1["foo"] = 2; m1["bar"] = 3; assert(m1.mappingValues.equal([2, 3]) || m1.mappingValues.equal([3, 2])); const cm1 = Node(["foo": 2, "bar": 3]); assert(cm1.mappingValues.equal([2, 3]) || cm1.mappingValues.equal([3, 2])); } /** Foreach over a sequence, getting each element as T. * * If T is Node, simply iterate over the nodes in the sequence. * Otherwise, convert each node to T during iteration. * * Throws: NodeException if the node is not a sequence or an * element could not be converted to specified type. */ int opApply(D)(D dg) if (isDelegate!D && (Parameters!D.length == 1)) { enforce(nodeID == NodeID.sequence, new NodeException("Trying to sequence-foreach over a " ~ nodeTypeString ~ " node", startMark_)); int result; foreach(ref node; get!(Node[])) { static if(is(Unqual!(Parameters!D[0]) == Node)) { result = dg(node); } else { Parameters!D[0] temp = node.as!(Parameters!D[0]); result = dg(temp); } if(result){break;} } return result; } /// ditto int opApply(D)(D dg) const if (isDelegate!D && (Parameters!D.length == 1)) { enforce(nodeID == NodeID.sequence, new NodeException("Trying to sequence-foreach over a " ~ nodeTypeString ~ " node", startMark_)); int result; foreach(ref node; get!(Node[])) { static if(is(Unqual!(Parameters!D[0]) == Node)) { result = dg(node); } else { Parameters!D[0] temp = node.as!(Parameters!D[0]); result = dg(temp); } if(result){break;} } return result; } @safe unittest { Node n1 = Node(11); Node n2 = Node(12); Node n3 = Node(13); Node n4 = Node(14); Node narray = Node([n1, n2, n3, n4]); const cNArray = narray; int[] array, array2, array3; foreach(int value; narray) { array ~= value; } foreach(Node node; narray) { array2 ~= node.as!int; } foreach (const Node node; cNArray) { array3 ~= node.as!int; } assert(array == [11, 12, 13, 14]); assert(array2 == [11, 12, 13, 14]); assert(array3 == [11, 12, 13, 14]); } @safe unittest { string[] testStrs = ["1", "2", "3"]; auto node1 = Node(testStrs); int i = 0; foreach (string elem; node1) { assert(elem == testStrs[i]); i++; } const node2 = Node(testStrs); i = 0; foreach (string elem; node2) { assert(elem == testStrs[i]); i++; } immutable node3 = Node(testStrs); i = 0; foreach (string elem; node3) { assert(elem == testStrs[i]); i++; } } @safe unittest { auto node = Node(["a":1, "b":2, "c":3]); const cNode = node; assertThrown({foreach (Node n; node) {}}()); assertThrown({foreach (const Node n; cNode) {}}()); } /** Foreach over a mapping, getting each key/value as K/V. * * If the K and/or V is Node, simply iterate over the nodes in the mapping. * Otherwise, convert each key/value to T during iteration. * * Throws: NodeException if the node is not a mapping or an * element could not be converted to specified type. */ int opApply(DG)(DG dg) if (isDelegate!DG && (Parameters!DG.length == 2)) { alias K = Parameters!DG[0]; alias V = Parameters!DG[1]; enforce(nodeID == NodeID.mapping, new NodeException("Trying to mapping-foreach over a " ~ nodeTypeString ~ " node", startMark_)); int result; foreach(ref pair; get!(Node.Pair[])) { static if(is(Unqual!K == Node) && is(Unqual!V == Node)) { result = dg(pair.key, pair.value); } else static if(is(Unqual!K == Node)) { V tempValue = pair.value.as!V; result = dg(pair.key, tempValue); } else static if(is(Unqual!V == Node)) { K tempKey = pair.key.as!K; result = dg(tempKey, pair.value); } else { K tempKey = pair.key.as!K; V tempValue = pair.value.as!V; result = dg(tempKey, tempValue); } if(result){break;} } return result; } /// ditto int opApply(DG)(DG dg) const if (isDelegate!DG && (Parameters!DG.length == 2)) { alias K = Parameters!DG[0]; alias V = Parameters!DG[1]; enforce(nodeID == NodeID.mapping, new NodeException("Trying to mapping-foreach over a " ~ nodeTypeString ~ " node", startMark_)); int result; foreach(ref pair; get!(Node.Pair[])) { static if(is(Unqual!K == Node) && is(Unqual!V == Node)) { result = dg(pair.key, pair.value); } else static if(is(Unqual!K == Node)) { V tempValue = pair.value.as!V; result = dg(pair.key, tempValue); } else static if(is(Unqual!V == Node)) { K tempKey = pair.key.as!K; result = dg(tempKey, pair.value); } else { K tempKey = pair.key.as!K; V tempValue = pair.value.as!V; result = dg(tempKey, tempValue); } if(result){break;} } return result; } @safe unittest { Node n1 = Node(cast(long)11); Node n2 = Node(cast(long)12); Node n3 = Node(cast(long)13); Node n4 = Node(cast(long)14); Node k1 = Node("11"); Node k2 = Node("12"); Node k3 = Node("13"); Node k4 = Node("14"); Node nmap1 = Node([Pair(k1, n1), Pair(k2, n2), Pair(k3, n3), Pair(k4, n4)]); int[string] expected = ["11" : 11, "12" : 12, "13" : 13, "14" : 14]; int[string] array; foreach(string key, int value; nmap1) { array[key] = value; } assert(array == expected); Node nmap2 = Node([Pair(k1, Node(cast(long)5)), Pair(k2, Node(true)), Pair(k3, Node(cast(real)1.0)), Pair(k4, Node("yarly"))]); foreach(string key, Node value; nmap2) { switch(key) { case "11": assert(value.as!int == 5 ); break; case "12": assert(value.as!bool == true ); break; case "13": assert(value.as!float == 1.0 ); break; case "14": assert(value.as!string == "yarly"); break; default: assert(false); } } const nmap3 = nmap2; foreach(const Node key, const Node value; nmap3) { switch(key.as!string) { case "11": assert(value.as!int == 5 ); break; case "12": assert(value.as!bool == true ); break; case "13": assert(value.as!float == 1.0 ); break; case "14": assert(value.as!string == "yarly"); break; default: assert(false); } } } @safe unittest { string[int] testStrs = [0: "1", 1: "2", 2: "3"]; auto node1 = Node(testStrs); foreach (const int i, string elem; node1) { assert(elem == testStrs[i]); } const node2 = Node(testStrs); foreach (const int i, string elem; node2) { assert(elem == testStrs[i]); } immutable node3 = Node(testStrs); foreach (const int i, string elem; node3) { assert(elem == testStrs[i]); } } @safe unittest { auto node = Node(["a", "b", "c"]); const cNode = node; assertThrown({foreach (Node a, Node b; node) {}}()); assertThrown({foreach (const Node a, const Node b; cNode) {}}()); } /** Add an element to a sequence. * * This method can only be called on sequence nodes. * * If value is a node, it is copied to the sequence directly. Otherwise * value is converted to a node and then stored in the sequence. * * $(P When emitting, all values in the sequence will be emitted. When * using the !!set tag, the user needs to ensure that all elements in * the sequence are unique, otherwise $(B invalid) YAML code will be * emitted.) * * Params: value = Value to _add to the sequence. */ void add(T)(T value) { if (!isValid) { setValue(Node[].init); } enforce(nodeID == NodeID.sequence, new NodeException("Trying to add an element to a " ~ nodeTypeString ~ " node", startMark_)); auto nodes = get!(Node[])(); static if(is(Unqual!T == Node)){nodes ~= value;} else {nodes ~= Node(value);} setValue(nodes); } @safe unittest { with(Node([1, 2, 3, 4])) { add(5.0f); assert(opIndex(4).as!float == 5.0f); } with(Node()) { add(5.0f); assert(opIndex(0).as!float == 5.0f); } with(Node(5.0f)) { assertThrown!NodeException(add(5.0f)); } with(Node([5.0f : true])) { assertThrown!NodeException(add(5.0f)); } } /** Add a key-value pair to a mapping. * * This method can only be called on mapping nodes. * * If key and/or value is a node, it is copied to the mapping directly. * Otherwise it is converted to a node and then stored in the mapping. * * $(P It is possible for the same key to be present more than once in a * mapping. When emitting, all key-value pairs will be emitted. * This is useful with the "!!pairs" tag, but will result in * $(B invalid) YAML with "!!map" and "!!omap" tags.) * * Params: key = Key to _add. * value = Value to _add. */ void add(K, V)(K key, V value) { if (!isValid) { setValue(Node.Pair[].init); } enforce(nodeID == NodeID.mapping, new NodeException("Trying to add a key-value pair to a " ~ nodeTypeString ~ " node", startMark_)); auto pairs = get!(Node.Pair[])(); pairs ~= Pair(key, value); setValue(pairs); } @safe unittest { with(Node([1, 2], [3, 4])) { add(5, "6"); assert(opIndex(5).as!string == "6"); } with(Node()) { add(5, "6"); assert(opIndex(5).as!string == "6"); } with(Node(5.0f)) { assertThrown!NodeException(add(5, "6")); } with(Node([5.0f])) { assertThrown!NodeException(add(5, "6")); } } /** Determine whether a key is in a mapping, and access its value. * * This method can only be called on mapping nodes. * * Params: key = Key to search for. * * Returns: A pointer to the value (as a Node) corresponding to key, * or null if not found. * * Note: Any modification to the node can invalidate the returned * pointer. * * See_Also: contains */ inout(Node*) opBinaryRight(string op, K)(K key) inout if (op == "in") { enforce(nodeID == NodeID.mapping, new NodeException("Trying to use 'in' on a " ~ nodeTypeString ~ " node", startMark_)); auto idx = findPair(key); if(idx < 0) { return null; } else { return &(get!(Node.Pair[])[idx].value); } } @safe unittest { auto mapping = Node(["foo", "baz"], ["bar", "qux"]); assert("bad" !in mapping && ("bad" in mapping) is null); Node* foo = "foo" in mapping; assert(foo !is null); assert(*foo == Node("bar")); assert(foo.get!string == "bar"); *foo = Node("newfoo"); assert(mapping["foo"] == Node("newfoo")); } @safe unittest { auto mNode = Node(["a": 2]); assert("a" in mNode); const cNode = Node(["a": 2]); assert("a" in cNode); immutable iNode = Node(["a": 2]); assert("a" in iNode); } /** Remove first (if any) occurence of a value in a collection. * * This method can only be called on collection nodes. * * If the node is a sequence, the first node matching value is removed. * If the node is a mapping, the first key-value pair where _value * matches specified value is removed. * * Params: rhs = Value to _remove. * * Throws: NodeException if the node is not a collection. */ void remove(T)(T rhs) { remove_!(T, No.key, "remove")(rhs); } @safe unittest { with(Node([1, 2, 3, 4, 3])) { remove(3); assert(length == 4); assert(opIndex(2).as!int == 4); assert(opIndex(3).as!int == 3); add(YAMLNull()); assert(length == 5); remove(YAMLNull()); assert(length == 4); } with(Node(["1", "2", "3"], [4, 5, 6])) { remove(4); assert(length == 2); add("nullkey", YAMLNull()); assert(length == 3); remove(YAMLNull()); assert(length == 2); } } /** Remove element at the specified index of a collection. * * This method can only be called on collection nodes. * * If the node is a sequence, index must be integral. * * If the node is a mapping, remove the first key-value pair where * key matches index. * * If the node is a mapping and no key matches index, nothing is removed * and no exception is thrown. This ensures behavior siilar to D arrays * and associative arrays. * * Params: index = Index to remove at. * * Throws: NodeException if the node is not a collection, index is out * of range or if a non-integral index is used on a sequence node. */ void removeAt(T)(T index) { remove_!(T, Yes.key, "removeAt")(index); } @safe unittest { with(Node([1, 2, 3, 4, 3])) { removeAt(3); assertThrown!NodeException(removeAt("3")); assert(length == 4); assert(opIndex(3).as!int == 3); } with(Node(["1", "2", "3"], [4, 5, 6])) { // no integer 2 key, so don't remove anything removeAt(2); assert(length == 3); removeAt("2"); assert(length == 2); add(YAMLNull(), "nullval"); assert(length == 3); removeAt(YAMLNull()); assert(length == 2); } } /// Compare with another _node. int opCmp(const ref Node rhs) const @safe { // Compare tags - if equal or both null, we need to compare further. const tagCmp = (tag_ is null) ? (rhs.tag_ is null) ? 0 : -1 : (rhs.tag_ is null) ? 1 : std.algorithm.comparison.cmp(tag_, rhs.tag_); if(tagCmp != 0){return tagCmp;} static int cmp(T1, T2)(T1 a, T2 b) { return a > b ? 1 : a < b ? -1 : 0; } // Compare validity: if both valid, we have to compare further. const v1 = isValid; const v2 = rhs.isValid; if(!v1){return v2 ? -1 : 0;} if(!v2){return 1;} const typeCmp = cmp(type, rhs.type); if(typeCmp != 0){return typeCmp;} static int compareCollections(T)(const ref Node lhs, const ref Node rhs) { const c1 = lhs.getValue!T; const c2 = rhs.getValue!T; if(c1 is c2){return 0;} if(c1.length != c2.length) { return cmp(c1.length, c2.length); } // Equal lengths, compare items. foreach(i; 0 .. c1.length) { const itemCmp = c1[i].opCmp(c2[i]); if(itemCmp != 0){return itemCmp;} } return 0; } final switch(type) { case NodeType.string: return std.algorithm.cmp(getValue!string, rhs.getValue!string); case NodeType.integer: return cmp(getValue!long, rhs.getValue!long); case NodeType.boolean: const b1 = getValue!bool; const b2 = rhs.getValue!bool; return b1 ? b2 ? 0 : 1 : b2 ? -1 : 0; case NodeType.binary: const b1 = getValue!(ubyte[]); const b2 = rhs.getValue!(ubyte[]); return std.algorithm.cmp(b1, b2); case NodeType.null_: return 0; case NodeType.decimal: const r1 = getValue!real; const r2 = rhs.getValue!real; if(isNaN(r1)) { return isNaN(r2) ? 0 : -1; } if(isNaN(r2)) { return 1; } // Fuzzy equality. if(r1 <= r2 + real.epsilon && r1 >= r2 - real.epsilon) { return 0; } return cmp(r1, r2); case NodeType.timestamp: const t1 = getValue!SysTime; const t2 = rhs.getValue!SysTime; return cmp(t1, t2); case NodeType.mapping: return compareCollections!(Pair[])(this, rhs); case NodeType.sequence: return compareCollections!(Node[])(this, rhs); case NodeType.merge: assert(false, "Cannot compare merge nodes"); case NodeType.invalid: assert(false, "Cannot compare invalid nodes"); } } // Ensure opCmp is symmetric for collections @safe unittest { auto node1 = Node( [ Node("New York Yankees", "tag:yaml.org,2002:str"), Node("Atlanta Braves", "tag:yaml.org,2002:str") ], "tag:yaml.org,2002:seq" ); auto node2 = Node( [ Node("Detroit Tigers", "tag:yaml.org,2002:str"), Node("Chicago cubs", "tag:yaml.org,2002:str") ], "tag:yaml.org,2002:seq" ); assert(node1 > node2); assert(node2 < node1); } // Compute hash of the node. hash_t toHash() nothrow const @trusted { const valueHash = value_.toHash(); return tag_ is null ? valueHash : tag_.hashOf(valueHash); } @safe unittest { assert(Node(42).toHash() != Node(41).toHash()); assert(Node(42).toHash() != Node(42, "some-tag").toHash()); } /// Get type of the node value. @property NodeType type() const @safe nothrow { if (value_.type is typeid(bool)) { return NodeType.boolean; } else if (value_.type is typeid(long)) { return NodeType.integer; } else if (value_.type is typeid(Node[])) { return NodeType.sequence; } else if (value_.type is typeid(ubyte[])) { return NodeType.binary; } else if (value_.type is typeid(string)) { return NodeType.string; } else if (value_.type is typeid(Node.Pair[])) { return NodeType.mapping; } else if (value_.type is typeid(SysTime)) { return NodeType.timestamp; } else if (value_.type is typeid(YAMLNull)) { return NodeType.null_; } else if (value_.type is typeid(YAMLMerge)) { return NodeType.merge; } else if (value_.type is typeid(real)) { return NodeType.decimal; } else if (!value_.hasValue) { return NodeType.invalid; } else assert(0, text(value_.type)); } /// Get the kind of node this is. @property NodeID nodeID() const @safe nothrow { final switch (type) { case NodeType.sequence: return NodeID.sequence; case NodeType.mapping: return NodeID.mapping; case NodeType.boolean: case NodeType.integer: case NodeType.binary: case NodeType.string: case NodeType.timestamp: case NodeType.null_: case NodeType.merge: case NodeType.decimal: return NodeID.scalar; case NodeType.invalid: return NodeID.invalid; } } package: // Get a string representation of the node tree. Used for debugging. // // Params: level = Level of the node in the tree. // // Returns: String representing the node tree. @property string debugString(uint level = 0) const @safe { string indent; foreach(i; 0 .. level){indent ~= " ";} final switch (nodeID) { case NodeID.invalid: return indent ~ "invalid"; case NodeID.sequence: string result = indent ~ "sequence:\n"; foreach(ref node; get!(Node[])) { result ~= node.debugString(level + 1); } return result; case NodeID.mapping: string result = indent ~ "mapping:\n"; foreach(ref pair; get!(Node.Pair[])) { result ~= indent ~ " pair\n"; result ~= pair.key.debugString(level + 2); result ~= pair.value.debugString(level + 2); } return result; case NodeID.scalar: return indent ~ "scalar(" ~ (convertsTo!string ? get!string : text(type)) ~ ")\n"; } } public: @property string nodeTypeString() const @safe nothrow { final switch (nodeID) { case NodeID.mapping: return "mapping"; case NodeID.sequence: return "sequence"; case NodeID.scalar: return "scalar"; case NodeID.invalid: return "invalid"; } } // Determine if the value can be converted to specified type. @property bool convertsTo(T)() const { if(isType!T){return true;} // Every type allowed in Value should be convertible to string. static if(isSomeString!T) {return true;} else static if(isFloatingPoint!T){return type.among!(NodeType.integer, NodeType.decimal);} else static if(isIntegral!T) {return type == NodeType.integer;} else static if(is(Unqual!T==bool)){return type == NodeType.boolean;} else {return false;} } /** * Sets the style of this node when dumped. * * Params: style = Any valid style. */ void setStyle(CollectionStyle style) @safe { enforce(!isValid || (nodeID.among(NodeID.mapping, NodeID.sequence)), new NodeException( "Cannot set collection style for non-collection nodes", startMark_)); collectionStyle = style; } /// Ditto void setStyle(ScalarStyle style) @safe { enforce(!isValid || (nodeID == NodeID.scalar), new NodeException( "Cannot set scalar style for non-scalar nodes", startMark_)); scalarStyle = style; } /// @safe unittest { import dyaml.dumper; auto stream = new Appender!string(); auto node = Node([1, 2, 3, 4, 5]); node.setStyle(CollectionStyle.block); auto dumper = dumper(); dumper.dump(stream, node); } /// @safe unittest { import dyaml.dumper; auto stream = new Appender!string(); auto node = Node(4); node.setStyle(ScalarStyle.literal); auto dumper = dumper(); dumper.dump(stream, node); } @safe unittest { assertThrown!NodeException(Node(4).setStyle(CollectionStyle.block)); assertThrown!NodeException(Node([4]).setStyle(ScalarStyle.literal)); } @safe unittest { import dyaml.dumper; { auto stream = new Appender!string(); auto node = Node([1, 2, 3, 4, 5]); node.setStyle(CollectionStyle.block); auto dumper = dumper(); dumper.explicitEnd = false; dumper.explicitStart = false; dumper.YAMLVersion = null; dumper.dump(stream, node); //Block style should start with a hyphen. assert(stream.data[0] == '-'); } { auto stream = new Appender!string(); auto node = Node([1, 2, 3, 4, 5]); node.setStyle(CollectionStyle.flow); auto dumper = dumper(); dumper.explicitEnd = false; dumper.explicitStart = false; dumper.YAMLVersion = null; dumper.dump(stream, node); //Flow style should start with a bracket. assert(stream.data[0] == '['); } { auto stream = new Appender!string(); auto node = Node(1); node.setStyle(ScalarStyle.singleQuoted); auto dumper = dumper(); dumper.explicitEnd = false; dumper.explicitStart = false; dumper.YAMLVersion = null; dumper.dump(stream, node); assert(stream.data == "!!int '1'\n"); } { auto stream = new Appender!string(); auto node = Node(1); node.setStyle(ScalarStyle.doubleQuoted); auto dumper = dumper(); dumper.explicitEnd = false; dumper.explicitStart = false; dumper.YAMLVersion = null; dumper.dump(stream, node); assert(stream.data == "!!int \"1\"\n"); } } private: // Determine if the value stored by the node is of specified type. // // This only works for default YAML types, not for user defined types. @property bool isType(T)() const { return value_.type is typeid(Unqual!T); } // Implementation of contains() and containsKey(). bool contains_(T, Flag!"key" key, string func)(T rhs) const { final switch (nodeID) { case NodeID.mapping: return findPair!(T, key)(rhs) >= 0; case NodeID.sequence: static if(!key) { foreach(ref node; getValue!(Node[])) { if(node == rhs){return true;} } return false; } else { throw new NodeException("Trying to use " ~ func ~ "() on a " ~ nodeTypeString ~ " node", startMark_); } case NodeID.scalar: case NodeID.invalid: throw new NodeException("Trying to use " ~ func ~ "() on a " ~ nodeTypeString ~ " node", startMark_); } } // Implementation of remove() and removeAt() void remove_(T, Flag!"key" key, string func)(T rhs) { static void removeElem(E, I)(ref Node node, I index) { auto elems = node.getValue!(E[]); moveAll(elems[cast(size_t)index + 1 .. $], elems[cast(size_t)index .. $ - 1]); elems.length = elems.length - 1; node.setValue(elems); } final switch (nodeID) { case NodeID.mapping: const index = findPair!(T, key)(rhs); if(index >= 0){removeElem!Pair(this, index);} break; case NodeID.sequence: static long getIndex(ref Node node, ref T rhs) { foreach(idx, ref elem; node.get!(Node[])) { if(elem.convertsTo!T && elem.as!(T, No.stringConversion) == rhs) { return idx; } } return -1; } const index = select!key(rhs, getIndex(this, rhs)); // This throws if the index is not integral. checkSequenceIndex(index); static if(isIntegral!(typeof(index))){removeElem!Node(this, index); break; } else {assert(false, "Non-integral sequence index");} case NodeID.scalar: case NodeID.invalid: throw new NodeException("Trying to " ~ func ~ "() from a " ~ nodeTypeString ~ " node", startMark_); } } // Get index of pair with key (or value, if key is false) matching index. // Cannot be inferred @safe due to https://issues.dlang.org/show_bug.cgi?id=16528 sizediff_t findPair(T, Flag!"key" key = Yes.key)(const ref T index) const @safe { const pairs = getValue!(Pair[])(); const(Node)* node; foreach(idx, ref const(Pair) pair; pairs) { static if(key){node = &pair.key;} else {node = &pair.value;} const bool typeMatch = (isFloatingPoint!T && (node.type.among!(NodeType.integer, NodeType.decimal))) || (isIntegral!T && node.type == NodeType.integer) || (is(Unqual!T==bool) && node.type == NodeType.boolean) || (isSomeString!T && node.type == NodeType.string) || (node.isType!T); if(typeMatch && *node == index) { return idx; } } return -1; } // Check if index is integral and in range. void checkSequenceIndex(T)(T index) const { assert(nodeID == NodeID.sequence, "checkSequenceIndex() called on a " ~ nodeTypeString ~ " node"); static if(!isIntegral!T) { throw new NodeException("Indexing a sequence with a non-integral type.", startMark_); } else { enforce(index >= 0 && index < getValue!(Node[]).length, new NodeException("Sequence index out of range: " ~ to!string(index), startMark_)); } } // Safe wrapper for getting a value out of the variant. inout(T) getValue(T)() @trusted inout { return value_.get!T; } // Safe wrapper for coercing a value out of the variant. inout(T) coerceValue(T)() @trusted inout { return (cast(Value)value_).coerce!T; } // Safe wrapper for setting a value for the variant. void setValue(T)(T value) @trusted { static if (allowed!T) { value_ = value; } else { auto tmpNode = cast(Node)value; tag_ = tmpNode.tag; scalarStyle = tmpNode.scalarStyle; collectionStyle = tmpNode.collectionStyle; value_ = tmpNode.value_; } } } package: // Merge pairs into an array of pairs based on merge rules in the YAML spec. // // Any new pair will only be added if there is not already a pair // with the same key. // // Params: pairs = Appender managing the array of pairs to merge into. // toMerge = Pairs to merge. void merge(ref Appender!(Node.Pair[]) pairs, Node.Pair[] toMerge) @safe { bool eq(ref Node.Pair a, ref Node.Pair b){return a.key == b.key;} foreach(ref pair; toMerge) if(!canFind!eq(pairs.data, pair)) { pairs.put(pair); } } enum hasNodeConstructor(T) = hasSimpleNodeConstructor!T || hasExpandedNodeConstructor!T; template hasSimpleNodeConstructor(T) { static if (is(T == struct)) { enum hasSimpleNodeConstructor = is(typeof(T(Node.init))); } else static if (is(T == class)) { enum hasSimpleNodeConstructor = is(typeof(new inout T(Node.init))); } else enum hasSimpleNodeConstructor = false; } template hasExpandedNodeConstructor(T) { static if (is(T == struct)) { enum hasExpandedNodeConstructor = is(typeof(T(Node.init, ""))); } else static if (is(T == class)) { enum hasExpandedNodeConstructor = is(typeof(new inout T(Node.init, ""))); } else enum hasExpandedNodeConstructor = false; } enum castableToNode(T) = (is(T == struct) || is(T == class)) && is(typeof(T.opCast!Node()) : Node);