cloudformation-plus-plus: cfn template preprocessor
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

277 lines
6.7 KiB

<?php
declare(strict_types=1);
namespace App\Engine\Cfnpp;
use App\Dom\Document;
use App\Dom\Node;
use App\Dom\NodeFunction;
/**
* Compiler that implements the cfnpp "language" and allows you to
* apply stacked documents on top of each other, transforming them.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Compiler implements \App\Engine\ICompile
{
/**
* Functions that can be called by the document.
*
* Signature:
* callable(Node $node, Node $function)
*
* @var array<string, callable>
*/
protected $functions;
/**
* Functions that merge two documents, and will be
* called with a node from the current state and
* document being compiled.
*
* Signature:
* callable(Node $current_state, Node $new_document, Node $function)
*
* @var array<string, callable>
*/
protected $merge_functions;
/**
* Register a function that can be called by the document.
*
* @param string $name
* @param callable $callback callback(Node $node, Node $function): ?Node
* @return void
*/
public function registerFunction(string $name, callable $callback): void
{
$this->functions[$name] = $callback;
}
/**
* Register a merge function that can be called by the document and passed
* both the current state and the new document.
*
* @param string $name
* @param callable $callback callback(Node $current, Node $new_doc, Node $function): ?Node
* @return void
*/
public function registerMergeFunction(string $name, callable $callback): void
{
$this->merge_functions[$name] = $callback;
}
/**
* Compile a set of documents and return the result.
*
* @param Document[] $documents
* @param \App\Engine\IOptions $options
* @return void
*/
public function compile(array $documents, \App\Engine\IOptions $options): Document
{
if (!($options instanceof Options))
{
throw new \Exception('Cfnpp\Compiler requires Cfnpp\Options');
}
// Initialize state
$document = new Document();
$this->functions = [];
$this->merge_functions = [];
// Register built-in functions
$cfnpp_functions = new Functions($this, $options);
$cfnpp_functions->register($this);
// Process each passed document
foreach ($documents as $next_document)
{
$this->runMergeFunctions($document, $next_document);
$this->merge($document, $next_document);
$this->runFunctions($document);
}
return $document;
}
/**
* Performs a basic recursive merge of two dom trees with target overwriting
* original.
*
* - Maps will be merged
* - Arrays will be appended
* - Other values will be overwritten
* - If there's any mismatch (e.g., array in original + map in target), then
* everything's just replaced with whatever's in the target.
*
* The original DOM tree will represent the final state. The target tree will
* be clobbered, as many nodes are simply reparented into the original tree
* where appropriate.
*
* @param Node $original
* @param Node $target
* @return void
*/
protected function merge(Node $original, Node $target): void
{
if ($original->isArray() && $target->isArray())
{
foreach ($target as $child)
{
$original->addChild($child);
$child->setParent($original);
}
}
elseif ($original->isMap() && $target->isMap())
{
foreach ($target as $child)
{
$orig_child = $original->getChildByName($child->getName());
// If the key doesn't exist on the source, just copy over and we're done
if (!isset($orig_child))
{
$original->addChild($child);
$child->setParent($original);
continue;
}
// If this is a map or array, we need to descend into it
if ($child->isMap() || $child->isArray())
{
$this->merge($orig_child, $child);
}
// Otherwise just replace it (nodefunction, nodevalue, whatever)
else
{
$original->removeChild($orig_child);
$original->addChild($child);
$child->setParent($original);
}
}
}
else
{
// If it's anything else, overwrite
$original->remove();
$original->getParent()->addChild($target);
$target->setParent($original);
}
}
/**
* Walks through two dom trees simultaneously, adds missing nodes from
* target to original, and executes any 'merge functions' it encounters
* along the way.
*
* @param Node $original
* @param Node $target
* @return void
*/
protected function runMergeFunctions(Node $original, Node $target): void
{
if ($target->isFunctionParent() && isset($this->merge_functions[$target[0]->getName()]))
{
$function_node = $target[0];
$result = $this->runMergeFunction($original, $target, $function_node);
$original->remove();
$target->remove();
if (isset($result))
{
$original_node = $result;
$original_node->setParent($original->getParent());
$original->getParent()->addChild($original_node);
$original = $original_node;
$target_node = clone $result;
$target_node->setParent($target->getParent());
$target->getParent()->addChild($target_node);
$target = $target_node;
}
}
foreach ($target as $node)
{
if ($node->hasName())
{
$orig_child = $original->getChildByName($node->getName());
if (!isset($orig_child))
{
$orig_child = new Node($original, $node->getName());
$original->addChild($orig_child);
}
$this->runMergeFunctions($orig_child, $node);
}
}
if (isset($result))
{
$target->remove();
}
}
/**
* Walks through the DOM tree and executes any functions it encounters.
*
* @param Node $node
* @return void
*/
protected function runFunctions(Node $node): void
{
if ($node->isFunctionParent() && isset($this->functions[$node[0]->getName()]))
{
$function_node = $node[0];
$result = $this->runFunction($node, $function_node);
$node->remove();
if (isset($result))
{
$node->getParent()->addChild($result);
$result->setParent($node->getParent());
$node = $result;
}
else
{
return;
}
}
$children = $node->getChildren();
foreach ($children as $child)
{
$this->runFunctions($child);
}
}
/**
* Runs the function referenced by the NodeFunction node, and returns the
* result.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
protected function runFunction(Node $node, NodeFunction $function): ?Node
{
return $this->functions[$function->getName()]($node, $function);
}
/**
* Runs a merge function referenced by the NodeFunction, and returns the
* result.
*
* @param Node $original
* @param Node $target
* @param NodeFunction $function
* @return ?Node
*/
protected function runMergeFunction(Node $original, Node $target, NodeFunction $function): ?Node
{
return $this->merge_functions[$function->getName()]($original, $target, $function);
}
}