From 1c1a2c585263751829af3283485900adb4ca5dcb Mon Sep 17 00:00:00 2001 From: Adam Pippin Date: Fri, 12 Feb 2021 23:12:15 -0800 Subject: [PATCH] Docblocks and code cleanup --- app/Commands/Stack/Compile.php | 36 +++++++- app/Dom/Document.php | 8 ++ app/Dom/Node.php | 75 ++++++++++++++++- app/Dom/NodeFunctionValue.php | 8 +- app/Dom/NodeValue.php | 8 +- app/Engine/Cfnpp.php | 119 +++++++++++++++++++++++++-- app/Engine/CfnppOptions.php | 5 ++ app/Engine/Engine.php | 92 +++++++++++++++++++-- app/Engine/File.php | 33 +++++++- app/Providers/AppServiceProvider.php | 28 ------- app/Serialize/ISerialize.php | 6 ++ app/Serialize/IUnserialize.php | 6 ++ app/Serialize/Yaml.php | 5 ++ config/app.php | 1 - 14 files changed, 376 insertions(+), 54 deletions(-) delete mode 100644 app/Providers/AppServiceProvider.php diff --git a/app/Commands/Stack/Compile.php b/app/Commands/Stack/Compile.php index 3a088b0..2dbccb4 100644 --- a/app/Commands/Stack/Compile.php +++ b/app/Commands/Stack/Compile.php @@ -6,6 +6,12 @@ namespace App\Commands\Stack; use LaravelZero\Framework\Commands\Command; +/** + * Read a file as input and perform all necessary compilation steps before + * writing it to an output file. + * + * @author Adam Pippin + */ class Compile extends Command { /** @@ -25,21 +31,45 @@ class Compile extends Command /** * Execute the console command. * - * @return mixed + * @return void */ public function handle() { $engine = new \App\Engine\Engine(); $serializer = $this->getSerializer(); - $engine->setSerializer($serializer)->setUnserializer($serializer)->setCompiler(new \App\Engine\Cfnpp()); + $unserializer = $this->getUnserializer(); + $engine->setSerializer($serializer)->setUnserializer($unserializer)->setCompiler(new \App\Engine\Cfnpp()); $output = $engine->process($this->argument('in_file'), new \App\Engine\CfnppOptions()); file_put_contents($this->argument('out_file'), $output); } - protected function getSerializer(): \App\Serialize\IUnserialize + /** + * Retrieve a serializer instance based on the format specified in the command options. + * + * @return \App\Serialize\ISerialize + */ + protected function getSerializer(): \App\Serialize\ISerialize + { + $format = $this->option('format'); + $class = '\\App\\Serialize\\'.$format; + + if (!class_exists($class)) + { + throw new \Exception('Unknown formatter: '.$format); + } + + return new $class(); + } + + /** + * Retrieve a unserializer instance based on the format specified in the command options. + * + * @return \App\Serialize\IUnserialize + */ + protected function getUnserializer(): \App\Serialize\IUnserialize { $format = $this->option('format'); $class = '\\App\\Serialize\\'.$format; diff --git a/app/Dom/Document.php b/app/Dom/Document.php index c56d539..9c52271 100644 --- a/app/Dom/Document.php +++ b/app/Dom/Document.php @@ -16,6 +16,12 @@ class Document extends Node $this->children = []; } + /** + * Retrieve a value from the meta `cfnpp` block in the document. + * + * @param string $path a dot-separated path to the value you want to retrieve + * @return mixed the value held by the node, or an array of values if the requested node was an array + */ public function getMeta(string $path) { $node = $this->getChildByPath('cfnpp.'.$path); @@ -36,5 +42,7 @@ class Document extends Node } return $values; } + + throw new \Exception('Cannot retrieve meta value: '.$path); } } diff --git a/app/Dom/Node.php b/app/Dom/Node.php index 3ae4759..5bbbd47 100644 --- a/app/Dom/Node.php +++ b/app/Dom/Node.php @@ -11,16 +11,29 @@ namespace App\Dom; */ class Node implements \ArrayAccess, \Iterator { - /** @var int */ + /** + * Index for Iterator implementation. + * @var int + */ private $_idx; - /** @var ?string */ + /** + * Name of this node, or null if none (e.g., array elements). + * @var ?string + */ protected $name; - /** @var Node */ + /** + * Parent node of this node, should be set on every node in the + * document besides the document itself. + * @var ?Node + */ protected $parent; - /** @var Node[] */ + /** + * Children nested under this node. + * @var Node[] + */ protected $children; /** @@ -74,6 +87,10 @@ class Node implements \ArrayAccess, \Iterator */ public function getParent(): Node { + if (!isset($this->parent)) + { + throw new \Exception('Cannot fetch parent -- none set'); + } return $this->parent; } @@ -147,12 +164,28 @@ class Node implements \ArrayAccess, \Iterator return null; } + /** + * Retrieve a child of this node by a dot-separated path. + * + * Each element of the path can either be a node name or a numeric + * index representing the offset into an array + * + * @param string $path + * @return ?Node the requested node, or null if it wasn't found + */ public function getChildByPath(string $path): ?Node { $path = explode('.', $path); return $this->findChild($path, $this); } + /** + * Recursively walk nodes to find a node based on a path. + * + * @param string[] $path + * @param Node $current + * @return ?Node + */ protected function findChild(array $path, Node $current): ?Node { $next = array_shift($path); @@ -205,6 +238,14 @@ class Node implements \ArrayAccess, \Iterator } } + /** + * Try and determine whether this node is a map (named sub-properties). + * + * Checks that this node is not a "FunctionParent" (see isFunctionParent) + * and whether it contains any children that don't have names. + * + * @return bool + */ public function isMap(): bool { if ($this->isFunctionParent()) @@ -221,6 +262,14 @@ class Node implements \ArrayAccess, \Iterator return true; } + /** + * Try and determine wether this node is an array. + * + * Checks that this node is not a "FunctionParent" (see isFunctionParent) + * and whether it contains any children that have names. + * + * @return bool + */ public function isArray(): bool { foreach ($this->children as $child) @@ -233,11 +282,29 @@ class Node implements \ArrayAccess, \Iterator return true; } + /** + * Check whether this node is a 'function parent', that is, whether it contains + * a single child node and that node is a function node. + * + * When we come across, in YAML, something like: + * Map: + * Value: !Func arg + * That's parsed as: + * Map (Node): + * Value (Node): !Func arg (FunctionNode) + * + * @return bool + */ public function isFunctionParent(): bool { return sizeof($this->children) == 1 && $this->children[0] instanceof NodeFunction; } + /** + * Remove this node from its parent. + * + * @return void + */ public function remove(): void { $this->getParent()->removeChild($this); diff --git a/app/Dom/NodeFunctionValue.php b/app/Dom/NodeFunctionValue.php index d64bb8f..6209406 100644 --- a/app/Dom/NodeFunctionValue.php +++ b/app/Dom/NodeFunctionValue.php @@ -11,13 +11,17 @@ namespace App\Dom; */ class NodeFunctionValue extends NodeFunction { - /** @var scalar */ + /** + * Value this node holds (parameter to the function). + * + * @var scalar + */ protected $value; /** * Create a new function node which directly contains a value. * - * @param Node $parent + * @param ?Node $parent * @param ?string $name * @param scalar $value */ diff --git a/app/Dom/NodeValue.php b/app/Dom/NodeValue.php index 3ef824c..cccc040 100644 --- a/app/Dom/NodeValue.php +++ b/app/Dom/NodeValue.php @@ -11,13 +11,17 @@ namespace App\Dom; */ class NodeValue extends Node { - /** @var scalar */ + /** + * Value this node holds. + * + * @var scalar + */ protected $value; /** * Create a new node containing a scalar value. * - * @param Node $parent + * @param ?Node $parent * @param ?string $name * @param scalar $value */ diff --git a/app/Engine/Cfnpp.php b/app/Engine/Cfnpp.php index 13a5676..5600bf7 100644 --- a/app/Engine/Cfnpp.php +++ b/app/Engine/Cfnpp.php @@ -7,18 +7,41 @@ namespace App\Engine; use App\Dom\Document; use App\Dom\Node; use App\Dom\NodeFunction; -use App\Dom\NodeFunctionValue; -use App\Dom\NodeValue; +/** + * Compiler that implements the cfnpp "language" and allows you to + * apply stacked documents on top of each other, transforming them. + * + * @author Adam Pippin + */ class Cfnpp implements ICompile { - /** @var Document */ + /** + * Current document state. + * @var Document + */ protected $document; - /** @var array */ + /** + * Functions that can be called by the document. + * + * Signature: + * callable(Node $node, Node $function) + * + * @var array + */ protected $functions; - /** @var array */ + /** + * 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 + */ protected $merge_functions; public function __construct() @@ -26,6 +49,8 @@ class Cfnpp implements ICompile $this->document = new Document(); $this->functions = []; + // Suppress, we'll move these out of here later and clean them up. + // @phan-suppress-next-line PhanPluginUnknownClosureReturnType $this->registerMergeFunction('Replace', static function(Node $orig_p, Node $tgt_p, Node $t) { // todo: nodefunctionvalue @@ -33,30 +58,64 @@ class Cfnpp implements ICompile $repl->setChildren($t->getChildren()); return $repl; }); + // @phan-suppress-next-line PhanPluginUnknownClosureReturnType $this->registerFunction('Unset', static function(Node $node, Node $func) { }); } + /** + * 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; } + /** + * Set the current state that new compilations will be applied to. + * + * @param Document $document + * @return void + */ public function setDocument(Document $document): void { $this->document = $document; } + /** + * Get the document representing the current state. + * + * @return Document + */ public function getDocument(): Document { return $this->document; } + /** + * Apply a new document to the current state. + * + * @param Document $document + * @param IOptions $options + * @return void + */ public function compile(Document $document, IOptions $options): void { $this->runMergeFunctions($this->document, $document); @@ -64,6 +123,24 @@ class Cfnpp implements ICompile $this->runFunctions($this->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()) @@ -110,6 +187,15 @@ class Cfnpp implements ICompile } } + /** + * 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()])) @@ -155,6 +241,12 @@ class Cfnpp implements ICompile } } + /** + * 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()])) @@ -181,11 +273,28 @@ class Cfnpp implements ICompile } } + /** + * 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); diff --git a/app/Engine/CfnppOptions.php b/app/Engine/CfnppOptions.php index 14ef9e8..101d403 100644 --- a/app/Engine/CfnppOptions.php +++ b/app/Engine/CfnppOptions.php @@ -4,6 +4,11 @@ declare(strict_types=1); namespace App\Engine; +/** + * Options for controlling the Cfnpp compilation process. + * + * @author Adam Pippin + */ class CfnppOptions implements IOptions { } diff --git a/app/Engine/Engine.php b/app/Engine/Engine.php index 270cf08..23946bc 100644 --- a/app/Engine/Engine.php +++ b/app/Engine/Engine.php @@ -11,30 +11,55 @@ namespace App\Engine; */ class Engine { - /** @var \App\Engine\ICompile */ + /** + * Compiler for processing documents. + * + * @var \App\Engine\ICompile + */ protected $compile; - /** @var \App\Serialize\ISerialize */ + /** + * Serializer to convert documents back to whatever line/disk format we + * want. + * + * @var \App\Serialize\ISerialize + */ protected $serialize; - /** @var \App\Serialize\IUnserialize */ + /** + * Unserializer to convert line/disk format documents into documents. + * + * @var \App\Serialize\IUnserialize + */ protected $unserialize; - /** @var \App\Dom\Document */ + /** + * Represents the current document state after any operations called. + * + * @var \App\Dom\Document + */ protected $document; + /** + * Process a domdocument, applying some higher level operations such as + * stacking documents. + * + * @param string $input_file + * @param IOptions $options + * @return string + */ public function process(string $input_file, IOptions $options): string { $file_helper = new File([dirname(realpath($input_file))]); - $files[] = $input_file; + $files = [$input_file]; $processed = []; for ($i = 0; $i < sizeof($files); $i++) { $document = $this->unserialize->unserialize(file_get_contents($file_helper->resolve($files[$i]))); if (!in_array($files[$i], $processed)) - { // only process includes once so when we hit the stack again we don't loop forever + { $additional_files = []; try { @@ -58,24 +83,48 @@ class Engine return $this->getOutput(); } + /** + * Set the compiler to use. + * + * @param ICompile $compiler + * @return Engine + */ public function setCompiler(ICompile $compiler): Engine { $this->compile = $compiler; return $this; } + /** + * Set the serializer to use. + * + * @param \App\Serialize\ISerialize $serializer + * @return Engine + */ public function setSerializer(\App\Serialize\ISerialize $serializer): Engine { $this->serialize = $serializer; return $this; } + /** + * Set the unserializer to use. + * + * @param \App\Serialize\IUnserialize $unserializer + * @return Engine + */ public function setUnserializer(\App\Serialize\IUnserialize $unserializer): Engine { $this->unserialize = $unserializer; return $this; } + /** + * Set the initial compiler state. + * + * @param \App\Dom\Document $document + * @return Engine + */ public function setInputDocument(\App\Dom\Document $document): Engine { $this->document = $document; @@ -83,6 +132,12 @@ class Engine return $this; } + /** + * Set the initial compiler state from a serialized string. + * + * @param string $document_string + * @return Engine + */ public function setInput(string $document_string): Engine { $document = $this->unserialize->unserialize($document_string); @@ -90,6 +145,13 @@ class Engine return $this; } + /** + * Compile a new document, mutating current state. + * + * @param string $document_string + * @param IOptions $options + * @return Engine + */ public function compile(string $document_string, IOptions $options): Engine { $document = $this->unserialize->unserialize($document_string); @@ -97,17 +159,35 @@ class Engine return $this; } + /** + * Compile a new document, mutating current state. + * + * @param \App\Dom\Document $document + * @param IOptions $options + * @return Engine + */ public function compileDocument(\App\Dom\Document $document, IOptions $options): Engine { $this->compile->compile($document, $options); return $this; } + /** + * Get the document state after all applied compile() calls. + * + * @return \App\Dom\Document + */ public function getOutputDocument(): \App\Dom\Document { return $this->compile->getDocument(); } + /** + * Get the document state after all applied compile() calls and serialize + * it, returning line/disk format. + * + * @return string + */ public function getOutput(): string { $document = $this->getOutputDocument(); diff --git a/app/Engine/File.php b/app/Engine/File.php index 45fd861..6a1a10e 100644 --- a/app/Engine/File.php +++ b/app/Engine/File.php @@ -4,17 +4,38 @@ declare(strict_types=1); namespace App\Engine; +/** + * File helper for handling resolving filenames in a variety of include paths. + * + * @author Adam Pippin + */ class File { - /** @var string */ + /** + * List of include paths to search, in order. + * + * @var string[] + */ protected $paths; + /** + * Create a new file helper. + * + * @param string[] $paths list of paths to search in order + */ public function __construct(array $paths) { $this->paths = $paths; } - public function resolve(string $file) + /** + * Resolve a filename by searching all include paths and checking if the + * file exists there. Returns the first one it finds. + * + * @param string $file + * @return string + */ + public function resolve(string $file): string { foreach ($this->paths as $path) { @@ -26,7 +47,13 @@ class File throw new \Exception('File not found: '.$file); } - public function addPath(string $path) + /** + * Add a new search path to the end of the paths. + * + * @param string $path + * @return void + */ + public function appendPath(string $path): void { $this->paths[] = $path; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php deleted file mode 100644 index c5a7e4b..0000000 --- a/app/Providers/AppServiceProvider.php +++ /dev/null @@ -1,28 +0,0 @@ - + */ class Yaml implements ISerialize, IUnserialize { /** diff --git a/config/app.php b/config/app.php index 80092ca..39468f5 100644 --- a/config/app.php +++ b/config/app.php @@ -56,7 +56,6 @@ return [ */ 'providers' => [ - App\Providers\AppServiceProvider::class, ], ];