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.
 
 

235 lines
5.2 KiB

<?php
declare(strict_types=1);
namespace App\Cfnpp\Expression;
use App\Util\GraphNode;
use App\Util\Stack;
class Expression
{
public const TOKEN_TYPES = [
Token\NumericLiteral::class,
Token\OperatorUnary::class,
Token\OperatorBinary::class,
Token\StringLiteral::class,
Token\Variable::class
];
protected $nodes;
protected $solved = false;
public function __construct(string $expression, \App\Engine\IOptions $options)
{
$tokens = static::tokenize($expression);
$nodes = static::parse($tokens);
$this->nodes = static::solve($nodes, $options->getVariables(), array_keys($options->getParameters()));
$this->options = $options;
}
public function isComplete()
{
// I think this works? If we resolved down to a flat set of values and
// all are scalar, then we're done?
$complete = true;
foreach ($this->nodes as $node)
{
if ($node->getValue() instanceof Token ||
$node->hasChildren())
{
$complete = false;
break;
}
}
return $complete;
}
public function count()
{
return sizeof($this->nodes);
}
public function getValue()
{
return $this->nodes[0]->getValue();
}
public function toArray(): array
{
return static::unwrap($this->nodes);
}
public function toCloudformation(): array
{
return static::cloudformation($this->nodes);
}
protected static function tokenize(string $expression): array
{
$tokens = [];
while (strlen($expression) > 0)
{
foreach (static::TOKEN_TYPES as $token_class)
{
if ($token_class::isToken($expression))
{
$tokens[] = $token_class::getToken($expression);
if (strlen($expression) > 0 && substr($expression, 0, 1) != ' ')
{
throw new \Exception('incompletely consumed token');
}
$expression = substr($expression, 1);
continue 2;
}
}
throw new \Exception('unparseable value');
}
return $tokens;
}
protected static function parse(array $tokens): array
{
$stack = new Stack();
foreach ($tokens as $token)
{
if ($token instanceof TokenLiteral)
{
$stack->push(new GraphNode($token));
}
elseif ($token instanceof TokenUnary)
{
$node = new GraphNode($token);
$node->appendChild($stack->pop());
$stack->push($node);
}
elseif ($token instanceof TokenBinary)
{
$node = new GraphNode($token);
$node->appendChild($stack->pop());
$node->appendChild($stack->pop());
$stack->push($node);
}
}
return $stack->get();
}
protected static function solve(array $nodes, array $variables = [], array $parameters = []): array
{
$root = new GraphNode();
foreach ($nodes as $node)
{
$root->appendChild($node);
}
$nodes = static::fillVariables($root->getChildren(), $variables, $parameters);
$nodes = static::collapse($root->getChildren());
return $root->getChildren();
}
protected static function execute(array $nodes, array $variables = [], array $parameters = [])
{
return static::unwrap(static::solve($nodes, $variables, $parameters));
}
protected static function cloudformation(array $nodes): array
{
foreach ($nodes as $node)
{
$node->walk(static function($node) {
if (is_scalar($node->getValue()))
{
return;
}
if (!($node->getValue() instanceof ICloudformationNative))
{
throw new \Exception('Token '.basename(get_class($node->getValue())).' is not natively supported by CloudFormation.');
}
$node->setValue($node->getValue()->toCloudformation($node->getChildren()));
$node->clearChildren();
});
}
if (sizeof($nodes) > 1)
{
throw new \Exception('Expression cannot be converted to CloudFormation -- contains multiple nodes');
}
return $nodes[0]->getValue();
}
protected static function fillVariables(array $nodes, array $variables, array $parameters): void
{
foreach ($nodes as $node)
{
$node->walk(static function($node) use ($variables, $parameters) {
if ($node->getValue() instanceof Token\Variable)
{
$var_name = $node->getValue()->getName();
if (in_array($var_name, $parameters))
{
$node->getParent()->replaceChild($node, new GraphNode(new Token\Parameter($var_name)));
}
elseif (!isset($variables[$var_name]))
{
throw new \Exception('Undefined variable: '.$var_name);
}
else
{
$node->setValue($variables[$var_name]);
}
}
});
}
}
protected static function collapse(array $nodes): void
{
foreach ($nodes as $node)
{
$node->walk(static function($node) {
if ($node->getValue() instanceof Token\Parameter)
{
return;
}
if ($node->getValue() instanceof Token)
{
$result = $node->getValue()->execute($node->getChildren());
if (is_scalar($result))
{
$node->setValue($result);
$node->clearChildren();
}
elseif ($result instanceof Token)
{
$node->setValue($result);
$node->clearChildren();
}
elseif ($result instanceof GraphNode)
{
$node->getParent()->replaceChild($node, $result);
}
}
});
}
}
protected static function unwrap(array $nodes)
{
return array_map(static function($node) {
if ($node->hasChildren())
{
throw new \Exception('Cannot unwrap node: still has children');
}
return $node->getValue();
}, $nodes);
}
}