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.string; 14 15 import dyaml.composer; 16 import dyaml.constructor; 17 import dyaml.event; 18 import dyaml.exception; 19 import dyaml.node; 20 import dyaml.parser; 21 import dyaml.reader; 22 import dyaml.resolver; 23 import dyaml.scanner; 24 import dyaml.token; 25 26 27 /** Loads YAML documents from files or char[]. 28 * 29 * User specified Constructor and/or Resolver can be used to support new 30 * tags / data types. 31 * 32 * Examples: 33 * 34 * Load single YAML document from a file: 35 * -------------------- 36 * auto rootNode = Loader("file.yaml").load(); 37 * ... 38 * -------------------- 39 * 40 * Load all YAML documents from a file: 41 * -------------------- 42 * auto nodes = Loader("file.yaml").loadAll(); 43 * ... 44 * -------------------- 45 * 46 * Iterate over YAML documents in a file, lazily loading them: 47 * -------------------- 48 * auto loader = Loader("file.yaml"); 49 * 50 * foreach(ref node; loader) 51 * { 52 * ... 53 * } 54 * -------------------- 55 * 56 * Load YAML from a string: 57 * -------------------- 58 * char[] yaml_input = "red: '#ff0000'\n" 59 * "green: '#00ff00'\n" 60 * "blue: '#0000ff'".dup; 61 * 62 * auto colors = Loader.fromString(yaml_input).load(); 63 * 64 * foreach(string color, string value; colors) 65 * { 66 * import std.stdio; 67 * writeln(color, " is ", value, " in HTML/CSS"); 68 * } 69 * -------------------- 70 * 71 * Load a file into a buffer in memory and then load YAML from that buffer: 72 * -------------------- 73 * try 74 * { 75 * import std.file; 76 * void[] buffer = std.file.read("file.yaml"); 77 * auto yamlNode = Loader(buffer); 78 * 79 * // Read data from yamlNode here... 80 * } 81 * catch(FileException e) 82 * { 83 * writeln("Failed to read file 'file.yaml'"); 84 * } 85 * -------------------- 86 * 87 * Use a custom constructor/resolver to support custom data types and/or implicit tags: 88 * -------------------- 89 * auto constructor = new Constructor(); 90 * auto resolver = new Resolver(); 91 * 92 * // Add constructor functions / resolver expressions here... 93 * 94 * auto loader = Loader("file.yaml"); 95 * loader.constructor = constructor; 96 * loader.resolver = resolver; 97 * auto rootNode = loader.load(node); 98 * -------------------- 99 */ 100 struct Loader 101 { 102 private: 103 // Reads character data from a stream. 104 Reader reader_; 105 // Processes character data to YAML tokens. 106 Scanner scanner_; 107 // Processes tokens to YAML events. 108 Parser parser_; 109 // Resolves tags (data types). 110 Resolver resolver_; 111 // Constructs YAML data types. 112 Constructor constructor_; 113 // Name of the input file or stream, used in error messages. 114 string name_ = "<unknown>"; 115 // Are we done loading? 116 bool done_ = false; 117 118 public: 119 @disable this(); 120 @disable int opCmp(ref Loader); 121 @disable bool opEquals(ref Loader); 122 123 /** Construct a Loader to load YAML from a file. 124 * 125 * Params: filename = Name of the file to load from. 126 * 127 * Throws: YAMLException if the file could not be opened or read. 128 */ 129 this(string filename) @trusted 130 { 131 name_ = filename; 132 try 133 { 134 this(std.file.read(filename)); 135 } 136 catch(FileException e) 137 { 138 throw new YAMLException("Unable to open file %s for YAML loading: %s" 139 .format(filename, e.msg)); 140 } 141 } 142 143 /** Construct a Loader to load YAML from a string (char []). 144 * 145 * Params: data = String to load YAML from. $(B will) be overwritten during 146 * parsing as D:YAML reuses memory. Use data.dup if you don't 147 * want to modify the original string. 148 * 149 * Returns: Loader loading YAML from given string. 150 * 151 * Throws: 152 * 153 * YAMLException if data could not be read (e.g. a decoding error) 154 */ 155 static Loader fromString(char[] data) @safe 156 { 157 return Loader(cast(ubyte[])data); 158 } 159 /// 160 unittest 161 { 162 assert(Loader.fromString(cast(char[])"42").load().as!int == 42); 163 } 164 165 /** Construct a Loader to load YAML from a buffer. 166 * 167 * Params: yamlData = Buffer with YAML data to load. This may be e.g. a file 168 * loaded to memory or a string with YAML data. Note that 169 * buffer $(B will) be overwritten, as D:YAML minimizes 170 * memory allocations by reusing the input _buffer. 171 * $(B Must not be deleted or modified by the user as long 172 * as nodes loaded by this Loader are in use!) - Nodes may 173 * refer to data in this buffer. 174 * 175 * Note that D:YAML looks for byte-order-marks YAML files encoded in 176 * UTF-16/UTF-32 (and sometimes UTF-8) use to specify the encoding and 177 * endianness, so it should be enough to load an entire file to a buffer and 178 * pass it to D:YAML, regardless of Unicode encoding. 179 * 180 * Throws: YAMLException if yamlData contains data illegal in YAML. 181 */ 182 this(void[] yamlData) @trusted 183 { 184 try 185 { 186 reader_ = new Reader(cast(ubyte[])yamlData); 187 scanner_ = new Scanner(reader_); 188 parser_ = new Parser(scanner_); 189 } 190 catch(YAMLException e) 191 { 192 throw new YAMLException("Unable to open %s for YAML loading: %s" 193 .format(name_, e.msg)); 194 } 195 } 196 197 /// Destroy the Loader. 198 @trusted ~this() 199 { 200 reader_.destroy(); 201 scanner_.destroy(); 202 parser_.destroy(); 203 } 204 205 /// Set stream _name. Used in debugging messages. 206 void name(string name) pure @safe nothrow @nogc 207 { 208 name_ = name; 209 } 210 211 /// Specify custom Resolver to use. 212 void resolver(Resolver resolver) pure @safe nothrow @nogc 213 { 214 resolver_ = resolver; 215 } 216 217 /// Specify custom Constructor to use. 218 void constructor(Constructor constructor) pure @safe nothrow @nogc 219 { 220 constructor_ = constructor; 221 } 222 223 /** Load single YAML document. 224 * 225 * If none or more than one YAML document is found, this throws a YAMLException. 226 * 227 * This can only be called once; this is enforced by contract. 228 * 229 * Returns: Root node of the document. 230 * 231 * Throws: YAMLException if there wasn't exactly one document 232 * or on a YAML parsing error. 233 */ 234 Node load() @safe 235 in 236 { 237 assert(!done_, "Loader: Trying to load YAML twice"); 238 } 239 body 240 { 241 try 242 { 243 lazyInitConstructorResolver(); 244 scope(exit) { done_ = true; } 245 auto composer = new Composer(parser_, resolver_, constructor_); 246 enforce(composer.checkNode(), new YAMLException("No YAML document to load")); 247 return composer.getSingleNode(); 248 } 249 catch(YAMLException e) 250 { 251 throw new YAMLException("Unable to load YAML from %s : %s" 252 .format(name_, e.msg)); 253 } 254 } 255 256 /** Load all YAML documents. 257 * 258 * This is just a shortcut that iterates over all documents and returns them 259 * all at once. Calling loadAll after iterating over the node or vice versa 260 * will not return any documents, as they have all been parsed already. 261 * 262 * This can only be called once; this is enforced by contract. 263 * 264 * Returns: Array of root nodes of all documents in the file/stream. 265 * 266 * Throws: YAMLException on a parsing error. 267 */ 268 Node[] loadAll() @trusted 269 { 270 Node[] nodes; 271 foreach(ref node; this) 272 { 273 nodes.assumeSafeAppend(); 274 nodes ~= node; 275 } 276 return nodes; 277 } 278 279 /** Foreach over YAML documents. 280 * 281 * Parses documents lazily, when they are needed. 282 * 283 * Foreach over a Loader can only be used once; this is enforced by contract. 284 * 285 * Throws: YAMLException on a parsing error. 286 */ 287 int opApply(int delegate(ref Node) dg) @trusted 288 in 289 { 290 assert(!done_, "Loader: Trying to load YAML twice"); 291 } 292 body 293 { 294 scope(exit) { done_ = true; } 295 try 296 { 297 lazyInitConstructorResolver(); 298 auto composer = new Composer(parser_, resolver_, constructor_); 299 300 int result = 0; 301 while(composer.checkNode()) 302 { 303 auto node = composer.getNode(); 304 result = dg(node); 305 if(result) { break; } 306 } 307 308 return result; 309 } 310 catch(YAMLException e) 311 { 312 throw new YAMLException("Unable to load YAML from %s : %s " 313 .format(name_, e.msg)); 314 } 315 } 316 317 package: 318 // Scan and return all tokens. Used for debugging. 319 Token[] scan() @trusted 320 { 321 try 322 { 323 Token[] result; 324 while(scanner_.checkToken()) 325 { 326 result.assumeSafeAppend(); 327 result ~= scanner_.getToken(); 328 } 329 return result; 330 } 331 catch(YAMLException e) 332 { 333 throw new YAMLException("Unable to scan YAML from stream " ~ 334 name_ ~ " : " ~ e.msg); 335 } 336 } 337 338 // Scan all tokens, throwing them away. Used for benchmarking. 339 void scanBench() @safe 340 { 341 try while(scanner_.checkToken()) 342 { 343 scanner_.getToken(); 344 } 345 catch(YAMLException e) 346 { 347 throw new YAMLException("Unable to scan YAML from stream " ~ 348 name_ ~ " : " ~ e.msg); 349 } 350 } 351 352 353 // Parse and return all events. Used for debugging. 354 immutable(Event)[] parse() @safe 355 { 356 try 357 { 358 immutable(Event)[] result; 359 while(parser_.checkEvent()) 360 { 361 result ~= parser_.getEvent(); 362 } 363 return result; 364 } 365 catch(YAMLException e) 366 { 367 throw new YAMLException("Unable to parse YAML from stream %s : %s " 368 .format(name_, e.msg)); 369 } 370 } 371 372 // Construct default constructor/resolver if the user has not yet specified 373 // their own. 374 void lazyInitConstructorResolver() @safe 375 { 376 if(resolver_ is null) { resolver_ = new Resolver(); } 377 if(constructor_ is null) { constructor_ = new Constructor(); } 378 } 379 } 380 381 unittest 382 { 383 char[] yaml_input = ("red: '#ff0000'\n" ~ 384 "green: '#00ff00'\n" ~ 385 "blue: '#0000ff'").dup; 386 387 auto colors = Loader.fromString(yaml_input).load(); 388 389 foreach(string color, string value; colors) 390 { 391 import std.stdio; 392 writeln(color, " is ", value, " in HTML/CSS"); 393 } 394 }