Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and add tests for MakePublicationTagCommand #727

Merged
merged 70 commits into from
Dec 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
ce83dc9
Fix assumed typo in PHPDoc
caendesilva Dec 3, 2022
3ae42ce
Replace self with static
caendesilva Dec 3, 2022
99e8a40
Remove unused parameter from getValuesForTagName helper
caendesilva Dec 3, 2022
f37d9b9
Move down method to group tag related code together
caendesilva Dec 3, 2022
d528e8d
Update test annotation
caendesilva Dec 3, 2022
3beda9f
Break if stream fails
caendesilva Dec 3, 2022
ae1296a
Extract method
caendesilva Dec 3, 2022
99e30b3
Create MakePublicationTagCommandTest.php
caendesilva Dec 3, 2022
c157622
Add helper to mock the standard input stream
caendesilva Dec 3, 2022
2a5ef06
Merge added tags with existing before save
caendesilva Dec 3, 2022
c78dbdf
Implement base test
caendesilva Dec 3, 2022
b950d1c
Display the tag selections
caendesilva Dec 3, 2022
d0bfe8e
Add output expectations
caendesilva Dec 3, 2022
b22c0f0
Test feed termination
caendesilva Dec 3, 2022
93f3948
Clean up test code with teardown method
caendesilva Dec 3, 2022
f948687
Revert "Break if stream fails"
caendesilva Dec 3, 2022
0bd14d6
Allow tag name to be passed as argument
caendesilva Dec 3, 2022
0bc773b
Extract method
caendesilva Dec 3, 2022
97c528e
Replace ?? with if
caendesilva Dec 3, 2022
434b9a0
Fix test method names
caendesilva Dec 3, 2022
0124a48
Display the tag name selection when using CLI argument
caendesilva Dec 3, 2022
7c7f937
Style the output
caendesilva Dec 3, 2022
14c79c1
Add a newline
caendesilva Dec 3, 2022
25c0f15
Add todo
caendesilva Dec 3, 2022
e836e84
Remove unused import
caendesilva Dec 3, 2022
4a2f3eb
Move down method
caendesilva Dec 3, 2022
61603c4
Display clickable filepath link
caendesilva Dec 3, 2022
0f5977d
Add todo
caendesilva Dec 3, 2022
6084503
Use shorter writeln syntax
caendesilva Dec 3, 2022
292619c
Add newline and style output
caendesilva Dec 3, 2022
8148b5e
Move variable declaration to just before first usage
caendesilva Dec 3, 2022
3bc7bac
Use absolute paths for filesystem interactions
caendesilva Dec 3, 2022
087007c
Expect absolute path in output
caendesilva Dec 3, 2022
66cc546
Convert 'sprintf()' call to concatenation
caendesilva Dec 3, 2022
cfc8246
Extract formatting helper to base class
caendesilva Dec 3, 2022
bf4174f
Refactor to use info comment helper
caendesilva Dec 3, 2022
0db8cad
Introduce local variable for the existing tags
caendesilva Dec 3, 2022
e824c0b
Shorten unit test code
caendesilva Dec 3, 2022
a229d4d
Fix and test conflict handling
caendesilva Dec 3, 2022
08c9600
Apply fixes from StyleCI
StyleCIBot Dec 3, 2022
6348c28
Extract reusable code to helper action
caendesilva Dec 3, 2022
d42763d
Normalize output styling
caendesilva Dec 3, 2022
6815b76
Extract helper methods
caendesilva Dec 3, 2022
a4757e3
Refactor to inline existingTags variable
caendesilva Dec 3, 2022
ff74b49
Refactor to use class properties instead of passing variables around
caendesilva Dec 3, 2022
ee2efe6
Inline local variable
caendesilva Dec 3, 2022
fbae774
Use the filesystem facade
caendesilva Dec 3, 2022
5eccb8b
Inline local variable
caendesilva Dec 3, 2022
fde02dc
Split comma-separated values into multiple lines
caendesilva Dec 3, 2022
cbd9f89
Apply fixes from StyleCI
StyleCIBot Dec 3, 2022
6df6df3
Clean up code
caendesilva Dec 3, 2022
2a51657
Add method stub with same result as parent for more clear test
caendesilva Dec 3, 2022
f48d81a
Add a dynamic test command class
caendesilva Dec 3, 2022
bc72416
Test the infoComment helper
caendesilva Dec 3, 2022
a5cfe65
Allow extra info to be passed to infoComment helper
caendesilva Dec 3, 2022
2f98b8d
Use the infoComment helper
caendesilva Dec 3, 2022
35fd2f7
Inline local variable
caendesilva Dec 3, 2022
9c821f8
Introduce new local variable
caendesilva Dec 3, 2022
372f462
Extract helper
caendesilva Dec 3, 2022
d183a52
Merge unnecessary local variable into single statement
caendesilva Dec 3, 2022
aa4616a
Inline local variable
caendesilva Dec 3, 2022
f72d43c
Swap to use safeHandle method
caendesilva Dec 3, 2022
2dcec09
Refactor to throw exception instead of returning error directly
caendesilva Dec 3, 2022
8847e5b
Extract method
caendesilva Dec 3, 2022
a9045d7
Shift down property assignment to helper method
caendesilva Dec 3, 2022
e42e3cd
Shorten helper method name
caendesilva Dec 3, 2022
d04819d
Shift down helper method call
caendesilva Dec 3, 2022
dc0964c
Revert "Shift down helper method call"
caendesilva Dec 3, 2022
864e8c8
Add todo
caendesilva Dec 3, 2022
6699dda
Apply fixes from StyleCI
StyleCIBot Dec 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Hyde\Console\Commands\Helpers;

use function array_shift;
use function explode;
use function fgets;
use Hyde\Framework\Concerns\InvokableAction;
use Illuminate\Support\Str;
use function trim;

/**
* Collects an array of lines from the standard input stream. Feed is terminated by a blank line.
*
* @todo Add dynamic support for detecting and using comma separated values?
*/
class InputStreamHandler extends InvokableAction
{
/** @internal Allows for mocking of the standard input stream */
private static ?array $streamBuffer = null;

public function __invoke(): array
{
return $this->getLinesFromInputStream();
}

protected function getLinesFromInputStream(): array
{
$lines = [];
do {
$line = Str::replace(["\n", "\r"], '', $this->readInputStream());
if ($line === '') {
break;
}
$lines[] = trim($line);
} while (true);

return $lines;
}

/** @codeCoverageIgnore Allows for mocking of the standard input stream */
protected function readInputStream(): array|string|false
{
if (self::$streamBuffer) {
return array_shift(self::$streamBuffer);
}

return fgets(STDIN);
}

/** @internal Allows for mocking of the standard input stream */
public static function mockInput(string $input): void
{
self::$streamBuffer = explode("\n", $input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,101 @@

namespace Hyde\Console\Commands;

use function array_merge;
use Hyde\Console\Commands\Helpers\InputStreamHandler;
use Hyde\Console\Commands\Interfaces\CommandHandleInterface;
use Hyde\Console\Concerns\ValidatingCommand;
use Hyde\Facades\Filesystem;
use Hyde\Framework\Features\Publications\PublicationService;
use Hyde\Framework\Services\DiscoveryService;
use Hyde\Hyde;
use Illuminate\Support\Str;
use function implode;
use LaravelZero\Framework\Commands\Command;
use function Safe\file_put_contents;
use RuntimeException;
use function Safe\json_encode;
use function sprintf;

/**
* Hyde Command to create a new publication type.
*
* @see \Hyde\Framework\Testing\Feature\Commands\MakePublicationTypeCommandTest
* @see \Hyde\Framework\Testing\Feature\Commands\MakePublicationTagCommandTest
*/
class MakePublicationTagCommand extends ValidatingCommand implements CommandHandleInterface
{
/** @var string */
protected $signature = 'make:publicationTag';
protected $signature = 'make:publicationTag {tagName? : The name of the tag to create}';

/** @var string */
protected $description = 'Create a new publication type tag definition';

public function handle(): int
protected array $tags;
protected string $tagName;

public function safeHandle(): int
{
$this->title('Creating a new Publication Type Tag!');

$filename = Hyde::pathToRelative('tags.json');
$tags = PublicationService::getAllTags();
$tagName = $this->askWithValidation('name', 'Tag name', ['required', 'string']);
if (isset($tags[$tagName])) {
$this->output->error("Tag [$tagName] already exists");
$this->getTagName();

return Command::FAILURE;
}
$this->validateTagName();

$lines = [];
$this->output->writeln('<bg=magenta;fg=white>Enter the tag values (end with an empty line):</>');
do {
$line = Str::replace(["\n", "\r"], '', fgets(STDIN));
if ($line === '') {
break;
}
$lines[] = trim($line);
} while (true);
$tags[$tagName] = $lines;
$this->collectTags();

$this->output->writeln(sprintf('Saving tag data to [%s]', $filename));
file_put_contents($filename, json_encode($tags, JSON_PRETTY_PRINT));
$this->printSelectionInformation();

$this->saveTagsToDisk();

return Command::SUCCESS;
}

protected function getTagName(): void
{
$this->tagName = $this->getTagNameFromArgument($this->argument('tagName'))
?? $this->askWithValidation('name', 'Tag name', ['required', 'string']);
}

protected function getTagNameFromArgument(?string $value): ?string
{
if ($value) {
$this->infoComment('Using tag name', $value, 'from command line argument');
$this->newLine();

return $value;
}

return null;
}

protected function validateTagName(): void
{
if (PublicationService::getAllTags()->has($this->tagName)) {
throw new RuntimeException("Tag [$this->tagName] already exists");
}
}

protected function collectTags(): void
{
$this->info('Enter the tag values: (end with an empty line)');
$this->tags = [$this->tagName => InputStreamHandler::call()];
}

protected function printSelectionInformation(): void
{
$this->line('Adding the following tags:');
foreach ($this->tags as $tag => $values) {
$this->line(sprintf(' <comment>%s</comment>: %s', $tag, implode(', ', $values)));
}
$this->newLine();
}

protected function saveTagsToDisk(): void
{
$this->infoComment('Saving tag data to',
DiscoveryService::createClickableFilepath(Hyde::path('tags.json'))
);

Filesystem::putContents('tags.json', json_encode(array_merge(
PublicationService::getAllTags()->toArray(), $this->tags
), JSON_PRETTY_PRINT));
}
}
8 changes: 8 additions & 0 deletions packages/framework/src/Console/Concerns/ValidatingCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ public function handleException(Exception $exception, ?string $file = null, ?int
return Command::FAILURE;
}

/**
* Write a nicely formatted and consistent message to the console. Using InfoComment for a lack of a better term.
*/
public function infoComment(string $info, string $comment, ?string $moreInfo = null): void
{
$this->line("<info>$info</info> [<comment>$comment</comment>]".($moreInfo ? " <info>$moreInfo</info>" : ''));
}

protected function translate($name, string $error): string
{
return $this->makeReplacements($name, Str::after($error, 'validation.'), $this->getTranslationLines());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,6 @@
*/
class PublicationService
{
/**
* Get all available/tags.
*
* @return \Rgasch\Collection\Collection
*
* @throws \Safe\Exceptions\FilesystemException
* @throws \Safe\Exceptions\JsonException
*/
public static function getAllTags(): Collection
{
$filename = Hyde::pathToRelative('tags.json');
if (! file_exists($filename)) {
return Collection::create();
}

return Collection::create(json_decode(file_get_contents($filename), true))->sortKeys();
}

/**
* Return a collection of all defined publication types, indexed by the directory name.
*
Expand Down Expand Up @@ -76,19 +58,36 @@ public static function getMediaForPubType(PublicationType $pubType): Collection
});
}

/**
* Get all available tags.
*
* @return \Rgasch\Collection\Collection
*
* @throws \Safe\Exceptions\FilesystemException
* @throws \Safe\Exceptions\JsonException
*/
public static function getAllTags(): Collection
{
$filename = Hyde::pathToRelative('tags.json');
if (! file_exists($filename)) {
return Collection::create();
}

return Collection::create(json_decode(file_get_contents($filename), true))->sortKeys();
}

/**
* Get all values for a given tag name.
*
* @param string $tagName
* @param \Hyde\Framework\Features\Publications\Models\PublicationType $publicationType
* @return \Rgasch\Collection\Collection|null
*
* @throws \Safe\Exceptions\FilesystemException
* @throws \Safe\Exceptions\JsonException
*/
public static function getValuesForTagName(string $tagName, PublicationType $publicationType): ?Collection
public static function getValuesForTagName(string $tagName): ?Collection
{
$tags = self::getAllTags($publicationType);
$tags = static::getAllTags();
if ($tags->has($tagName)) {
return $tags->$tagName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public static function getModelSourceDirectory(string $model): string

/**
* Create a filepath that can be opened in the browser from a terminal.
*
* @todo Add support for custom label?
* @todo Add option to treat path as already validated so paths that are not created yet can be printed?
*/
public static function createClickableFilepath(string $filepath): string
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Hyde\Framework\Testing\Feature\Commands;

use Hyde\Console\Commands\Helpers\InputStreamHandler;
use Hyde\Hyde;
use Hyde\Testing\TestCase;
use function unlink;

/**
* @covers \Hyde\Console\Commands\MakePublicationTagCommand
* @covers \Hyde\Console\Commands\Helpers\InputStreamHandler {@todo Extract this to a separate test class}
*/
class MakePublicationTagCommandTest extends TestCase
{
protected function tearDown(): void
{
unlink(Hyde::path('tags.json'));

parent::tearDown();
}

public function testCanCreateNewPublicationTag()
{
InputStreamHandler::mockInput("foo\nbar\nbaz\n");

$this->artisan('make:publicationTag')
->expectsQuestion('Tag name', 'foo')
->expectsOutput('Enter the tag values: (end with an empty line)')
->expectsOutput('Adding the following tags:')
->expectsOutput(' foo: foo, bar, baz')
->expectsOutput('Saving tag data to ['.Hyde::path('tags.json').']')
->assertExitCode(0);

$this->assertFileExists(Hyde::path('tags.json'));
$this->assertSame(
json_encode(['foo' => ['foo', 'bar', 'baz']], 128),
file_get_contents(Hyde::path('tags.json'))
);
}

public function testCanCreateNewPublicationTagWithTagNameArgument()
{
InputStreamHandler::mockInput("foo\nbar\nbaz\n");

$this->artisan('make:publicationTag foo')
->expectsOutput('Using tag name [foo] from command line argument')
->expectsOutput('Enter the tag values: (end with an empty line)')
->expectsOutput('Adding the following tags:')
->expectsOutput(' foo: foo, bar, baz')
->expectsOutput('Saving tag data to ['.Hyde::path('tags.json').']')
->assertExitCode(0);

$this->assertFileExists(Hyde::path('tags.json'));
$this->assertSame(
json_encode(['foo' => ['foo', 'bar', 'baz']], 128),
file_get_contents(Hyde::path('tags.json'))
);
}

public function testCommandFailsIfTagNameIsAlreadySet()
{
InputStreamHandler::mockInput("foo\nbar\nbaz\n");

$this->artisan('make:publicationTag foo')
->assertExitCode(0);

InputStreamHandler::mockInput("foo\nbar\nbaz\n");

$this->artisan('make:publicationTag foo')
->expectsOutput('Error: Tag [foo] already exists')
->assertExitCode(1);
}

public function testCanTerminateWithCarriageReturns()
{
InputStreamHandler::mockInput("foo\r\nbar\r\nbaz\r\n");

$this->artisan('make:publicationTag foo')->assertExitCode(0);
}

public function testCanTerminateWithUnixEndings()
{
InputStreamHandler::mockInput("foo\nbar\nbaz\n");

$this->artisan('make:publicationTag foo')->assertExitCode(0);
}
}
Loading