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 }