1 2 // Copyright Ferdinand Majerech 2011. 3 // Distributed under the Boost Software License, Version 1.0. 4 // (See accompanying file LICENSE_1_0.txt or copy at 5 // http://www.boost.org/LICENSE_1_0.txt) 6 7 /** 8 * Implements a class that resolves YAML tags. This can be used to implicitly 9 * resolve tags for custom data types, removing the need to explicitly 10 * specify tags in YAML. A tutorial can be found 11 * $(LINK2 ../tutorials/custom_types.html, here). 12 * 13 * Code based on $(LINK2 http://www.pyyaml.org, PyYAML). 14 */ 15 module dyaml.resolver; 16 17 18 import std.conv; 19 import std.regex; 20 import std.typecons; 21 import std.utf; 22 23 import dyaml.node; 24 import dyaml.exception; 25 26 27 /// Type of `regexes` 28 private alias RegexType = Tuple!(string, "tag", const Regex!char, "regexp", string, "chars"); 29 30 private immutable RegexType[] regexes; 31 32 shared static this() @safe 33 { 34 RegexType[] tmp; 35 tmp ~= RegexType("tag:yaml.org,2002:bool", 36 regex(r"^(?:yes|Yes|YES|no|No|NO|true|True|TRUE" ~ 37 "|false|False|FALSE|on|On|ON|off|Off|OFF)$"), 38 "yYnNtTfFoO"); 39 tmp ~= RegexType("tag:yaml.org,2002:float", 40 regex(r"^(?:[-+]?([0-9][0-9_]*)\\.[0-9_]*" ~ 41 "(?:[eE][-+][0-9]+)?|[-+]?(?:[0-9][0-9_]" ~ 42 "*)?\\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?" ~ 43 "[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]" ~ 44 "*|[-+]?\\.(?:inf|Inf|INF)|\\." ~ 45 "(?:nan|NaN|NAN))$"), 46 "-+0123456789."); 47 tmp ~= RegexType("tag:yaml.org,2002:int", 48 regex(r"^(?:[-+]?0b[0-1_]+" ~ 49 "|[-+]?0[0-7_]+" ~ 50 "|[-+]?(?:0|[1-9][0-9_]*)" ~ 51 "|[-+]?0x[0-9a-fA-F_]+" ~ 52 "|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$"), 53 "-+0123456789"); 54 tmp ~= RegexType("tag:yaml.org,2002:merge", regex(r"^<<$"), "<"); 55 tmp ~= RegexType("tag:yaml.org,2002:null", 56 regex(r"^$|^(?:~|null|Null|NULL)$"), "~nN\0"); 57 tmp ~= RegexType("tag:yaml.org,2002:timestamp", 58 regex(r"^[0-9][0-9][0-9][0-9]-[0-9][0-9]-" ~ 59 "[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9]" ~ 60 "[0-9]?-[0-9][0-9]?[Tt]|[ \t]+[0-9]" ~ 61 "[0-9]?:[0-9][0-9]:[0-9][0-9]" ~ 62 "(?:\\.[0-9]*)?(?:[ \t]*Z|[-+][0-9]" ~ 63 "[0-9]?(?::[0-9][0-9])?)?$"), 64 "0123456789"); 65 tmp ~= RegexType("tag:yaml.org,2002:value", regex(r"^=$"), "="); 66 67 68 //The following resolver is only for documentation purposes. It cannot work 69 //because plain scalars cannot start with '!', '&', or '*'. 70 tmp ~= RegexType("tag:yaml.org,2002:yaml", regex(r"^(?:!|&|\*)$"), "!&*"); 71 72 regexes = () @trusted { return cast(immutable)tmp; }(); 73 } 74 75 /** 76 * Resolves YAML tags (data types). 77 * 78 * Can be used to implicitly resolve custom data types of scalar values. 79 */ 80 struct Resolver 81 { 82 private: 83 // Default tag to use for scalars. 84 string defaultScalarTag_ = "tag:yaml.org,2002:str"; 85 // Default tag to use for sequences. 86 string defaultSequenceTag_ = "tag:yaml.org,2002:seq"; 87 // Default tag to use for mappings. 88 string defaultMappingTag_ = "tag:yaml.org,2002:map"; 89 90 /* 91 * Arrays of scalar resolver tuples indexed by starting character of a scalar. 92 * 93 * Each tuple stores regular expression the scalar must match, 94 * and tag to assign to it if it matches. 95 */ 96 Tuple!(string, const Regex!char)[][dchar] yamlImplicitResolvers_; 97 98 package: 99 static auto withDefaultResolvers() @safe 100 { 101 Resolver resolver; 102 foreach(pair; regexes) 103 { 104 resolver.addImplicitResolver(pair.tag, pair.regexp, pair.chars); 105 } 106 return resolver; 107 } 108 109 public: 110 @disable bool opEquals(ref Resolver); 111 @disable int opCmp(ref Resolver); 112 113 /** 114 * Add an implicit scalar resolver. 115 * 116 * If a scalar matches regexp and starts with any character in first, 117 * its _tag is set to tag. If it matches more than one resolver _regexp 118 * resolvers added _first override ones added later. Default resolvers 119 * override any user specified resolvers, but they can be disabled in 120 * Resolver constructor. 121 * 122 * If a scalar is not resolved to anything, it is assigned the default 123 * YAML _tag for strings. 124 * 125 * Params: tag = Tag to resolve to. 126 * regexp = Regular expression the scalar must match to have this _tag. 127 * first = String of possible starting characters of the scalar. 128 * 129 */ 130 void addImplicitResolver(string tag, const Regex!char regexp, string first) 131 pure @safe 132 { 133 foreach(const dchar c; first) 134 { 135 if((c in yamlImplicitResolvers_) is null) 136 { 137 yamlImplicitResolvers_[c] = []; 138 } 139 yamlImplicitResolvers_[c] ~= tuple(tag, regexp); 140 } 141 } 142 /// Resolve scalars starting with 'A' to !_tag 143 @safe unittest 144 { 145 import std.file : write; 146 import std.regex : regex; 147 import dyaml.loader : Loader; 148 import dyaml.resolver : Resolver; 149 150 write("example.yaml", "A"); 151 152 auto loader = Loader.fromFile("example.yaml"); 153 loader.resolver.addImplicitResolver("!tag", regex("A.*"), "A"); 154 155 auto node = loader.load(); 156 assert(node.tag == "!tag"); 157 } 158 159 package: 160 /** 161 * Resolve tag of a node. 162 * 163 * Params: kind = Type of the node. 164 * tag = Explicit tag of the node, if any. 165 * value = Value of the node, if any. 166 * implicit = Should the node be implicitly resolved? 167 * 168 * If the tag is already specified and not non-specific, that tag will 169 * be returned. 170 * 171 * Returns: Resolved tag. 172 */ 173 string resolve(const NodeID kind, const string tag, const string value, 174 const bool implicit) @safe 175 { 176 import std.array : empty, front; 177 if((tag !is null) && (tag != "!")) 178 { 179 return tag; 180 } 181 182 final switch (kind) 183 { 184 case NodeID.scalar: 185 if(!implicit) 186 { 187 return defaultScalarTag_; 188 } 189 190 //Get the first char of the value. 191 const dchar first = value.empty ? '\0' : value.front; 192 193 auto resolvers = (first in yamlImplicitResolvers_) is null ? 194 [] : yamlImplicitResolvers_[first]; 195 196 //If regexp matches, return tag. 197 foreach(resolver; resolvers) 198 { 199 if(!(match(value, resolver[1]).empty)) 200 { 201 return resolver[0]; 202 } 203 } 204 return defaultScalarTag_; 205 case NodeID.sequence: 206 return defaultSequenceTag_; 207 case NodeID.mapping: 208 return defaultMappingTag_; 209 case NodeID.invalid: 210 assert(false, "Cannot resolve an invalid node"); 211 } 212 } 213 @safe unittest 214 { 215 auto resolver = Resolver.withDefaultResolvers; 216 217 bool tagMatch(string tag, string[] values) @safe 218 { 219 const string expected = tag; 220 foreach(value; values) 221 { 222 const string resolved = resolver.resolve(NodeID.scalar, null, value, true); 223 if(expected != resolved) 224 { 225 return false; 226 } 227 } 228 return true; 229 } 230 231 assert(tagMatch("tag:yaml.org,2002:bool", 232 ["yes", "NO", "True", "on"])); 233 assert(tagMatch("tag:yaml.org,2002:float", 234 ["6.8523015e+5", "685.230_15e+03", "685_230.15", 235 "190:20:30.15", "-.inf", ".NaN"])); 236 assert(tagMatch("tag:yaml.org,2002:int", 237 ["685230", "+685_230", "02472256", "0x_0A_74_AE", 238 "0b1010_0111_0100_1010_1110", "190:20:30"])); 239 assert(tagMatch("tag:yaml.org,2002:merge", ["<<"])); 240 assert(tagMatch("tag:yaml.org,2002:null", ["~", "null", ""])); 241 assert(tagMatch("tag:yaml.org,2002:str", 242 ["abcd", "9a8b", "9.1adsf"])); 243 assert(tagMatch("tag:yaml.org,2002:timestamp", 244 ["2001-12-15T02:59:43.1Z", 245 "2001-12-14t21:59:43.10-05:00", 246 "2001-12-14 21:59:43.10 -5", 247 "2001-12-15 2:59:43.10", 248 "2002-12-14"])); 249 assert(tagMatch("tag:yaml.org,2002:value", ["="])); 250 assert(tagMatch("tag:yaml.org,2002:yaml", ["!", "&", "*"])); 251 } 252 253 ///Returns: Default scalar tag. 254 @property string defaultScalarTag() const pure @safe nothrow {return defaultScalarTag_;} 255 256 ///Returns: Default sequence tag. 257 @property string defaultSequenceTag() const pure @safe nothrow {return defaultSequenceTag_;} 258 259 ///Returns: Default mapping tag. 260 @property string defaultMappingTag() const pure @safe nothrow {return defaultMappingTag_;} 261 }