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

Add a publication type validation command and helpers #841

Merged
merged 50 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a09cd86
Create crude validation method for publication types
caendesilva Jan 17, 2023
c09a121
Add todos
caendesilva Jan 17, 2023
1a8b68e
Test with invalid schema
caendesilva Jan 17, 2023
c76de85
Refactor to use the Illuminate validator
caendesilva Jan 17, 2023
ba05c6f
Test with fields
caendesilva Jan 17, 2023
54dd038
Test with invalid fields
caendesilva Jan 17, 2023
8b755ea
Refactor to use the Illuminate validator
caendesilva Jan 17, 2023
8c4e69f
Merge Illuminate validator calls
caendesilva Jan 17, 2023
03a2a42
Coalesce to null for optional fields
caendesilva Jan 17, 2023
7b6a69d
Use prohibited rule to disallow directory schema property
caendesilva Jan 17, 2023
d435d72
Make directory rule nullable
caendesilva Jan 17, 2023
94c1d6d
Specify allowed array keys
caendesilva Jan 17, 2023
baed80c
Revert "Specify allowed array keys"
caendesilva Jan 17, 2023
5389235
Fix mismatched property names
caendesilva Jan 17, 2023
743b13e
Test with invalid directory
caendesilva Jan 17, 2023
52ef438
Expect ValidationExceptions
caendesilva Jan 17, 2023
caf36bb
Add option to buffer and return validation failures
caendesilva Jan 17, 2023
43d1124
Test complete buffered output
caendesilva Jan 18, 2023
2956011
Move publication schema validation logic to service
caendesilva Jan 18, 2023
d7f2a53
Add option to only validate the schemas
caendesilva Jan 18, 2023
c8d1624
Display field errors
caendesilva Jan 18, 2023
4be5a47
Split out publication type validation to new command class
caendesilva Jan 18, 2023
3dda1ac
Introduce local variables
caendesilva Jan 18, 2023
210e347
Update title
caendesilva Jan 18, 2023
d9a6366
Split out display logic
caendesilva Jan 18, 2023
85dece4
Reorder helper methods
caendesilva Jan 18, 2023
4c4d284
Import used functions
caendesilva Jan 18, 2023
a84e601
Remove unused imports
caendesilva Jan 18, 2023
2747ff4
Register the command
caendesilva Jan 18, 2023
f1635e3
Inline local variables
caendesilva Jan 18, 2023
5e97c07
Clarify PHPDoc comment
caendesilva Jan 18, 2023
d83f72b
Remove unused class constants
caendesilva Jan 18, 2023
ccdab4a
Replace class property with local variable
caendesilva Jan 18, 2023
6b19fcc
Reorder execution flow to merge conditionals
caendesilva Jan 18, 2023
d388934
Throw if there are no schema files to validate
caendesilva Jan 18, 2023
c483f74
Bring out title to run before validation
caendesilva Jan 18, 2023
5b2c320
Create custom function to get accurate error count
caendesilva Jan 18, 2023
b662cd8
Create ValidatePublicationTypesCommandTest.php
caendesilva Jan 18, 2023
835ae27
Add output expectations
caendesilva Jan 18, 2023
88f1058
Test with multiple publication types
caendesilva Jan 18, 2023
f9ec892
Test with no field definitions
caendesilva Jan 18, 2023
2e8ab1f
Test with Json output
caendesilva Jan 18, 2023
9a343df
Add additional Json test
caendesilva Jan 18, 2023
24d4c21
Extract code to setup method
caendesilva Jan 18, 2023
618f202
Apply fixes from StyleCI
StyleCIBot Jan 18, 2023
0798d8f
Merge branch 'publications-feature' into publication-type-validation
caendesilva Jan 18, 2023
8ec0267
Revert "Display field errors"
caendesilva Jan 18, 2023
dee1660
Revert "Add option to only validate the schemas"
caendesilva Jan 18, 2023
9333ae9
Refactor to use the publication name as argument
caendesilva Jan 18, 2023
858469c
Inline local variable
caendesilva Jan 18, 2023
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
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