diff --git a/packages/framework/src/Console/Commands/ValidatePublicationsCommand.php b/packages/framework/src/Console/Commands/ValidatePublicationsCommand.php new file mode 100644 index 00000000000..4a02f666f38 --- /dev/null +++ b/packages/framework/src/Console/Commands/ValidatePublicationsCommand.php @@ -0,0 +1,113 @@ +title('Validating publications!'); + + $pubTypesToValidate = PublicationService::getPublicationTypes(); + $verbose = $this->option('verbose'); + $name = $this->argument('publicationType'); + if ($name) { + if (! $pubTypesToValidate->has($name)) { + throw new InvalidArgumentException("Publication type [$name] does not exist"); + } + $pubTypesToValidate = [$name => $pubTypesToValidate->{$name}]; + } + + if (count($pubTypesToValidate) === 0) { + throw new InvalidArgumentException('No publication types to validate!'); + } + + $checkmark = "\u{2713}"; + $xMark = "\u{2717}"; + $countPubTypes = 0; + $countPubs = 0; + $countFields = 0; + $countErrors = 0; + $countWarnings = 0; + + foreach ($pubTypesToValidate as $name=>$pubType) { + $countPubTypes++; + $publications = PublicationService::getPublicationsForPubType($pubType); + $this->output->write("Validating publication type [$name]"); + $publicationFieldRules = $pubType->getFieldRules(false); + + /** @var \Hyde\Pages\PublicationPage $publication */ + foreach ($publications as $publication) { + $countPubs++; + $this->output->write("\n Validating publication [$publication->title]"); + $publication->matter->forget('__createdAt'); + + foreach ($publication->type->fields as $field) { + $countFields++; + $fieldName = $field['name']; + $pubTypeField = new PublicationFieldType($field['type'], $fieldName, $field['min'], $field['max'], $field['tagGroup'] ?? null, $pubType); + + try { + if ($verbose) { + $this->output->write("\n Validating field [$fieldName]"); + } + + if (! $publication->matter->has($fieldName)) { + throw new Exception("Field [$fieldName] is missing from publication"); + } + + $pubTypeField->validate($publication->matter->{$fieldName} ?? null, + $publicationFieldRules->{$fieldName} ?? null); + $this->output->writeln(" $checkmark"); + } catch (Exception $e) { + $countErrors++; + $this->output->writeln(" $xMark\n {$e->getMessage()}"); + } + $publication->matter->forget($fieldName); + } + + foreach ($publication->matter->data as $k=>$v) { + $countWarnings++; + $this->output->writeln(" Field [$k] is not defined in publication type"); + } + } + $this->output->newLine(); + } + + $warnColor = $countWarnings ? 'yellow' : 'green'; + $errorColor = $countErrors ? 'red' : 'green'; + $this->title('Summary:'); + $this->output->writeln("Validated $countPubTypes Publication Types, $countPubs Publications, $countFields Fields"); + $this->output->writeln("Found $countWarnings Warnings"); + $this->output->writeln("Found $countErrors Errors"); + if ($countErrors) { + return Command::FAILURE; + } + + return Command::SUCCESS; + } +} diff --git a/packages/framework/src/Console/HydeConsoleServiceProvider.php b/packages/framework/src/Console/HydeConsoleServiceProvider.php index 674585e3f40..5776a6dfab5 100644 --- a/packages/framework/src/Console/HydeConsoleServiceProvider.php +++ b/packages/framework/src/Console/HydeConsoleServiceProvider.php @@ -39,6 +39,8 @@ public function register(): void Commands\ValidateCommand::class, Commands\ServeCommand::class, Commands\DebugCommand::class, + + Commands\ValidatePublicationsCommand::class, ] ); } diff --git a/packages/framework/src/Framework/Features/Publications/Models/PublicationFieldType.php b/packages/framework/src/Framework/Features/Publications/Models/PublicationFieldType.php index a4f0a604582..233d5105db9 100644 --- a/packages/framework/src/Framework/Features/Publications/Models/PublicationFieldType.php +++ b/packages/framework/src/Framework/Features/Publications/Models/PublicationFieldType.php @@ -5,10 +5,12 @@ namespace Hyde\Framework\Features\Publications\Models; use Hyde\Framework\Features\Publications\Concerns\PublicationFieldTypes; +use Hyde\Framework\Features\Publications\PublicationService; use Hyde\Support\Concerns\Serializable; use Hyde\Support\Contracts\SerializableContract; use Illuminate\Support\Str; use InvalidArgumentException; +use Rgasch\Collection\Collection; use function strtolower; /** @@ -58,10 +60,66 @@ public function toArray(): array ]; } - public function validateInputAgainstRules(string $input): bool + public function getValidationRules(bool $reload = true): Collection { - // TODO: Implement this method. + $defaultRules = Collection::create(PublicationFieldTypes::values()); + $fieldRules = Collection::create($defaultRules->get($this->type->value)); - return true; + $doBetween = true; + // The trim command used to process the min/max input results in a string, so + // we need to test both int and string values to determine required status. + if (($this->min && ! $this->max) || ($this->min == '0' && $this->max == '0')) { + $fieldRules->forget($fieldRules->search('required')); + $doBetween = false; + } + + switch ($this->type->value) { + case 'array': + $fieldRules->add('array'); + break; + case 'datetime': + if ($doBetween) { + $fieldRules->add("after:$this->min"); + $fieldRules->add("before:$this->max"); + } + break; + case 'float': + case 'integer': + case 'string': + case 'text': + if ($doBetween) { + $fieldRules->add("between:$this->min,$this->max"); + } + break; + case 'image': + $mediaFiles = PublicationService::getMediaForPubType($this->publicationType, $reload); + $valueList = $mediaFiles->implode(','); + $fieldRules->add("in:$valueList"); + break; + case 'tag': + $tagValues = PublicationService::getValuesForTagName($this->tagGroup, $reload); + $valueList = $tagValues->implode(','); + $fieldRules->add("in:$valueList"); + break; + case 'url': + break; + default: + throw new \InvalidArgumentException( + "Unhandled field type [{$this->type->value}]. Possible field types are: ".implode(', ', PublicationFieldTypes::values()) + ); + } + + return $fieldRules; + } + + public function validate(mixed $input = null, Collection $fieldRules = null): array + { + if (! $fieldRules) { + $fieldRules = $this->getValidationRules(false); + } + + $validator = validator([$this->name => $input], [$this->name => $fieldRules->toArray()]); + + return $validator->validate(); } } diff --git a/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php b/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php index 08d538d65c4..05974c2e479 100644 --- a/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php +++ b/packages/framework/src/Framework/Features/Publications/Models/PublicationType.php @@ -105,6 +105,19 @@ public function getFields(): Collection return Collection::create($result, false); } + /** + * @param bool $reload + * @return \Rgasch\Collection\Collection + */ + public function getFieldRules(bool $reload): Collection + { + $result = $this->getFields()->mapWithKeys(function (PublicationFieldType $field) use ($reload) { + return [$field->name => $field->getValidationRules($reload)]; + }); + + return Collection::create($result, false); + } + public function save(?string $path = null): void { $path ??= $this->getSchemaFile(); diff --git a/packages/framework/src/Markdown/Models/FrontMatter.php b/packages/framework/src/Markdown/Models/FrontMatter.php index afcba3ebc25..7b6fb845928 100644 --- a/packages/framework/src/Markdown/Models/FrontMatter.php +++ b/packages/framework/src/Markdown/Models/FrontMatter.php @@ -72,6 +72,14 @@ public function toArray(): array return $this->data; } + /** + * @internal This method is experimental and may change or be removed in the future. + */ + public function forget(string $key): void + { + unset($this->data[$key]); + } + public static function fromArray(array $matter): static { return new static($matter); diff --git a/packages/framework/tests/Feature/Commands/ValidatePublicationsCommandTest.php b/packages/framework/tests/Feature/Commands/ValidatePublicationsCommandTest.php new file mode 100644 index 00000000000..c1a96858e79 --- /dev/null +++ b/packages/framework/tests/Feature/Commands/ValidatePublicationsCommandTest.php @@ -0,0 +1,111 @@ +artisan('validate:publications') + ->expectsOutput('Error: No publication types to validate!') + ->assertExitCode(1); + } + + public function testWithInvalidPublicationType() + { + $this->artisan('validate:publications', ['publicationType' => 'invalid']) + ->expectsOutput('Error: Publication type [invalid] does not exist') + ->assertExitCode(1); + } + + public function testWithPublicationType() + { + $this->directory('test-publication'); + $this->setupTestPublication(); + copy(Hyde::path('tests/fixtures/test-publication.md'), Hyde::path('test-publication/test.md')); + + $this->artisan('validate:publications') + ->expectsOutputToContain('Validating publications!') + ->expectsOutput('Validating publication type [test-publication]') + ->expectsOutputToContain('Validating publication [My Title]') + ->doesntExpectOutputToContain('Validating field') + ->expectsOutput('Validated 1 Publication Types, 1 Publications, 1 Fields') + ->expectsOutput('Found 0 Warnings') + ->expectsOutput('Found 0 Errors') + ->assertExitCode(0); + } + + public function testWithPublicationTypeAndVerboseOutput() + { + $this->directory('test-publication'); + $this->setupTestPublication(); + copy(Hyde::path('tests/fixtures/test-publication.md'), Hyde::path('test-publication/test.md')); + + $this->artisan('validate:publications', ['--verbose' => true]) + ->expectsOutputToContain('Validating publications!') + ->expectsOutput('Validating publication type [test-publication]') + ->expectsOutputToContain('Validating publication [My Title]') + ->expectsOutputToContain('Validating field') + ->expectsOutput('Validated 1 Publication Types, 1 Publications, 1 Fields') + ->expectsOutput('Found 0 Warnings') + ->expectsOutput('Found 0 Errors') + ->assertExitCode(0); + } + + public function testWithInvalidPublication() + { + $this->directory('test-publication'); + $this->setupTestPublication(); + file_put_contents(Hyde::path('test-publication/test.md'), '--- +Foo: bar +--- + +Hello World +'); + + $this->artisan('validate:publications') + ->expectsOutputToContain('Validating publications!') + ->expectsOutput('Validating publication type [test-publication]') + ->expectsOutputToContain('Validating publication [Test]') + ->doesntExpectOutputToContain('Validating field') + ->expectsOutput('Validated 1 Publication Types, 1 Publications, 1 Fields') + ->expectsOutput('Found 1 Warnings') + ->expectsOutput('Found 1 Errors') + ->assertExitCode(1); + } + + public function testWithMultiplePublicationTypes() + { + $this->directory('test-publication'); + $this->directory('test-publication-two'); + $this->setupTestPublication(); + $this->setupTestPublication('test-publication-two'); + + $this->artisan('validate:publications') + ->expectsOutput('Validating publication type [test-publication-two]') + ->expectsOutput('Validating publication type [test-publication]') + ->assertExitCode(0); + } + + public function testOnlySpecifiedTypeIsValidatedWhenUsingArgument() + { + $this->directory('test-publication'); + $this->directory('test-publication-two'); + $this->setupTestPublication(); + $this->setupTestPublication('test-publication-two'); + + $this->artisan('validate:publications test-publication-two') + ->expectsOutput('Validating publication type [test-publication-two]') + ->doesntExpectOutput('Validating publication type [test-publication]') + ->assertExitCode(0); + } +}