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.
343 lines
8.0 KiB
343 lines
8.0 KiB
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Cfnpp\Expression;
|
|
|
|
use App\Util\GraphNode;
|
|
use App\Util\Stack;
|
|
|
|
/**
|
|
* Represents an expression and handles tokenizing, parsing, and executing
|
|
* expressions.
|
|
*
|
|
* @author Adam Pippin <hello@adampippin.ca>
|
|
*/
|
|
class Expression
|
|
{
|
|
/**
|
|
* References to all Token implementations this class can handle.
|
|
* @var string[]
|
|
*/
|
|
public const TOKEN_TYPES = [
|
|
Token\BooleanLiteral::class,
|
|
Token\NumericLiteral::class,
|
|
Token\OperatorUnary::class,
|
|
Token\OperatorBinary::class,
|
|
Token\StringLiteral::class,
|
|
Token\Variable::class
|
|
];
|
|
|
|
/**
|
|
* Solved expression this represents.
|
|
* @var GraphNode[]
|
|
*/
|
|
protected $nodes;
|
|
|
|
/**
|
|
* Referencing to our compilation state for accessing variables/parameters.
|
|
* @var \App\Engine\IOptions
|
|
*/
|
|
protected $options;
|
|
|
|
/**
|
|
* Create a new expression.
|
|
*
|
|
* Expression is tokenized, parsed, and solved on creation.
|
|
*
|
|
* @param string $expression
|
|
* @param \App\Engine\IOptions $options
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Check whether this expression is 'complete'.
|
|
*
|
|
* Complete is defined as having been solved down to a single value, or array
|
|
* of single values none of which are still tokens. That is, we have a set of
|
|
* computed scalars and no unresolved var/param references.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isComplete(): bool
|
|
{
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Check how many values are contained in the solution.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function count(): int
|
|
{
|
|
return sizeof($this->nodes);
|
|
}
|
|
|
|
/**
|
|
* Assuming the solution contains only a single value, fetch it.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function getValue()
|
|
{
|
|
return $this->nodes[0]->getValue();
|
|
}
|
|
|
|
/**
|
|
* Fetch the solution as an array.
|
|
*
|
|
* Only valid if the solution is complete
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return static::unwrap($this->nodes);
|
|
}
|
|
|
|
/**
|
|
* Convert an incomplete solution into a CloudFormation condition using
|
|
* CloudFormation intrinsic functions.
|
|
*
|
|
* @return mixed[]
|
|
*/
|
|
public function toCloudformation(): array
|
|
{
|
|
return static::cloudformation($this->nodes);
|
|
}
|
|
|
|
/**
|
|
* Convert an expression string into a series of tokens.
|
|
*
|
|
* @param string $expression
|
|
* @return Token[]
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Build a tree out of a series of tokens.
|
|
*
|
|
* @param Token[] $tokens
|
|
* @return GraphNode[]
|
|
*/
|
|
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());
|
|
// @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
|
|
$node->appendChild($stack->pop());
|
|
$stack->push($node);
|
|
}
|
|
}
|
|
return $stack->get();
|
|
}
|
|
|
|
/**
|
|
* Solve a tree of tokens by inserting all variable values and wherever possible
|
|
* resolving all functions.
|
|
*
|
|
* @param GraphNode[] $nodes parsed tree
|
|
* @param array<string,mixed> $variables variable names and values
|
|
* @param string[] $parameters parameter names
|
|
* @return GraphNode[]
|
|
*/
|
|
protected static function solve(array $nodes, array $variables = [], array $parameters = []): array
|
|
{
|
|
$root = new GraphNode();
|
|
foreach ($nodes as $node)
|
|
{
|
|
$root->appendChild($node);
|
|
}
|
|
static::fillVariables($root->getChildren(), $variables, $parameters);
|
|
static::collapse($root->getChildren());
|
|
return $root->getChildren();
|
|
}
|
|
|
|
/**
|
|
* Convert a tree of of nodes into something that could be a valid CloudFormation
|
|
* condition.
|
|
*
|
|
* @param GraphNode[] $nodes
|
|
* @throws \Exception if the remaining nodes contain functions that cannot be expressed in cloudformation
|
|
* @return mixed[]
|
|
*/
|
|
protected static function cloudformation(array $nodes): array
|
|
{
|
|
foreach ($nodes as $node)
|
|
{
|
|
$node->walk(static function(GraphNode $node): void {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Fill all variable values by replacing variable tokens with their actual values.
|
|
*
|
|
* @param GraphNode[] $nodes
|
|
* @param array<string,mixed> $variables variable values
|
|
* @param string[] $parameters parameter names
|
|
* @throws \Exception if a reference is made to an undefined variable
|
|
* @return void
|
|
*/
|
|
protected static function fillVariables(array $nodes, array $variables, array $parameters): void
|
|
{
|
|
foreach ($nodes as $node)
|
|
{
|
|
$node->walk(static function(GraphNode $node) use ($variables, $parameters): void {
|
|
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]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 'collapse' a node tree by executing nodes.
|
|
*
|
|
* @param GraphNode[] $nodes
|
|
* @return void
|
|
*/
|
|
protected static function collapse(array $nodes): void
|
|
{
|
|
foreach ($nodes as $node)
|
|
{
|
|
$node->walk(static function(GraphNode $node): void {
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unwrap an array of nodes by returning their values.
|
|
*
|
|
* Not valid if any nodes still contain children, as the assumption is that
|
|
* those nodes are unresolved.
|
|
*
|
|
* @param GraphNode[] $nodes
|
|
* @return mixed[]
|
|
*/
|
|
protected static function unwrap(array $nodes)
|
|
{
|
|
return array_map(/** @return mixed */ static function(GraphNode $node) {
|
|
if ($node->hasChildren())
|
|
{
|
|
throw new \Exception('Cannot unwrap node: still has children');
|
|
}
|
|
|
|
return $node->getValue();
|
|
}, $nodes);
|
|
}
|
|
}
|
|
|