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