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:
parent
beb160f1eb
commit
7f913246ea
20 changed files with 1152 additions and 1679 deletions
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue