Move custom types to Node (#213)

Move custom types to Node
merged-on-behalf-of: BBasile <BBasile@users.noreply.github.com>
This commit is contained in:
Cameron Ross 2019-01-15 04:07:50 -03:30 committed by The Dlang Bot
parent beb160f1eb
commit 7f913246ea
20 changed files with 1152 additions and 1679 deletions

View file

@ -13,20 +13,8 @@ tags, as we will show later.
## Constructor
D:YAML uses the [Constructor](https://dyaml.dpldocs.info/dyaml.constructor.Constructor.html)
class to process each node to hold data type corresponding to its tag.
*Constructor* stores functions to process each supported tag. These are
supplied by the user using the *addConstructorXXX()* methods, where
*XXX* is *Scalar*, *Sequence* or *Mapping*. *Constructor* is then passed
to *Loader*, which parses YAML input.
Structs and classes must implement the *opCmp()* operator for YAML
support. This is used for duplicate detection in mappings, sorting and
equality comparisons of nodes. The signature of the operator that must
be implemented is `const int opCmp(ref const MyStruct s)` for structs
where *MyStruct* is the struct type, and `int opCmp(Object o)` for
classes. Note that the class *opCmp()* should not alter the compared
values - it is not const for compatibility reasons.
D:YAML supports conversion to user-defined types. Adding a constructor to read
the data from the node is all that is needed.
We will implement support for an RGB color type. It is implemented as
the following struct:
@ -37,77 +25,61 @@ struct Color
ubyte red;
ubyte green;
ubyte blue;
const int opCmp(ref const Color c)
{
if(red != c.red) {return red - c.red;}
if(green != c.green){return green - c.green;}
if(blue != c.blue) {return blue - c.blue;}
return 0;
}
}
```
First, we need a function to construct our data type. The function will
take a reference to *Node* to construct from. The node is guaranteed to
First, we need our type to have an appropriate constructor. The constructor
will take a const *Node* to construct from. The node is guaranteed to
contain either a *string*, an array of *Node* or of *Node.Pair*,
depending on whether we're constructing our value from a scalar,
sequence, or mapping, respectively. If this function throws any
exception, D:YAML handles it and adds its message to a *YAMLException*
that will be thrown when loading the file.
sequence, or mapping, respectively.
In this tutorial, we have functions to construct a color from a scalar,
In this tutorial, we have a constructor to construct a color from a scalar,
using CSS-like format, RRGGBB, or from a mapping, where we use the
following format: {r:RRR, g:GGG, b:BBB} . Code of these functions:
```D
Color constructColorScalar(ref Node node)
this(const Node node, string tag) @safe
{
string value = node.as!string;
if (tag == "!color-mapping")
{
//Will throw if a value is missing, is not an integer, or is out of range.
red = node["r"].as!ubyte;
green = node["g"].as!ubyte;
blue = node["b"].as!ubyte;
}
else
{
string value = node.as!string;
if(value.length != 6)
{
throw new Exception("Invalid color: " ~ value);
}
//We don't need to check for uppercase chars this way.
value = value.toLower();
if(value.length != 6)
{
throw new Exception("Invalid color: " ~ value);
}
//We don't need to check for uppercase chars this way.
value = value.toLower();
//Get value of a hex digit.
uint hex(char c)
{
import std.ascii;
if(!std.ascii.isHexDigit(c))
{
throw new Exception("Invalid color: " ~ value);
}
//Get value of a hex digit.
uint hex(char c)
{
import std.ascii;
if(!std.ascii.isHexDigit(c))
{
throw new Exception("Invalid color: " ~ value);
}
if(std.ascii.isDigit(c))
{
return c - '0';
}
return c - 'a' + 10;
}
if(std.ascii.isDigit(c))
{
return c - '0';
}
return c - 'a' + 10;
}
Color result;
result.red = cast(ubyte)(16 * hex(value[0]) + hex(value[1]));
result.green = cast(ubyte)(16 * hex(value[2]) + hex(value[3]));
result.blue = cast(ubyte)(16 * hex(value[4]) + hex(value[5]));
return result;
}
Color constructColorMapping(ref Node node)
{
ubyte r,g,b;
//Might throw if a value is missing is not an integer, or is out of range.
//If this happens, D:YAML will handle the exception and use its message
//in a YAMLException thrown when loading.
r = node["r"].as!ubyte;
g = node["g"].as!ubyte;
b = node["b"].as!ubyte;
return Color(cast(ubyte)r, cast(ubyte)g, cast(ubyte)b);
red = cast(ubyte)(16 * hex(value[0]) + hex(value[1]));
green = cast(ubyte)(16 * hex(value[2]) + hex(value[3]));
blue = cast(ubyte)(16 * hex(value[4]) + hex(value[5]));
}
}
```
@ -138,15 +110,7 @@ void main()
try
{
auto constructor = new Constructor;
//both functions handle the same tag, but one handles scalar, one mapping.
constructor.addConstructorScalar("!color", &constructColorScalar);
constructor.addConstructorMapping("!color-mapping", &constructColorMapping);
auto loader = Loader("input.yaml");
loader.constructor = constructor;
auto root = loader.load();
auto root = Loader.fromFile("input.yaml").load();
if(root["scalar-red"].as!Color == red &&
root["mapping-red"].as!Color == red &&
@ -166,10 +130,8 @@ void main()
}
```
First, we create a *Constructor* and pass functions to handle the
`!color` and `!color-mapping` tag. We construct a *Loader* and pass the
*Constructor* to it. We then load the YAML document, and finally, read
the colors to test if they were loaded as expected.
First we load the YAML document, and then have the resulting *Node*s converted
to Colors via their constructor.
You can find the source code for what we've done so far in the
`examples/constructor` directory in the D:YAML package.
@ -194,19 +156,14 @@ values must not be resolvable as any non-string YAML data type.
Add this to your code to add implicit resolution of `!color`.
```D
//code from the previous example...
auto resolver = new Resolver;
import std.regex;
resolver.addImplicitResolver("!color", std.regex.regex("[0-9a-fA-F]{6}"),
auto resolver = new Resolver;
resolver.addImplicitResolver("!color", regex("[0-9a-fA-F]{6}"),
"0123456789abcdefABCDEF");
auto loader = Loader("input.yaml");
auto loader = Loader.fromFile("input.yaml");
loader.constructor = constructor;
loader.resolver = resolver;
//code from the previous example...
```
Now, change contents of `input.yaml` to this:
@ -232,62 +189,45 @@ the D:YAML package.
## Representer
Now that you can load custom data types, it might be good to know how to
dump them. D:YAML uses the [Representer](https://dyaml.dpldocs.info/dyaml.representer.Representer.html)
class for this purpose.
dump them.
*Representer* processes YAML nodes into plain mapping, sequence or
scalar nodes ready for output. Just like with *Constructor*, this is
done by user specified functions. These functions take references to a
node to process and to the *Representer*, and return the processed node.
The *Node* struct simply attempts to cast all unrecognized types to *Node*.
This gives each type a consistent and simple way of being represented in a
document. All we need to do is specify a `Node opCast(T: Node)()` method for
any types we wish to support. It is also possible to specify specific styles
for each representation.
Representer functions can be added with the *addRepresenter()* method.
The *Representer* is then passed to *Dumper*, which dumps YAML
documents. Only one function per type can be specified. This is asserted
in *addRepresenter()* preconditions. Default YAML types already have
representer functions specified, but you can disable them by
constructing *Representer* with the *useDefaultRepresenters* parameter
set to false.
By default, tags are explicitly output for all non-default types. To
make dumped tags implicit, you can pass a *Resolver* that will resolve
them implicitly. Of course, you will need to use an identical *Resolver*
when loading the output.
Each type may only have one opCast!Node. Default YAML types are already
supported.
With the following code, we will add support for dumping the our Color
type.
```D
Node representColor(ref Node node, Representer representer)
Node opCast(T: Node)() const
{
//The node is guaranteed to be Color as we add representer for Color.
Color color = node.as!Color;
static immutable hex = "0123456789ABCDEF";
//Using the color format from the Constructor example.
string scalar;
foreach(channel; [color.red, color.green, color.blue])
foreach(channel; [red, green, blue])
{
scalar ~= hex[channel / 16];
scalar ~= hex[channel % 16];
}
//Representing as a scalar, with custom tag to specify this data type.
return representer.representScalar("!color", scalar);
return Node(scalar, "!color");
}
```
First we get the *Color* from the node. Then we convert it to a string
with the CSS-like format we've used before. Finally, we use the
*representScalar()* method of *Representer* to get a scalar node ready
for output. There are corresponding *representMapping()* and
*representSequence()* methods as well, with examples in the [Resolver
API documentation](https://dyaml.dpldocs.info/dyaml.resolver.html).
First we convert the colour data to a string with the CSS-like format we've
used before. Then, we create a scalar *Node* with our desired tag.
Since a type can only have one representer function, we don't dump
Since a type can only have one opCast!Node method, we don't dump
*Color* both in the scalar and mapping formats we've used before.
However, you can decide to dump the node with different formats/tags in
the representer function itself. E.g. you could dump the Color as a
the method itself. E.g. you could dump the Color as a
mapping based on some arbitrary condition, such as the color being
white.
@ -296,17 +236,7 @@ void main()
{
try
{
auto representer = new Representer;
representer.addRepresenter!Color(&representColor);
auto resolver = new Resolver;
import std.regex;
resolver.addImplicitResolver("!color", std.regex.regex("[0-9a-fA-F]{6}"),
"0123456789abcdefABCDEF");
auto dumper = Dumper("output.yaml");
dumper.representer = representer;
dumper.resolver = resolver;
auto dumper = dumper(File("output.yaml", "w").lockingTextWriter);
auto document = Node([Color(255, 0, 0),
Color(0, 255, 0),
@ -321,16 +251,8 @@ void main()
}
```
We construct a new *Representer*, and specify a representer function for
the *Color* (the template argument) type. We also construct a
*Resolver*, same as in the previous section, so the `!color` tag will be
implicit. Of course, identical *Resolver* would then have to be used
when loading the file. You don't need to do this if you want the tag to
be explicit.
We construct a *Dumper* to file `output.yaml` and pass the *Representer*
and *Resolver* to it. Then, we create a simple node containing a
sequence of colors and finally, we dump it.
We construct a *Dumper* to file `output.yaml`. Then, we create a simple node
containing a sequence of colors and finally, we dump it.
Source code for this section can be found in the `examples/representer`
directory of the D:YAML package.