Newer
Older
dub_jkp / source / dyaml / composer.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. /**
  8. * Composes nodes from YAML events provided by parser.
  9. * Code based on PyYAML: http://www.pyyaml.org
  10. */
  11. module dyaml.composer;
  12.  
  13. import core.memory;
  14.  
  15. import std.algorithm;
  16. import std.array;
  17. import std.conv;
  18. import std.exception;
  19. import std.format;
  20. import std.range;
  21. import std.typecons;
  22.  
  23. import dyaml.constructor;
  24. import dyaml.event;
  25. import dyaml.exception;
  26. import dyaml.node;
  27. import dyaml.parser;
  28. import dyaml.resolver;
  29.  
  30.  
  31. package:
  32. /**
  33. * Exception thrown at composer errors.
  34. *
  35. * See_Also: MarkedYAMLException
  36. */
  37. class ComposerException : MarkedYAMLException
  38. {
  39. mixin MarkedExceptionCtors;
  40. }
  41.  
  42. ///Composes YAML documents from events provided by a Parser.
  43. struct Composer
  44. {
  45. private:
  46. ///Parser providing YAML events.
  47. Parser parser_;
  48. ///Resolver resolving tags (data types).
  49. Resolver resolver_;
  50. ///Nodes associated with anchors. Used by YAML aliases.
  51. Node[string] anchors_;
  52.  
  53. ///Used to reduce allocations when creating pair arrays.
  54. ///
  55. ///We need one appender for each nesting level that involves
  56. ///a pair array, as the inner levels are processed as a
  57. ///part of the outer levels. Used as a stack.
  58. Appender!(Node.Pair[])[] pairAppenders_;
  59. ///Used to reduce allocations when creating node arrays.
  60. ///
  61. ///We need one appender for each nesting level that involves
  62. ///a node array, as the inner levels are processed as a
  63. ///part of the outer levels. Used as a stack.
  64. Appender!(Node[])[] nodeAppenders_;
  65.  
  66. public:
  67. /**
  68. * Construct a composer.
  69. *
  70. * Params: parser = Parser to provide YAML events.
  71. * resolver = Resolver to resolve tags (data types).
  72. */
  73. this(Parser parser, Resolver resolver) @safe
  74. {
  75. parser_ = parser;
  76. resolver_ = resolver;
  77. }
  78.  
  79. /**
  80. * Determine if there are any nodes left.
  81. *
  82. * Must be called before loading as it handles the stream start event.
  83. */
  84. bool checkNode() @safe
  85. {
  86. // If next event is stream start, skip it
  87. parser_.skipOver!"a.id == b"(EventID.streamStart);
  88.  
  89. //True if there are more documents available.
  90. return parser_.front.id != EventID.streamEnd;
  91. }
  92.  
  93. ///Get a YAML document as a node (the root of the document).
  94. Node getNode() @safe
  95. {
  96. //Get the root node of the next document.
  97. assert(parser_.front.id != EventID.streamEnd,
  98. "Trying to get a node from Composer when there is no node to " ~
  99. "get. use checkNode() to determine if there is a node.");
  100.  
  101. return composeDocument();
  102. }
  103.  
  104. private:
  105.  
  106. void skipExpected(const EventID id) @safe
  107. {
  108. const foundExpected = parser_.skipOver!"a.id == b"(id);
  109. assert(foundExpected, text("Expected ", id, " not found."));
  110. }
  111. ///Ensure that appenders for specified nesting levels exist.
  112. ///
  113. ///Params: pairAppenderLevel = Current level in the pair appender stack.
  114. /// nodeAppenderLevel = Current level the node appender stack.
  115. void ensureAppendersExist(const uint pairAppenderLevel, const uint nodeAppenderLevel)
  116. @safe
  117. {
  118. while(pairAppenders_.length <= pairAppenderLevel)
  119. {
  120. pairAppenders_ ~= appender!(Node.Pair[])();
  121. }
  122. while(nodeAppenders_.length <= nodeAppenderLevel)
  123. {
  124. nodeAppenders_ ~= appender!(Node[])();
  125. }
  126. }
  127.  
  128. ///Compose a YAML document and return its root node.
  129. Node composeDocument() @safe
  130. {
  131. skipExpected(EventID.documentStart);
  132.  
  133. //Compose the root node.
  134. Node node = composeNode(0, 0);
  135.  
  136. skipExpected(EventID.documentEnd);
  137.  
  138. anchors_.destroy();
  139. return node;
  140. }
  141.  
  142. /// Compose a node.
  143. ///
  144. /// Params: pairAppenderLevel = Current level of the pair appender stack.
  145. /// nodeAppenderLevel = Current level of the node appender stack.
  146. Node composeNode(const uint pairAppenderLevel, const uint nodeAppenderLevel) @safe
  147. {
  148. if(parser_.front.id == EventID.alias_)
  149. {
  150. const event = parser_.front;
  151. parser_.popFront();
  152. const anchor = event.anchor;
  153. enforce((anchor in anchors_) !is null,
  154. new ComposerException("Found undefined alias: " ~ anchor,
  155. event.startMark));
  156.  
  157. //If the node referenced by the anchor is uninitialized,
  158. //it's not finished, i.e. we're currently composing it
  159. //and trying to use it recursively here.
  160. enforce(anchors_[anchor] != Node(),
  161. new ComposerException("Found recursive alias: " ~ anchor,
  162. event.startMark));
  163.  
  164. return anchors_[anchor];
  165. }
  166.  
  167. const event = parser_.front;
  168. const anchor = event.anchor;
  169. if((anchor !is null) && (anchor in anchors_) !is null)
  170. {
  171. throw new ComposerException("Found duplicate anchor: " ~ anchor,
  172. event.startMark);
  173. }
  174.  
  175. Node result;
  176. //Associate the anchor, if any, with an uninitialized node.
  177. //used to detect duplicate and recursive anchors.
  178. if(anchor !is null)
  179. {
  180. anchors_[anchor] = Node();
  181. }
  182.  
  183. switch (parser_.front.id)
  184. {
  185. case EventID.scalar:
  186. result = composeScalarNode();
  187. break;
  188. case EventID.sequenceStart:
  189. result = composeSequenceNode(pairAppenderLevel, nodeAppenderLevel);
  190. break;
  191. case EventID.mappingStart:
  192. result = composeMappingNode(pairAppenderLevel, nodeAppenderLevel);
  193. break;
  194. default: assert(false, "This code should never be reached");
  195. }
  196.  
  197. if(anchor !is null)
  198. {
  199. anchors_[anchor] = result;
  200. }
  201. return result;
  202. }
  203.  
  204. ///Compose a scalar node.
  205. Node composeScalarNode() @safe
  206. {
  207. const event = parser_.front;
  208. parser_.popFront();
  209. const tag = resolver_.resolve(NodeID.scalar, event.tag, event.value,
  210. event.implicit);
  211.  
  212. Node node = constructNode(event.startMark, event.endMark, tag,
  213. event.value);
  214. node.scalarStyle = event.scalarStyle;
  215.  
  216. return node;
  217. }
  218.  
  219. /// Compose a sequence node.
  220. ///
  221. /// Params: pairAppenderLevel = Current level of the pair appender stack.
  222. /// nodeAppenderLevel = Current level of the node appender stack.
  223. Node composeSequenceNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
  224. @safe
  225. {
  226. ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
  227. auto nodeAppender = &(nodeAppenders_[nodeAppenderLevel]);
  228.  
  229. const startEvent = parser_.front;
  230. parser_.popFront();
  231. const tag = resolver_.resolve(NodeID.sequence, startEvent.tag, null,
  232. startEvent.implicit);
  233.  
  234. while(parser_.front.id != EventID.sequenceEnd)
  235. {
  236. nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1));
  237. }
  238.  
  239. Node node = constructNode(startEvent.startMark, parser_.front.endMark,
  240. tag, nodeAppender.data.dup);
  241. node.collectionStyle = startEvent.collectionStyle;
  242. parser_.popFront();
  243. nodeAppender.clear();
  244.  
  245. return node;
  246. }
  247.  
  248. /**
  249. * Flatten a node, merging it with nodes referenced through YAMLMerge data type.
  250. *
  251. * Node must be a mapping or a sequence of mappings.
  252. *
  253. * Params: root = Node to flatten.
  254. * startMark = Start position of the node.
  255. * endMark = End position of the node.
  256. * pairAppenderLevel = Current level of the pair appender stack.
  257. * nodeAppenderLevel = Current level of the node appender stack.
  258. *
  259. * Returns: Flattened mapping as pairs.
  260. */
  261. Node.Pair[] flatten(ref Node root, const Mark startMark, const Mark endMark,
  262. const uint pairAppenderLevel, const uint nodeAppenderLevel) @safe
  263. {
  264. void error(Node node)
  265. {
  266. //this is Composer, but the code is related to Constructor.
  267. throw new ConstructorException("While constructing a mapping, " ~
  268. "expected a mapping or a list of " ~
  269. "mappings for merging, but found: " ~
  270. text(node.type) ~
  271. " NOTE: line/column shows topmost parent " ~
  272. "to which the content is being merged",
  273. startMark, endMark);
  274. }
  275.  
  276. ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
  277. auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
  278.  
  279. final switch (root.nodeID)
  280. {
  281. case NodeID.mapping:
  282. Node[] toMerge;
  283. toMerge.reserve(root.length);
  284. foreach (ref Node key, ref Node value; root)
  285. {
  286. if(key.type == NodeType.merge)
  287. {
  288. toMerge ~= value;
  289. }
  290. else
  291. {
  292. auto temp = Node.Pair(key, value);
  293. pairAppender.put(temp);
  294. }
  295. }
  296. foreach (node; toMerge)
  297. {
  298. pairAppender.put(flatten(node, startMark, endMark,
  299. pairAppenderLevel + 1, nodeAppenderLevel));
  300. }
  301. break;
  302. case NodeID.sequence:
  303. foreach (ref Node node; root)
  304. {
  305. if (node.nodeID != NodeID.mapping)
  306. {
  307. error(node);
  308. }
  309. pairAppender.put(flatten(node, startMark, endMark,
  310. pairAppenderLevel + 1, nodeAppenderLevel));
  311. }
  312. break;
  313. case NodeID.scalar:
  314. case NodeID.invalid:
  315. error(root);
  316. break;
  317. }
  318.  
  319. auto flattened = pairAppender.data.dup;
  320. pairAppender.clear();
  321.  
  322. return flattened;
  323. }
  324.  
  325. /// Compose a mapping node.
  326. ///
  327. /// Params: pairAppenderLevel = Current level of the pair appender stack.
  328. /// nodeAppenderLevel = Current level of the node appender stack.
  329. Node composeMappingNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
  330. @safe
  331. {
  332. ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
  333. const startEvent = parser_.front;
  334. parser_.popFront();
  335. const tag = resolver_.resolve(NodeID.mapping, startEvent.tag, null,
  336. startEvent.implicit);
  337. auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
  338.  
  339. Tuple!(Node, Mark)[] toMerge;
  340. while(parser_.front.id != EventID.mappingEnd)
  341. {
  342. auto pair = Node.Pair(composeNode(pairAppenderLevel + 1, nodeAppenderLevel),
  343. composeNode(pairAppenderLevel + 1, nodeAppenderLevel));
  344.  
  345. //Need to flatten and merge the node referred by YAMLMerge.
  346. if(pair.key.type == NodeType.merge)
  347. {
  348. toMerge ~= tuple(pair.value, cast(Mark)parser_.front.endMark);
  349. }
  350. //Not YAMLMerge, just add the pair.
  351. else
  352. {
  353. pairAppender.put(pair);
  354. }
  355. }
  356. foreach(node; toMerge)
  357. {
  358. merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1],
  359. pairAppenderLevel + 1, nodeAppenderLevel));
  360. }
  361.  
  362. auto sorted = pairAppender.data.dup.sort!((x,y) => x.key > y.key);
  363. if (sorted.length) {
  364. foreach (index, const ref value; sorted[0 .. $ - 1].enumerate)
  365. if (value.key == sorted[index + 1].key) {
  366. const message = () @trusted {
  367. return format("Key '%s' appears multiple times in mapping (first: %s)",
  368. value.key.get!string, value.key.startMark);
  369. }();
  370. throw new ComposerException(message, sorted[index + 1].key.startMark);
  371. }
  372. }
  373.  
  374. Node node = constructNode(startEvent.startMark, parser_.front.endMark,
  375. tag, pairAppender.data.dup);
  376. node.collectionStyle = startEvent.collectionStyle;
  377. parser_.popFront();
  378.  
  379. pairAppender.clear();
  380. return node;
  381. }
  382. }
  383.  
  384. // Provide good error message on multiple keys (which JSON supports)
  385. // DUB: This unittest is `@safe` from v2.100 as `message` was made `@safe`, not before
  386. unittest
  387. {
  388. import dyaml.loader : Loader;
  389.  
  390. const str = `{
  391. "comment": "This is a common technique",
  392. "name": "foobar",
  393. "comment": "To write down comments pre-JSON5"
  394. }`;
  395.  
  396. try
  397. auto node = Loader.fromString(str).load();
  398. catch (ComposerException exc)
  399. assert(exc.message() ==
  400. "Key 'comment' appears multiple times in mapping " ~
  401. "(first: file <unknown>,line 2,column 5)\nfile <unknown>,line 4,column 5");
  402. }