394 lines
14 KiB
D
394 lines
14 KiB
D
|
|
// Copyright Ferdinand Majerech 2011.
|
|
// Distributed under the Boost Software License, Version 1.0.
|
|
// (See accompanying file LICENSE_1_0.txt or copy at
|
|
// http://www.boost.org/LICENSE_1_0.txt)
|
|
|
|
/**
|
|
* Composes nodes from YAML events provided by parser.
|
|
* Code based on PyYAML: http://www.pyyaml.org
|
|
*/
|
|
module dyaml.composer;
|
|
|
|
import core.memory;
|
|
|
|
import std.array;
|
|
import std.conv;
|
|
import std.exception;
|
|
import std.typecons;
|
|
|
|
import dyaml.anchor;
|
|
import dyaml.constructor;
|
|
import dyaml.event;
|
|
import dyaml.exception;
|
|
import dyaml.node;
|
|
import dyaml.parser;
|
|
import dyaml.resolver;
|
|
|
|
|
|
package:
|
|
/**
|
|
* Exception thrown at composer errors.
|
|
*
|
|
* See_Also: MarkedYAMLException
|
|
*/
|
|
class ComposerException : MarkedYAMLException
|
|
{
|
|
mixin MarkedExceptionCtors;
|
|
}
|
|
|
|
///Composes YAML documents from events provided by a Parser.
|
|
final class Composer
|
|
{
|
|
private:
|
|
///Parser providing YAML events.
|
|
Parser parser_;
|
|
///Resolver resolving tags (data types).
|
|
Resolver resolver_;
|
|
///Constructor constructing YAML values.
|
|
Constructor constructor_;
|
|
///Nodes associated with anchors. Used by YAML aliases.
|
|
Node[Anchor] anchors_;
|
|
|
|
///Used to reduce allocations when creating pair arrays.
|
|
///
|
|
///We need one appender for each nesting level that involves
|
|
///a pair array, as the inner levels are processed as a
|
|
///part of the outer levels. Used as a stack.
|
|
Appender!(Node.Pair[], Node.Pair)[] pairAppenders_;
|
|
///Used to reduce allocations when creating node arrays.
|
|
///
|
|
///We need one appender for each nesting level that involves
|
|
///a node array, as the inner levels are processed as a
|
|
///part of the outer levels. Used as a stack.
|
|
Appender!(Node[], Node)[] nodeAppenders_;
|
|
|
|
public:
|
|
/**
|
|
* Construct a composer.
|
|
*
|
|
* Params: parser = Parser to provide YAML events.
|
|
* resolver = Resolver to resolve tags (data types).
|
|
* constructor = Constructor to construct nodes.
|
|
*/
|
|
this(Parser parser, Resolver resolver, Constructor constructor) @safe
|
|
{
|
|
parser_ = parser;
|
|
resolver_ = resolver;
|
|
constructor_ = constructor;
|
|
}
|
|
|
|
///Destroy the composer.
|
|
pure @safe nothrow ~this()
|
|
{
|
|
parser_ = null;
|
|
resolver_ = null;
|
|
constructor_ = null;
|
|
anchors_.destroy();
|
|
anchors_ = null;
|
|
}
|
|
|
|
/**
|
|
* Determine if there are any nodes left.
|
|
*
|
|
* Must be called before loading as it handles the stream start event.
|
|
*/
|
|
bool checkNode() @safe
|
|
{
|
|
//Drop the STREAM-START event.
|
|
if(parser_.checkEvent(EventID.StreamStart))
|
|
{
|
|
parser_.getEvent();
|
|
}
|
|
|
|
//True if there are more documents available.
|
|
return !parser_.checkEvent(EventID.StreamEnd);
|
|
}
|
|
|
|
///Get a YAML document as a node (the root of the document).
|
|
Node getNode() @safe
|
|
{
|
|
//Get the root node of the next document.
|
|
assert(!parser_.checkEvent(EventID.StreamEnd),
|
|
"Trying to get a node from Composer when there is no node to "
|
|
"get. use checkNode() to determine if there is a node.");
|
|
|
|
return composeDocument();
|
|
}
|
|
|
|
///Get single YAML document, throwing if there is more than one document.
|
|
Node getSingleNode() @trusted
|
|
{
|
|
assert(!parser_.checkEvent(EventID.StreamEnd),
|
|
"Trying to get a node from Composer when there is no node to "
|
|
"get. use checkNode() to determine if there is a node.");
|
|
|
|
Node document = composeDocument();
|
|
|
|
//Ensure that the stream contains no more documents.
|
|
enforce(parser_.checkEvent(EventID.StreamEnd),
|
|
new ComposerException("Expected single document in the stream, "
|
|
"but found another document.",
|
|
parser_.getEvent().startMark));
|
|
|
|
//Drop the STREAM-END event.
|
|
parser_.getEvent();
|
|
|
|
return document;
|
|
}
|
|
|
|
private:
|
|
///Ensure that appenders for specified nesting levels exist.
|
|
///
|
|
///Params: pairAppenderLevel = Current level in the pair appender stack.
|
|
/// nodeAppenderLevel = Current level the node appender stack.
|
|
void ensureAppendersExist(const uint pairAppenderLevel, const uint nodeAppenderLevel)
|
|
@trusted
|
|
{
|
|
while(pairAppenders_.length <= pairAppenderLevel)
|
|
{
|
|
pairAppenders_ ~= appender!(Node.Pair[])();
|
|
}
|
|
while(nodeAppenders_.length <= nodeAppenderLevel)
|
|
{
|
|
nodeAppenders_ ~= appender!(Node[])();
|
|
}
|
|
}
|
|
|
|
///Compose a YAML document and return its root node.
|
|
Node composeDocument() @trusted
|
|
{
|
|
//Drop the DOCUMENT-START event.
|
|
parser_.getEvent();
|
|
|
|
//Compose the root node.
|
|
Node node = composeNode(0, 0);
|
|
|
|
//Drop the DOCUMENT-END event.
|
|
parser_.getEvent();
|
|
|
|
anchors_.destroy();
|
|
return node;
|
|
}
|
|
|
|
/// Compose a node.
|
|
///
|
|
/// Params: pairAppenderLevel = Current level of the pair appender stack.
|
|
/// nodeAppenderLevel = Current level of the node appender stack.
|
|
Node composeNode(const uint pairAppenderLevel, const uint nodeAppenderLevel) @system
|
|
{
|
|
if(parser_.checkEvent(EventID.Alias))
|
|
{
|
|
immutable event = parser_.getEvent();
|
|
const anchor = event.anchor;
|
|
enforce((anchor in anchors_) !is null,
|
|
new ComposerException("Found undefined alias: " ~ anchor.get,
|
|
event.startMark));
|
|
|
|
//If the node referenced by the anchor is uninitialized,
|
|
//it's not finished, i.e. we're currently composing it
|
|
//and trying to use it recursively here.
|
|
enforce(anchors_[anchor] != Node(),
|
|
new ComposerException("Found recursive alias: " ~ anchor.get,
|
|
event.startMark));
|
|
|
|
return anchors_[anchor];
|
|
}
|
|
|
|
immutable event = parser_.peekEvent();
|
|
const anchor = event.anchor;
|
|
if(!anchor.isNull() && (anchor in anchors_) !is null)
|
|
{
|
|
throw new ComposerException("Found duplicate anchor: " ~ anchor.get,
|
|
event.startMark);
|
|
}
|
|
|
|
Node result;
|
|
//Associate the anchor, if any, with an uninitialized node.
|
|
//used to detect duplicate and recursive anchors.
|
|
if(!anchor.isNull())
|
|
{
|
|
anchors_[anchor] = Node();
|
|
}
|
|
|
|
if(parser_.checkEvent(EventID.Scalar))
|
|
{
|
|
result = composeScalarNode();
|
|
}
|
|
else if(parser_.checkEvent(EventID.SequenceStart))
|
|
{
|
|
result = composeSequenceNode(pairAppenderLevel, nodeAppenderLevel);
|
|
}
|
|
else if(parser_.checkEvent(EventID.MappingStart))
|
|
{
|
|
result = composeMappingNode(pairAppenderLevel, nodeAppenderLevel);
|
|
}
|
|
else{assert(false, "This code should never be reached");}
|
|
|
|
if(!anchor.isNull())
|
|
{
|
|
anchors_[anchor] = result;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
///Compose a scalar node.
|
|
Node composeScalarNode() @system
|
|
{
|
|
immutable event = parser_.getEvent();
|
|
const tag = resolver_.resolve(NodeID.Scalar, event.tag, event.value,
|
|
event.implicit);
|
|
|
|
Node node = constructor_.node(event.startMark, event.endMark, tag,
|
|
event.value, event.scalarStyle);
|
|
|
|
return node;
|
|
}
|
|
|
|
/// Compose a sequence node.
|
|
///
|
|
/// Params: pairAppenderLevel = Current level of the pair appender stack.
|
|
/// nodeAppenderLevel = Current level of the node appender stack.
|
|
Node composeSequenceNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
|
|
@system
|
|
{
|
|
ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
|
|
auto nodeAppender = &(nodeAppenders_[nodeAppenderLevel]);
|
|
|
|
immutable startEvent = parser_.getEvent();
|
|
const tag = resolver_.resolve(NodeID.Sequence, startEvent.tag, null,
|
|
startEvent.implicit);
|
|
|
|
while(!parser_.checkEvent(EventID.SequenceEnd))
|
|
{
|
|
nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1));
|
|
}
|
|
|
|
core.memory.GC.disable();
|
|
scope(exit){core.memory.GC.enable();}
|
|
Node node = constructor_.node(startEvent.startMark, parser_.getEvent().endMark,
|
|
tag, nodeAppender.data.dup, startEvent.collectionStyle);
|
|
nodeAppender.clear();
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Flatten a node, merging it with nodes referenced through YAMLMerge data type.
|
|
*
|
|
* Node must be a mapping or a sequence of mappings.
|
|
*
|
|
* Params: root = Node to flatten.
|
|
* startMark = Start position of the node.
|
|
* endMark = End position of the node.
|
|
* pairAppenderLevel = Current level of the pair appender stack.
|
|
* nodeAppenderLevel = Current level of the node appender stack.
|
|
*
|
|
* Returns: Flattened mapping as pairs.
|
|
*/
|
|
Node.Pair[] flatten(ref Node root, const Mark startMark, const Mark endMark,
|
|
const uint pairAppenderLevel, const uint nodeAppenderLevel) @system
|
|
{
|
|
void error(Node node)
|
|
{
|
|
//this is Composer, but the code is related to Constructor.
|
|
throw new ConstructorException("While constructing a mapping, "
|
|
"expected a mapping or a list of "
|
|
"mappings for merging, but found: "
|
|
~ node.type.toString() ~
|
|
" NOTE: line/column shows topmost parent "
|
|
"to which the content is being merged",
|
|
startMark, endMark);
|
|
}
|
|
|
|
ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
|
|
auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
|
|
|
|
if(root.isMapping)
|
|
{
|
|
Node[] toMerge;
|
|
foreach(ref Node key, ref Node value; root)
|
|
{
|
|
if(key.isType!YAMLMerge)
|
|
{
|
|
toMerge.assumeSafeAppend();
|
|
toMerge ~= value;
|
|
}
|
|
else
|
|
{
|
|
auto temp = Node.Pair(key, value);
|
|
merge(*pairAppender, temp);
|
|
}
|
|
}
|
|
foreach(node; toMerge)
|
|
{
|
|
merge(*pairAppender, flatten(node, startMark, endMark,
|
|
pairAppenderLevel + 1, nodeAppenderLevel));
|
|
}
|
|
}
|
|
//Must be a sequence of mappings.
|
|
else if(root.isSequence) foreach(ref Node node; root)
|
|
{
|
|
if(!node.isType!(Node.Pair[])){error(node);}
|
|
merge(*pairAppender, flatten(node, startMark, endMark,
|
|
pairAppenderLevel + 1, nodeAppenderLevel));
|
|
}
|
|
else
|
|
{
|
|
error(root);
|
|
}
|
|
|
|
core.memory.GC.disable();
|
|
scope(exit){core.memory.GC.enable();}
|
|
auto flattened = pairAppender.data.dup;
|
|
pairAppender.clear();
|
|
|
|
return flattened;
|
|
}
|
|
|
|
/// Compose a mapping node.
|
|
///
|
|
/// Params: pairAppenderLevel = Current level of the pair appender stack.
|
|
/// nodeAppenderLevel = Current level of the node appender stack.
|
|
Node composeMappingNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
|
|
@system
|
|
{
|
|
ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
|
|
immutable startEvent = parser_.getEvent();
|
|
const tag = resolver_.resolve(NodeID.Mapping, startEvent.tag, null,
|
|
startEvent.implicit);
|
|
auto pairAppender = &(pairAppenders_[pairAppenderLevel]);
|
|
|
|
Tuple!(Node, Mark)[] toMerge;
|
|
while(!parser_.checkEvent(EventID.MappingEnd))
|
|
{
|
|
auto pair = Node.Pair(composeNode(pairAppenderLevel + 1, nodeAppenderLevel),
|
|
composeNode(pairAppenderLevel + 1, nodeAppenderLevel));
|
|
|
|
//Need to flatten and merge the node referred by YAMLMerge.
|
|
if(pair.key.isType!YAMLMerge)
|
|
{
|
|
toMerge ~= tuple(pair.value, cast(Mark)parser_.peekEvent().endMark);
|
|
}
|
|
//Not YAMLMerge, just add the pair.
|
|
else
|
|
{
|
|
merge(*pairAppender, pair);
|
|
}
|
|
}
|
|
foreach(node; toMerge)
|
|
{
|
|
merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1],
|
|
pairAppenderLevel + 1, nodeAppenderLevel));
|
|
}
|
|
|
|
core.memory.GC.disable();
|
|
scope(exit){core.memory.GC.enable();}
|
|
Node node = constructor_.node(startEvent.startMark, parser_.getEvent().endMark,
|
|
tag, pairAppender.data.dup, startEvent.collectionStyle);
|
|
|
|
pairAppender.clear();
|
|
return node;
|
|
}
|
|
}
|