diff --git a/config/services.php b/config/services.php index fbe7ba13..87dd672d 100644 --- a/config/services.php +++ b/config/services.php @@ -29,6 +29,7 @@ use CPSIT\ProjectBuilder\Twig; use GuzzleHttp\Client as GuzzleClient; use Nyholm\Psr7; +use Opis\JsonSchema; use Psr\Http\Client; use Psr\Http\Message; use SebastianFeldmann\Cli; @@ -80,6 +81,7 @@ // Add external services $services->set(ExpressionLanguage\ExpressionLanguage::class); $services->set(Filesystem\Filesystem::class); + $services->set(JsonSchema\Validator::class); $services->set(Slugify\Slugify::class); $services->set(Client\ClientInterface::class, GuzzleClient::class); $services->set(Loader\LoaderInterface::class, Loader\FilesystemLoader::class); diff --git a/docs/development/architecture/components.md b/docs/development/architecture/components.md index 3f82ef49..f89700aa 100644 --- a/docs/development/architecture/components.md +++ b/docs/development/architecture/components.md @@ -69,6 +69,24 @@ Each validator implements [`ValidatorInterface`](https://github.com/CPS-IT/proje Not all validators can be used for each interaction with the `InputReader`. ``` +## JSON schema validation + +While working with JSON files, it's often useful to validate them against a defined +JSON schema. For this, the [**`Json\SchemaValidator`**](https://github.com/CPS-IT/project-builder/blob/main/src/Json/SchemaValidator.php) +component can be used. + +Example: + +```php +/** @var \CPSIT\ProjectBuilder\Json\SchemaValidator $schemaValidator */ + +$data = json_decode($json); +$validationResult = $schemaValidator->validate($data, $schemaFile); + +$isValid = $validationResult->isValid(); // Check if JSON is valid +$error = $validationResult->error(); // Get validation errors +``` + ## Naming With the [**`Naming\NameVariantBuilder`**](https://github.com/CPS-IT/project-builder/blob/main/src/Naming/NameVariantBuilder.php) diff --git a/src/Builder/Config/ConfigFactory.php b/src/Builder/Config/ConfigFactory.php index 34b98f44..a820afc7 100644 --- a/src/Builder/Config/ConfigFactory.php +++ b/src/Builder/Config/ConfigFactory.php @@ -25,6 +25,7 @@ use CPSIT\ProjectBuilder\Exception; use CPSIT\ProjectBuilder\Helper; +use CPSIT\ProjectBuilder\Json; use CPSIT\ProjectBuilder\Paths; use CuyZ\Valinor\Cache; use CuyZ\Valinor\Mapper; @@ -50,7 +51,7 @@ final class ConfigFactory private function __construct( private readonly Mapper\TreeMapper $mapper, - private readonly JsonSchema\Validator $validator, + private readonly Json\SchemaValidator $schemaValidator, ) { } @@ -65,7 +66,7 @@ public static function create(): self ->mapper() ; - return new self($mapper, new JsonSchema\Validator()); + return new self($mapper, new Json\SchemaValidator(new JsonSchema\Validator())); } public function buildFromFile(string $file, string $identifier): Config @@ -87,7 +88,7 @@ public function buildFromFile(string $file, string $identifier): Config public function buildFromString(string $content, string $identifier, FileType $fileType): Config { $parsedContent = $this->parseContent($content, $fileType); - $validationResult = $this->validateConfig($parsedContent); + $validationResult = $this->schemaValidator->validate($parsedContent, Paths::PROJECT_SCHEMA_CONFIG); if (!$validationResult->isValid()) { throw Exception\InvalidConfigurationException::forValidationErrors($validationResult->error()); @@ -98,24 +99,6 @@ public function buildFromString(string $content, string $identifier, FileType $f return $this->mapper->map(Config::class, $source); } - private function validateConfig(stdClass $parsedContent): JsonSchema\ValidationResult - { - $schemaFile = Filesystem\Path::join(Helper\FilesystemHelper::getProjectRootPath(), Paths::PROJECT_SCHEMA_CONFIG); - $schemaReference = 'file://'.$schemaFile; - $schemaResolver = $this->validator->resolver(); - - // @codeCoverageIgnoreStart - if (null === $schemaResolver) { - $schemaResolver = new JsonSchema\Resolvers\SchemaResolver(); - $this->validator->setResolver($schemaResolver); - } - // @codeCoverageIgnoreEnd - - $schemaResolver->registerFile($schemaReference, $schemaFile); - - return $this->validator->validate($parsedContent, $schemaReference); - } - private function generateMapperSource(string $content, string $identifier, FileType $fileType): Mapper\Source\Source { $parsedContent = match ($fileType) { diff --git a/src/Helper/FilesystemHelper.php b/src/Helper/FilesystemHelper.php index c3faca52..2336aaf2 100644 --- a/src/Helper/FilesystemHelper.php +++ b/src/Helper/FilesystemHelper.php @@ -68,4 +68,13 @@ public static function getProjectRootPath(): string return $rootPath ?? dirname(__DIR__, 2); } + + public static function resolveRelativePath(string $relativePath): string + { + if (Filesystem\Path::isAbsolute($relativePath)) { + return $relativePath; + } + + return Filesystem\Path::makeAbsolute($relativePath, self::getProjectRootPath()); + } } diff --git a/src/Json/SchemaValidator.php b/src/Json/SchemaValidator.php new file mode 100644 index 00000000..2bbbd8fb --- /dev/null +++ b/src/Json/SchemaValidator.php @@ -0,0 +1,59 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace CPSIT\ProjectBuilder\Json; + +use CPSIT\ProjectBuilder\Helper; +use Opis\JsonSchema; + +/** + * SchemaValidator. + * + * @author Elias Häußler + * @license GPL-3.0-or-later + */ +final class SchemaValidator +{ + public function __construct( + private readonly JsonSchema\Validator $validator, + ) { + } + + public function validate(mixed $data, string $schemaFile): JsonSchema\ValidationResult + { + $schemaFile = Helper\FilesystemHelper::resolveRelativePath($schemaFile); + $schemaReference = 'file://'.$schemaFile; + $schemaResolver = $this->validator->resolver(); + + // @codeCoverageIgnoreStart + if (null === $schemaResolver) { + $schemaResolver = new JsonSchema\Resolvers\SchemaResolver(); + $this->validator->setResolver($schemaResolver); + } + // @codeCoverageIgnoreEnd + + $schemaResolver->registerFile($schemaReference, $schemaFile); + + return $this->validator->validate($data, $schemaReference); + } +} diff --git a/tests/src/Fixtures/Files/test.schema.json b/tests/src/Fixtures/Files/test.schema.json new file mode 100644 index 00000000..5f0e02af --- /dev/null +++ b/tests/src/Fixtures/Files/test.schema.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] +} diff --git a/tests/src/Helper/FilesystemHelperTest.php b/tests/src/Helper/FilesystemHelperTest.php index 8ee638c0..c8c4daf2 100644 --- a/tests/src/Helper/FilesystemHelperTest.php +++ b/tests/src/Helper/FilesystemHelperTest.php @@ -84,4 +84,25 @@ public function getProjectRootPathReturnsProjectRootPathFromComposerPackageArtif { self::assertSame(dirname(__DIR__, 3), Src\Helper\FilesystemHelper::getProjectRootPath()); } + + /** + * @test + */ + public function resolveRelativePathReturnsGivenPathIfItIsAnAbsolutePath(): void + { + $path = '/foo/baz'; + + self::assertSame($path, Src\Helper\FilesystemHelper::resolveRelativePath($path)); + } + + /** + * @test + */ + public function resolveRelativePathMakesRelativePathAbsolute(): void + { + $path = 'foo'; + $expected = dirname(__DIR__, 3).'/foo'; + + self::assertSame($expected, Src\Helper\FilesystemHelper::resolveRelativePath($path)); + } } diff --git a/tests/src/Json/SchemaValidatorTest.php b/tests/src/Json/SchemaValidatorTest.php new file mode 100644 index 00000000..2356bf73 --- /dev/null +++ b/tests/src/Json/SchemaValidatorTest.php @@ -0,0 +1,67 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace CPSIT\ProjectBuilder\Tests\Json; + +use CPSIT\ProjectBuilder as Src; +use CPSIT\ProjectBuilder\Tests; +use Generator; + +use function dirname; + +/** + * SchemaValidatorTest. + * + * @author Elias Häußler + * @license GPL-3.0-or-later + */ +final class SchemaValidatorTest extends Tests\ContainerAwareTestCase +{ + private Src\Json\SchemaValidator $subject; + + protected function setUp(): void + { + $this->subject = self::$container->get(Src\Json\SchemaValidator::class); + } + + /** + * @test + * + * @dataProvider validateValidatesJsonDataProvider + */ + public function validateValidatesJson(mixed $data, bool $expected): void + { + $schemaFile = dirname(__DIR__).'/Fixtures/Files/test.schema.json'; + + self::assertSame($expected, $this->subject->validate($data, $schemaFile)->isValid()); + } + + /** + * @return Generator + */ + public function validateValidatesJsonDataProvider(): Generator + { + yield 'valid json' => [(object) ['foo' => 'baz'], true]; + yield 'invalid json' => [null, false]; + } +}