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         // Processes character data to YAML tokens.
37         Scanner scanner_;
38         // Processes tokens to YAML events.
39         Parser parser_;
40         // Resolves tags (data types).
41         Resolver resolver_;
42         // Name of the input file or stream, used in error messages.
43         string name_ = "<unknown>";
44         // Are we done loading?
45         bool done_;
46         // Last node read from stream
47         Node currentNode;
48         // Has the range interface been initialized yet?
49         bool rangeInitialized;
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), filename);
68                 return loader;
69             }
70             catch(FileException e)
71             {
72                 throw new YAMLException("Unable to open file %s for YAML loading: %s"
73                                         .format(filename, e.msg), e.file, e.line);
74             }
75          }
76          /// ditto
77          static Loader fromFile(File file) @system
78          {
79             auto loader = Loader(file.byChunk(4096).join, file.name);
80             return loader;
81          }
82 
83         /** Construct a Loader to load YAML from a string.
84          *
85          * Params:  data = String to load YAML from. The char[] version $(B will)
86          *                 overwrite its input during parsing as D:YAML reuses memory.
87          *
88          * Returns: Loader loading YAML from given string.
89          *
90          * Throws:
91          *
92          * YAMLException if data could not be read (e.g. a decoding error)
93          */
94         static Loader fromString(char[] data) @safe
95         {
96             return Loader(cast(ubyte[])data);
97         }
98         /// Ditto
99         static Loader fromString(string data) @safe
100         {
101             return fromString(data.dup);
102         }
103         /// Load  a char[].
104         @safe unittest
105         {
106             assert(Loader.fromString("42".dup).load().as!int == 42);
107         }
108         /// Load a string.
109         @safe unittest
110         {
111             assert(Loader.fromString("42").load().as!int == 42);
112         }
113 
114         /** Construct a Loader to load YAML from a buffer.
115          *
116          * Params: yamlData = Buffer with YAML data to load. This may be e.g. a file
117          *                    loaded to memory or a string with YAML data. Note that
118          *                    buffer $(B will) be overwritten, as D:YAML minimizes
119          *                    memory allocations by reusing the input _buffer.
120          *                    $(B Must not be deleted or modified by the user  as long
121          *                    as nodes loaded by this Loader are in use!) - Nodes may
122          *                    refer to data in this buffer.
123          *
124          * Note that D:YAML looks for byte-order-marks YAML files encoded in
125          * UTF-16/UTF-32 (and sometimes UTF-8) use to specify the encoding and
126          * endianness, so it should be enough to load an entire file to a buffer and
127          * pass it to D:YAML, regardless of Unicode encoding.
128          *
129          * Throws:  YAMLException if yamlData contains data illegal in YAML.
130          */
131         static Loader fromBuffer(ubyte[] yamlData) @safe
132         {
133             return Loader(yamlData);
134         }
135         /// Ditto
136         static Loader fromBuffer(void[] yamlData) @system
137         {
138             return Loader(yamlData);
139         }
140         /// Ditto
141         private this(void[] yamlData, string name = "<unknown>") @system
142         {
143             this(cast(ubyte[])yamlData, name);
144         }
145         /// Ditto
146         private this(ubyte[] yamlData, string name = "<unknown>") @safe
147         {
148             resolver_ = Resolver.withDefaultResolvers;
149             name_ = name;
150             try
151             {
152                 auto reader_ = new Reader(yamlData, name);
153                 scanner_ = 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             scanner_.name = name;
169         }
170 
171         /// Specify custom Resolver to use.
172         auto ref resolver() pure @safe nothrow @nogc
173         {
174             return resolver_;
175         }
176 
177         /** Load single YAML document.
178          *
179          * If none or more than one YAML document is found, this throws a YAMLException.
180          *
181          * This can only be called once; this is enforced by contract.
182          *
183          * Returns: Root node of the document.
184          *
185          * Throws:  YAMLException if there wasn't exactly one document
186          *          or on a YAML parsing error.
187          */
188         Node load() @safe
189         {
190             enforce!YAMLException(!empty, "Zero documents in stream");
191             auto output = front;
192             popFront();
193             enforce!YAMLException(empty, "More than one document in stream");
194             return output;
195         }
196 
197         /** Implements the empty range primitive.
198         *
199         * If there's no more documents left in the stream, this will be true.
200         *
201         * Returns: `true` if no more documents left, `false` otherwise.
202         */
203         bool empty() @safe
204         {
205             // currentNode and done_ are both invalid until popFront is called once
206             if (!rangeInitialized)
207             {
208                 popFront();
209             }
210             return done_;
211         }
212         /** Implements the popFront range primitive.
213         *
214         * Reads the next document from the stream, if possible.
215         */
216         void popFront() @safe
217         {
218             // Composer initialization is done here in case the constructor is
219             // modified, which is a pretty common case.
220             static Composer composer;
221             if (!rangeInitialized)
222             {
223                 composer = Composer(parser_, resolver_);
224                 rangeInitialized = true;
225             }
226             assert(!done_, "Loader.popFront called on empty range");
227             if (composer.checkNode())
228             {
229                 currentNode = composer.getNode();
230             }
231             else
232             {
233                 done_ = true;
234             }
235         }
236         /** Implements the front range primitive.
237         *
238         * Returns: the current document as a Node.
239         */
240         Node front() @safe
241         {
242             // currentNode and done_ are both invalid until popFront is called once
243             if (!rangeInitialized)
244             {
245                 popFront();
246             }
247             return currentNode;
248         }
249 
250         // Scan all tokens, throwing them away. Used for benchmarking.
251         void scanBench() @safe
252         {
253             try
254             {
255                 while(!scanner_.empty)
256                 {
257                     scanner_.popFront();
258                 }
259             }
260             catch(YAMLException e)
261             {
262                 throw new YAMLException("Unable to scan YAML from stream " ~
263                                         name_ ~ " : " ~ e.msg, e.file, e.line);
264             }
265         }
266 
267 
268         // Parse and return all events. Used for debugging.
269         auto parse() @safe
270         {
271             return parser_;
272         }
273 }
274 /// Load single YAML document from a file:
275 @safe unittest
276 {
277     write("example.yaml", "Hello world!");
278     auto rootNode = Loader.fromFile("example.yaml").load();
279     assert(rootNode == "Hello world!");
280 }
281 /// Load single YAML document from an already-opened file:
282 @system unittest
283 {
284     // Open a temporary file
285     auto file = File.tmpfile;
286     // Write valid YAML
287     file.write("Hello world!");
288     // Return to the beginning
289     file.seek(0);
290     // Load document
291     auto rootNode = Loader.fromFile(file).load();
292     assert(rootNode == "Hello world!");
293 }
294 /// Load all YAML documents from a file:
295 @safe unittest
296 {
297     import std.array : array;
298     import std.file : write;
299     write("example.yaml",
300         "---\n"~
301         "Hello world!\n"~
302         "...\n"~
303         "---\n"~
304         "Hello world 2!\n"~
305         "...\n"
306     );
307     auto nodes = Loader.fromFile("example.yaml").array;
308     assert(nodes.length == 2);
309 }
310 /// Iterate over YAML documents in a file, lazily loading them:
311 @safe unittest
312 {
313     import std.file : write;
314     write("example.yaml",
315         "---\n"~
316         "Hello world!\n"~
317         "...\n"~
318         "---\n"~
319         "Hello world 2!\n"~
320         "...\n"
321     );
322     auto loader = Loader.fromFile("example.yaml");
323 
324     foreach(ref node; loader)
325     {
326         //Do something
327     }
328 }
329 /// Load YAML from a string:
330 @safe unittest
331 {
332     string yaml_input = ("red:   '#ff0000'\n" ~
333                         "green: '#00ff00'\n" ~
334                         "blue:  '#0000ff'");
335 
336     auto colors = Loader.fromString(yaml_input).load();
337 
338     foreach(string color, string value; colors)
339     {
340         // Do something with the color and its value...
341     }
342 }
343 
344 /// Load a file into a buffer in memory and then load YAML from that buffer:
345 @safe unittest
346 {
347     import std.file : read, write;
348     import std.stdio : writeln;
349     // Create a yaml document
350     write("example.yaml",
351         "---\n"~
352         "Hello world!\n"~
353         "...\n"~
354         "---\n"~
355         "Hello world 2!\n"~
356         "...\n"
357     );
358     try
359     {
360         string buffer = readText("example.yaml");
361         auto yamlNode = Loader.fromString(buffer);
362 
363         // Read data from yamlNode here...
364     }
365     catch(FileException e)
366     {
367         writeln("Failed to read file 'example.yaml'");
368     }
369 }
370 /// Use a custom resolver to support custom data types and/or implicit tags:
371 @safe unittest
372 {
373     import std.file : write;
374     // Create a yaml document
375     write("example.yaml",
376         "---\n"~
377         "Hello world!\n"~
378         "...\n"
379     );
380 
381     auto loader = Loader.fromFile("example.yaml");
382 
383     // Add resolver expressions here...
384     // loader.resolver.addImplicitResolver(...);
385 
386     auto rootNode = loader.load();
387 }
388 
389 //Issue #258 - https://github.com/dlang-community/D-YAML/issues/258
390 @safe unittest
391 {
392     auto yaml = "{\n\"root\": {\n\t\"key\": \"value\"\n    }\n}";
393     auto doc = Loader.fromString(yaml).load();
394     assert(doc.isValid);
395 }
396 
397 @safe unittest
398 {
399     import std.exception : collectException;
400 
401     auto yaml = q"EOS
402     value: invalid: string
403 EOS";
404     auto filename = "invalid.yml";
405     auto loader = Loader.fromString(yaml);
406     loader.name = filename;
407 
408     Node unused;
409     auto e = loader.load().collectException!ScannerException(unused);
410     assert(e.mark.name == filename);
411 }