b63ea1aaae
* make node types into enums and clean up code using them * add some tests for anchorable
518 lines
17 KiB
D
518 lines
17 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)
|
|
|
|
/**
|
|
* YAML node _representer. Prepares YAML nodes for output. A tutorial can be
|
|
* found $(LINK2 ../tutorials/custom_types.html, here).
|
|
*
|
|
* Code based on $(LINK2 http://www.pyyaml.org, PyYAML).
|
|
*/
|
|
module dyaml.representer;
|
|
|
|
|
|
import std.algorithm;
|
|
import std.array;
|
|
import std.base64;
|
|
import std.container;
|
|
import std.conv;
|
|
import std.datetime;
|
|
import std.exception;
|
|
import std.format;
|
|
import std.math;
|
|
import std.typecons;
|
|
import std.string;
|
|
|
|
import dyaml.exception;
|
|
import dyaml.node;
|
|
import dyaml.serializer;
|
|
import dyaml.style;
|
|
|
|
package:
|
|
///Exception thrown on Representer errors.
|
|
class RepresenterException : YAMLException
|
|
{
|
|
mixin ExceptionCtors;
|
|
}
|
|
|
|
/**
|
|
* Represents YAML nodes as scalar, sequence and mapping nodes ready for output.
|
|
*/
|
|
Node representData(const Node data, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
|
|
{
|
|
Node result;
|
|
final switch(data.type)
|
|
{
|
|
case NodeType.null_:
|
|
result = representNull();
|
|
break;
|
|
case NodeType.merge:
|
|
break;
|
|
case NodeType.boolean:
|
|
result = representBool(data);
|
|
break;
|
|
case NodeType.integer:
|
|
result = representLong(data);
|
|
break;
|
|
case NodeType.decimal:
|
|
result = representReal(data);
|
|
break;
|
|
case NodeType.binary:
|
|
result = representBytes(data);
|
|
break;
|
|
case NodeType.timestamp:
|
|
result = representSysTime(data);
|
|
break;
|
|
case NodeType.string:
|
|
result = representString(data);
|
|
break;
|
|
case NodeType.mapping:
|
|
result = representPairs(data, defaultScalarStyle, defaultCollectionStyle);
|
|
break;
|
|
case NodeType.sequence:
|
|
result = representNodes(data, defaultScalarStyle, defaultCollectionStyle);
|
|
break;
|
|
case NodeType.invalid:
|
|
assert(0);
|
|
}
|
|
|
|
final switch (result.nodeID)
|
|
{
|
|
case NodeID.scalar:
|
|
if (result.scalarStyle == ScalarStyle.invalid)
|
|
{
|
|
result.scalarStyle = defaultScalarStyle;
|
|
}
|
|
break;
|
|
case NodeID.sequence, NodeID.mapping:
|
|
if (defaultCollectionStyle != CollectionStyle.invalid)
|
|
{
|
|
result.collectionStyle = defaultCollectionStyle;
|
|
}
|
|
case NodeID.invalid:
|
|
}
|
|
|
|
|
|
//Override tag if specified.
|
|
if(data.tag_ !is null){result.tag_ = data.tag_;}
|
|
|
|
//Remember style if this was loaded before.
|
|
if(data.scalarStyle != ScalarStyle.invalid)
|
|
{
|
|
result.scalarStyle = data.scalarStyle;
|
|
}
|
|
if(data.collectionStyle != CollectionStyle.invalid)
|
|
{
|
|
result.collectionStyle = data.collectionStyle;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
// We don't emit yaml merge nodes.
|
|
assert(representData(Node(YAMLMerge()), ScalarStyle.invalid, CollectionStyle.invalid) == Node.init);
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(YAMLNull()), ScalarStyle.invalid, CollectionStyle.invalid) == Node("null", "tag:yaml.org,2002:null"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(cast(string)null), ScalarStyle.invalid, CollectionStyle.invalid) == Node("null", "tag:yaml.org,2002:null"));
|
|
assert(representData(Node("Hello world!"), ScalarStyle.invalid, CollectionStyle.invalid) == Node("Hello world!", "tag:yaml.org,2002:str"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(64), ScalarStyle.invalid, CollectionStyle.invalid) == Node("64", "tag:yaml.org,2002:int"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(true), ScalarStyle.invalid, CollectionStyle.invalid) == Node("true", "tag:yaml.org,2002:bool"));
|
|
assert(representData(Node(false), ScalarStyle.invalid, CollectionStyle.invalid) == Node("false", "tag:yaml.org,2002:bool"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
// Float comparison is pretty unreliable...
|
|
auto result = representData(Node(1.0), ScalarStyle.invalid, CollectionStyle.invalid);
|
|
assert(approxEqual(result.as!string.to!real, 1.0));
|
|
assert(result.tag == "tag:yaml.org,2002:float");
|
|
|
|
assert(representData(Node(real.nan), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".nan", "tag:yaml.org,2002:float"));
|
|
assert(representData(Node(real.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node(".inf", "tag:yaml.org,2002:float"));
|
|
assert(representData(Node(-real.infinity), ScalarStyle.invalid, CollectionStyle.invalid) == Node("-.inf", "tag:yaml.org,2002:float"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(SysTime(DateTime(2000, 03, 14, 12, 34, 56), UTC())), ScalarStyle.invalid, CollectionStyle.invalid) == Node("2000-03-14T12:34:56Z", "tag:yaml.org,2002:timestamp"));
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(Node[].init, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:set"));
|
|
assert(representData(Node(Node[].init, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:seq"));
|
|
{
|
|
auto nodes = [
|
|
Node("a"),
|
|
Node("b"),
|
|
Node("c"),
|
|
];
|
|
assert(representData(Node(nodes, "tag:yaml.org,2002:set"), ScalarStyle.invalid, CollectionStyle.invalid) ==
|
|
Node([
|
|
Node.Pair(
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("null", "tag:yaml.org,2002:null")
|
|
),
|
|
Node.Pair(
|
|
Node("b", "tag:yaml.org,2002:str"),
|
|
Node("null", "tag:yaml.org,2002:null")
|
|
),
|
|
Node.Pair(
|
|
Node("c", "tag:yaml.org,2002:str"),
|
|
Node("null", "tag:yaml.org,2002:null")
|
|
)
|
|
], "tag:yaml.org,2002:set"));
|
|
}
|
|
{
|
|
auto nodes = [
|
|
Node("a"),
|
|
Node("b"),
|
|
Node("c"),
|
|
];
|
|
assert(representData(Node(nodes, "tag:yaml.org,2002:seq"), ScalarStyle.invalid, CollectionStyle.invalid) ==
|
|
Node([
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("b", "tag:yaml.org,2002:str"),
|
|
Node("c", "tag:yaml.org,2002:str")
|
|
], "tag:yaml.org,2002:seq"));
|
|
}
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:omap"));
|
|
assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node[].init, "tag:yaml.org,2002:pairs"));
|
|
assert(representData(Node(Node.Pair[].init, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) == Node(Node.Pair[].init, "tag:yaml.org,2002:map"));
|
|
{
|
|
auto nodes = [
|
|
Node.Pair("a", "b"),
|
|
Node.Pair("a", "c")
|
|
];
|
|
assertThrown(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid));
|
|
}
|
|
// Yeah, this gets ugly really fast.
|
|
{
|
|
auto nodes = [
|
|
Node.Pair("a", "b"),
|
|
Node.Pair("a", "c")
|
|
];
|
|
assert(representData(Node(nodes, "tag:yaml.org,2002:pairs"), ScalarStyle.invalid, CollectionStyle.invalid) ==
|
|
Node([
|
|
Node(
|
|
[Node.Pair(
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("b", "tag:yaml.org,2002:str")
|
|
)],
|
|
"tag:yaml.org,2002:map"),
|
|
Node(
|
|
[Node.Pair(
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("c", "tag:yaml.org,2002:str")
|
|
)],
|
|
"tag:yaml.org,2002:map"),
|
|
], "tag:yaml.org,2002:pairs"));
|
|
}
|
|
{
|
|
auto nodes = [
|
|
Node.Pair("a", "b"),
|
|
Node.Pair("a", "c")
|
|
];
|
|
assertThrown(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid));
|
|
}
|
|
{
|
|
auto nodes = [
|
|
Node.Pair("a", "b"),
|
|
Node.Pair("c", "d")
|
|
];
|
|
assert(representData(Node(nodes, "tag:yaml.org,2002:omap"), ScalarStyle.invalid, CollectionStyle.invalid) ==
|
|
Node([
|
|
Node([
|
|
Node.Pair(
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("b", "tag:yaml.org,2002:str")
|
|
)
|
|
], "tag:yaml.org,2002:map"),
|
|
Node([
|
|
Node.Pair(
|
|
Node("c", "tag:yaml.org,2002:str"),
|
|
Node("d", "tag:yaml.org,2002:str")
|
|
)
|
|
], "tag:yaml.org,2002:map"
|
|
)], "tag:yaml.org,2002:omap"));
|
|
}
|
|
{
|
|
auto nodes = [
|
|
Node.Pair("a", "b"),
|
|
Node.Pair("c", "d")
|
|
];
|
|
assert(representData(Node(nodes, "tag:yaml.org,2002:map"), ScalarStyle.invalid, CollectionStyle.invalid) ==
|
|
Node([
|
|
Node.Pair(
|
|
Node("a", "tag:yaml.org,2002:str"),
|
|
Node("b", "tag:yaml.org,2002:str")
|
|
),
|
|
Node.Pair(
|
|
Node("c", "tag:yaml.org,2002:str"),
|
|
Node("d", "tag:yaml.org,2002:str")
|
|
),
|
|
], "tag:yaml.org,2002:map"));
|
|
}
|
|
}
|
|
|
|
private:
|
|
|
|
//Represent a _null _node as a _null YAML value.
|
|
Node representNull() @safe
|
|
{
|
|
return Node("null", "tag:yaml.org,2002:null");
|
|
}
|
|
|
|
//Represent a string _node as a string scalar.
|
|
Node representString(const Node node) @safe
|
|
{
|
|
string value = node.as!string;
|
|
return value is null
|
|
? Node("null", "tag:yaml.org,2002:null")
|
|
: Node(value, "tag:yaml.org,2002:str");
|
|
}
|
|
|
|
//Represent a bytes _node as a binary scalar.
|
|
Node representBytes(const Node node) @safe
|
|
{
|
|
const ubyte[] value = node.as!(ubyte[]);
|
|
if(value is null){return Node("null", "tag:yaml.org,2002:null");}
|
|
|
|
auto newNode = Node(Base64.encode(value).idup, "tag:yaml.org,2002:binary");
|
|
newNode.scalarStyle = ScalarStyle.literal;
|
|
return newNode;
|
|
}
|
|
|
|
//Represent a bool _node as a bool scalar.
|
|
Node representBool(const Node node) @safe
|
|
{
|
|
return Node(node.as!bool ? "true" : "false", "tag:yaml.org,2002:bool");
|
|
}
|
|
|
|
//Represent a long _node as an integer scalar.
|
|
Node representLong(const Node node) @safe
|
|
{
|
|
return Node(node.as!long.to!string, "tag:yaml.org,2002:int");
|
|
}
|
|
|
|
//Represent a real _node as a floating point scalar.
|
|
Node representReal(const Node node) @safe
|
|
{
|
|
real f = node.as!real;
|
|
string value = isNaN(f) ? ".nan":
|
|
f == real.infinity ? ".inf":
|
|
f == -1.0 * real.infinity ? "-.inf":
|
|
{auto a = appender!string();
|
|
formattedWrite(a, "%12f", f);
|
|
return a.data.strip();}();
|
|
|
|
return Node(value, "tag:yaml.org,2002:float");
|
|
}
|
|
|
|
//Represent a SysTime _node as a timestamp.
|
|
Node representSysTime(const Node node) @safe
|
|
{
|
|
return Node(node.as!SysTime.toISOExtString(), "tag:yaml.org,2002:timestamp");
|
|
}
|
|
|
|
//Represent a sequence _node as sequence/set.
|
|
Node representNodes(const Node node, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
|
|
{
|
|
auto nodes = node.as!(Node[]);
|
|
if(node.tag_ == "tag:yaml.org,2002:set")
|
|
{
|
|
//YAML sets are mapping with null values.
|
|
Node.Pair[] pairs;
|
|
pairs.length = nodes.length;
|
|
|
|
foreach(idx, key; nodes)
|
|
{
|
|
pairs[idx] = Node.Pair(key, Node("null", "tag:yaml.org,2002:null"));
|
|
}
|
|
Node.Pair[] value;
|
|
value.length = pairs.length;
|
|
|
|
auto bestStyle = CollectionStyle.flow;
|
|
foreach(idx, pair; pairs)
|
|
{
|
|
value[idx] = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
|
|
if(value[idx].shouldUseBlockStyle)
|
|
{
|
|
bestStyle = CollectionStyle.block;
|
|
}
|
|
}
|
|
|
|
auto newNode = Node(value, node.tag_);
|
|
newNode.collectionStyle = bestStyle;
|
|
return newNode;
|
|
}
|
|
else
|
|
{
|
|
Node[] value;
|
|
value.length = nodes.length;
|
|
|
|
auto bestStyle = CollectionStyle.flow;
|
|
foreach(idx, item; nodes)
|
|
{
|
|
value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
|
|
const isScalar = value[idx].nodeID == NodeID.scalar;
|
|
const s = value[idx].scalarStyle;
|
|
if(!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain))
|
|
{
|
|
bestStyle = CollectionStyle.block;
|
|
}
|
|
}
|
|
|
|
auto newNode = Node(value, "tag:yaml.org,2002:seq");
|
|
newNode.collectionStyle = bestStyle;
|
|
return newNode;
|
|
}
|
|
}
|
|
|
|
bool shouldUseBlockStyle(const Node value) @safe
|
|
{
|
|
const isScalar = value.nodeID == NodeID.scalar;
|
|
const s = value.scalarStyle;
|
|
return (!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain));
|
|
}
|
|
bool shouldUseBlockStyle(const Node.Pair value) @safe
|
|
{
|
|
const keyScalar = value.key.nodeID == NodeID.scalar;
|
|
const valScalar = value.value.nodeID == NodeID.scalar;
|
|
const keyStyle = value.key.scalarStyle;
|
|
const valStyle = value.value.scalarStyle;
|
|
if(!keyScalar ||
|
|
(keyStyle != ScalarStyle.invalid && keyStyle != ScalarStyle.plain))
|
|
{
|
|
return true;
|
|
}
|
|
if(!valScalar ||
|
|
(valStyle != ScalarStyle.invalid && valStyle != ScalarStyle.plain))
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//Represent a mapping _node as map/ordered map/pairs.
|
|
Node representPairs(const Node node, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe
|
|
{
|
|
auto pairs = node.as!(Node.Pair[]);
|
|
|
|
bool hasDuplicates(const Node.Pair[] pairs) @safe
|
|
{
|
|
//TODO this should be replaced by something with deterministic memory allocation.
|
|
auto keys = redBlackTree!Node();
|
|
foreach(pair; pairs)
|
|
{
|
|
if(pair.key in keys){return true;}
|
|
keys.insert(pair.key);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Node[] mapToSequence(const Node.Pair[] pairs) @safe
|
|
{
|
|
Node[] nodes;
|
|
nodes.length = pairs.length;
|
|
foreach(idx, pair; pairs)
|
|
{
|
|
Node.Pair value;
|
|
|
|
auto bestStyle = value.shouldUseBlockStyle ? CollectionStyle.block : CollectionStyle.flow;
|
|
value = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
|
|
|
|
auto newNode = Node([value], "tag:yaml.org,2002:map");
|
|
newNode.collectionStyle = bestStyle;
|
|
nodes[idx] = newNode;
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
if(node.tag_ == "tag:yaml.org,2002:omap")
|
|
{
|
|
enforce(!hasDuplicates(pairs),
|
|
new RepresenterException("Duplicate entry in an ordered map"));
|
|
auto sequence = mapToSequence(pairs);
|
|
Node[] value;
|
|
value.length = sequence.length;
|
|
|
|
auto bestStyle = CollectionStyle.flow;
|
|
foreach(idx, item; sequence)
|
|
{
|
|
value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
|
|
if(value[idx].shouldUseBlockStyle)
|
|
{
|
|
bestStyle = CollectionStyle.block;
|
|
}
|
|
}
|
|
|
|
auto newNode = Node(value, node.tag_);
|
|
newNode.collectionStyle = bestStyle;
|
|
return newNode;
|
|
}
|
|
else if(node.tag_ == "tag:yaml.org,2002:pairs")
|
|
{
|
|
auto sequence = mapToSequence(pairs);
|
|
Node[] value;
|
|
value.length = sequence.length;
|
|
|
|
auto bestStyle = CollectionStyle.flow;
|
|
foreach(idx, item; sequence)
|
|
{
|
|
value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle);
|
|
if(value[idx].shouldUseBlockStyle)
|
|
{
|
|
bestStyle = CollectionStyle.block;
|
|
}
|
|
}
|
|
|
|
auto newNode = Node(value, node.tag_);
|
|
newNode.collectionStyle = bestStyle;
|
|
return newNode;
|
|
}
|
|
else
|
|
{
|
|
enforce(!hasDuplicates(pairs),
|
|
new RepresenterException("Duplicate entry in an unordered map"));
|
|
Node.Pair[] value;
|
|
value.length = pairs.length;
|
|
|
|
auto bestStyle = CollectionStyle.flow;
|
|
foreach(idx, pair; pairs)
|
|
{
|
|
value[idx] = Node.Pair(representData(pair.key, defaultScalarStyle, defaultCollectionStyle), representData(pair.value, defaultScalarStyle, defaultCollectionStyle));
|
|
if(value[idx].shouldUseBlockStyle)
|
|
{
|
|
bestStyle = CollectionStyle.block;
|
|
}
|
|
}
|
|
|
|
auto newNode = Node(value, "tag:yaml.org,2002:map");
|
|
newNode.collectionStyle = bestStyle;
|
|
return newNode;
|
|
}
|
|
}
|