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