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