1
0

Improved validation

This commit is contained in:
Richard Dern 2024-05-09 21:52:08 +02:00
parent d98840c1dd
commit 89d8a897c4

View File

@ -3,9 +3,12 @@
namespace App\Console\Commands\Bundle; namespace App\Console\Commands\Bundle;
use App\Classes\Bundle; use App\Classes\Bundle;
use App\Services\HtmlValidator;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use JsonSchema\Validator; use JsonSchema\Validator;
@ -22,14 +25,23 @@ class Validate extends Command
protected Filesystem $sourceDisk; protected Filesystem $sourceDisk;
protected array $invalid = []; protected array $invalidJson = [];
protected array $invalidHtml = [];
protected array $invalidCss = [];
protected $bundles;
public function __construct() public function __construct()
{ {
$this->signature = 'bundle:validate $this->signature = 'bundle:validate
{ --r|recursive : Also render sub-bundles } { --no-css : Do not validate CSS }
{ --no-html : Do not validate HTML }
{ --no-json : Do not validate JSON }
{ --r|recursive : Also validate sub-bundles }
{ --source-disk= : Use specified content disk - Defaults to <info>' . env('CONTENT_DISK') . '</info> } { --source-disk= : Use specified content disk - Defaults to <info>' . env('CONTENT_DISK') . '</info> }
{ path? : Path to a specific bundle to render - Default to <info>/</info> } { path? : Path to a specific bundle to validate - Default to <info>/</info> }
'; ';
parent::__construct(); parent::__construct();
@ -41,8 +53,19 @@ public function __construct()
public function handle() public function handle()
{ {
$this->selectDisk() $this->selectDisk()
->validate() ->validateCss()
->selectBundles()
->validateJson()
->validateHtml()
->showReport(); ->showReport();
if (
!empty($this->invalidCss)
|| !empty($this->invalidJson)
|| !empty($this->invalidHtml)
) {
exit(1);
}
} }
/** /**
@ -54,9 +77,9 @@ private function selectDisk(): self
$this->sourceDisk = Storage::disk($sourceDisk); $this->sourceDisk = Storage::disk($sourceDisk);
$this->comment( $this->line(
sprintf( sprintf(
'Using `%s` as source disk', 'Using <info>%s</info> as source disk',
$sourceDisk $sourceDisk
) )
); );
@ -67,16 +90,16 @@ private function selectDisk(): self
/** /**
* Collect a list of bundles to validate * Collect a list of bundles to validate
*/ */
private function getBundles() private function selectBundles(): self
{ {
$path = $this->argument('path') ?? '/'; $path = $this->argument('path') ?? '/';
$comment = sprintf('Validating %s', $path); $comment = sprintf('Validating <info>%s</info>', $path);
if ($this->option('recursive')) { if ($this->option('recursive')) {
$comment .= ' and all sub-bundles'; $comment .= ' and <info>all sub-bundles</info>';
} }
$this->comment($comment); $this->line($comment);
$this->output->write('Collecting bundles... '); $this->output->write('Collecting bundles... ');
if ($this->option('recursive')) { if ($this->option('recursive')) {
@ -85,16 +108,18 @@ private function getBundles()
$bundles = [new Bundle($path, $this->sourceDisk)]; $bundles = [new Bundle($path, $this->sourceDisk)];
} }
$this->bundles = $bundles;
$this->info('OK'); $this->info('OK');
return $bundles; return $this;
} }
/** /**
* Return an associative array containing available bundles metadata files * Return an associative array containing available bundles metadata files
* as keys and json-decoded validation schemas as values * as keys and json-decoded validation schemas as values
*/ */
private function getValidators() private function getJsonValidators()
{ {
return array_map(function ($path) { return array_map(function ($path) {
return json_decode(file_get_contents($path)); return json_decode(file_get_contents($path));
@ -104,15 +129,21 @@ private function getValidators()
/** /**
* Perform json validation on selected bundles * Perform json validation on selected bundles
*/ */
private function validate(): self private function validateJson(): self
{ {
$bundles = $this->getBundles(); if ($this->option('no-json')) {
$validators = $this->getValidators(); return $this;
}
$this->line('Validating <info>JSON</info>...');
$bundles = $this->bundles;
$validators = $this->getJsonValidators();
progress( progress(
label: 'Validating... ', label: 'Validating... ',
steps: $bundles, steps: $bundles,
callback: fn (Bundle $bundle, $progress) => $this->handleBundle($bundle, $progress, $validators) callback: fn (Bundle $bundle, $progress) => $this->handleBundleJsonValidation($bundle, $progress, $validators)
); );
return $this; return $this;
@ -121,7 +152,7 @@ private function validate(): self
/** /**
* Handle specific bundle * Handle specific bundle
*/ */
private function handleBundle(Bundle $bundle, $progress, $validators) private function handleBundleJsonValidation(Bundle $bundle, $progress, $validators)
{ {
$progress->label(sprintf('Validating %s ...', $bundle->getPath())); $progress->label(sprintf('Validating %s ...', $bundle->getPath()));
@ -141,7 +172,7 @@ private function handleBundle(Bundle $bundle, $progress, $validators)
$validator->validate($data, $schema); $validator->validate($data, $schema);
if (!$validator->isValid()) { if (!$validator->isValid()) {
$this->invalid[$bundle->getPath()] = [ $this->invalidJson[$bundle->getPath()] = [
'metadata' => $metadataFilename, 'metadata' => $metadataFilename,
'errors' => $validator->getErrors(), 'errors' => $validator->getErrors(),
]; ];
@ -149,20 +180,109 @@ private function handleBundle(Bundle $bundle, $progress, $validators)
} }
} }
private function validateCss(): self
{
if ($this->option('no-css')) {
return $this;
}
$this->line('Validating <info>CSS</info>...');
$file = public_path(Vite::asset('resources/css/app.css'));
$cacheKey = sprintf('css_validation_%s', Str::slug($file));
$result = Cache::remember($cacheKey, now()->addMonth(), function () use ($file) {
$content = file_get_contents($file);
return HtmlValidator::validateCss($content);
});
$errors = collect($result['messages'])->where('type', '=', 'error');
if ($errors->count() > 0) {
$this->invalidCss = $result;
Cache::forever('css_is_valid', false);
} else {
Cache::forever('css_is_valid', true);
}
return $this;
}
/**
* Perform json validation on selected bundles
*/
private function validateHtml(): self
{
if ($this->option('no-html')) {
return $this;
}
$this->line('Validating <info>HTML</info>...');
$bundles = $this->bundles;
progress(
label: 'Validating... ',
steps: $bundles,
callback: fn (Bundle $bundle, $progress) => $this->handleBundleHtmlValidation($bundle, $progress)
);
return $this;
}
private function handleBundleHtmlValidation(Bundle $bundle, $progress)
{
$progress->label(sprintf('Validating %s ...', $bundle->getPath()));
$rendered = $bundle->render();
foreach ($rendered as $path => $content) {
$result = HtmlValidator::validateHtml($content);
$errors = collect($result['messages'])->where('type', '=', 'error');
$cacheKey = sprintf('html_is_valid_%s', Str::slug($path));
if ($errors->count() > 0) {
$this->invalidHtml[$path] = $result;
Cache::forever($cacheKey, false);
} else {
Cache::forever($cacheKey, true);
}
}
}
/** /**
* Show a report in case of need * Show a report in case of need
*/ */
private function showReport() private function showReport()
{ {
if (empty($this->invalid)) { $this->showCssReport();
$this->info('All files are valid'); $this->showJsonReport();
} else { $this->showHtmlReport();
$count = count($this->invalid); }
$this->error(sprintf('%d invalid %s reported', $count, Str::plural('file', $count))); /**
* Show a report in case of need
*/
private function showJsonReport()
{
if ($this->option('no-json')) {
return $this;
}
if (empty($this->invalidJson)) {
$this->info('Checked JSON files are valid');
} else {
$count = count($this->invalidJson);
$this->error(sprintf('%d invalid JSON %s reported', $count, Str::plural('file', $count)));
$this->newLine(2); $this->newLine(2);
foreach ($this->invalid as $bundlePath => $data) { foreach ($this->invalidJson as $bundlePath => $data) {
$this->line($bundlePath); $this->line($bundlePath);
$this->line(sprintf(' > In <comment>%s</comment>:', $data['metadata'])); $this->line(sprintf(' > In <comment>%s</comment>:', $data['metadata']));
@ -172,4 +292,73 @@ private function showReport()
} }
} }
} }
private function showCssReport()
{
if ($this->option('no-css')) {
return $this;
}
if (empty($this->invalidCss)) {
$this->info('Checked CSS files are valid');
} else {
$count = count($this->invalidCss);
$this->error(sprintf('%d CSS %s reported', $count, Str::plural('error', $count)));
$this->newLine();
$data = $this->invalidCss;
foreach ($data['messages'] as $message) {
$type = $message['type'];
$subType = $message['subType'] ?? $type;
$decoration = match ($subType) {
'error' => 'error',
'info' => 'comment',
'warning' => 'comment'
};
$this->newLine();
$this->line(sprintf(' > <%1$s>%2$s</%1$s>: %3$s', $decoration, $subType, $message['message']));
$this->line(' > > ' . $message['extract']);
}
}
}
private function showHtmlReport()
{
if ($this->option('no-html')) {
return $this;
}
if (empty($this->invalidHtml)) {
$this->info('Checked HTML files are valid');
} else {
$count = count($this->invalidHtml);
$this->error(sprintf('%d invalid HTML %s reported', $count, Str::plural('file', $count)));
$this->newLine(2);
foreach ($this->invalidHtml as $path => $data) {
$this->newLine(2);
$this->comment($path);
foreach ($data['messages'] as $message) {
$type = $message['type'];
$subType = $message['subType'] ?? $type;
$decoration = match ($subType) {
'error' => 'error',
'info' => 'comment',
'warning' => 'comment'
};
$this->newLine();
$this->line(sprintf(' > <%1$s>%2$s</%1$s>: %3$s', $decoration, $subType, $message['message']));
$this->line(' > > ' . $message['extract']);
}
}
}
}
} }