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