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 /// Class used to load YAML documents.
8 module dyaml.loader;
9 
10 
11 import std.exception;
12 import std.file;
13 import std.string;
14 
15 import dyaml.composer;
16 import dyaml.constructor;
17 import dyaml.event;
18 import dyaml.exception;
19 import dyaml.node;
20 import dyaml.parser;
21 import dyaml.reader;
22 import dyaml.resolver;
23 import dyaml.scanner;
24 import dyaml.token;
25 
26 
27 /** Loads YAML documents from files or char[].
28  *
29  * User specified Constructor and/or Resolver can be used to support new
30  * tags / data types.
31  *
32  * Examples:
33  *
34  * Load single YAML document from a file:
35  * --------------------
36  * auto rootNode = Loader("file.yaml").load();
37  * ...
38  * --------------------
39  *
40  * Load all YAML documents from a file:
41  * --------------------
42  * auto nodes = Loader("file.yaml").loadAll();
43  * ...
44  * --------------------
45  *
46  * Iterate over YAML documents in a file, lazily loading them:
47  * --------------------
48  * auto loader = Loader("file.yaml");
49  *
50  * foreach(ref node; loader)
51  * {
52  *     ...
53  * }
54  * --------------------
55  *
56  * Load YAML from a string:
57  * --------------------
58  * char[] yaml_input = "red:   '#ff0000'\n"
59  *                     "green: '#00ff00'\n"
60  *                     "blue:  '#0000ff'".dup;
61  *
62  * auto colors = Loader.fromString(yaml_input).load();
63  *
64  * foreach(string color, string value; colors)
65  * {
66  *     import std.stdio;
67  *     writeln(color, " is ", value, " in HTML/CSS");
68  * }
69  * --------------------
70  *
71  * Load a file into a buffer in memory and then load YAML from that buffer:
72  * --------------------
73  * try
74  * {
75  *     import std.file;
76  *     void[] buffer = std.file.read("file.yaml");
77  *     auto yamlNode = Loader(buffer);
78  *
79  *     // Read data from yamlNode here...
80  * }
81  * catch(FileException e)
82  * {
83  *     writeln("Failed to read file 'file.yaml'");
84  * }
85  * --------------------
86  *
87  * Use a custom constructor/resolver to support custom data types and/or implicit tags:
88  * --------------------
89  * auto constructor = new Constructor();
90  * auto resolver    = new Resolver();
91  *
92  * // Add constructor functions / resolver expressions here...
93  *
94  * auto loader = Loader("file.yaml");
95  * loader.constructor = constructor;
96  * loader.resolver    = resolver;
97  * auto rootNode      = loader.load(node);
98  * --------------------
99  */
100 struct Loader
101 {
102     private:
103         // Reads character data from a stream.
104         Reader reader_;
105         // Processes character data to YAML tokens.
106         Scanner scanner_;
107         // Processes tokens to YAML events.
108         Parser parser_;
109         // Resolves tags (data types).
110         Resolver resolver_;
111         // Constructs YAML data types.
112         Constructor constructor_;
113         // Name of the input file or stream, used in error messages.
114         string name_ = "<unknown>";
115         // Are we done loading?
116         bool done_ = false;
117 
118     public:
119         @disable this();
120         @disable int opCmp(ref Loader);
121         @disable bool opEquals(ref Loader);
122 
123         /** Construct a Loader to load YAML from a file.
124          *
125          * Params:  filename = Name of the file to load from.
126          *
127          * Throws:  YAMLException if the file could not be opened or read.
128          */
129         this(string filename) @trusted
130         {
131             name_ = filename;
132             try
133             {
134                 this(std.file.read(filename)); 
135             }
136             catch(FileException e)
137             {
138                 throw new YAMLException("Unable to open file %s for YAML loading: %s"
139                                         .format(filename, e.msg));
140             }
141         }
142 
143         /** Construct a Loader to load YAML from a string (char []).
144          *
145          * Params:  data = String to load YAML from. $(B will) be overwritten during
146          *                 parsing as D:YAML reuses memory. Use data.dup if you don't
147          *                 want to modify the original string.
148          *
149          * Returns: Loader loading YAML from given string.
150          *
151          * Throws:
152          *
153          * YAMLException if data could not be read (e.g. a decoding error)
154          */
155         static Loader fromString(char[] data) @safe
156         {
157             return Loader(cast(ubyte[])data);
158         }
159         ///
160         unittest
161         {
162             assert(Loader.fromString(cast(char[])"42").load().as!int == 42);
163         }
164 
165         /** Construct a Loader to load YAML from a buffer.
166          *
167          * Params: yamlData = Buffer with YAML data to load. This may be e.g. a file
168          *                    loaded to memory or a string with YAML data. Note that
169          *                    buffer $(B will) be overwritten, as D:YAML minimizes
170          *                    memory allocations by reusing the input _buffer.
171          *                    $(B Must not be deleted or modified by the user  as long
172          *                    as nodes loaded by this Loader are in use!) - Nodes may
173          *                    refer to data in this buffer.
174          *
175          * Note that D:YAML looks for byte-order-marks YAML files encoded in
176          * UTF-16/UTF-32 (and sometimes UTF-8) use to specify the encoding and
177          * endianness, so it should be enough to load an entire file to a buffer and
178          * pass it to D:YAML, regardless of Unicode encoding.
179          *
180          * Throws:  YAMLException if yamlData contains data illegal in YAML.
181          */
182         this(void[] yamlData) @trusted
183         {
184             try
185             {
186                 reader_      = new Reader(cast(ubyte[])yamlData);
187                 scanner_     = new Scanner(reader_);
188                 parser_      = new Parser(scanner_);
189             }
190             catch(YAMLException e)
191             {
192                 throw new YAMLException("Unable to open %s for YAML loading: %s"
193                                         .format(name_, e.msg));
194             }
195         }
196 
197         /// Destroy the Loader.
198         @trusted ~this()
199         {
200             reader_.destroy();
201             scanner_.destroy();
202             parser_.destroy();
203         }
204 
205         /// Set stream _name. Used in debugging messages.
206         void name(string name) pure @safe nothrow @nogc
207         {
208             name_ = name;
209         }
210 
211         /// Specify custom Resolver to use.
212         void resolver(Resolver resolver) pure @safe nothrow @nogc
213         {
214             resolver_ = resolver;
215         }
216 
217         /// Specify custom Constructor to use.
218         void constructor(Constructor constructor) pure @safe nothrow @nogc
219         {
220             constructor_ = constructor;
221         }
222 
223         /** Load single YAML document.
224          *
225          * If none or more than one YAML document is found, this throws a YAMLException.
226          *
227          * This can only be called once; this is enforced by contract.
228          *
229          * Returns: Root node of the document.
230          *
231          * Throws:  YAMLException if there wasn't exactly one document
232          *          or on a YAML parsing error.
233          */
234         Node load() @safe
235         in
236         {
237             assert(!done_, "Loader: Trying to load YAML twice");
238         }
239         body
240         {
241             try
242             {
243                 lazyInitConstructorResolver();
244                 scope(exit) { done_ = true; }
245                 auto composer = new Composer(parser_, resolver_, constructor_);
246                 enforce(composer.checkNode(), new YAMLException("No YAML document to load"));
247                 return composer.getSingleNode();
248             }
249             catch(YAMLException e)
250             {
251                 throw new YAMLException("Unable to load YAML from %s : %s"
252                                         .format(name_, e.msg));
253             }
254         }
255 
256         /** Load all YAML documents.
257          *
258          * This is just a shortcut that iterates over all documents and returns them
259          * all at once. Calling loadAll after iterating over the node or vice versa
260          * will not return any documents, as they have all been parsed already.
261          *
262          * This can only be called once; this is enforced by contract.
263          *
264          * Returns: Array of root nodes of all documents in the file/stream.
265          *
266          * Throws:  YAMLException on a parsing error.
267          */
268         Node[] loadAll() @trusted
269         {
270             Node[] nodes;
271             foreach(ref node; this) 
272             {
273                 nodes.assumeSafeAppend();
274                 nodes ~= node;
275             }
276             return nodes;
277         }
278 
279         /** Foreach over YAML documents.
280          *
281          * Parses documents lazily, when they are needed.
282          *
283          * Foreach over a Loader can only be used once; this is enforced by contract.
284          *
285          * Throws: YAMLException on a parsing error.
286          */
287         int opApply(int delegate(ref Node) dg) @trusted
288         in
289         {
290             assert(!done_, "Loader: Trying to load YAML twice");
291         }
292         body
293         {
294             scope(exit) { done_ = true; }
295             try
296             {
297                 lazyInitConstructorResolver();
298                 auto composer = new Composer(parser_, resolver_, constructor_);
299 
300                 int result = 0;
301                 while(composer.checkNode())
302                 {
303                     auto node = composer.getNode();
304                     result = dg(node);
305                     if(result) { break; }
306                 }
307 
308                 return result;
309             }
310             catch(YAMLException e)
311             {
312                 throw new YAMLException("Unable to load YAML from %s : %s "
313                                         .format(name_, e.msg));
314             }
315         }
316 
317     package:
318         // Scan and return all tokens. Used for debugging.
319         Token[] scan() @trusted
320         {
321             try
322             {
323                 Token[] result;
324                 while(scanner_.checkToken())
325                 {
326                     result.assumeSafeAppend();
327                     result ~= scanner_.getToken();
328                 }
329                 return result;
330             }
331             catch(YAMLException e)
332             {
333                 throw new YAMLException("Unable to scan YAML from stream " ~
334                                         name_ ~ " : " ~ e.msg);
335             }
336         }
337 
338         // Scan all tokens, throwing them away. Used for benchmarking.
339         void scanBench() @safe
340         {
341             try while(scanner_.checkToken())
342             {
343                 scanner_.getToken();
344             }
345             catch(YAMLException e)
346             {
347                 throw new YAMLException("Unable to scan YAML from stream " ~
348                                         name_ ~ " : " ~ e.msg);
349             }
350         }
351 
352 
353         // Parse and return all events. Used for debugging.
354         immutable(Event)[] parse() @safe
355         {
356             try
357             {
358                 immutable(Event)[] result;
359                 while(parser_.checkEvent())
360                 {
361                     result ~= parser_.getEvent();
362                 }
363                 return result;
364             }
365             catch(YAMLException e)
366             {
367                 throw new YAMLException("Unable to parse YAML from stream %s : %s "
368                                         .format(name_, e.msg));
369             }
370         }
371 
372         // Construct default constructor/resolver if the user has not yet specified
373         // their own.
374         void lazyInitConstructorResolver() @safe
375         {
376             if(resolver_ is null)    { resolver_    = new Resolver(); }
377             if(constructor_ is null) { constructor_ = new Constructor(); }
378         }
379 }
380 
381 unittest
382 {
383     char[] yaml_input = ("red:   '#ff0000'\n" ~
384                         "green: '#00ff00'\n" ~
385                         "blue:  '#0000ff'").dup;
386 
387     auto colors = Loader.fromString(yaml_input).load();
388 
389     foreach(string color, string value; colors)
390     {
391         import std.stdio;
392         writeln(color, " is ", value, " in HTML/CSS");
393     }
394 }