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)); 68 loader.name_ = filename; 69 return loader; 70 } 71 catch(FileException e) 72 { 73 throw new YAMLException("Unable to open file %s for YAML loading: %s" 74 .format(filename, e.msg), e.file, e.line); 75 } 76 } 77 /// ditto 78 static Loader fromFile(File file) @system 79 { 80 auto loader = Loader(file.byChunk(4096).join); 81 loader.name_ = file.name; 82 return loader; 83 } 84 85 /** Construct a Loader to load YAML from a string. 86 * 87 * Params: data = String to load YAML from. The char[] version $(B will) 88 * overwrite its input during parsing as D:YAML reuses memory. 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) @safe 97 { 98 return Loader(cast(ubyte[])data); 99 } 100 /// Ditto 101 static Loader fromString(string data) @safe 102 { 103 return fromString(data.dup); 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) @system 144 { 145 this(cast(ubyte[])yamlData); 146 } 147 /// Ditto 148 private this(ubyte[] yamlData) @safe 149 { 150 resolver_ = Resolver.withDefaultResolvers; 151 try 152 { 153 auto reader_ = new Reader(yamlData); 154 scanner_ = Scanner(reader_); 155 parser_ = new Parser(scanner_); 156 } 157 catch(YAMLException e) 158 { 159 throw new YAMLException("Unable to open %s for YAML loading: %s" 160 .format(name_, e.msg), e.file, e.line); 161 } 162 } 163 164 165 /// Set stream _name. Used in debugging messages. 166 void name(string name) pure @safe nothrow @nogc 167 { 168 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 }