dyaml/source/dyaml/node.d
Cameron Ross e37c4daa7e switch to a non-templated Dumper struct (#234)
switch to a non-templated Dumper struct
merged-on-behalf-of: Basile-z <Basile-z@users.noreply.github.com>
2019-02-07 08:48:50 +01:00

2482 lines
85 KiB
D

// 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);