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:
86          *   data = String to load YAML from. The char[] version $(B will)
87          *          overwrite its input during parsing as D:YAML reuses memory.
88          *   filename = The filename to give to the Loader, defaults to `"<unknown>"`
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, string filename = "<unknown>") @safe
97         {
98             return Loader(cast(ubyte[])data, filename);
99         }
100         /// Ditto
101         static Loader fromString(string data, string filename = "<unknown>") @safe
102         {
103             return fromString(data.dup, filename);
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, string name = "<unknown>") @system
144         {
145             this(cast(ubyte[])yamlData, name);
146         }
147         /// Ditto
148         private this(ubyte[] yamlData, string name = "<unknown>") @safe
149         {
150             resolver_ = Resolver.withDefaultResolvers;
151             name_ = name;
152             try
153             {
154                 auto reader_ = new Reader(yamlData, name);
155                 scanner_ = Scanner(reader_);
156                 parser_ = new Parser(scanner_);
157             }
158             catch(YAMLException e)
159             {
160                 throw new YAMLException("Unable to open %s for YAML loading: %s"
161                                         .format(name_, e.msg), e.file, e.line);
162             }
163         }
164 
165 
166         /// Set stream _name. Used in debugging messages.
167         void name(string name) pure @safe nothrow @nogc
168         {
169             name_ = name;
170             scanner_.name = name;
171         }
172 
173         /// Specify custom Resolver to use.
174         auto ref resolver() pure @safe nothrow @nogc
175         {
176             return resolver_;
177         }
178 
179         /** Load single YAML document.
180          *
181          * If none or more than one YAML document is found, this throws a YAMLException.
182          *
183          * This can only be called once; this is enforced by contract.
184          *
185          * Returns: Root node of the document.
186          *
187          * Throws:  YAMLException if there wasn't exactly one document
188          *          or on a YAML parsing error.
189          */
190         Node load() @safe
191         {
192             enforce!YAMLException(!empty, "Zero documents in stream");
193             auto output = front;
194             popFront();
195             enforce!YAMLException(empty, "More than one document in stream");
196             return output;
197         }
198 
199         /** Implements the empty range primitive.
200         *
201         * If there's no more documents left in the stream, this will be true.
202         *
203         * Returns: `true` if no more documents left, `false` otherwise.
204         */
205         bool empty() @safe
206         {
207             // currentNode and done_ are both invalid until popFront is called once
208             if (!rangeInitialized)
209             {
210                 popFront();
211             }
212             return done_;
213         }
214         /** Implements the popFront range primitive.
215         *
216         * Reads the next document from the stream, if possible.
217         */
218         void popFront() @safe
219         {
220             // Composer initialization is done here in case the constructor is
221             // modified, which is a pretty common case.
222             static Composer composer;
223             if (!rangeInitialized)
224             {
225                 composer = Composer(parser_, resolver_);
226                 rangeInitialized = true;
227             }
228             assert(!done_, "Loader.popFront called on empty range");
229             if (composer.checkNode())
230             {
231                 currentNode = composer.getNode();
232             }
233             else
234             {
235                 done_ = true;
236             }
237         }
238         /** Implements the front range primitive.
239         *
240         * Returns: the current document as a Node.
241         */
242         Node front() @safe
243         {
244             // currentNode and done_ are both invalid until popFront is called once
245             if (!rangeInitialized)
246             {
247                 popFront();
248             }
249             return currentNode;
250         }
251 
252         // Scan all tokens, throwing them away. Used for benchmarking.
253         void scanBench() @safe
254         {
255             try
256             {
257                 while(!scanner_.empty)
258                 {
259                     scanner_.popFront();
260                 }
261             }
262             catch(YAMLException e)
263             {
264                 throw new YAMLException("Unable to scan YAML from stream " ~
265                                         name_ ~ " : " ~ e.msg, e.file, e.line);
266             }
267         }
268 
269 
270         // Parse and return all events. Used for debugging.
271         auto parse() @safe
272         {
273             return parser_;
274         }
275 }
276 /// Load single YAML document from a file:
277 @safe unittest
278 {
279     write("example.yaml", "Hello world!");
280     auto rootNode = Loader.fromFile("example.yaml").load();
281     assert(rootNode == "Hello world!");
282 }
283 /// Load single YAML document from an already-opened file:
284 @system unittest
285 {
286     // Open a temporary file
287     auto file = File.tmpfile;
288     // Write valid YAML
289     file.write("Hello world!");
290     // Return to the beginning
291     file.seek(0);
292     // Load document
293     auto rootNode = Loader.fromFile(file).load();
294     assert(rootNode == "Hello world!");
295 }
296 /// Load all YAML documents from a file:
297 @safe unittest
298 {
299     import std.array : array;
300     import std.file : write;
301     write("example.yaml",
302         "---\n"~
303         "Hello world!\n"~
304         "...\n"~
305         "---\n"~
306         "Hello world 2!\n"~
307         "...\n"
308     );
309     auto nodes = Loader.fromFile("example.yaml").array;
310     assert(nodes.length == 2);
311 }
312 /// Iterate over YAML documents in a file, lazily loading them:
313 @safe unittest
314 {
315     import std.file : write;
316     write("example.yaml",
317         "---\n"~
318         "Hello world!\n"~
319         "...\n"~
320         "---\n"~
321         "Hello world 2!\n"~
322         "...\n"
323     );
324     auto loader = Loader.fromFile("example.yaml");
325 
326     foreach(ref node; loader)
327     {
328         //Do something
329     }
330 }
331 /// Load YAML from a string:
332 @safe unittest
333 {
334     string yaml_input = ("red:   '#ff0000'\n" ~
335                         "green: '#00ff00'\n" ~
336                         "blue:  '#0000ff'");
337 
338     auto colors = Loader.fromString(yaml_input).load();
339 
340     foreach(string color, string value; colors)
341     {
342         // Do something with the color and its value...
343     }
344 }
345 
346 /// Load a file into a buffer in memory and then load YAML from that buffer:
347 @safe unittest
348 {
349     import std.file : read, write;
350     import std.stdio : writeln;
351     // Create a yaml document
352     write("example.yaml",
353         "---\n"~
354         "Hello world!\n"~
355         "...\n"~
356         "---\n"~
357         "Hello world 2!\n"~
358         "...\n"
359     );
360     try
361     {
362         string buffer = readText("example.yaml");
363         auto yamlNode = Loader.fromString(buffer);
364 
365         // Read data from yamlNode here...
366     }
367     catch(FileException e)
368     {
369         writeln("Failed to read file 'example.yaml'");
370     }
371 }
372 /// Use a custom resolver to support custom data types and/or implicit tags:
373 @safe unittest
374 {
375     import std.file : write;
376     // Create a yaml document
377     write("example.yaml",
378         "---\n"~
379         "Hello world!\n"~
380         "...\n"
381     );
382 
383     auto loader = Loader.fromFile("example.yaml");
384 
385     // Add resolver expressions here...
386     // loader.resolver.addImplicitResolver(...);
387 
388     auto rootNode = loader.load();
389 }
390 
391 //Issue #258 - https://github.com/dlang-community/D-YAML/issues/258
392 @safe unittest
393 {
394     auto yaml = "{\n\"root\": {\n\t\"key\": \"value\"\n    }\n}";
395     auto doc = Loader.fromString(yaml).load();
396     assert(doc.isValid);
397 }
398 
399 @safe unittest
400 {
401     import std.exception : collectException;
402 
403     auto yaml = q"EOS
404     value: invalid: string
405 EOS";
406     auto filename = "invalid.yml";
407     auto loader = Loader.fromString(yaml);
408     loader.name = filename;
409 
410     Node unused;
411     auto e = loader.load().collectException!ScannerException(unused);
412     assert(e.mark.name == filename);
413 }