Skip to content

Commit

Permalink
Added CommandParser and accompanying test.
Browse files Browse the repository at this point in the history
Added ConversionChainBuilder that uses new CommandParser and accompanying test.
Added ConverterFactory and accompanying test.
Updated architecture diagram.
  • Loading branch information
Bilge committed Aug 19, 2014
1 parent 5399cd0 commit 49cc8d6
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 2 deletions.
3 changes: 2 additions & 1 deletion README.md
@@ -1,7 +1,7 @@
Open Dash
=========

[![Version][Version image]]()
[![Version][Version image]][Releases]
[![Build Status][Build image]][Build]
[![Code Coverage][Coverage image]][Coverage]
[![Scrutinizer Code Quality][Quality image]][Quality]
Expand All @@ -12,6 +12,7 @@ Architecture
------------
![Diagram](https://raw.githubusercontent.com/ScriptFUSION/Open-Dash/master/diagrams/Open%20Dash.png)

[Releases]: https://github.com/ScriptFUSION/Open-Dash/releases
[Version image]: http://img.shields.io/github/tag/ScriptFUSION/Open-Dash.svg "Latest version"
[Build]: http://travis-ci.org/ScriptFUSION/Open-Dash
[Build image]: http://img.shields.io/travis/ScriptFUSION/Open-Dash.svg "Build status"
Expand Down
2 changes: 1 addition & 1 deletion diagrams/Open Dash.gliffy

Large diffs are not rendered by default.

Binary file modified diagrams/Open Dash.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions src/Command/Argument.php
@@ -0,0 +1,29 @@
<?php
namespace ScriptFUSION\OpenDash\Command;

class Argument {
private
$value,
$quote
;

public function __construct($value, $quote = '') {
$this->value = Unescaper::unescape($value);
$this->quote = $quote;
}

/**
* @return mixed
*/
public function getValue() {
return $this->value;
}

public function getQuote() {
return $this->quote;
}

public function isQuoted() {
return !!$this->quote;
}
}
41 changes: 41 additions & 0 deletions src/Command/Command.php
@@ -0,0 +1,41 @@
<?php
namespace ScriptFUSION\OpenDash\Command;

class Command {
private
$name,
$arguments
;

public function __construct($name, array $arguments = []) {
$this->name = "$name";
$this->arguments = $arguments;
}

public function __toString() {
return "$this->name";
}

/**
* @return string
*/
public function getName() {
return "$this";
}

/**
* @return Argument[]
*/
public function getArguments() {
return $this->arguments;
}

/**
* @return array
*/
public function renderArguments() {
return array_map(function(Argument $arg) {
return $arg->getValue();
}, $this->arguments);
}
}
83 changes: 83 additions & 0 deletions src/Command/CommandParser.php
@@ -0,0 +1,83 @@
<?php
namespace ScriptFUSION\OpenDash\Command;

/**
* Parses a string command specification into a tree of Command and Argument objects.
*/
class CommandParser {
const
PARSE_COMMANDS = '
#Command name.
(?<command>[a-z][a-z\d]*+)
#Optional arguments list.
(?<args>(?:%s)*)
#Optional whitespace followed by pipe or end of expression.
\s*+(?:\||\z)
',

PARSE_ARGUMENTS = '
#Each argument must proceed whitespace.
\s++
(?J:
#Single or double quoted form.
(?<quote>[\'"])
#Only even quantities of backslahes may end the argument.
(?<arg>.*?(?<!\\\\)(?:\\\\\\\\)*+)
#Closing quote must match opening quote.
\k{quote}
|
#Unquoted form. Argument may not contain whitespace or pipes.
(?<arg>[^\s|]++)
)
',

REGEX_SHELL = '[%s]xS'
;

/**
* Parses the specified command line into a list of Commands.
*
* @param string $commandLine Command line.
* @return Command[] Command list.
*/
public function parseCommands($commandLine) {
$matches = [];
if (false === preg_match_all(
sprintf(static::REGEX_SHELL, sprintf(static::PARSE_COMMANDS, static::PARSE_ARGUMENTS)),
$commandLine, $matches
))
throw new ParseException("Unable to parse command line: $commandLine");

$commands = [];
foreach ($matches['command'] as $index => $command)
$commands[$index] = new Command($command, $this->parseArguments($matches['args'][$index]));

return $commands;
}

/**
* Parses the specified arguments into a list of Arguments.
*
* Note: arguments have to be parsed in a separate pass since PCRE does not
* support capturing multiple matches from quantified groups (see
* <http://www.rexegg.com/regex-capture.html#groupnumbers>).
*
* @param string $args Arguments.
* @return Argument[] Arguments list.
*/
private function parseArguments($args) {
$matches = [];
if (false === preg_match_all(
sprintf(static::REGEX_SHELL, static::PARSE_ARGUMENTS), $args, $matches, PREG_SET_ORDER
))
throw new ParseException("Unable to parse arguments: $args");

$args = [];
foreach ($matches as $index => $arg)
$args[$index] = new Argument($arg['arg'], $arg['quote']);

return $args;
}
}
4 changes: 4 additions & 0 deletions src/Command/ParseException.php
@@ -0,0 +1,4 @@
<?php
namespace ScriptFUSION\OpenDash\Command;

class ParseException extends \RuntimeException {}
8 changes: 8 additions & 0 deletions src/Command/Unescaper.php
@@ -0,0 +1,8 @@
<?php
namespace ScriptFUSION\OpenDash\Command;

class Unescaper {
public static function unescape($string) {
return preg_replace('[\\\\(.)]', '\1', $string);
}
}
38 changes: 38 additions & 0 deletions src/Convert/ConversionChainBuilder.php
@@ -0,0 +1,38 @@
<?php
namespace ScriptFUSION\OpenDash\Convert;

use ScriptFUSION\OpenDash\Command\Command;
use ScriptFUSION\OpenDash\Command\CommandParser;

/**
* Builds a ConversionChain from a string specification.
*/
class ConversionChainBuilder {
private
$factory,
$parser
;

public function __construct(ConverterFactory $factory) {
$this->parser = new CommandParser;
$this->factory = $factory;
}

/**
* Builds a ConversionChain from the specified specification.
*
* @param string $specification <converter> [arg1 [arg2 ...]][ | <converter>]...
* @return ConversionChain New ConversionChain.
*/
public function build($specification) {
$commands = $this->parser->parseCommands($specification);

$chain = new ConversionChain;

/** @var Command $command */
foreach ($commands as $command)
$chain->addConverter($this->factory->createConverter("$command", $command->renderArguments()));

return $chain;
}
}
59 changes: 59 additions & 0 deletions src/Convert/ConverterFactory.php
@@ -0,0 +1,59 @@
<?php
namespace ScriptFUSION\OpenDash\Convert;

/**
* Creates concrete instances of Convert.
*/
class ConverterFactory {
private $converters;

/**
* Initializes ConverterFactory.
*/
public function __construct() {
$this->converters = iterator_to_array($this->findConverters());
}

/**
* Creates a converter matching the specified name constructed with the
* specified arguments.
*
* @param string $name Converter name.
* @param array $arguments Converter arguments.
* @return Convert New Convert instance.
*/
public function createConverter($name, array $arguments = []) {
if (!isset($this->converters[$name]))
throw new \RuntimeException("Converter not found: $name");

return (new \ReflectionClass($this->converters[$name]))->newInstanceArgs($arguments);
}

/**
* Gets a list of available converters.
*
* @return array Lower-case converter name => fully qualified class name.
*/
public function getConverters() {
return $this->converters;
}

/**
* Searches for classes implementing Convert in the same directory as this class.
*
* @return \Generator Lower-case converter name => class name.
*/
private function findConverters() {
/** @var \DirectoryIterator $file */
foreach (new \DirectoryIterator(__DIR__) as $file) {
// Filter non-PHP files.
if ($file->getExtension() !== 'php')
continue;

$reflector = new \ReflectionClass(__NAMESPACE__ . '\\' . $basename = $file->getBasename('.php'));

if ($reflector->isInstantiable() && $reflector->implementsInterface(Convert::class))
yield strtolower($basename) => $reflector->getName();
}
}
}
9 changes: 9 additions & 0 deletions src/Data/Data.php
Expand Up @@ -2,9 +2,18 @@
namespace ScriptFUSION\OpenDash\Data;

interface Data extends \JsonSerializable {
/**
* @return string
*/
public function getError();

/**
* @return mixed
*/
public function getData();

/**
* @return bool
*/
public function isValid();
}

0 comments on commit 49cc8d6

Please sign in to comment.