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 }