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