dyaml/source/dyaml/representer.d
2018-08-26 21:49:14 -03:00

702 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)
/**
* 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;
///Exception thrown on Representer errors.
class RepresenterException : YAMLException
{
mixin ExceptionCtors;
}
/**
* Represents YAML nodes as scalar, sequence and mapping nodes ready for output.
*
* This class is used to add support for dumping of custom data types.
*
* It can also override default node formatting styles for output.
*/
final class Representer
{
private:
// Representer functions indexed by types.
Node function(ref Node, Representer) @safe[TypeInfo] representers_;
// Default style for scalar nodes.
ScalarStyle defaultScalarStyle_ = ScalarStyle.invalid;
// Default style for collection nodes.
CollectionStyle defaultCollectionStyle_ = CollectionStyle.invalid;
public:
@disable bool opEquals(ref Representer);
@disable int opCmp(ref Representer);
/**
* Construct a Representer.
*
* Params: useDefaultRepresenters = Use default representer functions
* for default YAML types? This can be
* disabled to use custom representer
* functions for default types.
*/
this(const Flag!"useDefaultRepresenters" useDefaultRepresenters = Yes.useDefaultRepresenters)
@safe pure
{
if(!useDefaultRepresenters){return;}
addRepresenter!YAMLNull(&representNull);
addRepresenter!string(&representString);
addRepresenter!(ubyte[])(&representBytes);
addRepresenter!bool(&representBool);
addRepresenter!long(&representLong);
addRepresenter!real(&representReal);
addRepresenter!(Node[])(&representNodes);
addRepresenter!(Node.Pair[])(&representPairs);
addRepresenter!SysTime(&representSysTime);
}
///Set default _style for scalars. If style is $(D ScalarStyle.invalid), the _style is chosen automatically.
@property void defaultScalarStyle(ScalarStyle style) pure @safe nothrow
{
defaultScalarStyle_ = style;
}
///Set default _style for collections. If style is $(D CollectionStyle.invalid), the _style is chosen automatically.
@property void defaultCollectionStyle(CollectionStyle style) pure @safe nothrow
{
defaultCollectionStyle_ = style;
}
/**
* Add a function to represent nodes with a specific data type.
*
* The representer function takes references to a $(D Node) storing the data
* type and to the $(D Representer). It returns the represented node and may
* throw a $(D RepresenterException). See the example for more information.
*
*
* Only one function may be specified for one data type. Default data
* types already have representer functions unless disabled in the
* $(D Representer) constructor.
*
*
* Structs and classes must implement the $(D opCmp()) operator for D:YAML
* support. The signature of the operator that must be implemented
* is $(D const int opCmp(ref const MyStruct s)) for structs where
* $(I MyStruct) is the struct type, and $(D int opCmp(Object o)) for
* classes. Note that the class $(D opCmp()) should not alter the compared
* values - it is not const for compatibility reasons.
*
* Params: representer = Representer function to add.
*/
void addRepresenter(T)(Node function(ref Node, Representer) @safe representer)
@safe pure
{
assert((typeid(T) in representers_) is null,
"Representer function for data type " ~ T.stringof ~
" already specified. Can't specify another one");
representers_[typeid(T)] = representer;
}
/// Representing a simple struct:
unittest {
import std.string;
import dyaml;
struct MyStruct
{
int x, y, z;
//Any D:YAML type must have a custom opCmp operator.
//This is used for ordering in mappings.
const int opCmp(ref const MyStruct s)
{
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
}
static Node representMyStruct(ref Node node, Representer representer) @safe
{
//The node is guaranteed to be MyStruct as we add representer for MyStruct.
auto value = node.as!MyStruct;
//Using custom scalar format, x:y:z.
auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
//Representing as a scalar, with custom tag to specify this data type.
return representer.representScalar("!mystruct.tag", scalar);
}
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyStruct(&representMyStruct);
dumper.representer = representer;
dumper.dump(Node(MyStruct(1,2,3)));
}
/// Representing a class:
unittest {
import std.string;
import dyaml;
class MyClass
{
int x, y, z;
this(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
//Any D:YAML type must have a custom opCmp operator.
//This is used for ordering in mappings.
override int opCmp(Object o)
{
MyClass s = cast(MyClass)o;
if(s is null){return -1;}
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
///Useful for Node.as!string .
override string toString()
{
return format("MyClass(%s, %s, %s)", x, y, z);
}
}
//Same as representMyStruct.
static Node representMyClass(ref Node node, Representer representer) @safe
{
//The node is guaranteed to be MyClass as we add representer for MyClass.
auto value = node.as!MyClass;
//Using custom scalar format, x:y:z.
auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
//Representing as a scalar, with custom tag to specify this data type.
return representer.representScalar("!myclass.tag", scalar);
}
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyClass(&representMyClass);
dumper.representer = representer;
dumper.dump(Node(new MyClass(1,2,3)));
}
//If profiling shows a bottleneck on tag construction in these 3 methods,
//we'll need to take Tag directly and have string based wrappers for
//user code.
/**
* Represent a _scalar with specified _tag.
*
* This is used by representer functions that produce scalars.
*
* Params: tag = Tag of the _scalar.
* scalar = Scalar value.
* style = Style of the _scalar. If invalid, default _style will be used.
* If the node was loaded before, previous _style will always be used.
*
* Returns: The represented node.
*/
Node representScalar(string tag, string scalar,
ScalarStyle style = ScalarStyle.invalid) @safe
{
if(style == ScalarStyle.invalid){style = defaultScalarStyle_;}
auto newNode = Node(scalar, tag);
newNode.scalarStyle = style;
return newNode;
}
///
@safe unittest
{
struct MyStruct
{
int x, y, z;
//Any D:YAML type must have a custom opCmp operator.
//This is used for ordering in mappings.
const int opCmp(ref const MyStruct s)
{
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
}
static Node representMyStruct(ref Node node, Representer representer)
{
auto value = node.as!MyStruct;
auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
return representer.representScalar("!mystruct.tag", scalar);
}
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyStruct(&representMyStruct);
dumper.representer = representer;
dumper.dump(Node(MyStruct(1,2,3)));
}
/**
* Represent a _sequence with specified _tag, representing children first.
*
* This is used by representer functions that produce sequences.
*
* Params: tag = Tag of the _sequence.
* sequence = Sequence of nodes.
* style = Style of the _sequence. If invalid, default _style will be used.
* If the node was loaded before, previous _style will always be used.
*
* Returns: The represented node.
*
* Throws: $(D RepresenterException) if a child could not be represented.
*/
Node representSequence(string tag, Node[] sequence,
CollectionStyle style = CollectionStyle.invalid) @safe
{
Node[] value;
value.length = sequence.length;
auto bestStyle = CollectionStyle.flow;
foreach(idx, ref item; sequence)
{
value[idx] = representData(item);
const isScalar = value[idx].isScalar;
const s = value[idx].scalarStyle;
if(!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain))
{
bestStyle = CollectionStyle.block;
}
}
if(style == CollectionStyle.invalid)
{
style = defaultCollectionStyle_ != CollectionStyle.invalid
? defaultCollectionStyle_
: bestStyle;
}
auto newNode = Node(value, tag);
newNode.collectionStyle = style;
return newNode;
}
///
@safe unittest
{
struct MyStruct
{
int x, y, z;
//Any D:YAML type must have a custom opCmp operator.
//This is used for ordering in mappings.
const int opCmp(ref const MyStruct s)
{
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
}
static Node representMyStruct(ref Node node, Representer representer)
{
auto value = node.as!MyStruct;
auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
//use flow style
return representer.representSequence("!mystruct.tag", nodes,
CollectionStyle.flow);
}
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyStruct(&representMyStruct);
dumper.representer = representer;
dumper.dump(Node(MyStruct(1,2,3)));
}
/**
* Represent a mapping with specified _tag, representing children first.
*
* This is used by representer functions that produce mappings.
*
* Params: tag = Tag of the mapping.
* pairs = Key-value _pairs of the mapping.
* style = Style of the mapping. If invalid, default _style will be used.
* If the node was loaded before, previous _style will always be used.
*
* Returns: The represented node.
*
* Throws: $(D RepresenterException) if a child could not be represented.
*/
Node representMapping(string tag, Node.Pair[] pairs,
CollectionStyle style = CollectionStyle.invalid) @safe
{
Node.Pair[] value;
value.length = pairs.length;
auto bestStyle = CollectionStyle.flow;
foreach(idx, ref pair; pairs)
{
value[idx] = Node.Pair(representData(pair.key), representData(pair.value));
const keyScalar = value[idx].key.isScalar;
const valScalar = value[idx].value.isScalar;
const keyStyle = value[idx].key.scalarStyle;
const valStyle = value[idx].value.scalarStyle;
if(!keyScalar ||
(keyStyle != ScalarStyle.invalid && keyStyle != ScalarStyle.plain))
{
bestStyle = CollectionStyle.block;
}
if(!valScalar ||
(valStyle != ScalarStyle.invalid && valStyle != ScalarStyle.plain))
{
bestStyle = CollectionStyle.block;
}
}
if(style == CollectionStyle.invalid)
{
style = defaultCollectionStyle_ != CollectionStyle.invalid
? defaultCollectionStyle_
: bestStyle;
}
auto newNode = Node(value, tag);
newNode.collectionStyle = style;
return newNode;
}
///
@safe unittest
{
struct MyStruct
{
int x, y, z;
//Any D:YAML type must have a custom opCmp operator.
//This is used for ordering in mappings.
const int opCmp(ref const MyStruct s)
{
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
}
static Node representMyStruct(ref Node node, Representer representer)
{
auto value = node.as!MyStruct;
auto pairs = [Node.Pair("x", value.x),
Node.Pair("y", value.y),
Node.Pair("z", value.z)];
return representer.representMapping("!mystruct.tag", pairs);
}
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyStruct(&representMyStruct);
dumper.representer = representer;
dumper.dump(Node(MyStruct(1,2,3)));
}
package:
//Represent a node based on its type, and return the represented result.
Node representData(ref Node data) @safe
{
//User types are wrapped in YAMLObject.
auto type = data.isUserType ? data.as!YAMLObject.type : data.type;
enforce((type in representers_) !is null,
new RepresenterException("No representer function for type "
~ type.toString() ~ " , cannot represent."));
Node result = representers_[type](data, this);
//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;
}
//Represent a node, serializing with specified Serializer.
void represent(Range, CharType)(ref Serializer!(Range, CharType) serializer, ref Node node) @safe
{
auto data = representData(node);
serializer.serialize(data);
}
}
///Represent a _null _node as a _null YAML value.
Node representNull(ref Node node, Representer representer) @safe
{
return representer.representScalar("tag:yaml.org,2002:null", "null");
}
///Represent a string _node as a string scalar.
Node representString(ref Node node, Representer representer) @safe
{
string value = node.as!string;
return value is null
? representNull(node, representer)
: representer.representScalar("tag:yaml.org,2002:str", value);
}
///Represent a bytes _node as a binary scalar.
Node representBytes(ref Node node, Representer representer) @safe
{
const ubyte[] value = node.as!(ubyte[]);
if(value is null){return representNull(node, representer);}
return representer.representScalar("tag:yaml.org,2002:binary",
Base64.encode(value).idup,
ScalarStyle.literal);
}
///Represent a bool _node as a bool scalar.
Node representBool(ref Node node, Representer representer) @safe
{
return representer.representScalar("tag:yaml.org,2002:bool",
node.as!bool ? "true" : "false");
}
///Represent a long _node as an integer scalar.
Node representLong(ref Node node, Representer representer) @safe
{
return representer.representScalar("tag:yaml.org,2002:int",
to!string(node.as!long));
}
///Represent a real _node as a floating point scalar.
Node representReal(ref Node node, Representer representer) @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 representer.representScalar("tag:yaml.org,2002:float", value);
}
///Represent a SysTime _node as a timestamp.
Node representSysTime(ref Node node, Representer representer) @safe
{
return representer.representScalar("tag:yaml.org,2002:timestamp",
node.as!SysTime.toISOExtString());
}
///Represent a sequence _node as sequence/set.
Node representNodes(ref Node node, Representer representer) @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;
Node dummy;
foreach(idx, ref key; nodes)
{
pairs[idx] = Node.Pair(key, representNull(dummy, representer));
}
return representer.representMapping(node.tag_, pairs);
}
else
{
return representer.representSequence("tag:yaml.org,2002:seq", nodes);
}
}
///Represent a mapping _node as map/ordered map/pairs.
Node representPairs(ref Node node, Representer representer) @safe
{
auto pairs = node.as!(Node.Pair[]);
bool hasDuplicates(Node.Pair[] pairs) @safe
{
//TODO this should be replaced by something with deterministic memory allocation.
auto keys = redBlackTree!Node();
foreach(ref pair; pairs)
{
if(pair.key in keys){return true;}
keys.insert(pair.key);
}
return false;
}
Node[] mapToSequence(Node.Pair[] pairs) @safe
{
Node[] nodes;
nodes.length = pairs.length;
foreach(idx, ref pair; pairs)
{
nodes[idx] = representer.representMapping("tag:yaml.org,2002:map", [pair]);
}
return nodes;
}
if(node.tag_ == "tag:yaml.org,2002:omap")
{
enforce(!hasDuplicates(pairs),
new RepresenterException("Duplicate entry in an ordered map"));
return representer.representSequence(node.tag_, mapToSequence(pairs));
}
else if(node.tag_ == "tag:yaml.org,2002:pairs")
{
return representer.representSequence(node.tag_, mapToSequence(pairs));
}
else
{
enforce(!hasDuplicates(pairs),
new RepresenterException("Duplicate entry in an unordered map"));
return representer.representMapping("tag:yaml.org,2002:map", pairs);
}
}
//Unittests
//These should really all be encapsulated in unittests.
private:
import dyaml.dumper;
struct MyStruct
{
int x, y, z;
int opCmp(ref const MyStruct s) const pure @safe nothrow
{
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
}
Node representMyStruct(ref Node node, Representer representer) @safe
{
//The node is guaranteed to be MyStruct as we add representer for MyStruct.
auto value = node.as!MyStruct;
//Using custom scalar format, x:y:z.
auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
//Representing as a scalar, with custom tag to specify this data type.
return representer.representScalar("!mystruct.tag", scalar);
}
Node representMyStructSeq(ref Node node, Representer representer) @safe
{
auto value = node.as!MyStruct;
auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
return representer.representSequence("!mystruct.tag", nodes);
}
Node representMyStructMap(ref Node node, Representer representer) @safe
{
auto value = node.as!MyStruct;
auto pairs = [Node.Pair("x", value.x),
Node.Pair("y", value.y),
Node.Pair("z", value.z)];
return representer.representMapping("!mystruct.tag", pairs);
}
class MyClass
{
int x, y, z;
this(int x, int y, int z) pure @safe nothrow
{
this.x = x;
this.y = y;
this.z = z;
}
override int opCmp(Object o) pure @safe nothrow
{
MyClass s = cast(MyClass)o;
if(s is null){return -1;}
if(x != s.x){return x - s.x;}
if(y != s.y){return y - s.y;}
if(z != s.z){return z - s.z;}
return 0;
}
///Useful for Node.as!string .
override string toString() @safe
{
return format("MyClass(%s, %s, %s)", x, y, z);
}
}
//Same as representMyStruct.
Node representMyClass(ref Node node, Representer representer) @safe
{
//The node is guaranteed to be MyClass as we add representer for MyClass.
auto value = node.as!MyClass;
//Using custom scalar format, x:y:z.
auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
//Representing as a scalar, with custom tag to specify this data type.
return representer.representScalar("!myclass.tag", scalar);
}
@safe unittest
{
foreach(r; [&representMyStruct,
&representMyStructSeq,
&representMyStructMap])
{
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyStruct(r);
dumper.representer = representer;
dumper.dump(Node(MyStruct(1,2,3)));
}
}
@safe unittest
{
auto dumper = dumper(new Appender!string);
auto representer = new Representer;
representer.addRepresenter!MyClass(&representMyClass);
dumper.representer = representer;
dumper.dump(Node(new MyClass(1,2,3)));
}