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.
 
 

254 lines
6.0 KiB

<?php
declare(strict_types=1);
namespace App\Cfnpp;
use App\Dom\Node;
use App\Dom\NodeValue;
use App\Dom\NodeFunction;
use App\Dom\NodeFunctionValue;
/**
* Functions available in a cfnpp document.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Functions
{
/**
* For now, generate incremental condition names.
* @todo
* @var int
*/
public static $condition_idx = 0;
/**
* cfnpp compiler.
* @var Compiler
*/
protected $compiler;
/**
* cfnpp compiler options, stores state.
* @var Options
*/
protected $options;
public function __construct(Compiler $compiler, Options $options)
{
$this->compiler = $compiler;
$this->options = $options;
}
/**
* Examines this class with reflection and registers all functions on a cfnpp
* compiler instance.
*
* @param Compiler $compiler
* @return void
*/
public function register(Compiler $compiler): void
{
$reflection = new \ReflectionClass(static::class);
$methods = $reflection->getMethods();
foreach ($methods as $method)
{
if (!stristr($method->name, '_'))
{
continue;
}
[$method_type, $method_name] = explode('_', $method->name, 2);
switch ($method_type)
{
case 'mf':
$compiler->registerMergeFunction($method_name, [$this, $method->name]);
break;
case 'f':
$compiler->registerFunction($method_name, [$this, $method->name]);
break;
}
}
}
/**
* Replace all children nodes in original with those from target, rather than
* merging them.
*
* @param Node $original
* @param Node $target
* @param NodeFunction $function
* @return ?Node
*/
public function mf_replace(Node $original, Node $target, NodeFunction $function): ?Node
{
if ($function instanceof NodeFunctionValue)
{
return new NodeValue(null, $target->hasName() ? $target->getName() : null, $function->getValue());
}
$replacement = new Node(null, $target->hasName() ? $target->getName() : null);
$replacement->setChildren($function->getChildren());
return $replacement;
}
/**
* Unset a node, completely removing it from the document.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_unset(Node $node, NodeFunction $function): ?Node
{
return null;
}
/**
* Get the value of a variable.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_var(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunctionValue))
{
throw new \Exception('!var requires scalar argument');
}
$value = $this->options->getVariable($function->getValue());
return new NodeValue(null, $node->hasName() ? $node->getName() : null, $value);
}
/**
* Create a reference to a CloudFormation parameter.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_param(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunctionValue))
{
throw new \Exception('!param requires scalar argument');
}
$node = new Node(null, $node->hasName() ? $node->getName() : null);
$ref = new NodeValue($node, 'Ref', $function->getValue());
$node->addChild($ref);
return $node;
}
/**
* Conditionally include part of the file.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_if(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunction))
{
throw new \Exception('!if expects array of parameters');
}
$nodes = $function->getChildren();
$condition = $nodes[0];
if (!($condition instanceof NodeValue))
{
throw new \Exception('!if requires scalar condition');
}
$if_true = $nodes[1];
$if_false = sizeof($nodes) == 3 ? $nodes[2] : null;
$expression = new \App\Cfnpp\Expression\Expression($condition->getValue());
$expression->solve($this->options);
// We need a single resulting node as a final value either as a scalar or
// if we want to convert to cloudformation.
if ($expression->count() > 1)
{
throw new \Exception('!if expression must evaluate down to a single value: '.$condition->getValue());
}
if ($expression->isComplete())
{
// We know the final result, we can resolve this directly.
if ($expression->getValue())
{
$if_true->setName($node->hasName() ? $node->getName() : null);
return $if_true;
}
elseif (isset($if_false))
{
$if_false->setName($node->hasName() ? $node->getName() : null);
return $if_false;
}
return null;
}
// Create condition
// Generate Cfn Fn::If
$condition_name = 'Condition'.(static::$condition_idx++);
$this->compiler->addCondition($condition_name, Node::fromPhp($expression->toCloudformation()));
$n_orig = new Node(null, $node->hasName() ? $node->getName() : null);
$n_if = new Node($n_orig, 'Fn::If');
$n_orig->addChild($n_if);
$n_if->addChild(new NodeValue($n_if, null, $condition_name));
$n_if->addChild($if_true);
if (isset($if_false))
{
$n_if->addChild($if_false);
}
return $n_orig;
}
/**
* Calculate an expression.
*
* @param Node $node
* @param NodeFunction $function
* @return ?Node
*/
public function f_expr(Node $node, NodeFunction $function): ?Node
{
if (!($function instanceof NodeFunctionValue))
{
throw new \Exception('!expr requires scalar argument');
}
$expression = new \App\Cfnpp\Expression\Expression($function->getValue());
$expression->solve($this->options);
if ($expression->isComplete())
{
// If we computed a final value/set of values, we can just insert those
// directly.
if ($expression->count() == 1)
{
$solution_node = Node::fromPhp($expression->getValue());
}
else
{
$solution_node = Node::fromPhp($expression->toArray());
}
}
else
{
// Otherwise let's convert it to cfn intrinsics
$solution_node = Node::fromPhp($expression->toCloudformation());
}
if ($node->hasName())
{
$solution_node->setName($node->getName());
}
return $solution_node;
}
}