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