From 7f913246ea9aa4a9d37a3bb26708b4d0aab16e91 Mon Sep 17 00:00:00 2001 From: Cameron Ross Date: Tue, 15 Jan 2019 04:07:50 -0330 Subject: [PATCH] Move custom types to Node (#213) Move custom types to Node merged-on-behalf-of: BBasile --- docs/tutorials/custom_types.md | 210 ++-- examples/constructor/main.d | 120 ++- examples/representer/main.d | 47 +- examples/resolver/main.d | 127 +-- examples/yaml_bench/yaml_bench.d | 4 +- source/dyaml/composer.d | 22 +- source/dyaml/constructor.d | 625 ++++-------- source/dyaml/dumper.d | 67 +- source/dyaml/loader.d | 13 +- source/dyaml/node.d | 459 ++++++--- source/dyaml/package.d | 2 - source/dyaml/representer.d | 950 ++++++++----------- source/dyaml/scanner.d | 89 +- source/dyaml/test/constructor.d | 69 +- source/dyaml/test/emitter.d | 3 - source/dyaml/test/representer.d | 8 - source/dyaml/test/resolver.d | 13 +- test/data/empty-python-module.loader-error | 1 - test/data/empty-python-name.loader-error | 1 - test/data/undefined-constructor.loader-error | 1 - 20 files changed, 1152 insertions(+), 1679 deletions(-) delete mode 100644 test/data/empty-python-module.loader-error delete mode 100644 test/data/empty-python-name.loader-error delete mode 100644 test/data/undefined-constructor.loader-error diff --git a/docs/tutorials/custom_types.md b/docs/tutorials/custom_types.md index 69ccace..7e4e10b 100644 --- a/docs/tutorials/custom_types.md +++ b/docs/tutorials/custom_types.md @@ -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. diff --git a/examples/constructor/main.d b/examples/constructor/main.d index 4e572a2..50826f1 100644 --- a/examples/constructor/main.d +++ b/examples/constructor/main.d @@ -8,80 +8,70 @@ struct Color ubyte green; ubyte blue; - const int opCmp(ref const Color c) + this(ubyte r, ubyte g, ubyte b) @safe { - 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; + red = r; + green = g; + blue = b; + } + + this(const Node node, string tag) @safe + { + 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(); + + //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; + } + + 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])); + } } } -Color constructColorScalar(ref Node node) @safe +void main(string[] args) { - 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(); - - //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; - } - - 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) @safe -{ - 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); -} - -void main() -{ - auto red = Color(255, 0, 0); + auto red = Color(255, 0, 0); auto orange = Color(255, 255, 0); + string path = "input.yaml"; + if (args.length > 1) + { + path = args[1]; + } + 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.fromFile("input.yaml"); - loader.constructor = constructor; - - auto root = loader.load(); + auto root = Loader.fromFile(path).load(); if(root["scalar-red"].as!Color == red && root["mapping-red"].as!Color == red && diff --git a/examples/representer/main.d b/examples/representer/main.d index 0ab4d5d..7c35de0 100644 --- a/examples/representer/main.d +++ b/examples/representer/main.d @@ -7,49 +7,28 @@ struct Color ubyte green; ubyte blue; - const int opCmp(ref const Color c) + Node opCast(T: Node)() const { - 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; + static immutable hex = "0123456789ABCDEF"; + + //Using the color format from the Constructor example. + string scalar; + 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 Node(scalar, "!color"); } } -Node representColor(ref Node node, Representer representer) @safe -{ - //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]) - { - 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); -} - 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(File("output.yaml", "w").lockingTextWriter); - dumper.representer = representer; - dumper.resolver = resolver; auto document = Node([Color(255, 0, 0), Color(0, 255, 0), diff --git a/examples/resolver/main.d b/examples/resolver/main.d index 7276552..9bde099 100644 --- a/examples/resolver/main.d +++ b/examples/resolver/main.d @@ -1,107 +1,38 @@ +import std.regex; import std.stdio; -import std.string; import dyaml; -struct Color +int main(string[] args) { - 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; - } -} - -Color constructColorScalar(ref Node node) @safe -{ - string value = node.as!string; - - if(value.length != 6) + string path = "input.yaml"; + if (args.length > 1) { - 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); - } - - if(std.ascii.isDigit(c)) - { - return c - '0'; - } - return c - 'a' + 10; + path = args[1]; } - 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])); + try + { + auto resolver = new Resolver; + resolver.addImplicitResolver("!color", regex("[0-9a-fA-F]{6}"), + "0123456789abcdefABCDEF"); - return result; -} - -Color constructColorMapping(ref Node node) @safe -{ - 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); -} - -void main() -{ - auto red = Color(255, 0, 0); - auto orange = Color(255, 255, 0); - - 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 resolver = new Resolver; - import std.regex; - resolver.addImplicitResolver("!color", std.regex.regex("[0-9a-fA-F]{6}"), - "0123456789abcdefABCDEF"); - - auto loader = Loader.fromFile("input.yaml"); - loader.constructor = constructor; - loader.resolver = resolver; - - auto root = loader.load(); - - if(root["scalar-red"].as!Color == red && - root["mapping-red"].as!Color == red && - root["scalar-orange"].as!Color == orange && - root["mapping-orange"].as!Color == orange) - { - writeln("SUCCESS"); - return; - } - } - catch(YAMLException e) - { - writeln(e.msg); - } - - writeln("FAILURE"); + auto loader = Loader.fromFile("input.yaml"); + loader.resolver = resolver; + + auto root = loader.load(); + + if(root["scalar-red"].tag == "!color" && + root["scalar-orange"].tag == "!color") + { + writeln("SUCCESS"); + return 0; + } + } + catch(YAMLException e) + { + writeln(e.msg); + } + + writeln("FAILURE"); + return 1; } diff --git a/examples/yaml_bench/yaml_bench.d b/examples/yaml_bench/yaml_bench.d index 0540179..1ef7a1c 100644 --- a/examples/yaml_bench/yaml_bench.d +++ b/examples/yaml_bench/yaml_bench.d @@ -91,7 +91,6 @@ void main(string[] args) //@safe // Instead of constructing a resolver/constructor with each Loader, // construct them once to remove noise when profiling. auto resolver = new Resolver(); - auto constructor = new Constructor(); auto constructTime = stopWatch.peek(); @@ -113,7 +112,6 @@ void main(string[] args) //@safe } loader.resolver = resolver; - loader.constructor = constructor; nodes = loader.array; } void runDumpBenchmark() @safe @@ -150,7 +148,7 @@ void main(string[] args) //@safe { writeln("Time to load file: ", loadTime); } - writeln("Time to set up resolver & constructor: ", constructTime); + writeln("Time to set up resolver: ", constructTime); } writeln("Runs: ", runs); foreach(time, func, enabled; lockstep(totalTime[], only("Loader", "Dumper", "Get"), only(true, dump, get))) diff --git a/source/dyaml/composer.d b/source/dyaml/composer.d index 0a75262..b85fd59 100644 --- a/source/dyaml/composer.d +++ b/source/dyaml/composer.d @@ -46,8 +46,6 @@ final class Composer Parser parser_; ///Resolver resolving tags (data types). Resolver resolver_; - ///Constructor constructing YAML values. - Constructor constructor_; ///Nodes associated with anchors. Used by YAML aliases. Node[string] anchors_; @@ -70,13 +68,11 @@ final class Composer * * Params: parser = Parser to provide YAML events. * resolver = Resolver to resolve tags (data types). - * constructor = Constructor to construct nodes. */ - this(Parser parser, Resolver resolver, Constructor constructor) @safe + this(Parser parser, Resolver resolver) @safe { parser_ = parser; resolver_ = resolver; - constructor_ = constructor; } ///Destroy the composer. @@ -84,7 +80,6 @@ final class Composer { parser_ = null; resolver_ = null; - constructor_ = null; anchors_.destroy(); anchors_ = null; } @@ -222,8 +217,9 @@ final class Composer const tag = resolver_.resolve(NodeID.scalar, event.tag, event.value, event.implicit); - Node node = constructor_.node(event.startMark, event.endMark, tag, - event.value, event.scalarStyle); + Node node = constructNode(event.startMark, event.endMark, tag, + event.value); + node.scalarStyle = event.scalarStyle; return node; } @@ -248,8 +244,9 @@ final class Composer nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1)); } - Node node = constructor_.node(startEvent.startMark, parser_.front.endMark, - tag, nodeAppender.data.dup, startEvent.collectionStyle); + Node node = constructNode(startEvent.startMark, parser_.front.endMark, + tag, nodeAppender.data.dup); + node.collectionStyle = startEvent.collectionStyle; parser_.popFront(); nodeAppender.clear(); @@ -370,8 +367,9 @@ final class Composer enforce(numUnique == pairAppender.data.length, new ComposerException("Duplicate key found in mapping", parser_.front.startMark)); - Node node = constructor_.node(startEvent.startMark, parser_.front.endMark, - tag, pairAppender.data.dup, startEvent.collectionStyle); + Node node = constructNode(startEvent.startMark, parser_.front.endMark, + tag, pairAppender.data.dup); + node.collectionStyle = startEvent.collectionStyle; parser_.popFront(); pairAppender.clear(); diff --git a/source/dyaml/constructor.d b/source/dyaml/constructor.d index 4b76856..13dbac0 100644 --- a/source/dyaml/constructor.d +++ b/source/dyaml/constructor.d @@ -28,9 +28,10 @@ import dyaml.node; import dyaml.exception; import dyaml.style; +package: // Exception thrown at constructor errors. -package class ConstructorException : YAMLException +class ConstructorException : YAMLException { /// Construct a ConstructorException. /// @@ -59,408 +60,148 @@ package class ConstructorException : YAMLException * * If a tag is detected with no known constructor function, it is considered an error. */ -final class Constructor +/* + * Construct a node. + * + * Params: start = Start position of the node. + * end = End position of the node. + * tag = Tag (data type) of the node. + * value = Value to construct node from (string, nodes or pairs). + * style = Style of the node (scalar or collection style). + * + * Returns: Constructed node. + */ +Node constructNode(T)(const Mark start, const Mark end, const string tag, + T value) @safe + if((is(T : string) || is(T == Node[]) || is(T == Node.Pair[]))) { - private: - // Constructor functions from scalars. - Node delegate(ref Node) @safe[string] fromScalar_; - // Constructor functions from sequences. - Node delegate(ref Node) @safe[string] fromSequence_; - // Constructor functions from mappings. - Node delegate(ref Node) @safe[string] fromMapping_; - - public: - /// Construct a Constructor. - /// - /// If you don't want to support default YAML tags/data types, you can use - /// defaultConstructors to disable constructor functions for these. - /// - /// Params: defaultConstructors = Use constructors for default YAML tags? - this(const Flag!"useDefaultConstructors" defaultConstructors = Yes.useDefaultConstructors) - @safe nothrow + Node newNode; + try + { + switch(tag) { - if(!defaultConstructors){return;} - - addConstructorScalar("tag:yaml.org,2002:null", &constructNull); - addConstructorScalar("tag:yaml.org,2002:bool", &constructBool); - addConstructorScalar("tag:yaml.org,2002:int", &constructLong); - addConstructorScalar("tag:yaml.org,2002:float", &constructReal); - addConstructorScalar("tag:yaml.org,2002:binary", &constructBinary); - addConstructorScalar("tag:yaml.org,2002:timestamp", &constructTimestamp); - addConstructorScalar("tag:yaml.org,2002:str", &constructString); - - ///In a mapping, the default value is kept as an entry with the '=' key. - addConstructorScalar("tag:yaml.org,2002:value", &constructString); - - addConstructorSequence("tag:yaml.org,2002:omap", &constructOrderedMap); - addConstructorSequence("tag:yaml.org,2002:pairs", &constructPairs); - addConstructorMapping("tag:yaml.org,2002:set", &constructSet); - addConstructorSequence("tag:yaml.org,2002:seq", &constructSequence); - addConstructorMapping("tag:yaml.org,2002:map", &constructMap); - addConstructorScalar("tag:yaml.org,2002:merge", &constructMerge); - } - - /** Add a constructor function from scalar. - * - * The function must take a reference to $(D Node) to construct from. - * The node contains a string for scalars, $(D Node[]) for sequences and - * $(D Node.Pair[]) for mappings. - * - * Any exception thrown by this function will be caught by D:YAML and - * its message will be added to a $(D YAMLException) that will also tell - * the user which type failed to construct, and position in the file. - * - * - * The value returned by this function will be stored in the resulting node. - * - * Only one constructor function can be set for one tag. - * - * - * 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: tag = Tag for the function to handle. - * ctor = Constructor function. - */ - void addConstructorScalar(T)(const string tag, T function(ref Node) @safe ctor) - { - const t = tag; - const deleg = addConstructor!T(t, ctor); - (*delegates!string)[t] = deleg; - } - /// - @safe unittest - { - static struct MyStruct - { - int x, y, z; - - //Any D:YAML type must have a custom opCmp operator. - //This is used for ordering in mappings. - int opCmp(ref const MyStruct s) const + case "tag:yaml.org,2002:null": + newNode = Node(YAMLNull(), tag); + break; + case "tag:yaml.org,2002:bool": + static if(is(T == string)) { - 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; + newNode = Node(constructBool(value), tag); + break; } - } - - static MyStruct constructMyStructScalar(ref Node node) @safe - { - //Guaranteed to be string as we construct from scalar. - //!mystruct x:y:z - auto parts = node.as!string().split(":"); - // If this throws, the D:YAML will handle it and throw a YAMLException. - return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2])); - } - - import dyaml.loader : Loader; - auto loader = Loader.fromString("!mystruct 12:34:56"); - auto constructor = new Constructor; - constructor.addConstructorScalar("!mystruct", &constructMyStructScalar); - loader.constructor = constructor; - Node node = loader.load(); - assert(node.get!MyStruct == MyStruct(12, 34, 56)); - } - - /** Add a constructor function from sequence. - * - * See_Also: addConstructorScalar - */ - void addConstructorSequence(T)(const string tag, T function(ref Node) @safe ctor) - { - const t = tag; - const deleg = addConstructor!T(t, ctor); - (*delegates!(Node[]))[t] = deleg; - } - /// - @safe unittest - { - static struct MyStruct - { - int x, y, z; - - //Any D:YAML type must have a custom opCmp operator. - //This is used for ordering in mappings. - int opCmp(ref const MyStruct s) const + else throw new Exception("Only scalars can be bools"); + case "tag:yaml.org,2002:int": + static if(is(T == string)) { - 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; + newNode = Node(constructLong(value), tag); + break; } - } - - static MyStruct constructMyStructSequence(ref Node node) @safe - { - //node is guaranteed to be sequence. - //!mystruct [x, y, z] - return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int); - } - - import dyaml.loader : Loader; - auto loader = Loader.fromString("!mystruct [1,2,3]"); - auto constructor = new Constructor; - constructor.addConstructorSequence("!mystruct", &constructMyStructSequence); - loader.constructor = constructor; - Node node = loader.load(); - assert(node.get!MyStruct == MyStruct(1, 2, 3)); - } - - /** Add a constructor function from a mapping. - * - * See_Also: addConstructorScalar - */ - void addConstructorMapping(T)(const string tag, T function(ref Node) @safe ctor) - { - const t = tag; - const deleg = addConstructor!T(t, ctor); - (*delegates!(Node.Pair[]))[t] = deleg; - } - /// - @safe unittest { - static struct MyStruct - { - int x, y, z; - - //Any D:YAML type must have a custom opCmp operator. - //This is used for ordering in mappings. - int opCmp(ref const MyStruct s) const + else throw new Exception("Only scalars can be ints"); + case "tag:yaml.org,2002:float": + static if(is(T == string)) { - 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; + newNode = Node(constructReal(value), tag); + break; } - } - - static MyStruct constructMyStructMapping(ref Node node) @safe - { - //node is guaranteed to be mapping. - //!mystruct {"x": x, "y": y, "z": z} - return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int); - } - - import dyaml.loader : Loader; - auto loader = Loader.fromString(`!mystruct {"x": 11, "y": 22, "z": 33}`); - auto constructor = new Constructor; - constructor.addConstructorMapping("!mystruct", &constructMyStructMapping); - loader.constructor = constructor; - Node node = loader.load(); - assert(node.get!MyStruct == MyStruct(11, 22, 33)); - } - - package: - /* - * Construct a node. - * - * Params: start = Start position of the node. - * end = End position of the node. - * tag = Tag (data type) of the node. - * value = Value to construct node from (string, nodes or pairs). - * style = Style of the node (scalar or collection style). - * - * Returns: Constructed node. - */ - Node node(T, U)(const Mark start, const Mark end, const string tag, - T value, U style) @safe - if((is(T : string) || is(T == Node[]) || is(T == Node.Pair[])) && - (is(U : CollectionStyle) || is(U : ScalarStyle))) - { - enum type = is(T : string) ? "scalar" : - is(T == Node[]) ? "sequence" : - is(T == Node.Pair[]) ? "mapping" : - "ERROR"; - enforce((tag in *delegates!T) !is null, - new ConstructorException("No constructor function from " ~ type ~ - " for tag " ~ tag, start, end)); - - Node node = Node(value); - try - { - static if(is(U : ScalarStyle)) + else throw new Exception("Only scalars can be floats"); + case "tag:yaml.org,2002:binary": + static if(is(T == string)) { - auto newNode = (*delegates!T)[tag](node); - newNode.startMark_ = start; - newNode.scalarStyle = style; - return newNode; + newNode = Node(constructBinary(value), tag); + break; } - else static if(is(U : CollectionStyle)) + else throw new Exception("Only scalars can be binary data"); + case "tag:yaml.org,2002:timestamp": + static if(is(T == string)) { - auto newNode = (*delegates!T)[tag](node); - newNode.startMark_ = start; - newNode.collectionStyle = style; - return newNode; + newNode = Node(constructTimestamp(value), tag); + break; } - else static assert(false); - } - catch(Exception e) - { - throw new ConstructorException("Error constructing " ~ typeid(T).toString() - ~ ":\n" ~ e.msg, start, end); - } - } - - private: - /* - * Add a constructor function. - * - * Params: tag = Tag for the function to handle. - * ctor = Constructor function. - */ - auto addConstructor(T)(const string tag, T function(ref Node) @safe ctor) - { - assert((tag in fromScalar_) is null && - (tag in fromSequence_) is null && - (tag in fromMapping_) is null, - "Constructor function for tag " ~ tag ~ " is already " ~ - "specified. Can't specify another one."); - - - return (ref Node n) @safe - { - return Node(ctor(n), tag); - }; - } - - //Get the array of constructor functions for scalar, sequence or mapping. - @property auto delegates(T)() - { - static if(is(T : string)) {return &fromScalar_;} - else static if(is(T : Node[])) {return &fromSequence_;} - else static if(is(T : Node.Pair[])){return &fromMapping_;} - else static assert(false); - } -} - -///Construct a struct from a scalar -@safe unittest -{ - static 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; + else throw new Exception("Only scalars can be timestamps"); + case "tag:yaml.org,2002:str": + static if(is(T == string)) + { + newNode = Node(constructString(value), tag); + break; + } + else throw new Exception("Only scalars can be strings"); + case "tag:yaml.org,2002:value": + static if(is(T == string)) + { + newNode = Node(constructString(value), tag); + break; + } + else throw new Exception("Only scalars can be values"); + case "tag:yaml.org,2002:omap": + static if(is(T == Node[])) + { + newNode = Node(constructOrderedMap(value), tag); + break; + } + else throw new Exception("Only sequences can be ordered maps"); + case "tag:yaml.org,2002:pairs": + static if(is(T == Node[])) + { + newNode = Node(constructPairs(value), tag); + break; + } + else throw new Exception("Only sequences can be pairs"); + case "tag:yaml.org,2002:set": + static if(is(T == Node.Pair[])) + { + newNode = Node(constructSet(value), tag); + break; + } + else throw new Exception("Only mappings can be sets"); + case "tag:yaml.org,2002:seq": + static if(is(T == Node[])) + { + newNode = Node(constructSequence(value), tag); + break; + } + else throw new Exception("Only sequences can be sequences"); + case "tag:yaml.org,2002:map": + static if(is(T == Node.Pair[])) + { + newNode = Node(constructMap(value), tag); + break; + } + else throw new Exception("Only mappings can be maps"); + case "tag:yaml.org,2002:merge": + newNode = Node(YAMLMerge(), tag); + break; + default: + newNode = Node(value, tag); + break; } } - - static MyStruct constructMyStructScalar(ref Node node) @safe + catch(Exception e) { - // Guaranteed to be string as we construct from scalar. - auto parts = node.as!string().split(":"); - return MyStruct(to!int(parts[0]), to!int(parts[1]), to!int(parts[2])); + throw new ConstructorException("Error constructing " ~ typeid(T).toString() + ~ ":\n" ~ e.msg, start, end); } - import dyaml.loader : Loader; - string data = "!mystruct 1:2:3"; - auto loader = Loader.fromString(data); - auto constructor = new Constructor; - constructor.addConstructorScalar("!mystruct", &constructMyStructScalar); - loader.constructor = constructor; - Node node = loader.load(); + newNode.startMark_ = start; - assert(node.as!MyStruct == MyStruct(1, 2, 3)); -} -///Construct a struct from a sequence -@safe unittest -{ - static 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; - } - } - static MyStruct constructMyStructSequence(ref Node node) @safe - { - // node is guaranteed to be sequence. - return MyStruct(node[0].as!int, node[1].as!int, node[2].as!int); - } - - import dyaml.loader : Loader; - string data = "!mystruct [1, 2, 3]"; - auto loader = Loader.fromString(data); - auto constructor = new Constructor; - constructor.addConstructorSequence("!mystruct", &constructMyStructSequence); - loader.constructor = constructor; - Node node = loader.load(); - - assert(node.as!MyStruct == MyStruct(1, 2, 3)); -} -///Construct a struct from a mapping -@safe unittest -{ - static 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; - } - } - static MyStruct constructMyStructMapping(ref Node node) @safe - { - // node is guaranteed to be mapping. - return MyStruct(node["x"].as!int, node["y"].as!int, node["z"].as!int); - } - - import dyaml.loader : Loader; - string data = "!mystruct {x: 1, y: 2, z: 3}"; - auto loader = Loader.fromString(data); - auto constructor = new Constructor; - constructor.addConstructorMapping("!mystruct", &constructMyStructMapping); - loader.constructor = constructor; - Node node = loader.load(); - - assert(node.as!MyStruct == MyStruct(1, 2, 3)); + return newNode; } -/// Construct a _null _node. -YAMLNull constructNull(ref Node node) @safe pure nothrow @nogc -{ - return YAMLNull(); -} - -/// Construct a merge _node - a _node that merges another _node into a mapping. -YAMLMerge constructMerge(ref Node node) @safe pure nothrow @nogc -{ - return YAMLMerge(); -} - -/// Construct a boolean _node. -bool constructBool(ref Node node) @safe +private: +// Construct a boolean _node. +bool constructBool(const string str) @safe { static yes = ["yes", "true", "on"]; static no = ["no", "false", "off"]; - string value = node.as!string().toLower(); + string value = str.toLower(); if(yes.canFind(value)){return true;} if(no.canFind(value)) {return false;} throw new Exception("Unable to parse boolean value: " ~ value); } -/// Construct an integer (long) _node. -long constructLong(ref Node node) @safe +// Construct an integer (long) _node. +long constructLong(const string str) @safe { - string value = node.as!string().replace("_", ""); + string value = str.replace("_", ""); const char c = value[0]; const long sign = c != '-' ? 1 : -1; if(c == '-' || c == '+') @@ -505,12 +246,6 @@ long constructLong(ref Node node) @safe } @safe unittest { - long getLong(string str) @safe - { - auto node = Node(str); - return constructLong(node); - } - string canonical = "685230"; string decimal = "+685_230"; string octal = "02472256"; @@ -518,18 +253,18 @@ long constructLong(ref Node node) @safe string binary = "0b1010_0111_0100_1010_1110"; string sexagesimal = "190:20:30"; - assert(685230 == getLong(canonical)); - assert(685230 == getLong(decimal)); - assert(685230 == getLong(octal)); - assert(685230 == getLong(hexadecimal)); - assert(685230 == getLong(binary)); - assert(685230 == getLong(sexagesimal)); + assert(685230 == constructLong(canonical)); + assert(685230 == constructLong(decimal)); + assert(685230 == constructLong(octal)); + assert(685230 == constructLong(hexadecimal)); + assert(685230 == constructLong(binary)); + assert(685230 == constructLong(sexagesimal)); } -/// Construct a floating point (real) _node. -real constructReal(ref Node node) @safe +// Construct a floating point (real) _node. +real constructReal(const string str) @safe { - string value = node.as!string().replace("_", "").toLower(); + string value = str.replace("_", "").toLower(); const char c = value[0]; const real sign = c != '-' ? 1.0 : -1.0; if(c == '-' || c == '+') @@ -576,12 +311,6 @@ real constructReal(ref Node node) @safe return a >= (b - epsilon) && a <= (b + epsilon); } - real getReal(string str) @safe - { - auto node = Node(str); - return constructReal(node); - } - string canonical = "6.8523015e+5"; string exponential = "685.230_15e+03"; string fixed = "685_230.15"; @@ -589,21 +318,20 @@ real constructReal(ref Node node) @safe string negativeInf = "-.inf"; string NaN = ".NaN"; - assert(eq(685230.15, getReal(canonical))); - assert(eq(685230.15, getReal(exponential))); - assert(eq(685230.15, getReal(fixed))); - assert(eq(685230.15, getReal(sexagesimal))); - assert(eq(-real.infinity, getReal(negativeInf))); - assert(to!string(getReal(NaN)) == "nan"); + assert(eq(685230.15, constructReal(canonical))); + assert(eq(685230.15, constructReal(exponential))); + assert(eq(685230.15, constructReal(fixed))); + assert(eq(685230.15, constructReal(sexagesimal))); + assert(eq(-real.infinity, constructReal(negativeInf))); + assert(to!string(constructReal(NaN)) == "nan"); } -/// Construct a binary (base64) _node. -ubyte[] constructBinary(ref Node node) @safe +// Construct a binary (base64) _node. +ubyte[] constructBinary(const string value) @safe { import std.ascii : newline; import std.array : array; - string value = node.as!string; // For an unknown reason, this must be nested to work (compiler bug?). try { @@ -621,16 +349,15 @@ ubyte[] constructBinary(ref Node node) @safe char[] buffer; buffer.length = 256; string input = Base64.encode(test, buffer).idup; - auto node = Node(input); - const value = constructBinary(node); + const value = constructBinary(input); assert(value == test); assert(value == [84, 104, 101, 32, 65, 110, 115, 119, 101, 114, 58, 32, 52, 50]); } -/// Construct a timestamp (SysTime) _node. -SysTime constructTimestamp(ref Node node) @safe +// Construct a timestamp (SysTime) _node. +SysTime constructTimestamp(const string str) @safe { - string value = node.as!string; + string value = str; auto YMDRegexp = regex("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)"); auto HMSRegexp = regex("^[Tt \t]+([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(\\.[0-9]*)?"); @@ -704,8 +431,7 @@ SysTime constructTimestamp(ref Node node) @safe { string timestamp(string value) { - auto node = Node(value); - return constructTimestamp(node).toISOString(); + return constructTimestamp(value).toISOString(); } string canonical = "2001-12-15T02:59:43.1Z"; @@ -727,18 +453,18 @@ SysTime constructTimestamp(ref Node node) @safe assert(timestamp(ymd) == "20021214T000000Z"); } -/// Construct a string _node. -string constructString(ref Node node) @safe +// Construct a string _node. +string constructString(const string str) @safe { - return node.as!string; + return str; } -/// Convert a sequence of single-element mappings into a sequence of pairs. -Node.Pair[] getPairs(string type, Node[] nodes) @safe +// Convert a sequence of single-element mappings into a sequence of pairs. +Node.Pair[] getPairs(string type, const Node[] nodes) @safe { Node.Pair[] pairs; pairs.reserve(nodes.length); - foreach(ref node; nodes) + foreach(node; nodes) { enforce(node.isMapping && node.length == 1, new Exception("While constructing " ~ type ~ @@ -750,10 +476,10 @@ Node.Pair[] getPairs(string type, Node[] nodes) @safe return pairs; } -/// Construct an ordered map (ordered sequence of key:value pairs without duplicates) _node. -Node.Pair[] constructOrderedMap(ref Node node) @safe +// Construct an ordered map (ordered sequence of key:value pairs without duplicates) _node. +Node.Pair[] constructOrderedMap(const Node[] nodes) @safe { - auto pairs = getPairs("ordered map", node.as!(Node[])); + auto pairs = getPairs("ordered map", nodes); //Detect duplicates. //TODO this should be replaced by something with deterministic memory allocation. @@ -791,38 +517,30 @@ Node.Pair[] constructOrderedMap(ref Node node) @safe return pairs; } - bool hasDuplicates(Node[] nodes) @safe - { - auto node = Node(nodes); - return null !is collectException(constructOrderedMap(node)); - } - - assert(hasDuplicates(alternateTypes(8) ~ alternateTypes(2))); - assert(!hasDuplicates(alternateTypes(8))); - assert(hasDuplicates(sameType(64) ~ sameType(16))); - assert(hasDuplicates(alternateTypes(64) ~ alternateTypes(16))); - assert(!hasDuplicates(sameType(64))); - assert(!hasDuplicates(alternateTypes(64))); + assertThrown(constructOrderedMap(alternateTypes(8) ~ alternateTypes(2))); + assertNotThrown(constructOrderedMap(alternateTypes(8))); + assertThrown(constructOrderedMap(sameType(64) ~ sameType(16))); + assertThrown(constructOrderedMap(alternateTypes(64) ~ alternateTypes(16))); + assertNotThrown(constructOrderedMap(sameType(64))); + assertNotThrown(constructOrderedMap(alternateTypes(64))); } -/// Construct a pairs (ordered sequence of key: value pairs allowing duplicates) _node. -Node.Pair[] constructPairs(ref Node node) @safe +// Construct a pairs (ordered sequence of key: value pairs allowing duplicates) _node. +Node.Pair[] constructPairs(const Node[] nodes) @safe { - return getPairs("pairs", node.as!(Node[])); + return getPairs("pairs", nodes); } -/// Construct a set _node. -Node[] constructSet(ref Node node) @safe +// Construct a set _node. +Node[] constructSet(const Node.Pair[] pairs) @safe { - auto pairs = node.as!(Node.Pair[]); - // In future, the map here should be replaced with something with deterministic // memory allocation if possible. // Detect duplicates. ubyte[Node] map; Node[] nodes; nodes.reserve(pairs.length); - foreach(ref pair; pairs) + foreach(pair; pairs) { enforce((pair.key in map) is null, new Exception("Duplicate entry in a set")); map[pair.key] = 0; @@ -862,27 +580,26 @@ Node[] constructSet(ref Node node) @safe return true; } - auto nodeDuplicatesShort = Node(DuplicatesShort.dup); - auto nodeNoDuplicatesShort = Node(noDuplicatesShort.dup); - auto nodeDuplicatesLong = Node(DuplicatesLong.dup); - auto nodeNoDuplicatesLong = Node(noDuplicatesLong.dup); + auto nodeDuplicatesShort = DuplicatesShort.dup; + auto nodeNoDuplicatesShort = noDuplicatesShort.dup; + auto nodeDuplicatesLong = DuplicatesLong.dup; + auto nodeNoDuplicatesLong = noDuplicatesLong.dup; - assert(null !is collectException(constructSet(nodeDuplicatesShort))); - assert(null is collectException(constructSet(nodeNoDuplicatesShort))); - assert(null !is collectException(constructSet(nodeDuplicatesLong))); - assert(null is collectException(constructSet(nodeNoDuplicatesLong))); + assertThrown(constructSet(nodeDuplicatesShort)); + assertNotThrown(constructSet(nodeNoDuplicatesShort)); + assertThrown(constructSet(nodeDuplicatesLong)); + assertNotThrown(constructSet(nodeNoDuplicatesLong)); } -/// Construct a sequence (array) _node. -Node[] constructSequence(ref Node node) @safe +// Construct a sequence (array) _node. +Node[] constructSequence(Node[] nodes) @safe { - return node.as!(Node[]); + return nodes; } -/// Construct an unordered map (unordered set of key:value _pairs without duplicates) _node. -Node.Pair[] constructMap(ref Node node) @safe +// Construct an unordered map (unordered set of key:value _pairs without duplicates) _node. +Node.Pair[] constructMap(Node.Pair[] pairs) @safe { - auto pairs = node.as!(Node.Pair[]); //Detect duplicates. //TODO this should be replaced by something with deterministic memory allocation. auto keys = redBlackTree!Node(); diff --git a/source/dyaml/dumper.d b/source/dyaml/dumper.d index bf57deb..ef78fae 100644 --- a/source/dyaml/dumper.d +++ b/source/dyaml/dumper.d @@ -23,6 +23,7 @@ import dyaml.node; import dyaml.representer; import dyaml.resolver; import dyaml.serializer; +import dyaml.style; import dyaml.tagdirective; @@ -45,8 +46,6 @@ struct Dumper(Range) private: //Resolver to resolve tags. Resolver resolver_; - //Representer to represent data types. - Representer representer_; //Stream to write to. Range stream_; @@ -71,7 +70,24 @@ struct Dumper(Range) //Name of the output file or stream, used in error messages. string name_ = ""; + // Default style for scalar nodes. + ScalarStyle defaultScalarStyle_ = ScalarStyle.invalid; + // Default style for collection nodes. + CollectionStyle defaultCollectionStyle_ = CollectionStyle.invalid; + + public: + ///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; + } @disable this(); @disable bool opEquals(ref Dumper!Range); @disable int opCmp(ref Dumper!Range); @@ -84,7 +100,6 @@ struct Dumper(Range) this(Range stream) @safe { resolver_ = new Resolver(); - representer_ = new Representer(); stream_ = stream; } @@ -100,12 +115,6 @@ struct Dumper(Range) resolver_ = resolver; } - ///Specify custom Representer to use. - @property void representer(Representer representer) @safe - { - representer_ = representer; - } - ///Write scalars in _canonical form? @property void canonical(bool canonical) pure @safe nothrow { @@ -219,7 +228,8 @@ struct Dumper(Range) explicitEnd_, YAMLVersion_, tags_); foreach(ref document; documents) { - representer_.represent(serializer, document); + auto data = representData(document, defaultScalarStyle_, defaultCollectionStyle_); + serializer.serialize(data); } } catch(YAMLException e) @@ -277,18 +287,47 @@ struct Dumper(Range) auto node = Node([1, 2, 3, 4, 5]); dumper(stream).dump(node); } -///Use a custom representer/resolver to support custom data types and/or implicit tags +///Use a custom resolver to support custom data types and/or implicit tags @safe unittest { auto node = Node([1, 2, 3, 4, 5]); - auto representer = new Representer(); auto resolver = new Resolver(); - //Add representer functions / resolver expressions here... auto dumper = dumper(new Appender!string()); - dumper.representer = representer; dumper.resolver = resolver; dumper.dump(node); } +/// Set default scalar style +@safe unittest +{ + auto stream = new Appender!string(); + auto node = Node("Hello world!"); + auto dumper = dumper(stream); + dumper.defaultScalarStyle = ScalarStyle.singleQuoted; + dumper.dump(node); +} +/// Set default collection style +@safe unittest +{ + auto stream = new Appender!string(); + auto node = Node(["Hello", "world!"]); + auto dumper = dumper(stream); + dumper.defaultCollectionStyle = CollectionStyle.flow; + dumper.dump(node); +} +// Make sure the styles are actually used +@safe unittest +{ + auto stream = new Appender!string(); + auto node = Node([Node("Hello world!"), Node(["Hello", "world!"])]); + auto dumper = dumper(stream); + dumper.defaultScalarStyle = ScalarStyle.singleQuoted; + dumper.defaultCollectionStyle = CollectionStyle.flow; + dumper.explicitEnd = false; + dumper.explicitStart = false; + dumper.YAMLVersion = null; + dumper.dump(node); + assert(stream.data == "[!!str 'Hello world!', [!!str 'Hello', !!str 'world!']]\n"); +} // Explicit document start/end markers @safe unittest { diff --git a/source/dyaml/loader.d b/source/dyaml/loader.d index e241bca..0270646 100644 --- a/source/dyaml/loader.d +++ b/source/dyaml/loader.d @@ -41,8 +41,6 @@ struct Loader Parser parser_; // Resolves tags (data types). Resolver resolver_; - // Constructs YAML data types. - Constructor constructor_; // Name of the input file or stream, used in error messages. string name_ = ""; // Are we done loading? @@ -177,12 +175,6 @@ struct Loader resolver_ = resolver; } - /// Specify custom Constructor to use. - void constructor(Constructor constructor) pure @safe nothrow @nogc - { - constructor_ = constructor; - } - /** Load single YAML document. * * If none or more than one YAML document is found, this throws a YAMLException. @@ -230,7 +222,7 @@ struct Loader if (!rangeInitialized) { lazyInitConstructorResolver(); - composer = new Composer(parser_, resolver_, constructor_); + composer = new Composer(parser_, resolver_); rangeInitialized = true; } assert(!done_, "Loader.popFront called on empty range"); @@ -301,7 +293,6 @@ struct Loader void lazyInitConstructorResolver() @safe { if(resolver_ is null) { resolver_ = new Resolver(); } - if(constructor_ is null) { constructor_ = new Constructor(); } } } /// Load single YAML document from a file: @@ -410,13 +401,11 @@ struct Loader "Hello world!\n"~ "...\n" ); - auto constructor = new Constructor(); auto resolver = new Resolver(); // Add constructor functions / resolver expressions here... auto loader = Loader.fromFile("example.yaml"); - loader.constructor = constructor; loader.resolver = resolver; auto rootNode = loader.load(); } diff --git a/source/dyaml/node.d b/source/dyaml/node.d index 8a76fdc..a7087b4 100644 --- a/source/dyaml/node.d +++ b/source/dyaml/node.d @@ -58,59 +58,6 @@ struct YAMLNull // Merge YAML type, used to support "tag:yaml.org,2002:merge". package struct YAMLMerge{} -// Interface for YAMLContainer - used for user defined YAML types. -package interface YAMLObject -{ - public: - // Get type of the stored value. - @property TypeInfo type() const pure @safe nothrow; - - protected: - // Compare with another YAMLObject. - int cmp(const YAMLObject) const @system; -} - -// Stores a user defined YAML data type. -package class YAMLContainer(T) if (!Node.allowed!T): YAMLObject -{ - private: - // Stored value. - T value_; - // Construct a YAMLContainer holding specified value. - this(T value) @safe - { - value_ = value; - } - - public: - // Get type of the stored value. - @property override TypeInfo type() const pure @safe nothrow - { - return typeid(T); - } - - // Get string representation of the container. - override string toString() @system - { - return format("YAMLContainer(%s)", value_); - } - - protected: - // Compare with another YAMLObject. - override int cmp(const YAMLObject rhs) const @system - { - const typeCmp = type.opCmp(rhs.type); - if(typeCmp != 0){return typeCmp;} - - // Const-casting here as Object opCmp is not const. - T* v1 = cast(T*)&value_; - T* v2 = cast(T*)&((cast(YAMLContainer)rhs).value_); - return (*v1).opCmp(*v2); - } - -} - - // Key-value pair of YAML nodes, used in mappings. private struct Pair { @@ -150,6 +97,20 @@ private struct Pair @disable int opCmp(ref Pair); } +enum NodeType +{ + null_, + merge, + boolean, + integer, + decimal, + binary, + timestamp, + string, + mapping, + sequence +} + /** YAML node. * * This is a pseudo-dynamic type that can store any YAML value, including a @@ -164,9 +125,9 @@ struct Node package: // YAML value type. alias Value = Algebraic!(YAMLNull, YAMLMerge, bool, long, real, ubyte[], SysTime, string, - Node.Pair[], Node[], YAMLObject); + Node.Pair[], Node[]); - // Can Value hold this type without wrapping it in a YAMLObject? + // Can Value hold this type naturally? enum allowed(T) = isIntegral!T || isFloatingPoint!T || isSomeString!T || @@ -212,7 +173,8 @@ struct Node * be in full form, e.g. "tag:yaml.org,2002:int", not * a shortcut, like "!!int". */ - this(T)(T value, const string tag = null) + this(T)(T value, const string tag = null) @safe + if (allowed!T || isArray!T || isAssociativeArray!T || is(Unqual!T == Node) || castableToNode!T) { tag_ = tag; @@ -333,15 +295,10 @@ struct Node { { auto node = Node(42); - assert(node.isScalar && !node.isSequence && !node.isMapping && !node.isUserType); + assert(node.isScalar && !node.isSequence && !node.isMapping); assert(node.as!int == 42 && node.as!float == 42.0f && node.as!string == "42"); - assert(!node.isUserType); } - { - auto node = Node(new class{int a = 5;}); - assert(node.isUserType); - } { auto node = Node("string"); assert(node.as!string == "string"); @@ -351,7 +308,7 @@ struct Node { with(Node([1, 2, 3])) { - assert(!isScalar() && isSequence && !isMapping && !isUserType); + assert(!isScalar() && isSequence && !isMapping); assert(length == 3); assert(opIndex(2).as!int == 3); } @@ -364,7 +321,7 @@ struct Node aa["2"] = 2; with(Node(aa)) { - assert(!isScalar() && !isSequence && isMapping && !isUserType); + assert(!isScalar() && !isSequence && isMapping); assert(length == 2); assert(opIndex("2").as!int == 2); } @@ -433,7 +390,7 @@ struct Node { with(Node(["1", "2"], [1, 2])) { - assert(!isScalar() && !isSequence && isMapping && !isUserType); + assert(!isScalar() && !isSequence && isMapping); assert(length == 2); assert(opIndex("2").as!int == 2); } @@ -464,12 +421,6 @@ struct Node return isType!(Pair[]); } - /// Is this node a user defined type? - @property bool isUserType() const @safe nothrow - { - return isType!YAMLObject; - } - /// Is this node null? @property bool isNull() const @safe nothrow { @@ -552,64 +503,85 @@ struct Node * the value is out of range of requested type. */ inout(T) get(T, Flag!"stringConversion" stringConversion = Yes.stringConversion)() inout + if (allowed!(Unqual!T) || hasNodeConstructor!(Unqual!T)) { if(isType!(Unqual!T)){return getValue!T;} - /// Must go before others, as even string/int/etc could be stored in a YAMLObject. - static if(!allowed!(Unqual!T)) if(isUserType) + static if(!allowed!(Unqual!T)) { - auto object = getValue!YAMLObject(); - enforce(object.type is typeid(T), - new NodeException("Node has unexpected type: " ~ object.type.toString() ~ - ". Expected: " ~ typeid(T).toString, startMark_)); - return (cast(inout YAMLContainer!(Unqual!T))(object)).value_; - } - - // 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, stringConversion);} - - static if(isSomeString!T) - { - static if(!stringConversion) + static if (hasSimpleNodeConstructor!T) { - if(isString){return to!T(getValue!string);} - throw new NodeException("Node stores unexpected type: " ~ type.toString() ~ - ". Expected: " ~ typeid(T).toString(), startMark_); + alias params = AliasSeq!(this); + } + else static if (hasExpandedNodeConstructor!T) + { + alias params = AliasSeq!(this, tag_); } else { - // Try to convert to string. - try + static assert(0, "Unknown Node constructor?"); + } + + static if (is(T == class)) + { + return new inout T(params); + } + else static if (is(T == struct)) + { + return T(params); + } + else + { + static assert(0, "Unhandled user type"); + } + } else { + + // 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, stringConversion);} + + static if(isSomeString!T) + { + static if(!stringConversion) { - return coerceValue!T(); + if(isString){return to!T(getValue!string);} + throw new NodeException("Node stores unexpected type: " ~ type.toString() ~ + ". Expected: " ~ typeid(T).toString(), startMark_); } - catch(VariantException e) + else { - throw new NodeException("Unable to convert node value to string", startMark_); + // Try to convert to string. + try + { + return coerceValue!T(); + } + catch(VariantException e) + { + throw new NodeException("Unable to convert node value to string", startMark_); + } } } - } - else static if(isFloatingPoint!T) - { - /// Can convert int to float. - if(isInt()) {return to!T(getValue!long);} - else if(isFloat()){return to!T(getValue!real);} + else static if(isFloatingPoint!T) + { + /// Can convert int to float. + if(isInt()) {return to!T(getValue!long);} + else if(isFloat()){return to!T(getValue!real);} + else throw new NodeException("Node stores unexpected type: " ~ type.toString() ~ + ". Expected: " ~ typeid(T).toString, startMark_); + } + else static if(isIntegral!T) + { + enforce(isInt(), new NodeException("Node stores unexpected type: " ~ type.toString() ~ + ". Expected: " ~ typeid(T).toString, startMark_)); + immutable temp = getValue!long; + enforce(temp >= T.min && temp <= T.max, + new NodeException("Integer value of type " ~ typeid(T).toString() ~ + " out of range. Value: " ~ to!string(temp), startMark_)); + return temp.to!T; + } else throw new NodeException("Node stores unexpected type: " ~ type.toString() ~ ". Expected: " ~ typeid(T).toString, startMark_); } - else static if(isIntegral!T) - { - enforce(isInt(), new NodeException("Node stores unexpected type: " ~ type.toString() ~ - ". Expected: " ~ typeid(T).toString, startMark_)); - immutable temp = getValue!long; - enforce(temp >= T.min && temp <= T.max, - new NodeException("Integer value of type " ~ typeid(T).toString() ~ - " out of range. Value: " ~ to!string(temp), startMark_)); - return temp.to!T; - } - else throw new NodeException("Node stores unexpected type: " ~ type.toString() ~ - ". Expected: " ~ typeid(T).toString, startMark_); } /// Automatic type conversion @safe unittest @@ -620,6 +592,186 @@ struct Node assert(node.get!string == "42"); assert(node.get!double == 42.0); } + /// Scalar node to struct and vice versa + @safe unittest + { + import dyaml.dumper : dumper; + import dyaml.loader : Loader; + static struct MyStruct + { + int x, y, z; + + this(int x, int y, int z) @safe + { + this.x = x; + this.y = y; + this.z = z; + } + + this(Node node) @safe + { + auto parts = node.as!string().split(":"); + x = parts[0].to!int; + y = parts[1].to!int; + z = parts[2].to!int; + } + + Node opCast(T: Node)() @safe + { + //Using custom scalar format, x:y:z. + auto scalar = format("%s:%s:%s", x, y, z); + //Representing as a scalar, with custom tag to specify this data type. + return Node(scalar, "!mystruct.tag"); + } + } + + auto appender = new Appender!string; + + // Dump struct to yaml document + dumper(appender).dump(Node(MyStruct(1,2,3))); + + // Read yaml document back as a MyStruct + auto loader = Loader.fromString(appender.data); + Node node = loader.load(); + assert(node.as!MyStruct == MyStruct(1,2,3)); + } + /// Sequence node to struct and vice versa + @safe unittest + { + import dyaml.dumper : dumper; + import dyaml.loader : Loader; + static struct MyStruct + { + int x, y, z; + + this(int x, int y, int z) @safe + { + this.x = x; + this.y = y; + this.z = z; + } + + this(Node node) @safe + { + x = node[0].as!int; + y = node[1].as!int; + z = node[2].as!int; + } + + Node opCast(T: Node)() + { + return Node([x, y, z], "!mystruct.tag"); + } + } + + auto appender = new Appender!string; + + // Dump struct to yaml document + dumper(appender).dump(Node(MyStruct(1,2,3))); + + // Read yaml document back as a MyStruct + auto loader = Loader.fromString(appender.data); + Node node = loader.load(); + assert(node.as!MyStruct == MyStruct(1,2,3)); + } + /// Mapping node to struct and vice versa + @safe unittest + { + import dyaml.dumper : dumper; + import dyaml.loader : Loader; + static struct MyStruct + { + int x, y, z; + + Node opCast(T: Node)() + { + auto pairs = [Node.Pair("x", x), + Node.Pair("y", y), + Node.Pair("z", z)]; + return Node(pairs, "!mystruct.tag"); + } + + this(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + + this(Node node) @safe + { + x = node["x"].as!int; + y = node["y"].as!int; + z = node["z"].as!int; + } + } + + auto appender = new Appender!string; + + // Dump struct to yaml document + dumper(appender).dump(Node(MyStruct(1,2,3))); + + // Read yaml document back as a MyStruct + auto loader = Loader.fromString(appender.data); + Node node = loader.load(); + assert(node.as!MyStruct == MyStruct(1,2,3)); + } + /// Classes can be used too + @system unittest { + import dyaml.dumper : dumper; + import dyaml.loader : Loader; + + static class MyClass + { + int x, y, z; + + this(int x, int y, int z) + { + this.x = x; + this.y = y; + this.z = z; + } + + this(Node node) @safe inout + { + auto parts = node.as!string().split(":"); + x = parts[0].to!int; + y = parts[1].to!int; + z = parts[2].to!int; + } + + ///Useful for Node.as!string. + override string toString() + { + return format("MyClass(%s, %s, %s)", x, y, z); + } + + Node opCast(T: Node)() @safe + { + //Using custom scalar format, x:y:z. + auto scalar = format("%s:%s:%s", x, y, z); + //Representing as a scalar, with custom tag to specify this data type. + return Node(scalar, "!myclass.tag"); + } + override bool opEquals(Object o) + { + if (auto other = cast(MyClass)o) + { + return (other.x == x) && (other.y == y) && (other.z == z); + } + return false; + } + } + auto appender = new Appender!string; + + // Dump class to yaml document + dumper(appender).dump(Node(new MyClass(1,2,3))); + + // Read yaml document back as a MyClass + auto loader = Loader.fromString(appender.data); + Node node = loader.load(); + assert(node.as!MyClass == new MyClass(1,2,3)); + } @safe unittest { assertThrown!NodeException(Node("42").get!int); @@ -1876,10 +2028,6 @@ struct Node const t2 = rhs.getValue!SysTime; return cmp(t1, t2); } - else if(isUserType) - { - return getValue!YAMLObject.cmp(rhs.getValue!YAMLObject); - } assert(false, "Unknown type of node for comparison : " ~ type.toString()); } @@ -1923,12 +2071,58 @@ struct Node assert(false); } - // Get type of the node value (YAMLObject for user types). + // Get type of the node value. @property TypeInfo type() const @safe nothrow { return value_.type; } + // Get type of the node value. + @property NodeType newType() const @safe nothrow + { + if (value_.type is typeid(bool)) + { + return NodeType.boolean; + } + else if (value_.type is typeid(long)) + { + return NodeType.integer; + } + else if (value_.type is typeid(Node[])) + { + return NodeType.sequence; + } + else if (value_.type is typeid(ubyte[])) + { + return NodeType.binary; + } + else if (value_.type is typeid(string)) + { + return NodeType.string; + } + else if (value_.type is typeid(Node.Pair[])) + { + return NodeType.mapping; + } + else if (value_.type is typeid(SysTime)) + { + return NodeType.timestamp; + } + else if (value_.type is typeid(YAMLNull)) + { + return NodeType.null_; + } + else if (value_.type is typeid(YAMLMerge)) + { + return NodeType.merge; + } + else if (value_.type is typeid(real)) + { + return NodeType.decimal; + } + else assert(0, text(value_.type)); + } + public: // Determine if the value stored by the node is of specified type. // @@ -2210,7 +2404,7 @@ struct Node } else { - value_ = cast(YAMLObject)new YAMLContainer!T(value); + value_ = (cast(Node)value).value_; } } } @@ -2232,3 +2426,30 @@ void merge(ref Appender!(Node.Pair[]) pairs, Node.Pair[] toMerge) @safe pairs.put(pair); } } + +enum hasNodeConstructor(T) = hasSimpleNodeConstructor!T || hasExpandedNodeConstructor!T; +template hasSimpleNodeConstructor(T) +{ + static if (is(T == struct)) + { + enum hasSimpleNodeConstructor = is(typeof(T(Node.init))); + } + else static if (is(T == class)) + { + enum hasSimpleNodeConstructor = is(typeof(new inout T(Node.init))); + } + else enum hasSimpleNodeConstructor = false; +} +template hasExpandedNodeConstructor(T) +{ + static if (is(T == struct)) + { + enum hasExpandedNodeConstructor = is(typeof(T(Node.init, ""))); + } + else static if (is(T == class)) + { + enum hasExpandedNodeConstructor = is(typeof(new inout T(Node.init, ""))); + } + else enum hasExpandedNodeConstructor = false; +} +enum castableToNode(T) = (is(T == struct) || is(T == class)) && is(typeof(T.opCast!Node()) : Node); diff --git a/source/dyaml/package.d b/source/dyaml/package.d index 6085992..e61b716 100644 --- a/source/dyaml/package.d +++ b/source/dyaml/package.d @@ -5,13 +5,11 @@ module dyaml; -public import dyaml.constructor; public import dyaml.dumper; public import dyaml.encoding; public import dyaml.exception; public import dyaml.linebreak; public import dyaml.loader; -public import dyaml.representer; public import dyaml.resolver; public import dyaml.style; public import dyaml.node; diff --git a/source/dyaml/representer.d b/source/dyaml/representer.d index b6a8876..f60ec64 100644 --- a/source/dyaml/representer.d +++ b/source/dyaml/representer.d @@ -30,7 +30,7 @@ import dyaml.node; import dyaml.serializer; import dyaml.style; - +package: ///Exception thrown on Representer errors. class RepresenterException : YAMLException { @@ -39,473 +39,276 @@ class RepresenterException : YAMLException /** * 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 +Node representData(const Node data, ScalarStyle defaultScalarStyle, CollectionStyle defaultCollectionStyle) @safe { - 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; + Node result; + final switch(data.newType) { + 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; + } - public: - @disable bool opEquals(ref Representer); - @disable int opCmp(ref Representer); + if (result.isScalar && (result.scalarStyle == ScalarStyle.invalid)) + { + result.scalarStyle = defaultScalarStyle; + } - /** - * 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); - } + if ((result.isSequence || result.isMapping) && (defaultCollectionStyle != CollectionStyle.invalid)) + { + result.collectionStyle = defaultCollectionStyle; + } - ///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; - } + //Override tag if specified. + if(data.tag_ !is null){result.tag_ = data.tag_;} - ///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); - } + //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 _null _node as a _null YAML value. -Node representNull(ref Node node, Representer representer) @safe +@safe unittest { - return representer.representScalar("tag:yaml.org,2002:null", "null"); + // We don't emit yaml merge nodes. + assert(representData(Node(YAMLMerge()), ScalarStyle.invalid, CollectionStyle.invalid) == Node.init); } -///Represent a string _node as a string scalar. -Node representString(ref Node node, Representer representer) @safe +@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 - ? representNull(node, representer) - : representer.representScalar("tag:yaml.org,2002:str", value); + ? Node("null", "tag:yaml.org,2002:null") + : Node(value, "tag:yaml.org,2002:str"); } -///Represent a bytes _node as a binary scalar. -Node representBytes(ref Node node, Representer representer) @safe +//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 representNull(node, representer);} - return representer.representScalar("tag:yaml.org,2002:binary", - Base64.encode(value).idup, - ScalarStyle.literal); + 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(ref Node node, Representer representer) @safe +//Represent a bool _node as a bool scalar. +Node representBool(const Node node) @safe { - return representer.representScalar("tag:yaml.org,2002:bool", - node.as!bool ? "true" : "false"); + return Node(node.as!bool ? "true" : "false", "tag:yaml.org,2002:bool"); } -///Represent a long _node as an integer scalar. -Node representLong(ref Node node, Representer representer) @safe +//Represent a long _node as an integer scalar. +Node representLong(const Node node) @safe { - return representer.representScalar("tag:yaml.org,2002:int", - to!string(node.as!long)); + return Node(node.as!long.to!string, "tag:yaml.org,2002:int"); } -///Represent a real _node as a floating point scalar. -Node representReal(ref Node node, Representer representer) @safe +//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": @@ -515,48 +318,104 @@ Node representReal(ref Node node, Representer representer) @safe formattedWrite(a, "%12f", f); return a.data.strip();}(); - return representer.representScalar("tag:yaml.org,2002:float", value); + return Node(value, "tag:yaml.org,2002:float"); } -///Represent a SysTime _node as a timestamp. -Node representSysTime(ref Node node, Representer representer) @safe +//Represent a SysTime _node as a timestamp. +Node representSysTime(const Node node) @safe { - return representer.representScalar("tag:yaml.org,2002:timestamp", - node.as!SysTime.toISOExtString()); + return Node(node.as!SysTime.toISOExtString(), "tag:yaml.org,2002:timestamp"); } -///Represent a sequence _node as sequence/set. -Node representNodes(ref Node node, Representer representer) @safe +//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. + //YAML sets are mapping with null values. Node.Pair[] pairs; pairs.length = nodes.length; - Node dummy; - foreach(idx, ref key; nodes) + + foreach(idx, key; nodes) { - pairs[idx] = Node.Pair(key, representNull(dummy, representer)); + pairs[idx] = Node.Pair(key, Node("null", "tag:yaml.org,2002:null")); } - return representer.representMapping(node.tag_, pairs); + 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 { - return representer.representSequence("tag:yaml.org,2002:seq", nodes); + Node[] value; + value.length = nodes.length; + + auto bestStyle = CollectionStyle.flow; + foreach(idx, item; nodes) + { + value[idx] = representData(item, defaultScalarStyle, defaultCollectionStyle); + const isScalar = value[idx].isScalar; + 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; } } -///Represent a mapping _node as map/ordered map/pairs. -Node representPairs(ref Node node, Representer representer) @safe +bool shouldUseBlockStyle(const Node value) @safe +{ + const isScalar = value.isScalar; + const s = value.scalarStyle; + return (!isScalar || (s != ScalarStyle.invalid && s != ScalarStyle.plain)); +} +bool shouldUseBlockStyle(const Node.Pair value) @safe +{ + const keyScalar = value.key.isScalar; + const valScalar = value.value.isScalar; + 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(Node.Pair[] pairs) @safe + bool hasDuplicates(const Node.Pair[] pairs) @safe { //TODO this should be replaced by something with deterministic memory allocation. auto keys = redBlackTree!Node(); - foreach(ref pair; pairs) + foreach(pair; pairs) { if(pair.key in keys){return true;} keys.insert(pair.key); @@ -564,13 +423,20 @@ Node representPairs(ref Node node, Representer representer) @safe return false; } - Node[] mapToSequence(Node.Pair[] pairs) @safe + Node[] mapToSequence(const Node.Pair[] pairs) @safe { Node[] nodes; nodes.length = pairs.length; - foreach(idx, ref pair; pairs) + foreach(idx, pair; pairs) { - nodes[idx] = representer.representMapping("tag:yaml.org,2002:map", [pair]); + 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; } @@ -579,123 +445,63 @@ Node representPairs(ref Node node, Representer representer) @safe { enforce(!hasDuplicates(pairs), new RepresenterException("Duplicate entry in an ordered map")); - return representer.representSequence(node.tag_, mapToSequence(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 if(node.tag_ == "tag:yaml.org,2002:pairs") { - return representer.representSequence(node.tag_, mapToSequence(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")); - return representer.representMapping("tag:yaml.org,2002:map", pairs); + 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; } } - -//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))); -} diff --git a/source/dyaml/scanner.d b/source/dyaml/scanner.d index 589c0bb..aa7cc0e 100644 --- a/source/dyaml/scanner.d +++ b/source/dyaml/scanner.d @@ -1951,91 +1951,36 @@ final class Scanner /// In case of an error, error_ is set. Use throwIfError() to handle this. void scanURIEscapesToSlice(string name)(const Mark startMark) { + import core.exception : UnicodeException; // URI escapes encode a UTF-8 string. We store UTF-8 code units here for // decoding into UTF-32. - char[4] bytes; - size_t bytesUsed; + Appender!string buffer; - // Get one dchar by decoding data from bytes. - // - // This is probably slow, but simple and URI escapes are extremely uncommon - // in YAML. - // - // Returns the number of bytes used by the dchar in bytes on success, - // size_t.max on failure. - static size_t getDchar(char[] bytes, Reader reader_) @safe - { - size_t nextChar; - dchar c; - if(bytes[0] < 0x80) - { - c = bytes[0]; - ++nextChar; - } - else - { - try - { - c = decode(bytes[], nextChar); - } - catch (UTFException) - { - return size_t.max; - } - } - reader_.sliceBuilder.write(c); - if(bytes.length - nextChar > 0) - { - copy(bytes[nextChar..bytes.length], bytes[0..bytes.length-nextChar]); - } - return bytes.length - nextChar; - } enum contextMsg = "While scanning a " ~ name; while(reader_.peekByte() == '%') { reader_.forward(); - if(bytesUsed == bytes.length) + char[2] nextByte = [reader_.peekByte(), reader_.peekByte(1)]; + if(!nextByte[0].isHexDigit || !nextByte[1].isHexDigit) { - bytesUsed = getDchar(bytes[], reader_); - if(bytesUsed == size_t.max) - { - error(contextMsg, startMark, - "Invalid UTF-8 data encoded in URI escape sequence", - reader_.mark); - return; - } + auto msg = expected("URI escape sequence of 2 hexadecimal " ~ + "numbers", nextByte); + error(contextMsg, startMark, msg, reader_.mark); + return; } - - char b = 0; - uint mult = 16; - // Converting 2 hexadecimal digits to a byte. - foreach(k; 0 .. 2) - { - const dchar c = reader_.peek(k); - if(!c.isHexDigit) - { - auto msg = expected("URI escape sequence of 2 hexadecimal " ~ - "numbers", c); - error(contextMsg, startMark, msg, reader_.mark); - return; - } - - uint digit; - if(c - '0' < 10) { digit = c - '0'; } - else if(c - 'A' < 6) { digit = 10 + c - 'A'; } - else if(c - 'a' < 6) { digit = 10 + c - 'a'; } - else { assert(false); } - b += mult * digit; - mult /= 16; - } - bytes[bytesUsed++] = b; + buffer ~= nextByte[].to!ubyte(16); reader_.forward(2); } - - bytesUsed = getDchar(bytes[0 .. bytesUsed], reader_); - if(bytesUsed == size_t.max) + try + { + foreach (dchar chr; buffer.data) + { + reader_.sliceBuilder.write(chr); + } + } + catch (UnicodeException) { error(contextMsg, startMark, "Invalid UTF-8 data encoded in URI escape sequence", diff --git a/source/dyaml/test/constructor.d b/source/dyaml/test/constructor.d index 264795c..41ae3fb 100644 --- a/source/dyaml/test/constructor.d +++ b/source/dyaml/test/constructor.d @@ -328,21 +328,12 @@ class TestClass 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) @safe + Node opCast(T: Node)() @safe { - TestClass s = cast(TestClass)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; - } - - override string toString() @safe - { - return format("TestClass(", x, ", ", y, ", ", z, ")"); + auto pairs = [Node.Pair("x", x), + Node.Pair("y", y), + Node.Pair("z", z)]; + return Node(pairs, "!tag1"); } } @@ -351,43 +342,22 @@ struct TestStruct { int value; - //Any D:YAML type must have a custom opCmp operator. - //This is used for ordering in mappings. - int opCmp(ref const TestStruct s) const @safe + this (int x) @safe { - return value - s.value; + value = x; } -} -///Constructor function for TestClass. -TestClass constructClass(ref Node node) @safe -{ - return new TestClass(node["x"].as!int, node["y"].as!int, node["z"].as!int); -} + ///Constructor function for TestStruct. + this(ref Node node) @safe + { + value = node.as!string.to!int; + } -Node representClass(ref Node node, Representer representer) @safe -{ - auto value = node.as!TestClass; - auto pairs = [Node.Pair("x", value.x), - Node.Pair("y", value.y), - Node.Pair("z", value.z)]; - auto result = representer.representMapping("!tag1", pairs); - - return result; -} - -///Constructor function for TestStruct. -TestStruct constructStruct(ref Node node) @safe -{ - return TestStruct(to!int(node.as!string)); -} - -///Representer function for TestStruct. -Node representStruct(ref Node node, Representer representer) @safe -{ - string[] keys, values; - auto value = node.as!TestStruct; - return representer.representScalar("!tag2", to!string(value.value)); + ///Representer function for TestStruct. + Node opCast(T: Node)() @safe + { + return Node(value.to!string, "!tag2"); + } } /** @@ -403,12 +373,7 @@ void testConstructor(string dataFilename, string codeDummy) @safe enforce((base in expected) !is null, new Exception("Unimplemented constructor test: " ~ base)); - auto constructor = new Constructor; - constructor.addConstructorMapping("!tag1", &constructClass); - constructor.addConstructorScalar("!tag2", &constructStruct); - auto loader = Loader.fromFile(dataFilename); - loader.constructor = constructor; loader.resolver = new Resolver; Node[] exp = expected[base]; diff --git a/source/dyaml/test/emitter.d b/source/dyaml/test/emitter.d index b0627b4..e55bed0 100644 --- a/source/dyaml/test/emitter.d +++ b/source/dyaml/test/emitter.d @@ -90,7 +90,6 @@ void testEmitterOnData(string dataFilename, string canonicalFilename) @safe auto loader2 = Loader.fromString(emitStream.data); loader2.name = "TEST"; - loader2.constructor = new Constructor; loader2.resolver = new Resolver; auto newEvents = loader2.parse(); assert(compareEvents(events, newEvents)); @@ -119,7 +118,6 @@ void testEmitterOnCanonical(string canonicalFilename) @safe } auto loader2 = Loader.fromString(emitStream.data); loader2.name = "TEST"; - loader2.constructor = new Constructor; loader2.resolver = new Resolver; auto newEvents = loader2.parse(); assert(compareEvents(events, newEvents)); @@ -177,7 +175,6 @@ void testEmitterStyles(string dataFilename, string canonicalFilename) @safe } auto loader2 = Loader.fromString(emitStream.data); loader2.name = "TEST"; - loader2.constructor = new Constructor; loader2.resolver = new Resolver; auto newEvents = loader2.parse(); assert(compareEvents(events, newEvents)); diff --git a/source/dyaml/test/representer.d b/source/dyaml/test/representer.d index 1600813..3261fd0 100644 --- a/source/dyaml/test/representer.d +++ b/source/dyaml/test/representer.d @@ -53,21 +53,13 @@ void testRepresenterTypes(string codeFilename) @safe } auto emitStream = new Appender!(immutable(encoding)[]); - auto representer = new Representer; - representer.addRepresenter!TestClass(&representClass); - representer.addRepresenter!TestStruct(&representStruct); auto dumper = dumper(emitStream); - dumper.representer = representer; dumper.dump!encoding(expectedNodes); output = emitStream.data; - auto constructor = new Constructor; - constructor.addConstructorMapping("!tag1", &constructClass); - constructor.addConstructorScalar("!tag2", &constructStruct); auto loader = Loader.fromString(emitStream.data.toUTF8); loader.name = "TEST"; - loader.constructor = constructor; readNodes = loader.array; assert(expectedNodes.length == readNodes.length); diff --git a/source/dyaml/test/resolver.d b/source/dyaml/test/resolver.d index f71fa3c..8732f8a 100644 --- a/source/dyaml/test/resolver.d +++ b/source/dyaml/test/resolver.d @@ -16,12 +16,6 @@ import std.string; import dyaml.test.common; -string construct(ref Node node) @safe -{ - return node.as!string; -} - - /** * Implicit tag resolution unittest. * @@ -44,12 +38,7 @@ void testImplicitResolver(string dataFilename, string detectFilename) @safe correctTag = readText(detectFilename).strip(); - auto constructor = new Constructor; - constructor.addConstructorScalar("tag:example.com,2000:app/tag🤔", &construct); - auto loader = Loader.fromFile(dataFilename); - loader.constructor = constructor; - - node = loader.load(); + node = Loader.fromFile(dataFilename).load(); assert(node.isSequence); foreach(ref Node scalar; node) { diff --git a/test/data/empty-python-module.loader-error b/test/data/empty-python-module.loader-error deleted file mode 100644 index 83d3232..0000000 --- a/test/data/empty-python-module.loader-error +++ /dev/null @@ -1 +0,0 @@ ---- !!python:module: diff --git a/test/data/empty-python-name.loader-error b/test/data/empty-python-name.loader-error deleted file mode 100644 index 6162957..0000000 --- a/test/data/empty-python-name.loader-error +++ /dev/null @@ -1 +0,0 @@ ---- !!python/name: empty diff --git a/test/data/undefined-constructor.loader-error b/test/data/undefined-constructor.loader-error deleted file mode 100644 index 9a37ccc..0000000 --- a/test/data/undefined-constructor.loader-error +++ /dev/null @@ -1 +0,0 @@ ---- !foo bar