Newer
Older
dub_jkp / source / dyaml / loader.d
  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. }