Skip to content

Commit

Permalink
Removed a lot of old code.
Browse files Browse the repository at this point in the history
Refactored token replacement to fix #85. (mostly)
  • Loading branch information
beporter committed Jun 30, 2015
1 parent 8e60c7b commit 7e3cc07
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 162 deletions.
126 changes: 53 additions & 73 deletions src/Console/FileParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,28 @@ public function __construct(Event $event, $dir)
}

/**
* Ensure a string matches the token format.
*
* Converts a token name and default value back into the tokenized format.
* Recursively searches for files ending with .template in the skeleton directory.
*
* @param string $string The token name.
* @param string $default Default value for the token.
* @return string The tokenized version of the provided token name and default value.
* @return array List of *.template files.
*/
public function tokenize($string, $default = null) {
$out = '{{' . $string;
if (!is_null($default)) {
$out .= ':' . $default;
public function findTemplates()
{
$templates = [];
$Regex = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->dir)
),
'/^.+\.template$/',
\RecursiveRegexIterator::GET_MATCH
);

foreach ($Regex as $file) {
$template = str_replace($this->dir, '', $file[0]);
$templates[] = $template;
$this->writeVerbose('Found template: `' . $template . '`');
}

return $out . '}}';
return $templates;
}

/**
Expand All @@ -92,16 +99,16 @@ public function getTokensInFiles(array $files)
* @param string $file Relative filesystem path from ::$dir to the file.
* @return array An array of [token => default value]s found in the file.
*/
public function getTokensInFile($file)
public function getTokensInFile($source)
{
$file = $this->dir . $file;
$file = $this->dir . $source;

// Skip unreadable files.
if (!is_readable($file)) {
return [];
}

$this->writeVerbose('Reading tokens from `' . $file . '`...');
$this->writeVerbose('Reading tokens from `' . $source . '`...');
$fileContents = file_get_contents($file);
if (!$fileContents) {
return [];
Expand All @@ -112,82 +119,55 @@ public function getTokensInFile($file)
}

/**
* Recursively searches for files ending with .template in the skeleton directory.
* Process a given template, replacing all recognized tokens with the
* user-entered or default value.
*
* @return array List of *.template files.
*/
public function findTemplates()
{
$templates = [];
$Regex = new \RegexIterator(
new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->dir)
),
'/^.+\.template$/',
\RecursiveRegexIterator::GET_MATCH
);

foreach ($Regex as $file) {
$template = str_replace($this->dir, '', $file[0]);
$templates[] = $template;
$this->writeVerbose('Found template: `' . $template . '`');
}

return $templates;
}

/**
* Parse a given template, replacing text matching a config key with the config value.
* Once replacement is complete, the file is copied to the
* non-`*.template` version of the file name (overwriting any existing
* file) and the .template file is removed.
*
* @param string $template File system path to the file to read.
* @param array $config Array of discovered [token => value] pairs.
* @return true Always true.
*/
public function parseTemplate($template, $config)
{
$fs = new \Composer\Util\Filesystem;
$this->writeVerbose('Parsing template: ' . $template);
$template = $this->dir . $template;
$file = str_replace('.template', '', $template);

foreach ($config as $token => $value) {
if ($token == $value) {
continue;
$source = $this->dir . $template;
$destination = str_replace('.template', '', $source);

$replacer = function(array $matches) use ($config) {
$tokenName = $matches[1];
if (!array_key_exists($tokenName, $config)) {
return $matches[0]; // Can't replace-- no value available.
}
if ($value === ' ') {
$value = '';
$replacement = $config[$tokenName];
if ($tokenName === $replacement) {
return $matches[0]; // Don't replace.
}
if ($replacement === ' ') {
$replacement = '';
}
$this->replaceInFile($this->tokenize($token, $value), $value, $template);
}

$this->writeVerbose('Replacing `' . $file . '` with `' . $template . '`');
$fs->copyThenRemove($template, $file);
return true;
}
$this->writeVerbose("\tReplacing `{$tokenName}` with `{$replacement}`.");
return $replacement;
};
$contents = file_get_contents($source);
$contents = preg_replace_callback($this->tokenExpression, $replacer, $contents);

/**
* Replaces all occourences of $search with $replace in $file.
*
* @param string $search The text to be replaced.
* @param string $replace The text to replace $search with.
* @param string $file Path to the file to perform the search and replace.
* @return mixed Number of occurences of $search replaced, false on failure.
*/
protected function replaceInFile($search, $replace, $file)
{
$this->writeVerbose('Replacing "' . $search . '" with "' . $replace . '" in `' . $file . '`');
$contents = file_get_contents($file);
if ($contents === false) {
$this->writeVerbose('Unable to read from file: `' . $file . '`');
return false;
}
$contents = str_replace($search, $replace, $contents, $count);
if (file_put_contents($file, $contents) === false) {
$this->writeVerbose('Unable to write to file: `' . $file . '`');
// Update the template file in-place.
if (file_put_contents($source, $contents) === false) {
$this->writeVerbose('Unable to write to file: `' . $source . '`');
return false;
}

return $count;
// Then replace the original with the updated template.
$this->writeVerbose("Using `{$source}` to replace `{$destination}`.");
$fs = new \Composer\Util\Filesystem;
$fs->copyThenRemove($source, $destination);

return true;
}

/**
Expand Down
30 changes: 3 additions & 27 deletions src/Console/LoadsysInstaller.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ public static function postInstall(Event $event)
$rootDir = dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR;

static::welcome($io);
$config = new InstallerConfigurer($event);
static::parseTemplates($config, $event, $rootDir);
static::parseTemplates($event, $rootDir);

}

Expand All @@ -52,13 +51,13 @@ protected static function welcome($io)
/**
* Finds *.template files and parses tokens strings within each.
*
* @param InstallerConfigurer $config InstallerConfigurer instance.
* @param Composer\Script\Event $event Composer's Event instance
* @param string $rootDir The application's root directory.
* @return void
*/
protected static function parseTemplates(InstallerConfigurer $config, Event $event, $rootDir)
protected static function parseTemplates(Event $event, $rootDir)
{
$config = new InstallerConfigurer($event);
$fileParser = new FileParser($event, $rootDir);
$templates = $fileParser->findTemplates();

Expand All @@ -73,27 +72,4 @@ protected static function parseTemplates(InstallerConfigurer $config, Event $eve
$fileParser->parseTemplate($template, $config->read());
}
}

/**
* Asks a question with a yes or no answer to the user and returns a boolean.
*
* @param \Composer\IO\IOInterface $io IO interface to write to console.
* @param string $question Question to ask user with a yes or no answer.
* @param string $default default value. Any of 'Y', 'y', 'N', or 'n'
* @throws \Exception Exception raised by validator.
* @return bool user's aster to $question
*/
protected static function askBool($io, $question, $default = 'Y')
{
$validator = (function ($arg) {
if (in_array($arg, ['Y', 'y', 'N', 'n'])) {
return $arg;
}
throw new Exception('Please enter Y or n.');
});
$input = $io->askAndValidate($question, $validator, 10, $default);

return in_array($input,['Y', 'y']);
}

}
4 changes: 4 additions & 0 deletions tests/Sample/Console/FileParser/README.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ This is a templated Markdown file with a few {{TOKEN:overlaps with token from su

* {{NO_DEFAULT}}
* {{SECOND_ITEM:The second thing in the list.}}
* {{MATCHING}}
* {{SHOULD_BE_REMOVED:default does not matter}}

Other uses of the `{{` and `}}` characters should be correctly ignored.

This token isn't in our test cases, so it should be left alone: {{NOT_IN_TESTS:blah}}

When the tokens in this file are replaced, it should overwrite the existing README.md file.

85 changes: 23 additions & 62 deletions tests/TestCase/Console/FileParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function tearDown()
unset($this->io);
unset($this->parser);
$this->ioWriteBuffer = [];
//$this->wipeSampleDir($this->testDir);
$this->wipeSampleDir($this->testDir);

parent::tearDown();
}
Expand Down Expand Up @@ -203,58 +203,6 @@ public function testConstruct()
);
}

/**
* Test the tokenize() method.
*
* @param string $token The token name to process.
* @param string $default The default token value. `null` is treated differently from empty string "".
* @param string $expected The expected processed string.
* @param string $msg Optional PHPUnit assertion failure message.
* @dataProvider provideTokenizeArgs
*/
public function testTokenize($token, $default, $expected, $msg = '')
{
$this->assertEquals(
$expected,
$this->parser->tokenize($token, $default),
$msg
);
}

/**
* Provide data sets to testTokenize().
*
* @return array Sets of [token, default, expected, assertion failure message].
*/
public function provideTokenizeArgs()
{
return [
[
'', null, // token, default
'{{}}', // expected
'Empty string should produce empty output.', // message
],

[
'SIMPLE', null,
'{{SIMPLE}}',
'Null default value should produce simple token string.'
],

[
'EMPTY_DEFAULT', '',
'{{EMPTY_DEFAULT:}}',
'Blank default should produce a colon with nothing after it.'
],

[
'WITH_DEFAULT_VAL', 'fizz buzz',
'{{WITH_DEFAULT_VAL:fizz buzz}}',
'Non-empty default value should be included in output.'
],
];
}

/**
* Test the getTokensInFiles() method.
*
Expand Down Expand Up @@ -303,6 +251,8 @@ public function provideGetTokensInFilesArgs()
'TOKEN' => 'overlaps with token from ../README.md',
'NO_DEFAULT' => '',
'SECOND_ITEM' => 'The second thing in the list.',
'NOT_IN_TESTS' => 'blah',
'SHOULD_BE_REMOVED' => 'default does not matter',
],
'Produced token list must match those from the listed files.',
],
Expand Down Expand Up @@ -333,19 +283,24 @@ public function testParseTemplate()
$file = 'README.md.template';
$expectedFileName = 'README.md';
$expectedMessages = [
"Parsing template: $file",
"Failed to replace `$expectedFileName` with `$file`",
"<comment>Parsing template: $file</comment>",
"<comment> Replacing `TOKEN` with `foo bar`.</comment>",
"<comment> Replacing `NO_DEFAULT` with `tic tac toe`.</comment>",
"<comment> Replacing `SECOND_ITEM` with `a quick brown fox`.</comment>",
"<comment> Replacing `SHOULD_BE_REMOVED` with ``.</comment>",
"<comment>Using `{$this->testDir}{$file}` to replace `{$this->testDir}{$expectedFileName}`.</comment>",
];
$tokens = [
$replacements = [
'TOKEN' => 'foo bar',
'NO_DEFAULT' => 'tic tac toe',
'SECOND_ITEM' => 'a quick brown fox',
'UNUSED' => 'UNUSED',
'USE_EMPTY_STR' => ' ',
'MATCHING' => 'MATCHING',
'SHOULD_BE_REMOVED' => ' ',
'NOT_IN_FILE' => 'This token is not in the file being processed.',
];

$this->assertTrue(
$this->parser->parseTemplate($file, $tokens),
$this->parser->parseTemplate($file, $replacements),
'parseTemplate() should always return true.'
);

Expand All @@ -364,12 +319,18 @@ public function testParseTemplate()
$contents,
'The contents of the pre-existing file should have been overwritten by our template.'
);
$this->assertContains(
'{{NOT_IN_TESTS:blah}}',
$contents,
'A non-matched token should still exist in the finished file. (Technically impossible in actual use.)'
);

$matches = [];
preg_match_all($this->parser->tokenExpression(), $contents, $matches);
$this->assertEmpty(
$matches,
'The replaced file should not have any {{TOKEN}}s left in it.'
$this->assertContains(
'NOT_IN_TESTS',
$matches[1], // token name sub-matches
'The replaced file should not have a single unmatched {{TOKEN}} left in it.'
);

$this->assertEquals(
Expand Down

0 comments on commit 7e3cc07

Please sign in to comment.