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 }