Skip to content

Commit

Permalink
Merge pull request #841 from hydephp/publication-type-validation
Browse files Browse the repository at this point in the history
Add a publication type validation command and helpers
  • Loading branch information
caendesilva authored Jan 18, 2023
2 parents e921f61 + 858469c commit febfaae
Show file tree
Hide file tree
Showing 7 changed files with 648 additions and 0 deletions.
142 changes: 142 additions & 0 deletions packages/publications/src/Commands/ValidatePublicationTypesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

namespace Hyde\Publications\Commands;

use function array_filter;
use function basename;
use function dirname;
use function glob;
use Hyde\Hyde;
use Hyde\Publications\PublicationService;
use function implode;
use InvalidArgumentException;
use function json_encode;
use LaravelZero\Framework\Commands\Command;
use function memory_get_peak_usage;
use function microtime;
use function next;
use function round;
use function sprintf;

/**
* Hyde Command to validate all publication schema file..
*
* @see \Hyde\Publications\Testing\Feature\ValidatePublicationTypesCommandTest
*
* @internal This command is not part of the public API and may change without notice.
*/
class ValidatePublicationTypesCommand extends ValidatingCommand
{
protected const CROSS_MARK = 'x';

/** @var string */
protected $signature = 'validate:publicationTypes {--json : Display results as JSON.}';

/** @var string */
protected $description = 'Validate all publication schema files.';

protected array $results = [];

public function safeHandle(): int
{
$timeStart = microtime(true);

if (! $this->option('json')) {
$this->title('Validating publication schemas!');
}

$this->validateSchemaFiles();

if ($this->option('json')) {
$this->outputJson();
} else {
$this->displayResults();
$this->outputSummary($timeStart);
}

if ($this->countErrors() > 0) {
return Command::FAILURE;
}

return Command::SUCCESS;
}

protected function validateSchemaFiles(): void
{
/** Uses the same glob pattern as {@see PublicationService::getSchemaFiles()} */
$schemaFiles = glob(Hyde::path(Hyde::getSourceRoot()).'/*/schema.json');

if (empty($schemaFiles)) {
throw new InvalidArgumentException('No publication types to validate!');
}

foreach ($schemaFiles as $schemaFile) {
$publicationName = basename(dirname($schemaFile));
$this->results[$publicationName] = PublicationService::validateSchemaFile($publicationName, false);
}
}

protected function displayResults(): void
{
foreach ($this->results as $name => $errors) {
$this->infoComment('Validating schema file for', $name);

$schemaErrors = $errors['schema'];
if (empty($schemaErrors)) {
$this->line('<info> No top-level schema errors found</info>');
} else {
$this->line(sprintf(' <fg=red>Found %s top-level schema errors:</>', count($schemaErrors)));
foreach ($schemaErrors as $error) {
$this->line(sprintf(' <fg=red>%s</> <comment>%s</comment>', self::CROSS_MARK, implode(' ', $error)));
}
}

$schemaFields = $errors['fields'];
if (empty(array_filter($schemaFields))) {
$this->line('<info> No field-level schema errors found</info>');
} else {
$this->newLine();
$this->line(sprintf(' <fg=red>Found errors in %s field definitions:</>', count($schemaFields)));
foreach ($schemaFields as $fieldNumber => $fieldErrors) {
$this->line(sprintf(' <fg=cyan>Field #%s:</>', $fieldNumber + 1));
foreach ($fieldErrors as $error) {
$this->line(sprintf(' <fg=red>%s</> <comment>%s</comment>', self::CROSS_MARK,
implode(' ', $error)));
}
}
}

if (next($this->results)) {
$this->newLine();
}
}
}

protected function outputSummary($timeStart): void
{
$this->newLine();
$this->info(sprintf('All done in %sms using %sMB peak memory!',
round((microtime(true) - $timeStart) * 1000),
round(memory_get_peak_usage() / 1024 / 1024)
));
}

protected function outputJson(): void
{
$this->output->writeln(json_encode($this->results, JSON_PRETTY_PRINT));
}

protected function countErrors(): int
{
$errors = 0;

foreach ($this->results as $results) {
$errors += count($results['schema']);
$errors += count(array_filter($results['fields']));
}

return $errors;
}
}
10 changes: 10 additions & 0 deletions packages/publications/src/Models/PublicationType.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,14 @@ protected function withoutNullValues(array $array): array
{
return array_filter($array, fn (mixed $value): bool => ! is_null($value));
}

/**
* Validate the schema.json file is valid.
*
* @internal This method is experimental and may be removed without notice
*/
public function validateSchemaFile(bool $throw = true): array
{
return PublicationService::validateSchemaFile($this->getIdentifier(), $throw);
}
}
76 changes: 76 additions & 0 deletions packages/publications/src/PublicationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
namespace Hyde\Publications;

use function glob;
use Hyde\Facades\Filesystem;
use Hyde\Hyde;
use Hyde\Publications\Models\PublicationPage;
use Hyde\Publications\Models\PublicationTags;
use Hyde\Publications\Models\PublicationType;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use function json_decode;
use function validator;

/**
* @see \Hyde\Publications\Testing\Feature\PublicationServiceTest
Expand Down Expand Up @@ -89,6 +92,79 @@ public static function publicationTypeExists(string $pubTypeName): bool
return static::getPublicationTypes()->has(Str::slug($pubTypeName));
}

/**
* Validate the schema.json file is valid.
*
* @internal This method is experimental and may be removed without notice
*/
public static function validateSchemaFile(string $pubTypeName, bool $throw = true): array
{
$schema = json_decode(Filesystem::getContents("$pubTypeName/schema.json"));
$errors = [];

$schemaValidator = validator([
'name' => $schema->name ?? null,
'canonicalField' => $schema->canonicalField ?? null,
'detailTemplate' => $schema->detailTemplate ?? null,
'listTemplate' => $schema->listTemplate ?? null,
'sortField' => $schema->sortField ?? null,
'sortAscending' => $schema->sortAscending ?? null,
'pageSize' => $schema->pageSize ?? null,
'fields' => $schema->fields ?? null,
'directory' => $schema->directory ?? null,
], [
'name' => 'required|string',
'canonicalField' => 'nullable|string',
'detailTemplate' => 'nullable|string',
'listTemplate' => 'nullable|string',
'sortField' => 'nullable|string',
'sortAscending' => 'nullable|boolean',
'pageSize' => 'nullable|integer',
'fields' => 'nullable|array',
'directory' => 'nullable|prohibited',
]);

$errors['schema'] = $schemaValidator->errors()->toArray();

if ($throw) {
$schemaValidator->validate();
}

// TODO warn if fields are empty?
// TODO warn if canonicalField does not match meta field or actual?
// TODO Warn if template files do not exist (assuming files not vendor views)?
// TODO warn if pageSize is less than 0 (as that equals no pagination)?
$errors['fields'] = [];

foreach ($schema->fields as $field) {
$fieldValidator = validator([
'type' => $field->type ?? null,
'name' => $field->name ?? null,
'rules' => $field->rules ?? null,
'tagGroup' => $field->tagGroup ?? null,
], [
'type' => 'required|string',
'name' => 'required|string',
'rules' => 'nullable|array',
'tagGroup' => 'nullable|string',
]);

// TODO check tag group exists?
$errors['fields'][] = $fieldValidator->errors()->toArray();

if ($throw) {
$fieldValidator->validate();
}
}

return $errors;
}

protected static function getSchemaFiles(): array
{
return glob(Hyde::path(Hyde::getSourceRoot()).'/*/schema.json');
Expand Down
1 change: 1 addition & 0 deletions packages/publications/src/PublicationsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function register(): void
Commands\MakePublicationTypeCommand::class,
Commands\MakePublicationCommand::class,

Commands\ValidatePublicationTypesCommand::class,
Commands\ValidatePublicationsCommand::class,
Commands\SeedPublicationCommand::class,
]);
Expand Down
122 changes: 122 additions & 0 deletions packages/publications/tests/Feature/PublicationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Hyde\Testing\TestCase;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Validation\ValidationException;
use function json_encode;
use function mkdir;

Expand Down Expand Up @@ -214,6 +215,127 @@ public function testGetValuesForTagName()
$this->assertSame(['bar', 'baz'], PublicationService::getValuesForTagName('foo')->toArray());
}

public function testValidateSchemaFile()
{
$this->directory('test-publication');
$publicationType = new PublicationType('test-publication', fields: [
['name' => 'myField', 'type' => 'string'],
]);
$publicationType->save();

$publicationType->validateSchemaFile();

$this->assertTrue(true);
}

public function testValidateSchemaFileWithInvalidSchema()
{
$this->directory('test-publication');
$publicationType = new PublicationType('test-publication');
$publicationType->save();

$this->file('test-publication/schema.json', <<<'JSON'
{
"name": 123,
"canonicalField": 123,
"detailTemplate": 123,
"listTemplate": 123,
"sortField": 123,
"sortAscending": 123,
"pageSize": "123",
"fields": 123,
"directory": "foo"
}
JSON
);

$this->expectException(ValidationException::class);
$publicationType->validateSchemaFile();
}

public function testValidateSchemaFileWithInvalidFields()
{
$this->directory('test-publication');
$publicationType = new PublicationType('test-publication');
$publicationType->save();

$this->file('test-publication/schema.json', <<<'JSON'
{
"name": "test-publication",
"canonicalField": "__createdAt",
"detailTemplate": "detail.blade.php",
"listTemplate": "list.blade.php",
"sortField": "__createdAt",
"sortAscending": true,
"pageSize": 0,
"fields": [
{
"name": 123,
"type": 123
},
{
"noName": "myField",
"noType": "string"
}
]
}
JSON
);

$this->expectException(ValidationException::class);
$publicationType->validateSchemaFile();
}

public function testValidateSchemaFileWithInvalidDataBuffered()
{
$this->directory('test-publication');
$publicationType = new PublicationType('test-publication');
$publicationType->save();

$this->file('test-publication/schema.json', <<<'JSON'
{
"name": 123,
"canonicalField": 123,
"detailTemplate": 123,
"listTemplate": 123,
"sortField": 123,
"sortAscending": 123,
"pageSize": "123",
"fields": [
{
"name": 123,
"type": 123
},
{
"noName": "myField",
"noType": "string"
}
],
"directory": "foo"
}
JSON
);

$this->assertSame([
'schema' => [
'name' => ['The name must be a string.'],
'canonicalField' => ['The canonical field must be a string.'],
'detailTemplate' => ['The detail template must be a string.'],
'listTemplate' => ['The list template must be a string.'],
'sortField' => ['The sort field must be a string.'],
'sortAscending' => ['The sort ascending field must be true or false.'],
'directory' => ['The directory field is prohibited.'],
],
'fields' => [[
'type' => ['The type must be a string.'],
'name' => ['The name must be a string.'],
], [
'type' => ['The type field is required.'],
'name' => ['The name field is required.'],
]],
], $publicationType->validateSchemaFile(false));
}

protected function createPublicationType(): void
{
(new PublicationType('test-publication'))->save();
Expand Down
Loading

0 comments on commit febfaae

Please sign in to comment.