23290239a7
Anchor and TagDirectives in separate threads anymore. D:YAML still is not thread safe itself, though (it's the user's responsibility to handle synchronization when working with a single node from two threads, for example).
1376 lines
47 KiB
D
1376 lines
47 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.conv;
|
|
import std.datetime;
|
|
import std.exception;
|
|
import std.math;
|
|
import std.stdio;
|
|
import std.string;
|
|
import std.traits;
|
|
import std.typecons;
|
|
import std.variant;
|
|
|
|
import dyaml.event;
|
|
import dyaml.exception;
|
|
import dyaml.tag;
|
|
|
|
|
|
///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__)
|
|
{
|
|
super(msg ~ "\nNode at:" ~ start.toString(), file, line);
|
|
}
|
|
}
|
|
|
|
//Node kinds.
|
|
package enum NodeID : ubyte
|
|
{
|
|
Scalar,
|
|
Sequence,
|
|
Mapping
|
|
}
|
|
|
|
///Null YAML type. Used in nodes with _null values.
|
|
struct YAMLNull{}
|
|
|
|
//Merge YAML type, used to support "tag:yaml.org,2002:merge".
|
|
package struct YAMLMerge{}
|
|
|
|
//Base class for YAMLContainer - used for user defined YAML types.
|
|
package abstract class YAMLObject
|
|
{
|
|
public:
|
|
///Get type of the stored value.
|
|
@property TypeInfo type() const;
|
|
|
|
protected:
|
|
///Test for equality with another YAMLObject.
|
|
bool equals(const YAMLObject rhs) const;
|
|
}
|
|
|
|
//Stores a user defined YAML data type.
|
|
package class YAMLContainer(T) : YAMLObject
|
|
{
|
|
private:
|
|
//Stored value.
|
|
T value_;
|
|
|
|
public:
|
|
//Get type of the stored value.
|
|
@property override TypeInfo type() const {return typeid(T);}
|
|
|
|
//Get string representation of the container.
|
|
override string toString()
|
|
{
|
|
static if(!hasMember!(T, "toString"))
|
|
{
|
|
return super.toString();
|
|
}
|
|
else
|
|
{
|
|
return format("YAMLContainer(", value_.toString(), ")");
|
|
}
|
|
}
|
|
|
|
protected:
|
|
//Test for equality with another YAMLObject.
|
|
override bool equals(const YAMLObject rhs) const
|
|
{
|
|
if(rhs.type !is typeid(T)){return false;}
|
|
return value_ == (cast(YAMLContainer)rhs).value_;
|
|
}
|
|
|
|
private:
|
|
//Construct a YAMLContainer holding specified value.
|
|
this(T value){value_ = value;}
|
|
}
|
|
|
|
|
|
/**
|
|
* 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:
|
|
///Key-value pair of YAML nodes, used in mappings.
|
|
struct Pair
|
|
{
|
|
public:
|
|
///Key node.
|
|
Node key;
|
|
///Value node.
|
|
Node value;
|
|
|
|
public:
|
|
///Construct a Pair from two values. Will be converted to Nodes if needed.
|
|
this(K, V)(K key, V value)
|
|
{
|
|
static if(is(K == Node)){this.key = key;}
|
|
else {this.key = Node(key);}
|
|
static if(is(V == Node)){this.value = value;}
|
|
else {this.value = Node(value);}
|
|
}
|
|
|
|
///Equality test with another Pair.
|
|
bool equals(ref Pair rhs)
|
|
{
|
|
return equals_!true(rhs);
|
|
}
|
|
|
|
private:
|
|
/*
|
|
* Equality test with another Pair.
|
|
*
|
|
* useTag determines whether or not we consider node tags
|
|
* in the test.
|
|
*/
|
|
bool equals_(bool useTag)(ref Pair rhs)
|
|
{
|
|
return key.equals!(Node, useTag)(rhs.key) &&
|
|
value.equals!(Node, useTag)(rhs.value);
|
|
}
|
|
}
|
|
|
|
package:
|
|
//YAML value type.
|
|
alias Algebraic!(YAMLNull, YAMLMerge, bool, long, real, ubyte[], SysTime, string,
|
|
Node.Pair[], Node[], YAMLObject) Value;
|
|
|
|
private:
|
|
///Stored value.
|
|
Value value_;
|
|
///Start position of the node.
|
|
Mark startMark_;
|
|
///Tag of the node.
|
|
Tag tag_;
|
|
|
|
public:
|
|
/**
|
|
* Construct a Node from a value.
|
|
*
|
|
* Any type except of Node can be stored in a Node, but default YAML
|
|
* types (integers, floats, strings, timestamps, etc.) will be stored
|
|
* more efficiently.
|
|
*
|
|
*
|
|
* 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, in string tag = null) if (isSomeString!T ||
|
|
(!isArray!T && !isAssociativeArray!T))
|
|
{
|
|
tag_ = Tag(tag);
|
|
|
|
//No copyconstruction.
|
|
static assert(!is(T == Node));
|
|
|
|
//We can easily convert ints, floats, strings.
|
|
static if(isIntegral!T) {value_ = Value(cast(long) value);}
|
|
else static if(isFloatingPoint!T){value_ = Value(cast(real) value);}
|
|
else static if(isSomeString!T) {value_ = Value(to!string(value));}
|
|
//Other directly supported type.
|
|
else static if(Value.allowed!T) {value_ = Value(value);}
|
|
//User defined type.
|
|
else {value_ = userValue(value);}
|
|
}
|
|
unittest
|
|
{
|
|
with(Node(42))
|
|
{
|
|
assert(isScalar() && !isSequence && !isMapping && !isUserType);
|
|
assert(get!int == 42 && get!float == 42.0f && get!string == "42");
|
|
assert(!isUserType());
|
|
}
|
|
with(Node(new class{int a = 5;}))
|
|
{
|
|
assert(isUserType());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct a node from an _array.
|
|
*
|
|
* If _array 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.
|
|
*
|
|
* Params: array = Values 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.
|
|
* This is used to differentiate between YAML sequences
|
|
* ("!!seq") and sets ("!!set"), which both are
|
|
* internally represented as an array_ of nodes. Tag
|
|
* must be in full form, e.g. "tag:yaml.org,2002:set",
|
|
* not a shortcut, like "!!set".
|
|
*
|
|
* Examples:
|
|
* --------------------
|
|
* //Will be emitted as a sequence (default for arrays)
|
|
* auto seq = Node([1, 2, 3, 4, 5]);
|
|
* //Will be emitted as a set (overriden tag)
|
|
* auto set = Node([1, 2, 3, 4, 5], "tag:yaml.org,2002:set");
|
|
* --------------------
|
|
*/
|
|
this(T)(T[] array, in string tag = null) if (!isSomeString!(T[]))
|
|
{
|
|
tag_ = Tag(tag);
|
|
|
|
static if(is(T == Node) || is(T == Node.Pair))
|
|
{
|
|
value_ = Value(array);
|
|
}
|
|
//Need to handle byte buffers separately
|
|
else static if(is(T == byte) || is(T == ubyte))
|
|
{
|
|
value_ = Value(cast(ubyte[]) array);
|
|
}
|
|
else
|
|
{
|
|
Node[] nodes;
|
|
foreach(ref value; array){nodes ~= Node(value);}
|
|
value_ = Value(nodes);
|
|
}
|
|
}
|
|
unittest
|
|
{
|
|
with(Node([1, 2, 3]))
|
|
{
|
|
assert(!isScalar() && isSequence && !isMapping && !isUserType);
|
|
assert(length == 3);
|
|
assert(opIndex(2).get!int == 3);
|
|
}
|
|
|
|
//Will be emitted as a sequence (default for arrays)
|
|
auto seq = Node([1, 2, 3, 4, 5]);
|
|
//Will be emitted as a set (overriden tag)
|
|
auto set = Node([1, 2, 3, 4, 5], "tag:yaml.org,2002:set");
|
|
}
|
|
|
|
/**
|
|
* Construct a node from an associative _array.
|
|
*
|
|
* If keys and/or values of _array are nodes, they stored directly.
|
|
* Otherwise they are converted to nodes and then stored.
|
|
*
|
|
* Params: array = Values 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.
|
|
* 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".
|
|
*
|
|
* Examples:
|
|
* --------------------
|
|
* //Will be emitted as an unordered mapping (default for mappings)
|
|
* auto map = Node([1 : "a", 2 : "b"]);
|
|
* //Will be emitted as an ordered map (overriden tag)
|
|
* auto omap = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:omap");
|
|
* //Will be emitted as pairs (overriden tag)
|
|
* auto pairs = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:pairs");
|
|
* --------------------
|
|
*/
|
|
this(K, V)(V[K] array, in string tag = null)
|
|
{
|
|
tag_ = Tag(tag);
|
|
|
|
Node.Pair[] pairs;
|
|
foreach(key, ref value; array){pairs ~= Pair(key, value);}
|
|
value_ = Value(pairs);
|
|
}
|
|
unittest
|
|
{
|
|
int[string] aa;
|
|
aa["1"] = 1;
|
|
aa["2"] = 2;
|
|
with(Node(aa))
|
|
{
|
|
assert(!isScalar() && !isSequence && isMapping && !isUserType);
|
|
assert(length == 2);
|
|
assert(opIndex("2").get!int == 2);
|
|
}
|
|
|
|
//Will be emitted as an unordered mapping (default for mappings)
|
|
auto map = Node([1 : "a", 2 : "b"]);
|
|
//Will be emitted as an ordered map (overriden tag)
|
|
auto omap = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:omap");
|
|
//Will be emitted as pairs (overriden tag)
|
|
auto pairs = Node([1 : "a", 2 : "b"], "tag:yaml.org,2002:pairs");
|
|
}
|
|
|
|
/**
|
|
* 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".
|
|
*
|
|
* Examples:
|
|
* --------------------
|
|
* //Will be emitted as an unordered mapping (default for mappings)
|
|
* auto map = Node([1, 2], ["a", "b"]);
|
|
* //Will be emitted as an ordered map (overriden 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");
|
|
* --------------------
|
|
*/
|
|
this(K, V)(K[] keys, V[] values, in string tag = null)
|
|
if(!(isSomeString!(K[]) || isSomeString!(V[])))
|
|
in
|
|
{
|
|
assert(keys.length == values.length,
|
|
"Lengths of keys and values arrays to construct "
|
|
"a YAML node from don't match");
|
|
}
|
|
body
|
|
{
|
|
tag_ = Tag(tag);
|
|
|
|
Node.Pair[] pairs;
|
|
foreach(i; 0 .. keys.length){pairs ~= Pair(keys[i], values[i]);}
|
|
value_ = Value(pairs);
|
|
}
|
|
unittest
|
|
{
|
|
with(Node(["1", "2"], [1, 2]))
|
|
{
|
|
assert(!isScalar() && !isSequence && isMapping && !isUserType);
|
|
assert(length == 2);
|
|
assert(opIndex("2").get!int == 2);
|
|
}
|
|
|
|
//Will be emitted as an unordered mapping (default for mappings)
|
|
auto map = Node([1, 2], ["a", "b"]);
|
|
//Will be emitted as an ordered map (overriden 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");
|
|
}
|
|
|
|
///Is this node valid (initialized)?
|
|
@property bool isValid() const {return value_.hasValue;}
|
|
|
|
///Is this node a scalar value?
|
|
@property bool isScalar() const {return !(isMapping || isSequence);}
|
|
|
|
///Is this node a sequence?
|
|
@property bool isSequence() const {return isType!(Node[]);}
|
|
|
|
///Is this node a mapping?
|
|
@property bool isMapping() const {return isType!(Pair[]);}
|
|
|
|
///Is this node a user defined type?
|
|
@property bool isUserType() const {return isType!YAMLObject;}
|
|
|
|
/**
|
|
* Equality test.
|
|
*
|
|
* If T is Node, recursively compare all subnodes.
|
|
* This might be quite expensive if testing entire documents.
|
|
*
|
|
* If T is not Node, convert the node to T and test equality with that.
|
|
*
|
|
* Examples:
|
|
* --------------------
|
|
* auto node = Node(42);
|
|
*
|
|
* assert(node == 42);
|
|
* assert(node == "42");
|
|
* assert(node != "43");
|
|
* --------------------
|
|
*
|
|
* Params: rhs = Variable to test equality with.
|
|
*
|
|
* Returns: true if equal, false otherwise.
|
|
*/
|
|
bool opEquals(T)(ref T rhs)
|
|
{
|
|
return equals!(T, true)(rhs);
|
|
}
|
|
|
|
/**
|
|
* Get the value of the node as specified type.
|
|
*
|
|
* If the specifed type does not match type in the node,
|
|
* conversion is attempted.
|
|
*
|
|
* Timestamps are stored as std.datetime.SysTime.
|
|
* Binary values are decoded and stored as ubyte[].
|
|
*
|
|
* $(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.
|
|
* )
|
|
*
|
|
* Examples:
|
|
*
|
|
* Automatic type conversion:
|
|
* --------------------
|
|
* auto node = Node(42);
|
|
*
|
|
* assert(node.get!int == 42);
|
|
* assert(node.get!string == "42");
|
|
* assert(node.get!double == 42.0);
|
|
* --------------------
|
|
*
|
|
* Returns: Value of the node as specified type.
|
|
*
|
|
* Throws: NodeException if unable to convert to specified type.
|
|
*/
|
|
@property T get(T)()
|
|
{
|
|
T result;
|
|
getToVar(result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Write the value of the node to target.
|
|
*
|
|
* If the target type does not match node type,
|
|
* conversion is attempted.
|
|
*
|
|
* Params: target = Variable to write to.
|
|
*
|
|
* Throws: NodeException if unable to convert to specified type.
|
|
*/
|
|
void getToVar(T)(out T target)
|
|
{
|
|
if(isType!T)
|
|
{
|
|
target = value_.get!T;
|
|
return;
|
|
}
|
|
|
|
///Must go before others, as even string/int/etc could be stored in a YAMLObject.
|
|
if(isUserType)
|
|
{
|
|
auto object = get!YAMLObject;
|
|
if(object.type is typeid(T))
|
|
{
|
|
target = (cast(YAMLContainer!T)object).value_;
|
|
return;
|
|
}
|
|
}
|
|
|
|
//If we're getting from a mapping and we're not getting Node.Pair[],
|
|
//we're getting the default value.
|
|
if(isMapping){return this["="].get!T;}
|
|
|
|
void throwUnexpectedType()
|
|
{
|
|
//Can't get the value.
|
|
throw new NodeException("Node has unexpected type " ~ type.toString ~
|
|
". Expected " ~ typeid(T).toString, startMark_);
|
|
}
|
|
|
|
static if(isSomeString!T)
|
|
{
|
|
//Try to convert to string.
|
|
try
|
|
{
|
|
target = value_.coerce!T();
|
|
return;
|
|
}
|
|
catch(VariantException e)
|
|
{
|
|
throw new NodeException("Unable to convert node value to a string",
|
|
startMark_);
|
|
}
|
|
}
|
|
else static if(isFloatingPoint!T)
|
|
{
|
|
///Can convert int to float.
|
|
if(isInt())
|
|
{
|
|
target = to!T(value_.get!long);
|
|
return;
|
|
}
|
|
else if(isFloat())
|
|
{
|
|
target = to!T(value_.get!real);
|
|
return;
|
|
}
|
|
}
|
|
else static if(isIntegral!T)
|
|
{
|
|
if(isInt())
|
|
{
|
|
long temp = value_.get!long;
|
|
if(temp < T.min || temp > T.max)
|
|
{
|
|
throw new NodeException("Integer value out of range of type " ~
|
|
typeid(T).toString ~ "Value: " ~
|
|
to!string(temp), startMark_);
|
|
}
|
|
target = to!T(temp);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
throwUnexpectedType();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throwUnexpectedType();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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()
|
|
{
|
|
if(isSequence) {return get!(Node[]).length;}
|
|
else if(isMapping){return get!(Pair[]).length;}
|
|
throw new NodeException("Trying to get length of a node that is not a collection",
|
|
startMark_);
|
|
}
|
|
|
|
/**
|
|
* 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, even after conversion. I.e; node["12"] will
|
|
* return value of the first key that equals "12", even if it's an integer.
|
|
*
|
|
* 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.
|
|
*/
|
|
Node opIndex(T)(T index)
|
|
{
|
|
if(isSequence)
|
|
{
|
|
checkSequenceIndex(index);
|
|
static if(isIntegral!T){return value_.get!(Node[])[index];}
|
|
assert(false);
|
|
}
|
|
else if(isMapping)
|
|
{
|
|
auto idx = findPair(index);
|
|
if(idx >= 0){return get!(Pair[])[idx].value;}
|
|
|
|
throw new NodeException("Mapping index not found" ~
|
|
isSomeString!T ? ": " ~ to!string(index) : "",
|
|
startMark_);
|
|
}
|
|
throw new NodeException("Trying to index node that does not support indexing",
|
|
startMark_);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node opIndex unittest");
|
|
|
|
alias Node.Value Value;
|
|
alias Node.Pair Pair;
|
|
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 narray = Node(Value([n1, n2, n3, n4]));
|
|
Node nmap = Node(Value([Pair(k1, n1),
|
|
Pair(k2, n2),
|
|
Pair(k3, n3),
|
|
Pair(k4, n4)]));
|
|
|
|
assert(narray[0].get!int == 11);
|
|
assert(null !is collectException(narray[42]));
|
|
assert(nmap["11"].get!int == 11);
|
|
assert(nmap["14"].get!int == 14);
|
|
assert(null !is collectException(nmap["42"]));
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* Params: 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)
|
|
{
|
|
if(isSequence())
|
|
{
|
|
//This ensures K is integral.
|
|
checkSequenceIndex(index);
|
|
static if(isIntegral!K)
|
|
{
|
|
auto nodes = value_.get!(Node[]);
|
|
static if(is(V == Node)){nodes[index] = value;}
|
|
else {nodes[index] = Node(value);}
|
|
value_ = Value(nodes);
|
|
return;
|
|
}
|
|
assert(false);
|
|
}
|
|
else if(isMapping())
|
|
{
|
|
auto idx = findPair(index);
|
|
if(idx < 0){add(index, value);}
|
|
else
|
|
{
|
|
auto pairs = get!(Node.Pair[])();
|
|
static if(is(V == Node)){pairs[idx].value = value;}
|
|
else {pairs[idx].value = Node(value);}
|
|
value_ = Value(pairs);
|
|
}
|
|
return;
|
|
}
|
|
|
|
throw new NodeException("Trying to index a YAML node that is not a collection.",
|
|
startMark_);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node opIndexAssign unittest");
|
|
|
|
with(Node([1, 2, 3, 4, 3]))
|
|
{
|
|
opIndexAssign(42, 3);
|
|
assert(length == 5);
|
|
assert(opIndex(3).get!int == 42);
|
|
}
|
|
with(Node(["1", "2", "3"], [4, 5, 6]))
|
|
{
|
|
opIndexAssign(42, "3");
|
|
opIndexAssign(123, 456);
|
|
assert(length == 4);
|
|
assert(opIndex("3").get!int == 42);
|
|
assert(opIndex(456).get!int == 123);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Iterate 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(T)(int delegate(ref T) dg)
|
|
{
|
|
enforce(isSequence,
|
|
new NodeException("Trying to iterate over a node that is not a sequence",
|
|
startMark_));
|
|
|
|
int result = 0;
|
|
foreach(ref node; get!(Node[]))
|
|
{
|
|
static if(is(T == Node))
|
|
{
|
|
result = dg(node);
|
|
}
|
|
else
|
|
{
|
|
T temp = node.get!T;
|
|
result = dg(temp);
|
|
}
|
|
if(result){break;}
|
|
}
|
|
return result;
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node opApply unittest 1");
|
|
|
|
alias Node.Value Value;
|
|
alias Node.Pair Pair;
|
|
|
|
Node n1 = Node(Value(cast(long)11));
|
|
Node n2 = Node(Value(cast(long)12));
|
|
Node n3 = Node(Value(cast(long)13));
|
|
Node n4 = Node(Value(cast(long)14));
|
|
Node narray = Node(Value([n1, n2, n3, n4]));
|
|
|
|
int[] array, array2;
|
|
foreach(int value; narray)
|
|
{
|
|
array ~= value;
|
|
}
|
|
foreach(Node node; narray)
|
|
{
|
|
array2 ~= node.get!int;
|
|
}
|
|
assert(array == [11, 12, 13, 14]);
|
|
assert(array2 == [11, 12, 13, 14]);
|
|
}
|
|
|
|
/**
|
|
* Iterate 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(K, V)(int delegate(ref K, ref V) dg)
|
|
{
|
|
enforce(isMapping,
|
|
new NodeException("Trying to iterate over a node that is not a mapping",
|
|
startMark_));
|
|
|
|
int result = 0;
|
|
foreach(ref pair; get!(Node.Pair[]))
|
|
{
|
|
static if(is(K == Node) && is(V == Node))
|
|
{
|
|
result = dg(pair.key, pair.value);
|
|
}
|
|
else static if(is(K == Node))
|
|
{
|
|
V tempValue = pair.value.get!V;
|
|
result = dg(pair.key, tempValue);
|
|
}
|
|
else static if(is(V == Node))
|
|
{
|
|
K tempKey = pair.key.get!K;
|
|
result = dg(tempKey, pair.value);
|
|
}
|
|
else
|
|
{
|
|
K tempKey = pair.key.get!K;
|
|
V tempValue = pair.value.get!V;
|
|
result = dg(tempKey, tempValue);
|
|
}
|
|
|
|
if(result){break;}
|
|
}
|
|
return result;
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node opApply unittest 2");
|
|
|
|
alias Node.Value Value;
|
|
alias Node.Pair Pair;
|
|
|
|
Node n1 = Node(Value(cast(long)11));
|
|
Node n2 = Node(Value(cast(long)12));
|
|
Node n3 = Node(Value(cast(long)13));
|
|
Node n4 = Node(Value(cast(long)14));
|
|
|
|
Node k1 = Node(Value("11"));
|
|
Node k2 = Node(Value("12"));
|
|
Node k3 = Node(Value("13"));
|
|
Node k4 = Node(Value("14"));
|
|
|
|
Node nmap1 = Node(Value([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(Value([Pair(k1, Node(Value(cast(long)5))),
|
|
Pair(k2, Node(Value(true))),
|
|
Pair(k3, Node(Value(cast(real)1.0))),
|
|
Pair(k4, Node(Value("yarly")))]));
|
|
|
|
foreach(string key, Node value; nmap2)
|
|
{
|
|
switch(key)
|
|
{
|
|
case "11": assert(value.get!int == 5 ); break;
|
|
case "12": assert(value.get!bool == true ); break;
|
|
case "13": assert(value.get!float == 1.0 ); break;
|
|
case "14": assert(value.get!string == "yarly"); break;
|
|
default: assert(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
enforce(isSequence(),
|
|
new NodeException("Trying to add an element to a "
|
|
"non-sequence YAML node", startMark_));
|
|
|
|
auto nodes = get!(Node[])();
|
|
static if(is(T == Node)){nodes ~= value;}
|
|
else {nodes ~= Node(value);}
|
|
value_ = Value(nodes);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node add unittest 1");
|
|
|
|
with(Node([1, 2, 3, 4]))
|
|
{
|
|
add(5.0f);
|
|
assert(opIndex(4).get!float == 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)
|
|
{
|
|
enforce(isMapping(),
|
|
new NodeException("Trying to add a key-value pair to a "
|
|
"non-mapping YAML node", startMark_));
|
|
|
|
auto pairs = get!(Node.Pair[])();
|
|
pairs ~= Pair(key, value);
|
|
value_ = Value(pairs);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node add unittest 2");
|
|
with(Node([1, 2], [3, 4]))
|
|
{
|
|
add(5, "6");
|
|
assert(opIndex(5).get!string == "6");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 (including
|
|
* conversion, so e.g. "42" matches 42) is removed.
|
|
* If the node is a mapping, the first key-value pair where _value
|
|
* matches specified value is removed.
|
|
*
|
|
* Params: value = Value to _remove.
|
|
*
|
|
* Throws: NodeException if the node is not a collection.
|
|
*/
|
|
void remove(T)(T value)
|
|
{
|
|
if(isSequence())
|
|
{
|
|
foreach(idx, ref elem; get!(Node[]))
|
|
{
|
|
if(elem.convertsTo!T && elem.get!T == value)
|
|
{
|
|
removeAt(idx);
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
else if(isMapping())
|
|
{
|
|
auto idx = findPair!(T, true)(value);
|
|
if(idx >= 0)
|
|
{
|
|
auto pairs = get!(Node.Pair[])();
|
|
moveAll(pairs[idx + 1 .. $], pairs[idx .. $ - 1]);
|
|
pairs.length = pairs.length - 1;
|
|
value_ = Value(pairs);
|
|
}
|
|
return;
|
|
}
|
|
throw new NodeException("Trying to remove an element from a YAML node that "
|
|
"is not a collection.", startMark_);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node remove unittest");
|
|
with(Node([1, 2, 3, 4, 3]))
|
|
{
|
|
remove(3);
|
|
assert(length == 4);
|
|
assert(opIndex(2).get!int == 4);
|
|
assert(opIndex(3).get!int == 3);
|
|
}
|
|
with(Node(["1", "2", "3"], [4, 5, 6]))
|
|
{
|
|
remove(4);
|
|
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 (including conversion, so e.g. "42" matches 42).
|
|
*
|
|
* 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)
|
|
{
|
|
if(isSequence())
|
|
{
|
|
//This ensures T is integral.
|
|
checkSequenceIndex(index);
|
|
static if(isIntegral!T)
|
|
{
|
|
auto nodes = value_.get!(Node[]);
|
|
moveAll(nodes[index + 1 .. $], nodes[index .. $ - 1]);
|
|
nodes.length = nodes.length - 1;
|
|
value_ = Value(nodes);
|
|
return;
|
|
}
|
|
assert(false);
|
|
}
|
|
else if(isMapping())
|
|
{
|
|
auto idx = findPair(index);
|
|
if(idx >= 0)
|
|
{
|
|
auto pairs = get!(Node.Pair[])();
|
|
moveAll(pairs[idx + 1 .. $], pairs[idx .. $ - 1]);
|
|
pairs.length = pairs.length - 1;
|
|
value_ = Value(pairs);
|
|
}
|
|
return;
|
|
}
|
|
throw new NodeException("Trying to remove an element from a YAML node that "
|
|
"is not a collection.", startMark_);
|
|
}
|
|
unittest
|
|
{
|
|
writeln("D:YAML Node removeAt unittest");
|
|
with(Node([1, 2, 3, 4, 3]))
|
|
{
|
|
removeAt(3);
|
|
assert(length == 4);
|
|
assert(opIndex(3).get!int == 3);
|
|
}
|
|
with(Node(["1", "2", "3"], [4, 5, 6]))
|
|
{
|
|
removeAt("2");
|
|
assert(length == 2);
|
|
}
|
|
}
|
|
|
|
package:
|
|
/*
|
|
* Construct a node from raw data.
|
|
*
|
|
* Params: value = Value of the node.
|
|
* startMark = Start position of the node in file.
|
|
* tag = Tag of the node.
|
|
*
|
|
* Returns: Constructed node.
|
|
*/
|
|
static Node rawNode(Value value, in Mark startMark = Mark(), in Tag tag = Tag("DUMMY_TAG"))
|
|
{
|
|
Node node;
|
|
node.value_ = value;
|
|
node.startMark_ = startMark;
|
|
node.tag_ = tag;
|
|
|
|
return node;
|
|
}
|
|
|
|
/*
|
|
* Equality test with any value.
|
|
*
|
|
* useTag determines whether or not to consider tags in node-node comparisons.
|
|
*/
|
|
bool equals(T, bool useTag)(ref T rhs)
|
|
{
|
|
static if(is(T == Node))
|
|
{
|
|
static if(useTag)
|
|
{
|
|
if(tag_ != rhs.tag_){return false;}
|
|
}
|
|
|
|
if(!isValid){return !rhs.isValid;}
|
|
if(!rhs.isValid || !hasEqualType(rhs))
|
|
{
|
|
return false;
|
|
}
|
|
if(isSequence)
|
|
{
|
|
auto seq1 = get!(Node[]);
|
|
auto seq2 = rhs.get!(Node[]);
|
|
if(seq1 is seq2){return true;}
|
|
if(seq1.length != seq2.length){return false;}
|
|
foreach(node; 0 .. seq1.length)
|
|
{
|
|
if(!seq1[node].equals!(T, useTag)(seq2[node])){return false;}
|
|
}
|
|
return true;
|
|
}
|
|
if(isMapping)
|
|
{
|
|
auto map1 = get!(Node.Pair[]);
|
|
auto map2 = rhs.get!(Node.Pair[]);
|
|
if(map1 is map2){return true;}
|
|
if(map1.length != map2.length){return false;}
|
|
foreach(pair; 0 .. map1.length)
|
|
{
|
|
if(!map1[pair].equals_!useTag(map2[pair])){return false;}
|
|
}
|
|
return true;
|
|
}
|
|
if(isScalar)
|
|
{
|
|
if(isUserType)
|
|
{
|
|
if(!rhs.isUserType){return false;}
|
|
return get!YAMLObject.equals(rhs.get!YAMLObject);
|
|
}
|
|
if(isFloat)
|
|
{
|
|
if(!rhs.isFloat){return false;}
|
|
real r1 = get!real;
|
|
real r2 = rhs.get!real;
|
|
bool equals(real r1, real r2)
|
|
{
|
|
return r1 <= r2 + real.epsilon && r1 >= r2 - real.epsilon;
|
|
}
|
|
if(isNaN(r1)){return isNaN(r2);}
|
|
return equals(r1, r2);
|
|
}
|
|
else
|
|
{
|
|
return value_ == rhs.value_;
|
|
}
|
|
}
|
|
assert(false, "Unknown kind of node");
|
|
}
|
|
else
|
|
{
|
|
try{return rhs == get!T;}
|
|
catch(NodeException e){return false;}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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)
|
|
{
|
|
string indent;
|
|
foreach(i; 0 .. level){indent ~= " ";}
|
|
|
|
if(!isValid){return indent ~ "invalid";}
|
|
|
|
if(isSequence)
|
|
{
|
|
string result = indent ~ "sequence:\n";
|
|
foreach(ref node; get!(Node[]))
|
|
{
|
|
result ~= node.debugString(level + 1);
|
|
}
|
|
return result;
|
|
}
|
|
if(isMapping)
|
|
{
|
|
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;
|
|
}
|
|
if(isScalar)
|
|
{
|
|
return indent ~ "scalar(" ~
|
|
(convertsTo!string ? get!string : type.toString) ~ ")\n";
|
|
}
|
|
assert(false);
|
|
}
|
|
|
|
//Construct Node.Value from user defined type.
|
|
static Value userValue(T)(T value)
|
|
{
|
|
return Value(cast(YAMLObject)new YAMLContainer!T(value));
|
|
}
|
|
|
|
//Get type of the node value (YAMLObject for user types).
|
|
@property TypeInfo type() const {return value_.type;}
|
|
|
|
/*
|
|
* 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(T);}
|
|
|
|
//Return tag of the node.
|
|
@property Tag tag() const {return tag_;}
|
|
|
|
//Set tag of the node.
|
|
@property void tag(Tag tag) {tag_ = tag;}
|
|
|
|
private:
|
|
//Is the value an integer of some kind?
|
|
alias isType!long isInt;
|
|
|
|
//Is the value a floating point number of some kind?
|
|
alias isType!real isFloat;
|
|
|
|
//Is the value a string of some kind?
|
|
alias isType!string isString;
|
|
|
|
//Does given node have the same type as this node?
|
|
bool hasEqualType(ref Node node)
|
|
{
|
|
return value_.type is node.value_.type;
|
|
}
|
|
|
|
//Determine if the value can be converted to specified type.
|
|
bool convertsTo(T)()
|
|
{
|
|
if(isType!T){return true;}
|
|
|
|
static if(isSomeString!T)
|
|
{
|
|
try
|
|
{
|
|
auto dummy = value_.coerce!T();
|
|
return true;
|
|
}
|
|
catch(VariantException e){return false;}
|
|
}
|
|
else static if(isFloatingPoint!T){return isInt() || isFloat();}
|
|
else static if(isIntegral!T) {return isInt();}
|
|
else {return false;}
|
|
}
|
|
|
|
//Get index of pair with key (or value, if value is true) matching index.
|
|
long findPair(T, bool value = false)(const ref T index)
|
|
{
|
|
auto pairs = get!(Node.Pair[])();
|
|
Node* node;
|
|
foreach(idx, ref pair; pairs)
|
|
{
|
|
static if(value){node = &pair.value;}
|
|
else{node = &pair.key;}
|
|
|
|
static if(is(T == Node))
|
|
{
|
|
if(*node == index){return idx;}
|
|
}
|
|
else static if(isFloatingPoint!T)
|
|
{
|
|
//Need to handle NaNs separately.
|
|
if((node.get!T == index) ||
|
|
(isFloat && isNaN(index) && isNaN(node.get!real)))
|
|
{
|
|
return idx;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
if(node.get!T == index){return idx;}
|
|
}
|
|
catch(NodeException e)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//Check if index is integral and in range.
|
|
void checkSequenceIndex(T)(T index)
|
|
{
|
|
static if(!isIntegral!T)
|
|
{
|
|
throw new NodeException("Indexing a YAML sequence with a non-integral type.",
|
|
startMark_);
|
|
}
|
|
else
|
|
{
|
|
enforce(index >= 0 && index < value_.get!(Node[]).length,
|
|
new NodeException("Index to a YAML sequence out of range: "
|
|
~ to!string(index), startMark_));
|
|
}
|
|
}
|
|
}
|
|
|
|
package:
|
|
/*
|
|
* Merge a pair into an array of pairs based on merge rules in the YAML spec.
|
|
*
|
|
* The new pair will only be added if there is not already a pair
|
|
* with the same key.
|
|
*
|
|
* Params: pairs = Array of pairs to merge into.
|
|
* toMerge = Pair to merge.
|
|
*/
|
|
void merge(ref Node.Pair[] pairs, ref Node.Pair toMerge)
|
|
{
|
|
foreach(ref pair; pairs)
|
|
{
|
|
if(pair.key == toMerge.key){return;}
|
|
}
|
|
pairs ~= toMerge;
|
|
}
|
|
|
|
/*
|
|
* 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 = Array of pairs to merge into.
|
|
* toMerge = Pairs to merge.
|
|
*/
|
|
void merge(ref Node.Pair[] pairs, Node.Pair[] toMerge)
|
|
{
|
|
foreach(ref pair; toMerge){merge(pairs, pair);}
|
|
}
|