// Copyright Ferdinand Majerech 2011. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) /** * Implements a class that resolves YAML tags. This can be used to implicitly * resolve tags for custom data types, removing the need to explicitly * specify tags in YAML. A tutorial can be found * $(LINK2 ../tutorials/custom_types.html, here). * * Code based on $(LINK2 http://www.pyyaml.org, PyYAML). */ module dyaml.resolver; import std.conv; import std.regex; import std.typecons; import std.utf; import dyaml.node; import dyaml.exception; /// Type of `regexes` private alias RegexType = Tuple!(string, "tag", const Regex!char, "regexp", string, "chars"); private immutable RegexType[] regexes; shared static this() @safe { RegexType[] tmp; tmp ~= RegexType("tag:yaml.org,2002:bool", regex(r"^(?:yes|Yes|YES|no|No|NO|true|True|TRUE" ~ "|false|False|FALSE|on|On|ON|off|Off|OFF)$"), "yYnNtTfFoO"); tmp ~= RegexType("tag:yaml.org,2002:float", regex(r"^(?:[-+]?([0-9][0-9_]*)\\.[0-9_]*" ~ "(?:[eE][-+][0-9]+)?|[-+]?(?:[0-9][0-9_]" ~ "*)?\\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?" ~ "[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]" ~ "*|[-+]?\\.(?:inf|Inf|INF)|\\." ~ "(?:nan|NaN|NAN))$"), "-+0123456789."); tmp ~= RegexType("tag:yaml.org,2002:int", regex(r"^(?:[-+]?0b[0-1_]+" ~ "|[-+]?0[0-7_]+" ~ "|[-+]?(?:0|[1-9][0-9_]*)" ~ "|[-+]?0x[0-9a-fA-F_]+" ~ "|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$"), "-+0123456789"); tmp ~= RegexType("tag:yaml.org,2002:merge", regex(r"^<<$"), "<"); tmp ~= RegexType("tag:yaml.org,2002:null", regex(r"^$|^(?:~|null|Null|NULL)$"), "~nN\0"); tmp ~= RegexType("tag:yaml.org,2002:timestamp", regex(r"^[0-9][0-9][0-9][0-9]-[0-9][0-9]-" ~ "[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9]" ~ "[0-9]?-[0-9][0-9]?[Tt]|[ \t]+[0-9]" ~ "[0-9]?:[0-9][0-9]:[0-9][0-9]" ~ "(?:\\.[0-9]*)?(?:[ \t]*Z|[-+][0-9]" ~ "[0-9]?(?::[0-9][0-9])?)?$"), "0123456789"); tmp ~= RegexType("tag:yaml.org,2002:value", regex(r"^=$"), "="); //The following resolver is only for documentation purposes. It cannot work //because plain scalars cannot start with '!', '&', or '*'. tmp ~= RegexType("tag:yaml.org,2002:yaml", regex(r"^(?:!|&|\*)$"), "!&*"); regexes = () @trusted { return cast(immutable)tmp; }(); } /** * Resolves YAML tags (data types). * * Can be used to implicitly resolve custom data types of scalar values. */ struct Resolver { private: // Default tag to use for scalars. string defaultScalarTag_ = "tag:yaml.org,2002:str"; // Default tag to use for sequences. string defaultSequenceTag_ = "tag:yaml.org,2002:seq"; // Default tag to use for mappings. string defaultMappingTag_ = "tag:yaml.org,2002:map"; /* * Arrays of scalar resolver tuples indexed by starting character of a scalar. * * Each tuple stores regular expression the scalar must match, * and tag to assign to it if it matches. */ Tuple!(string, const Regex!char)[][dchar] yamlImplicitResolvers_; package: static auto withDefaultResolvers() @safe { Resolver resolver; foreach(pair; regexes) { resolver.addImplicitResolver(pair.tag, pair.regexp, pair.chars); } return resolver; } public: @disable bool opEquals(ref Resolver); @disable int opCmp(ref Resolver); /** * Add an implicit scalar resolver. * * If a scalar matches regexp and starts with any character in first, * its _tag is set to tag. If it matches more than one resolver _regexp * resolvers added _first override ones added later. Default resolvers * override any user specified resolvers, but they can be disabled in * Resolver constructor. * * If a scalar is not resolved to anything, it is assigned the default * YAML _tag for strings. * * Params: tag = Tag to resolve to. * regexp = Regular expression the scalar must match to have this _tag. * first = String of possible starting characters of the scalar. * */ void addImplicitResolver(string tag, const Regex!char regexp, string first) pure @safe { foreach(const dchar c; first) { if((c in yamlImplicitResolvers_) is null) { yamlImplicitResolvers_[c] = []; } yamlImplicitResolvers_[c] ~= tuple(tag, regexp); } } /// Resolve scalars starting with 'A' to !_tag @safe unittest { import std.file : write; import std.regex : regex; import dyaml.loader : Loader; import dyaml.resolver : Resolver; write("example.yaml", "A"); auto loader = Loader.fromFile("example.yaml"); loader.resolver.addImplicitResolver("!tag", regex("A.*"), "A"); auto node = loader.load(); assert(node.tag == "!tag"); } package: /** * Resolve tag of a node. * * Params: kind = Type of the node. * tag = Explicit tag of the node, if any. * value = Value of the node, if any. * implicit = Should the node be implicitly resolved? * * If the tag is already specified and not non-specific, that tag will * be returned. * * Returns: Resolved tag. */ string resolve(const NodeID kind, const string tag, const string value, const bool implicit) @safe { import std.array : empty, front; if((tag !is null) && (tag != "!")) { return tag; } final switch (kind) { case NodeID.scalar: if(!implicit) { return defaultScalarTag_; } //Get the first char of the value. const dchar first = value.empty ? '\0' : value.front; auto resolvers = (first in yamlImplicitResolvers_) is null ? [] : yamlImplicitResolvers_[first]; //If regexp matches, return tag. foreach(resolver; resolvers) { if(!(match(value, resolver[1]).empty)) { return resolver[0]; } } return defaultScalarTag_; case NodeID.sequence: return defaultSequenceTag_; case NodeID.mapping: return defaultMappingTag_; case NodeID.invalid: assert(false, "Cannot resolve an invalid node"); } } @safe unittest { auto resolver = Resolver.withDefaultResolvers; bool tagMatch(string tag, string[] values) @safe { const string expected = tag; foreach(value; values) { const string resolved = resolver.resolve(NodeID.scalar, null, value, true); if(expected != resolved) { return false; } } return true; } assert(tagMatch("tag:yaml.org,2002:bool", ["yes", "NO", "True", "on"])); assert(tagMatch("tag:yaml.org,2002:float", ["6.8523015e+5", "685.230_15e+03", "685_230.15", "190:20:30.15", "-.inf", ".NaN"])); assert(tagMatch("tag:yaml.org,2002:int", ["685230", "+685_230", "02472256", "0x_0A_74_AE", "0b1010_0111_0100_1010_1110", "190:20:30"])); assert(tagMatch("tag:yaml.org,2002:merge", ["<<"])); assert(tagMatch("tag:yaml.org,2002:null", ["~", "null", ""])); assert(tagMatch("tag:yaml.org,2002:str", ["abcd", "9a8b", "9.1adsf"])); assert(tagMatch("tag:yaml.org,2002:timestamp", ["2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", "2001-12-14 21:59:43.10 -5", "2001-12-15 2:59:43.10", "2002-12-14"])); assert(tagMatch("tag:yaml.org,2002:value", ["="])); assert(tagMatch("tag:yaml.org,2002:yaml", ["!", "&", "*"])); } ///Returns: Default scalar tag. @property string defaultScalarTag() const pure @safe nothrow {return defaultScalarTag_;} ///Returns: Default sequence tag. @property string defaultSequenceTag() const pure @safe nothrow {return defaultSequenceTag_;} ///Returns: Default mapping tag. @property string defaultMappingTag() const pure @safe nothrow {return defaultMappingTag_;} }