*/ 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 */ public function __construct(string $expression) { $tokens = static::tokenize($expression); $this->nodes = static::parse($tokens); } /** * Solve this expression down to the minimal set of nodes we can * * @param \App\Engine\IOptions $options * @return void */ public function solve(\App\Engine\IOptions $options): void { $this->nodes = static::_solve($this->nodes, $options->getVariables(), array_keys($options->getParameters())); } /** * 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); } /** * Examine the expression to determine which variables are referenced so we * can figure out the dependencies between them * * @return string[] names of variables referenced */ public function getReferencedVariables(): array { $variables = []; foreach ($this->nodes as $node) { $node->walk(function(GraphNode $node) use (&$variables): void { if ($node->getValue() instanceof Token\Variable) { $variables[] = $node->getValue()->getName(); } }); } return array_values(array_unique($variables)); } /** * 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 $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 $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); } }