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