diff --git a/packages/framework/src/Console/Commands/MakePublicationCommand.php b/packages/framework/src/Console/Commands/MakePublicationCommand.php index 414c93add60..5d8264bee82 100644 --- a/packages/framework/src/Console/Commands/MakePublicationCommand.php +++ b/packages/framework/src/Console/Commands/MakePublicationCommand.php @@ -4,6 +4,7 @@ namespace Hyde\Console\Commands; +use Hyde\Console\Commands\Helpers\InputStreamHandler; use Hyde\Console\Concerns\ValidatingCommand; use Hyde\Framework\Actions\CreatesNewPublicationPage; use Hyde\Framework\Features\Publications\Concerns\PublicationFieldTypes; @@ -11,6 +12,7 @@ use Hyde\Framework\Features\Publications\Models\PublicationType; use Hyde\Framework\Features\Publications\PublicationService; use Illuminate\Support\Str; +use function implode; use InvalidArgumentException; use LaravelZero\Framework\Commands\Command; use Rgasch\Collection\Collection; @@ -134,34 +136,18 @@ protected function hasForceOption(): bool return (bool) $this->option('force'); } - protected function captureTextFieldInput(PublicationFieldType $field): array + protected function captureTextFieldInput(PublicationFieldType $field): string { - $lines = []; - $this->output->writeln($field->name." (end with a line containing only '<<<')"); - do { - $line = Str::replace("\n", '', fgets(STDIN)); - if ($line === '<<<') { - break; - } - $lines[] = $line; - } while (true); + $this->output->writeln($field->name.' (end with an empty line)'); - return $lines; + return implode("\n", InputStreamHandler::call()); } protected function captureArrayFieldInput(PublicationFieldType $field): array { - $lines = []; $this->output->writeln($field->name.' (end with an empty line)'); - do { - $line = Str::replace("\n", '', fgets(STDIN)); - if ($line === '') { - break; - } - $lines[] = trim($line); - } while (true); - return $lines; + return InputStreamHandler::call(); } protected function captureImageFieldInput(PublicationFieldType $field, PublicationType $pubType): string diff --git a/packages/framework/src/Framework/Actions/CreatesNewPublicationPage.php b/packages/framework/src/Framework/Actions/CreatesNewPublicationPage.php index a6f33d65c26..9491cef06a5 100644 --- a/packages/framework/src/Framework/Actions/CreatesNewPublicationPage.php +++ b/packages/framework/src/Framework/Actions/CreatesNewPublicationPage.php @@ -6,11 +6,14 @@ use Hyde\Framework\Actions\Concerns\CreateAction; use Hyde\Framework\Actions\Contracts\CreateActionContract; +use Hyde\Framework\Features\Publications\Concerns\PublicationFieldTypes; use Hyde\Framework\Features\Publications\Models\PublicationFieldType; use Hyde\Framework\Features\Publications\Models\PublicationType; use Illuminate\Console\OutputStyle; use Illuminate\Support\Carbon; use Illuminate\Support\Str; +use InvalidArgumentException; +use function is_string; use Rgasch\Collection\Collection; use RuntimeException; @@ -29,8 +32,8 @@ public function __construct( protected ?OutputStyle $output = null, ) { $canonicalFieldName = $this->pubType->canonicalField; - $canonicalFieldDefinition = $this->pubType->getFields()->filter(fn (PublicationFieldType $field): bool => $field->name === $canonicalFieldName)->first() ?? throw new RuntimeException("Could not find field definition for '$canonicalFieldName'"); - $canonicalValue = $canonicalFieldDefinition->type !== 'array' ? $this->fieldData->{$canonicalFieldName} : $this->fieldData->{$canonicalFieldName}[0]; + $canonicalFieldDefinition = $this->pubType->getCanonicalFieldDefinition(); + $canonicalValue = $this->getCanonicalValue($canonicalFieldDefinition, $canonicalFieldName); $canonicalStr = Str::of($canonicalValue)->substr(0, 64); $fileName = $this->formatStringForStorage($canonicalStr->slug()->toString()); @@ -47,15 +50,18 @@ protected function handleCreate(): void /** @var PublicationFieldType $fieldDefinition */ $fieldDefinition = $this->pubType->getFields()->where('name', $name)->firstOrFail(); - if ($fieldDefinition->type == 'text') { + if ($fieldDefinition->type === PublicationFieldTypes::Text) { $output .= "$name: |\n"; + if (is_string($value)) { + $value = Str::of($value)->explode("\n"); + } foreach ($value as $line) { $output .= " $line\n"; } continue; } - if ($fieldDefinition->type == 'array') { + if ($fieldDefinition->type === PublicationFieldTypes::Array) { $output .= "$name:\n"; foreach ($value as $item) { $output .= " - \"$item\"\n"; @@ -72,4 +78,24 @@ protected function handleCreate(): void $this->save($output); } + + protected function getCanonicalValue(PublicationFieldType $canonicalFieldDefinition, string $canonicalFieldName): string + { + if ($canonicalFieldName === '__createdAt') { + return Carbon::now()->format('Y-m-d H:i:s'); + } + + try { + // TODO: Is it reasonable to use arrays as canonical field values? + if ($canonicalFieldDefinition->type === PublicationFieldTypes::Array) { + $canonicalValue = $this->fieldData->{$canonicalFieldName}[0]; + } else { + $canonicalValue = $this->fieldData->{$canonicalFieldName}; + } + + return $canonicalValue; + } catch (InvalidArgumentException $exception) { + throw new RuntimeException("Could not find field value for '$canonicalFieldName' which is required for as it's the type's canonical field", 404, $exception); + } + } } diff --git a/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php b/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php index aa197c9ffb9..310bb19026e 100644 --- a/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php +++ b/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php @@ -15,6 +15,7 @@ use function json_decode; use Rgasch\Collection\Collection; use RuntimeException; +use function str_starts_with; /** * @see \Hyde\Framework\Testing\Feature\PublicationTypeTest @@ -117,6 +118,15 @@ public function getFieldRules(bool $reload = false): Collection }), false); } + public function getCanonicalFieldDefinition(): PublicationFieldType + { + if (str_starts_with($this->canonicalField, '__')) { + return new PublicationFieldType('string', $this->canonicalField, 0, 0); + } + + return $this->getFields()->filter(fn (PublicationFieldType $field): bool => $field->name === $this->canonicalField)->first(); + } + public function save(?string $path = null): void { $path ??= $this->getSchemaFile(); diff --git a/packages/framework/tests/Feature/Actions/CreatesNewPublicationPageTest.php b/packages/framework/tests/Feature/Actions/CreatesNewPublicationPageTest.php index cd357811858..ff39d5be183 100644 --- a/packages/framework/tests/Feature/Actions/CreatesNewPublicationPageTest.php +++ b/packages/framework/tests/Feature/Actions/CreatesNewPublicationPageTest.php @@ -4,6 +4,7 @@ namespace Hyde\Framework\Testing\Feature\Actions; +use function file_get_contents; use Hyde\Facades\Filesystem; use Hyde\Framework\Actions\CreatesNewPublicationPage; use Hyde\Framework\Features\Publications\Models\PublicationType; @@ -11,7 +12,10 @@ use Hyde\Testing\TestCase; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\File; +use Illuminate\Support\Str; use Rgasch\Collection\Collection; +use RuntimeException; +use Symfony\Component\Yaml\Yaml; /** * @covers \Hyde\Framework\Actions\CreatesNewPublicationPage @@ -53,19 +57,200 @@ public function testCreate() ', file_get_contents(Hyde::path('test-publication/hello-world.md'))); } - protected function makePublicationType(): PublicationType + public function testWithTextType() + { + $pubType = $this->makePublicationType([[ + 'type' => 'string', + 'name' => 'title', + 'min' => 0, + 'max' => 128, + ], [ + 'type' => 'text', + 'name' => 'description', + 'min' => 0, + 'max' => 128, + ]]); + + $fieldData = Collection::make([ + 'title' => 'Hello World', + 'description' => 'This is a description +It can be multiple lines.', + ]); + + $creator = new CreatesNewPublicationPage($pubType, $fieldData); + $creator->create(); + + $this->assertTrue(File::exists(Hyde::path('test-publication/hello-world.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +title: Hello World +description: | + This is a description + It can be multiple lines. +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/hello-world.md'))); + } + + public function testWithArrayType() + { + $pubType = $this->makePublicationType([[ + 'type' => 'string', + 'name' => 'title', + 'min' => 0, + 'max' => 128, + ], [ + 'type' => 'array', + 'name' => 'tags', + 'min' => 0, + 'max' => 128, + ]]); + + $fieldData = Collection::make([ + 'title' => 'Hello World', + 'tags' => ['tag1', 'tag2'], + ]); + + $creator = new CreatesNewPublicationPage($pubType, $fieldData); + $creator->create(); + + $this->assertTrue(File::exists(Hyde::path('test-publication/hello-world.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +title: Hello World +tags: + - "tag1" + - "tag2" +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/hello-world.md'))); + } + + public function testCreateWithoutSupplyingCanonicalField() + { + $pubType = $this->makePublicationType(); + $fieldData = Collection::make([]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not find field value for 'title' which is required for as it's the type's canonical field"); + $creator = new CreatesNewPublicationPage($pubType, $fieldData); + $creator->create(); + } + + public function testCreateWithoutSupplyingRequiredField() + { + $pubType = $this->makePublicationType([[ + 'type' => 'string', + 'name' => 'title', + 'min' => 0, + 'max' => 128, + ], [ + 'type' => 'string', + 'name' => 'slug', + 'min' => 0, + 'max' => 128, + ]]); + + $fieldData = Collection::make([ + 'title' => 'Hello World', + ]); + + $creator = new CreatesNewPublicationPage($pubType, $fieldData); + $creator->create(); + + // Since the inputs are collected by the command, with the shipped code this should never happen. + // If a developer is using the action directly, it's their responsibility to ensure the data is valid. + + $this->assertTrue(File::exists(Hyde::path('test-publication/hello-world.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +title: Hello World +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/hello-world.md'))); + } + + public function testItCreatesValidYaml() + { + $pubType = $this->makePublicationType([[ + 'type' => 'string', + 'name' => 'title', + 'min' => 0, + 'max' => 128, + ], [ + 'type' => 'text', + 'name' => 'description', + 'min' => 0, + 'max' => 128, + ], [ + + 'type' => 'array', + 'name' => 'tags', + 'min' => 0, + 'max' => 128, + ]]); + + $fieldData = Collection::make([ + 'title' => 'Hello World', + 'description' => 'This is a description. +It can be multiple lines.', + 'tags' => ['tag1', 'tag2'], + ]); + + $creator = new CreatesNewPublicationPage($pubType, $fieldData); + $creator->create(); + + $this->assertTrue(File::exists(Hyde::path('test-publication/hello-world.md'))); + $contents = file_get_contents(Hyde::path('test-publication/hello-world.md')); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +title: Hello World +description: | + This is a description. + It can be multiple lines. +tags: + - "tag1" + - "tag2" +--- + +## Write something awesome. + +', + $contents + ); + + $this->assertSame([ + '__createdAt' => 1640995200, + 'title' => 'Hello World', + 'description' => 'This is a description. +It can be multiple lines. +', + 'tags' => [ + 'tag1', + 'tag2', + ], + ], Yaml::parse(Str::between($contents, '---', '---'))); + } + + protected function makePublicationType(array $fields = [ + [ + 'type' => 'string', + 'name' => 'title', + 'min' => 0, + 'max' => 128, + ], + ]): PublicationType { return new PublicationType( 'test', 'title', - fields: [ - [ - 'type' => 'string', - 'name' => 'title', - 'min' => 0, - 'max' => 128, - ], - ], + fields: $fields, directory: 'test-publication', ); } diff --git a/packages/framework/tests/Feature/Commands/MakePublicationCommandTest.php b/packages/framework/tests/Feature/Commands/MakePublicationCommandTest.php index d373b092090..8d4b4e86c1e 100644 --- a/packages/framework/tests/Feature/Commands/MakePublicationCommandTest.php +++ b/packages/framework/tests/Feature/Commands/MakePublicationCommandTest.php @@ -4,7 +4,10 @@ namespace Hyde\Framework\Testing\Feature\Commands; +use function array_merge; +use function config; use function file_get_contents; +use Hyde\Console\Commands\Helpers\InputStreamHandler; use Hyde\Facades\Filesystem; use Hyde\Hyde; use Hyde\Testing\TestCase; @@ -20,6 +23,7 @@ class MakePublicationCommandTest extends TestCase protected function setUp(): void { parent::setUp(); + config(['app.throw_on_console_exception' => true]); Filesystem::makeDirectory('test-publication'); Carbon::setTestNow(Carbon::create(2022)); @@ -50,6 +54,7 @@ public function test_command_creates_publication() public function test_command_with_no_publication_types() { + config(['app.throw_on_console_exception' => false]); $this->artisan('make:publication') ->expectsOutputToContain('Creating a new Publication!') ->expectsOutput('Error: Unable to locate any publication types. Did you create any?') @@ -123,6 +128,7 @@ public function test_command_with_publication_type_passed_as_argument() public function test_command_with_invalid_publication_type_passed_as_argument() { + config(['app.throw_on_console_exception' => false]); $this->makeSchemaFile(); $this->artisan('make:publication foo') @@ -130,11 +136,94 @@ public function test_command_with_invalid_publication_type_passed_as_argument() ->assertExitCode(1); } - protected function makeSchemaFile(): void + public function test_command_with_schema_using_canonical_meta_field() + { + InputStreamHandler::mockInput("Foo\nBar"); + $this->makeSchemaFile([ + 'canonicalField' => '__createdAt', + 'fields' => [], + ]); + + $this->artisan('make:publication test-publication') + ->assertExitCode(0); + + $this->assertTrue(File::exists(Hyde::path('test-publication/2022-01-01-000000.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/2022-01-01-000000.md'))); + } + + // text + public function test_command_with_text_input() + { + InputStreamHandler::mockInput("Hello\nWorld"); + $this->makeSchemaFile([ + 'canonicalField' => 'description', + 'fields' => [[ + 'type' => 'text', + 'name' => 'description', + 'min' => '0', + 'max' => '0', + ], + ], + ]); + $this->artisan('make:publication test-publication') + ->assertExitCode(0); + + $this->assertTrue(File::exists(Hyde::path('test-publication/hello-world.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +description: | + Hello + World +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/hello-world.md'))); + } + + // array + public function test_command_with_array_input() + { + InputStreamHandler::mockInput("First Tag\nSecond Tag\nThird Tag"); + $this->makeSchemaFile([ + 'canonicalField' => 'tags', + 'fields' => [[ + 'type' => 'array', + 'name' => 'tags', + 'min' => '0', + 'max' => '0', + ], + ], + ]); + + $this->artisan('make:publication test-publication') + ->assertExitCode(0); + + $this->assertTrue(File::exists(Hyde::path('test-publication/first-tag.md'))); + $this->assertEquals('--- +__createdAt: 2022-01-01 00:00:00 +tags: + - "First Tag" + - "Second Tag" + - "Third Tag" +--- + +## Write something awesome. + +', file_get_contents(Hyde::path('test-publication/first-tag.md'))); + } + + protected function makeSchemaFile(array $merge = []): void { file_put_contents( Hyde::path('test-publication/schema.json'), - json_encode([ + json_encode(array_merge([ 'name' => 'Test Publication', 'canonicalField' => 'title', 'detailTemplate' => 'test-publication_detail', @@ -145,7 +234,7 @@ protected function makeSchemaFile(): void 'sortField' => '__createdAt', 'sortAscending' => true, ], - 'fields' => [ + 'fields' => [ [ 'name' => 'title', 'min' => '0', @@ -153,7 +242,7 @@ protected function makeSchemaFile(): void 'type' => 'string', ], ], - ]) + ], $merge)) ); }