Skip to content

Commit

Permalink
[FEATURE] Move JSON schema validation to dedicated service class
Browse files Browse the repository at this point in the history
  • Loading branch information
eliashaeussler committed Mar 22, 2023
1 parent 8589ad5 commit c6d3533
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 21 deletions.
2 changes: 2 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions docs/development/architecture/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 4 additions & 21 deletions src/Builder/Config/ConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
) {
}

Expand All @@ -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
Expand All @@ -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());
Expand All @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions src/Helper/FilesystemHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
59 changes: 59 additions & 0 deletions src/Json/SchemaValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Composer package "cpsit/project-builder".
*
* Copyright (C) 2023 Elias Häußler <[email protected]>
*
* 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 <https://www.gnu.org/licenses/>.
*/

namespace CPSIT\ProjectBuilder\Json;

use CPSIT\ProjectBuilder\Helper;
use Opis\JsonSchema;

/**
* SchemaValidator.
*
* @author Elias Häußler <[email protected]>
* @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);
}
}
12 changes: 12 additions & 0 deletions tests/src/Fixtures/Files/test.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema#",
"type": "object",
"properties": {
"foo": {
"type": "string"
}
},
"required": [
"foo"
]
}
21 changes: 21 additions & 0 deletions tests/src/Helper/FilesystemHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
67 changes: 67 additions & 0 deletions tests/src/Json/SchemaValidatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Composer package "cpsit/project-builder".
*
* Copyright (C) 2023 Elias Häußler <[email protected]>
*
* 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 <https://www.gnu.org/licenses/>.
*/

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 <[email protected]>
* @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<string, array{mixed, bool}>
*/
public function validateValidatesJsonDataProvider(): Generator
{
yield 'valid json' => [(object) ['foo' => 'baz'], true];
yield 'invalid json' => [null, false];
}
}

0 comments on commit c6d3533

Please sign in to comment.