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 }