Skip to content

Commit

Permalink
Merge pull request #786 from hydephp/publications-feature-refactors
Browse files Browse the repository at this point in the history
Update the make:publication command
  • Loading branch information
caendesilva authored Dec 29, 2022
2 parents f9f30f7 + b8588ab commit 2caf1f4
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ public static function mockInput(string $input): void
}

/** Format a consistent message that can be output to the console */
public static function formatMessage(string $name): string
public static function formatMessage(string $name, string $type = 'values'): string
{
return "Enter $name (end with an empty line)";
return "<info>Enter $type for field </>[<comment>$name</comment>] (end with an empty line)";
}
}
65 changes: 58 additions & 7 deletions packages/framework/src/Console/Commands/MakePublicationCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Hyde\Console\Commands;

use function array_flip;
use Closure;
use Hyde\Console\Commands\Helpers\InputStreamHandler;
use Hyde\Console\Concerns\ValidatingCommand;
Expand All @@ -17,6 +18,7 @@
use function in_array;
use InvalidArgumentException;
use LaravelZero\Framework\Commands\Command;
use function str_starts_with;

/**
* Hyde Command to create a new publication for a given publication type.
Expand Down Expand Up @@ -102,27 +104,39 @@ protected function collectFieldData(): Collection

/** @var PublicationField $field */
foreach ($this->publicationType->getFields() as $field) {
if (str_starts_with($field->name, '__')) {
continue;
}
$this->newLine();
$data->put($field->name, $this->captureFieldInput($field));
}

return $data;
}

protected function captureFieldInput(PublicationField $field): string|array|null
protected function captureFieldInput(PublicationField $field): bool|string|array|null
{
return match ($field->type) {
$selection = match ($field->type) {
PublicationFieldTypes::Text => $this->captureTextFieldInput($field),
PublicationFieldTypes::Array => $this->captureArrayFieldInput($field),
PublicationFieldTypes::Image => $this->captureImageFieldInput($field),
PublicationFieldTypes::Tag => $this->captureTagFieldInput($field),
default => $this->askWithValidation($field->name, $field->name, $field->type->rules()),
PublicationFieldTypes::Boolean => $this->captureBooleanFieldInput($field),
default => $this->askWithValidation($field->name, "Enter data for field </>[<comment>$field->name</comment>]", $field->getValidationRules()->toArray()),
};

if (empty($selection)) {
$this->line("<fg=gray> > Skipping field $field->name</>");

return null;
}

return $selection;
}

protected function captureTextFieldInput(PublicationField $field): string
{
$this->line(InputStreamHandler::formatMessage($field->name));
$this->line(InputStreamHandler::formatMessage($field->name, 'lines'));

return implode("\n", InputStreamHandler::call());
}
Expand Down Expand Up @@ -160,13 +174,50 @@ protected function captureTagFieldInput(PublicationField $field): array|string|n

$this->tip('You can enter multiple tags separated by commas');

return $this->reloadableChoice($this->getTagValuesArrayClosure(),
return $this->reloadableChoice($this->getReloadableTagValuesArrayClosure(),
'Which tag would you like to use?',
'Reload tags.json',
true
);
}

/**
* @deprecated Will be refactored into a dedicated rule
*/
protected function captureBooleanFieldInput(PublicationField $field, $retryCount = 1): ?bool
{
// Return null when retry count is exceeded to prevent infinite loop
if ($retryCount > 30) {
return null;
}

// Since the Laravel validation rule for booleans doesn't accept the string input provided by the console,
// we need to do some logic of our own to support validating booleans through the console.

$rules = $field->type->rules();
$rules = array_flip($rules);
unset($rules['boolean']);
$rules = array_flip($rules);

$selection = $this->askWithValidation($field->name, "Enter data for field </>[<comment>$field->name</comment>]", $rules);

if (empty($selection)) {
return null;
}

$acceptable = ['true', 'false', true, false, 0, 1, '0', '1'];

// Strict parameter is needed as for some reason `in_array($selection, [true])` is always true no matter what the value of $selection is.
if (in_array($selection, $acceptable, true)) {
return (bool) $selection;
} else {
// Match the formatting of the standard Laravel validation error message.
$this->error("The $field->name field must be true or false.");

return $this->captureBooleanFieldInput($field, $retryCount + 1);
}
}

/** @return null */
protected function handleEmptyOptionsCollection(PublicationField $field, string $type, string $message)
{
Expand All @@ -175,7 +226,7 @@ protected function handleEmptyOptionsCollection(PublicationField $field, string
}

$this->newLine();
$this->warn("Warning: $message");
$this->warn(" <fg=red>Warning:</> $message");
if ($this->confirm('Would you like to skip this field?', true)) {
return null;
} else {
Expand All @@ -189,7 +240,7 @@ protected function tip(string $message): void
}

/** @return Closure<array<string>> */
protected function getTagValuesArrayClosure(): Closure
protected function getReloadableTagValuesArrayClosure(): Closure
{
return function (): array {
return PublicationService::getValuesForTagName($this->publicationType->getIdentifier())->toArray();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,22 @@ protected function getMergedData(): array
protected function normalizeData(array $array): array
{
foreach ($array as $key => $value) {
if (empty($value)) {
unset($array[$key]);
continue;
}

$type = $this->pubType->getFields()->get($key);

if ($type->type === PublicationFieldTypes::Text) {
// In order to properly store text fields as block literals,
// we need to make sure they end with a newline.
$array[$key] = trim($value)."\n";
}

if ($type->type === PublicationFieldTypes::Integer) {
$array[$key] = (int) $value;
}
}

return $array;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function test_command_creates_publication()
->expectsOutputToContain('Creating a new publication!')
->expectsChoice('Which publication type would you like to create a publication item for?', 0, ['test-publication'])
->expectsOutput('Creating a new publication of type [test-publication]')
->expectsQuestion('Title', 'Hello World')
->expectsQuestion('Enter data for field </>[<comment>title</comment>]', 'Hello World')
->expectsOutput('Created file test-publication/hello-world.md')
->assertExitCode(0);

Expand All @@ -69,7 +69,7 @@ public function test_command_with_existing_publication()
$this->artisan('make:publication')
->expectsOutputToContain('Creating a new publication!')
->expectsChoice('Which publication type would you like to create a publication item for?', 0, ['test-publication'])
->expectsQuestion('Title', 'Hello World')
->expectsQuestion('Enter data for field </>[<comment>title</comment>]', 'Hello World')
->expectsOutput('Error: A publication already exists with the same canonical field value')
->expectsConfirmation('Do you wish to overwrite the existing file?')
->expectsOutput('Exiting without overwriting existing publication file!')
Expand All @@ -87,7 +87,7 @@ public function test_command_with_existing_publication_and_overwrite()
$this->artisan('make:publication')
->expectsOutputToContain('Creating a new publication!')
->expectsChoice('Which publication type would you like to create a publication item for?', 0, ['test-publication'])
->expectsQuestion('Title', 'Hello World')
->expectsQuestion('Enter data for field </>[<comment>title</comment>]', 'Hello World')
->expectsOutput('Error: A publication already exists with the same canonical field value')
->expectsConfirmation('Do you wish to overwrite the existing file?', 'yes')
->assertExitCode(0);
Expand All @@ -103,7 +103,7 @@ public function test_can_overwrite_existing_publication_by_passing_force_flag()
$this->artisan('make:publication', ['--force' => true])
->expectsOutputToContain('Creating a new publication!')
->expectsChoice('Which publication type would you like to create a publication item for?', 0, ['test-publication'])
->expectsQuestion('Title', 'Hello World')
->expectsQuestion('Enter data for field </>[<comment>title</comment>]', 'Hello World')
->assertExitCode(0);

$this->assertNotEquals('foo', file_get_contents(Hyde::path('test-publication/hello-world.md')));
Expand All @@ -115,7 +115,7 @@ public function test_command_with_publication_type_passed_as_argument()

$this->artisan('make:publication test-publication')
->expectsOutput('Creating a new publication of type [test-publication]')
->expectsQuestion('Title', 'Hello World')
->expectsQuestion('Enter data for field </>[<comment>title</comment>]', 'Hello World')
->expectsOutput('Created file test-publication/hello-world.md')
->assertExitCode(0);

Expand All @@ -135,7 +135,6 @@ public function test_command_with_invalid_publication_type_passed_as_argument()

public function test_command_with_schema_using_canonical_meta_field()
{
InputStreamHandler::mockInput("Foo\nBar");
$this->makeSchemaFile([
'canonicalField' => '__createdAt',
'fields' => [],
Expand All @@ -157,6 +156,24 @@ public function test_command_with_schema_using_canonical_meta_field()
MARKDOWN, file_get_contents(Hyde::path('test-publication/2022-01-01-000000.md')));
}

public function test_command_does_not_ask_user_to_fill_in_meta_fields()
{
$this->makeSchemaFile([
'canonicalField' => '__createdAt',
'fields' => [[
'type' => 'string',
'name' => '__createdAt',
]],
]);

$this->artisan('make:publication test-publication')
->doesntExpectOutput('Enter data for field </>[<comment>__createdAt</comment>]')
->doesntExpectOutputToContain('__createdAt')
->assertExitCode(0);

$this->assertDatedPublicationExists();
}

public function test_command_with_text_input()
{
InputStreamHandler::mockInput("Hello\nWorld");
Expand All @@ -178,6 +195,24 @@ public function test_command_with_text_input()
);
}

public function test_command_with_boolean_input()
{
$this->makeSchemaFile([
'canonicalField' => '__createdAt',
'fields' => [[
'type' => 'boolean',
'name' => 'published',
],
],
]);
$this->artisan('make:publication test-publication')
->expectsQuestion('Enter data for field </>[<comment>published</comment>]', 'true')
->assertExitCode(0);

$this->assertDatedPublicationExists();
$this->assertCreatedPublicationMatterEquals('published: true');
}

public function test_command_with_array_input()
{
InputStreamHandler::mockInput("First Tag\nSecond Tag\nThird Tag");
Expand Down Expand Up @@ -259,6 +294,7 @@ public function test_command_with_multiple_tag_inputs()
],
]);

/** @noinspection PhpParamsInspection as array is allowed by this method */
$this->artisan('make:publication test-publication')
->expectsQuestion('Which tag would you like to use?', ['foo', 'bar'])
->assertExitCode(0);
Expand All @@ -282,7 +318,7 @@ public function test_image_input_with_no_images()
]);

$this->artisan('make:publication test-publication')
->expectsOutput('Warning: No media files found in directory _media/test-publication/')
->expectsOutput(' Warning: No media files found in directory _media/test-publication/')
->expectsConfirmation('Would you like to skip this field?')
->expectsOutput('Error: Unable to locate any media files for this publication type')
->assertExitCode(1);
Expand All @@ -302,13 +338,22 @@ public function test_image_input_with_no_images_but_skips()
]);

$this->artisan('make:publication test-publication')
->expectsOutput('Warning: No media files found in directory _media/test-publication/')
->expectsOutput(' Warning: No media files found in directory _media/test-publication/')
->expectsConfirmation('Would you like to skip this field?', 'yes')
->doesntExpectOutput('Error: Unable to locate any media files for this publication type')
->assertExitCode(0);

$this->assertDatedPublicationExists();
$this->assertCreatedPublicationMatterEquals('image: null');
$this->assertEquals(
<<<'MARKDOWN'
---
__createdAt: 2022-01-01T00:00:00+00:00
---
## Write something awesome.


MARKDOWN, $this->getDatedPublicationContents());
}

public function test_tag_input_with_no_tags()
Expand All @@ -324,7 +369,7 @@ public function test_tag_input_with_no_tags()
]);

$this->artisan('make:publication test-publication')
->expectsOutput('Warning: No tags for this publication type found in tags.json')
->expectsOutput(' Warning: No tags for this publication type found in tags.json')
->expectsConfirmation('Would you like to skip this field?')
->expectsOutput('Error: Unable to locate any tags for this publication type')
->assertExitCode(1);
Expand All @@ -344,13 +389,22 @@ public function test_tag_input_with_no_tags_but_skips()
]);

$this->artisan('make:publication test-publication')
->expectsOutput('Warning: No tags for this publication type found in tags.json')
->expectsOutput(' Warning: No tags for this publication type found in tags.json')
->expectsConfirmation('Would you like to skip this field?', 'yes')
->doesntExpectOutput('Error: Unable to locate any tags for this publication type')
->assertExitCode(0);

$this->assertDatedPublicationExists();
$this->assertCreatedPublicationMatterEquals('tag: null');
$this->assertEquals(
<<<'MARKDOWN'
---
__createdAt: 2022-01-01T00:00:00+00:00
---
## Write something awesome.


MARKDOWN, $this->getDatedPublicationContents());
}

public function test_handleEmptyOptionsCollection_for_required_field()
Expand All @@ -367,11 +421,35 @@ public function test_handleEmptyOptionsCollection_for_required_field()
]);

$this->artisan('make:publication test-publication')
->doesntExpectOutput('Warning: No tags for this publication type found in tags.json')
->doesntExpectOutput(' Warning: No tags for this publication type found in tags.json')
->expectsOutput('Error: Unable to create publication: No tags for this publication type found in tags.json')
->assertExitCode(1);
}

public function test_with_custom_validation_rules()
{
$this->makeSchemaFile([
'canonicalField' => '__createdAt',
'fields' => [[
'type' => 'integer',
'name' => 'integer',
'rules' => ['max:10'],
],
],
]);

$this->artisan('make:publication test-publication')
->expectsQuestion('Enter data for field </>[<comment>integer</comment>]', 'string')
->expectsOutput('The integer must be a number.')
->expectsQuestion('Enter data for field </>[<comment>integer</comment>]', 15)
->expectsOutput('The integer must not be greater than 10.')
->expectsQuestion('Enter data for field </>[<comment>integer</comment>]', 5)
->assertExitCode(0);

$this->assertDatedPublicationExists();
$this->assertCreatedPublicationMatterEquals('integer: 5');
}

protected function makeSchemaFile(array $merge = []): void
{
file_put_contents(
Expand Down
2 changes: 1 addition & 1 deletion packages/framework/tests/Unit/InputStreamHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function testCanTerminateWithUnixEndings()

public function testFormatMessageHelperMethod()
{
$this->assertSame('Enter foo (end with an empty line)', InputStreamHandler::formatMessage('foo'));
$this->assertSame('<info>Enter values for field </>[<comment>foo</comment>] (end with an empty line)', InputStreamHandler::formatMessage('foo'));
}

protected function makeCommand(array $expected): TestCommand
Expand Down

0 comments on commit 2caf1f4

Please sign in to comment.