dyaml/dyaml/node.d
Ferdinand Majerech 283c42bf8f Initial commit.
2011-08-16 14:53:13 +02:00

721 lines
24 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.
*/
module dyaml.node;
import std.algorithm;
import std.conv;
import std.datetime;
import std.exception;
import std.math;
import std.stdio;
import std.traits;
import std.typecons;
import std.variant;
import dyaml.event;
import dyaml.exception;
///Exception thrown at node related errors.
class NodeException : YAMLException
{
package:
/*
* Construct a NodeException.
*
* Params: msg = Error message.
* start = Start position of the node.
* end = End position of the node.
*/
this(string msg, Mark start, Mark end)
{
super(msg ~ "\nstart:" ~ start.toString() ~ "\nend:" ~ end.toString());
}
}
//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.
private abstract class YAMLObject
{
protected:
///Get type of the stored value.
@property TypeInfo type() const;
///Test for equality with another YAMLObject.
bool equals(YAMLObject rhs);
}
//Stores a user defined YAML data type.
private class YAMLContainer(T) : YAMLObject
{
private:
//Stored value.
T value_;
//Construct a YAMLContainer holding specified value.
this(T value){value_ = value;}
protected:
//Get type of the stored value.
@property override TypeInfo type() const {return typeid(T);}
//Test for equality with another YAMLObject.
override bool equals(YAMLObject rhs)
{
if(rhs.type !is typeid(T)){return false;}
return value_ == (cast(YAMLContainer)rhs).value_;
}
}
/**
* YAML node.
*
* This is a pseudo-dynamic type that can store any YAML value, including sequence
* or a mapping of nodes. You can get data from a Node directly or iterate over it
* if it's a sequence or a mapping.
*/
struct Node
{
public:
///Pair of YAML nodes, used in mappings.
struct Pair
{
///Key node.
Node key;
///Value node.
Node value;
///Test for equality with another Pair.
bool equals(ref Pair rhs)
{
return key == rhs.key && value == rhs.value;
}
}
package:
//YAML value type.
alias Algebraic!(YAMLNull, YAMLMerge, bool, long, real, ubyte[], SysTime, string,
Node.Pair[], Node[], YAMLObject) Value;
//Stored value.
Value value_;
private:
///Start position of the node.
Mark startMark_;
///End position of the node.
Mark endMark_;
public:
///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 of nodes?
@property bool isSequence() const {return isType!(Node[]);}
///Is this node a mapping of nodes?
@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 and might be quite expensive if testing entire documents.
*
* If T is not Node, convert the node to T and test equality with that.
*
* Examples:
* --------------------
* //node is a Node that contains integer 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)
{
static if(is(T == Node))
{
if(!isValid){return !rhs.isValid;}
if(!rhs.isValid || (value_.type !is rhs.value_.type))
{
return false;
}
if(isSequence)
{
auto seq1 = get!(Node[]);
auto seq2 = rhs.get!(Node[]);
if(seq1.length != seq2.length){return false;}
foreach(node; 0 .. seq1.length)
{
if(seq1[node] != seq2[node]){return false;}
}
return true;
}
if(isMapping)
{
auto map1 = get!(Node.Pair[]);
auto map2 = rhs.get!(Node.Pair[]);
if(map1.length != map2.length){return false;}
foreach(pair; 0 .. map1.length)
{
if(!map1[pair].equals(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;
if(isNaN(r1)){return isNaN(r2);}
return 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 the value of the node as specified type.
*
* If the specifed type does not match type in the node,
* conversion is attempted if possible.
*
* 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 expects the node to be a scalar.
* )
*
* Examples:
*
* Automatic type conversion:
* --------------------
* //node is a node that contains integer 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 type of target does not match type of the node,
* conversion is attempted, if possible.
*
* 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;}
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_, endMark_);
}
}
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_, endMark_);
}
target = to!T(temp);
return;
}
}
else
{
//Can't get the value.
throw new NodeException("Node has unexpected type " ~ value_.type.toString ~
". Expected " ~ typeid(T).toString, startMark_, endMark_);
}
}
/**
* If this is a sequence or a mapping, 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_, endMark_);
}
/**
* Get the element with 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.
*/
Node opIndex(T)(in T index)
{
if(isSequence)
{
//Sequence, index must be integral.
static if(isIntegral!T)
{
auto nodes = value_.get!(Node[]);
enforce(index >= 0 && index < nodes.length,
new NodeException("Index to a sequence out of range: "
~ to!string(index), startMark_, endMark_));
return nodes[index];
}
else
{
throw new NodeException("Indexing a sequence with a non-integer type.",
startMark_, endMark_);
}
}
else if(isMapping)
{
//Mapping, look for keys convertible to T with value of index.
foreach(ref pair; get!(Pair[]))
{
//Handle NaN.
static if(isFloatingPoint!T)
{
if(isFloat && isNaN(index) && isNaN(pair.key.get!real))
{
return pair.value;
}
}
//If we can get the key as type T, get it and compare to
//index, and return value if the key matches.
if(pair.key.convertsTo!T && pair.key.get!T == index)
{
return pair.value;
}
}
throw new NodeException("Mapping index not found" ~
isSomeString!T ? ": " ~ to!string(index) : "",
startMark_, endMark_);
}
throw new NodeException("Trying to index node that does not support indexing",
startMark_, endMark_);
}
unittest
{
writeln("D:YAML Node opIndex unittest");
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 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"]));
}
/**
* 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_, endMark_));
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_, endMark_));
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);
}
}
}
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)
{
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 : value_.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));
}
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(T);}
///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;
//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;}
}
}
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);}
}