Browse Source

Docblocks and code cleanup

master
Adam Pippin 3 years ago
parent
commit
1c1a2c5852
  1. 36
      app/Commands/Stack/Compile.php
  2. 8
      app/Dom/Document.php
  3. 75
      app/Dom/Node.php
  4. 8
      app/Dom/NodeFunctionValue.php
  5. 8
      app/Dom/NodeValue.php
  6. 119
      app/Engine/Cfnpp.php
  7. 5
      app/Engine/CfnppOptions.php
  8. 92
      app/Engine/Engine.php
  9. 33
      app/Engine/File.php
  10. 28
      app/Providers/AppServiceProvider.php
  11. 6
      app/Serialize/ISerialize.php
  12. 6
      app/Serialize/IUnserialize.php
  13. 5
      app/Serialize/Yaml.php
  14. 1
      config/app.php

36
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 <hello@adampippin.ca>
*/
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;

8
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);
}
}

75
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);

8
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
*/

8
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
*/

119
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 <hello@adampippin.ca>
*/
class Cfnpp implements ICompile
{
/** @var Document */
/**
* Current document state.
* @var Document
*/
protected $document;
/** @var array<string, callable> */
/**
* Functions that can be called by the document.
*
* Signature:
* callable(Node $node, Node $function)
*
* @var array<string, callable>
*/
protected $functions;
/** @var array<string, callable> */
/**
* 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<string, callable>
*/
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);

5
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 <hello@adampippin.ca>
*/
class CfnppOptions implements IOptions
{
}

92
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();

33
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 <hello@adampippin.ca>
*/
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;
}

28
app/Providers/AppServiceProvider.php

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
}
}

6
app/Serialize/ISerialize.php

@ -11,5 +11,11 @@ namespace App\Serialize;
*/
interface ISerialize
{
/**
* Convert a Dom\Document into a serialized string.
*
* @param \App\Dom\Document $document
* @return string
*/
public function serialize(\App\Dom\Document $document): string;
}

6
app/Serialize/IUnserialize.php

@ -11,5 +11,11 @@ namespace App\Serialize;
*/
interface IUnserialize
{
/**
* Convert a string into a Dom\Document.
*
* @param string $document
* @return \App\Dom\Document
*/
public function unserialize(string $document): \App\Dom\Document;
}

5
app/Serialize/Yaml.php

@ -12,6 +12,11 @@ use App\Dom\NodeValue;
use App\Dom\NodeFunction;
use App\Dom\NodeFunctionValue;
/**
* Handles serializing and unserializing between documents and Yaml.
*
* @author Adam Pippin <hello@adampippin.ca>
*/
class Yaml implements ISerialize, IUnserialize
{
/**

1
config/app.php

@ -56,7 +56,6 @@ return [
*/
'providers' => [
App\Providers\AppServiceProvider::class,
],
];

Loading…
Cancel
Save