diff --git a/composer.json b/composer.json index 59517d0..80c350f 100644 --- a/composer.json +++ b/composer.json @@ -1,56 +1,62 @@ { - "name": "event-engine/php-json-schema", - "description": "Event Engine JSON Schema PHP Package", - "homepage": "https://event-engine.io/", - "license": "MIT", - "authors": [ - { - "name": "Alexander Miertsch", - "email": "contact@prooph.de", - "homepage": "http://www.prooph.de" + "name": "event-engine/php-json-schema", + "description": "Event Engine JSON Schema PHP Package", + "homepage": "https://event-engine.io/", + "license": "MIT", + "authors": [ + { + "name": "Alexander Miertsch", + "email": "contact@prooph.de", + "homepage": "http://www.prooph.de" + }, + { + "name": "Sandro Keil", + "email": "contact@prooph.de", + "homepage": "http://prooph-software.com/" + } + ], + "require": { + "php": "^7.2", + "event-engine/php-data": "^0.1", + "event-engine/php-engine-utils": "^0.1", + "event-engine/php-schema": "^0.1", + "ramsey/uuid": "^3.6" }, - { - "name": "Sandro Keil", - "email": "contact@prooph.de", - "homepage": "http://prooph-software.com/" - } - ], - "require": { - "php": "^7.2", - "roave/security-advisories": "dev-master", - "event-engine/php-schema": "^0.1", - "event-engine/php-data": "^0.1", - "event-engine/php-engine-utils": "^0.1", - "ramsey/uuid" : "^3.6" - }, - "require-dev": { - "phpunit/phpunit": "^7.0", - "justinrainbow/json-schema": "^5.2", - "opis/json-schema": "^1.0", - "prooph/php-cs-fixer-config": "^0.3", - "satooshi/php-coveralls": "^1.0", - "malukenho/docheader": "^0.1.4" - }, - "autoload": { - "psr-4": { - "EventEngine\\JsonSchema\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "EventEngineTest\\JsonSchema\\": "tests/" + "require-dev": { + "ext-json": "*", + "justinrainbow/json-schema": "^5.2", + "malukenho/docheader": "^0.1.4", + "opis/json-schema": "^1.0", + "phpunit/phpunit": "^7.0", + "prooph/php-cs-fixer-config": "^0.3", + "roave/security-advisories": "dev-master", + "satooshi/php-coveralls": "^1.0" + }, + "autoload": { + "psr-4": { + "EventEngine\\JsonSchema\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "EventEngineTest\\JsonSchema\\": "tests/" + } + }, + "prefer-stable": true, + "config": { + "sort-packages": true, + "platform": { + } + }, + "scripts": { + "check": [ + "@cs", + "@docheader", + "@test" + ], + "docheader": "vendor/bin/docheader check examples/ src/ tests/", + "cs": "php-cs-fixer fix -v --diff --dry-run", + "cs-fix": "php-cs-fixer fix -v --diff", + "test": "vendor/bin/phpunit" } - }, - "prefer-stable": true, - "scripts": { - "check": [ - "@cs", - "@docheader", - "@test" - ], - "docheader": "vendor/bin/docheader check examples/ src/ tests/", - "cs": "php-cs-fixer fix -v --diff --dry-run", - "cs-fix": "php-cs-fixer fix -v --diff", - "test": "vendor/bin/phpunit" - } } diff --git a/src/Exception/JsonValidationError.php b/src/Exception/JsonValidationError.php new file mode 100644 index 0000000..f839fa4 --- /dev/null +++ b/src/Exception/JsonValidationError.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\JsonSchema\Exception; + + +class JsonValidationError extends InvalidArgumentException +{ +} \ No newline at end of file diff --git a/src/Exception/JustinRainbowJsonValidationError.php b/src/Exception/JustinRainbowJsonValidationError.php new file mode 100644 index 0000000..337d0a2 --- /dev/null +++ b/src/Exception/JustinRainbowJsonValidationError.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\JsonSchema\Exception; + + +class JustinRainbowJsonValidationError extends JsonValidationError +{ + /** + * @var array + */ + private $errors; + + public static function withError(string $objectName, array ...$errors): JustinRainbowJsonValidationError + { + $self = new self('Validation of "' . $objectName . '" failed: '); + $self->errors = $errors; + + $self->message .= \array_reduce( + $errors, + static function ($message, array $error) use ($self) { + return $message . "\n" . $self->errorMessage($error); + } + ); + + return $self; + } + + public function errors(): array + { + return $this->errors; + } + + private function errorMessage(array $error): string + { + $dataPointer = $error['pointer']; + + if ($dataPointer === '') { + return \sprintf('[%s] %s', $error['constraint'], $error['message']); + } + + return \sprintf('field "%s" [%s] %s', + $error['property'], + $error['constraint'], + $error['message'] + ); + +// return sprintf('field "%s" [%s] %s', $error['constraint'], $error['property'], $error['message']); + } +} diff --git a/src/Exception/OpisJsonValidationError.php b/src/Exception/OpisJsonValidationError.php new file mode 100644 index 0000000..80a1646 --- /dev/null +++ b/src/Exception/OpisJsonValidationError.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngine\JsonSchema\Exception; + + +use Opis\JsonSchema\ValidationError; + +class OpisJsonValidationError extends JsonValidationError +{ + /** + * @var ValidationError[] + */ + private $errors; + + public static function withError(string $objectName, ValidationError ...$validationErrors): OpisJsonValidationError + { + $self = new self('Validation of "' . $objectName . '" failed: '); + $self->errors = $validationErrors; + + foreach ($validationErrors as $error) { + $self->message .= $self->errorMessage($error); + + if ($error->subErrorsCount()) { + $self->message .= \array_reduce( + $error->subErrors(), + static function ($message, ValidationError $error) use ($self) { + return $message . "\n" . $self->errorMessage($error); + } + ); + } + } + + return $self; + } + + /** + * @return ValidationError[] + */ + public function errors(): array + { + return $this->errors; + } + + private function errorMessage(ValidationError $error): string + { + $dataPointer = $error->dataPointer(); + + if (count($dataPointer) === 0) { + return \sprintf('[%s] %s', $error->keyword(), \json_encode($error->keywordArgs(), JSON_PRETTY_PRINT)); + } + + return \sprintf('field "%s" [%s] %s', + implode('.', $dataPointer), + $error->keyword(), + \json_encode($error->keywordArgs(), JSON_PRETTY_PRINT) + ); + } +} diff --git a/src/JustinRainbowJsonSchema.php b/src/JustinRainbowJsonSchema.php index 4604ab2..caa52c5 100644 --- a/src/JustinRainbowJsonSchema.php +++ b/src/JustinRainbowJsonSchema.php @@ -11,7 +11,7 @@ namespace EventEngine\JsonSchema; -use EventEngine\JsonSchema\Exception\InvalidArgumentException; +use EventEngine\JsonSchema\Exception\JustinRainbowJsonValidationError; use JsonSchema\Validator; final class JustinRainbowJsonSchema extends AbstractJsonSchema @@ -31,16 +31,8 @@ public function assert(string $objectName, array $data, array $jsonSchema) if (! $this->jsonValidator()->isValid()) { $errors = $this->jsonValidator()->getErrors(); - $this->jsonValidator()->reset(); - - foreach ($errors as $i => $error) { - $errors[$i] = \sprintf("[%s] %s\n", $error['property'], $error['message']); - } - - throw new InvalidArgumentException( - "Validation of $objectName failed: " . \implode("\n", $errors) - ); + throw JustinRainbowJsonValidationError::withError($objectName, ...$errors); } $this->jsonValidator()->reset(); diff --git a/src/OpisJsonSchema.php b/src/OpisJsonSchema.php index 0f19d35..73725bc 100644 --- a/src/OpisJsonSchema.php +++ b/src/OpisJsonSchema.php @@ -11,7 +11,7 @@ namespace EventEngine\JsonSchema; -use EventEngine\JsonSchema\Exception\InvalidArgumentException; +use EventEngine\JsonSchema\Exception\OpisJsonValidationError; use Opis\JsonSchema\Schema as OpisSchema; use Opis\JsonSchema\Validator; @@ -38,21 +38,7 @@ public function assert(string $objectName, array $data, array $jsonSchema) $result = $this->jsonValidator()->schemaValidation($enforcedObjectData, OpisSchema::fromJsonString(\json_encode($jsonSchema))); if (! $result->isValid()) { - $errors = []; - - foreach ($result->getErrors() as $error) { - $errors[] = \sprintf('[%s] %s', $error->keyword(), \json_encode($error->keywordArgs(), JSON_PRETTY_PRINT)); - - if ($error->subErrorsCount()) { - foreach ($error->subErrors() as $subError) { - $errors[] = \sprintf("[%s] %s\n", $subError->keyword(), \json_encode($subError->keywordArgs(), JSON_PRETTY_PRINT)); - } - } - } - - throw new InvalidArgumentException( - "Validation of $objectName failed: " . \implode("\n", $errors) - ); + throw OpisJsonValidationError::withError($objectName, ...$result->getErrors()); } } diff --git a/tests/JustinRainbowJsonSchemaTest.php b/tests/JustinRainbowJsonSchemaTest.php new file mode 100644 index 0000000..5c840b0 --- /dev/null +++ b/tests/JustinRainbowJsonSchemaTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngineTest\JsonSchema; + +use EventEngine\JsonSchema\Exception\JsonValidationError; +use EventEngine\JsonSchema\Exception\JustinRainbowJsonValidationError; +use EventEngine\JsonSchema\JustinRainbowJsonSchema; + +class JustinRainbowJsonSchemaTest extends BasicTestCase +{ + private function schema(): array + { + $schema = <<<'JSON' +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 3 + }, + "hasValue": {"type": "boolean"}, + "age": {"type": "number"}, + "subObject": { + "type": "object", + "properties": { + "p1": { + "type": "string", + "minLength": 3 + }, + "p2": {"type": "boolean"} + }, + "required": ["p1", "p2"], + "additionalProperties": false + }, + "type": { + "enum": ["Foo", "Bar", "Baz"] + } + }, + "required": ["name", "hasValue", "age", "type"], + "additionalProperties": false +} +JSON; + return json_decode($schema, true); + } + + private function validData(): array + { + return [ + 'name' => 'Tester', + 'hasValue' => true, + 'age' => 40, + 'type' => 'Bar', + ]; + } + + /** + * @test + */ + public function it_validates_json_schema(): void + { + $data = $this->validData(); + + $cut = new JustinRainbowJsonSchema(); + $cut->assert('myObject', $data, $this->schema()); + $this->assertTrue(true); + } + + /** + * @test + */ + public function it_throws_json_validation_error_exception(): void + { + $data = $this->validData(); + $data['unknown'] = 'set'; + + $expectedMessage = <<<'Msg' +Validation of "myObject" failed: +[additionalProp] The property unknown is not defined and the definition does not allow additional properties +Msg; + + $cut = new JustinRainbowJsonSchema(); + try { + $cut->assert('myObject', $data, $this->schema()); + } catch (JsonValidationError $e) { + $this->assertSame(400, $e->getCode()); + $this->assertStringStartsWith($expectedMessage, $e->getMessage()); + } + } + + /** + * @test + */ + public function it_throws_justin_rainbow_json_validation_error_exception(): void + { + $data = $this->validData(); + $data['subObject']['unknown'] = 'set'; + + $expectedMessage = <<<'Msg' +Validation of "myObject" failed: +field "subObject.p1" [required] The property p1 is required +field "subObject.p2" [required] The property p2 is required +field "subObject" [additionalProp] The property unknown is not defined and the definition does not allow additional properties +Msg; + + + $cut = new JustinRainbowJsonSchema(); + try { + $cut->assert('myObject', $data, $this->schema()); + } catch (JustinRainbowJsonValidationError $e) { + $this->assertSame(400, $e->getCode()); + $this->assertCount(3, $e->errors()); + $this->assertStringStartsWith($expectedMessage, $e->getMessage()); + } + } +} diff --git a/tests/OpisJsonSchemaTest.php b/tests/OpisJsonSchemaTest.php new file mode 100644 index 0000000..adaff5e --- /dev/null +++ b/tests/OpisJsonSchemaTest.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace EventEngineTest\JsonSchema; + +use EventEngine\JsonSchema\Exception\JsonValidationError; +use EventEngine\JsonSchema\Exception\OpisJsonValidationError; +use EventEngine\JsonSchema\OpisJsonSchema; + +class OpisJsonSchemaTest extends BasicTestCase +{ + private function schema(): array + { + $schema = <<<'JSON' +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 3 + }, + "hasValue": {"type": "boolean"}, + "age": {"type": "number"}, + "subObject": { + "type": "object", + "properties": { + "p1": { + "type": "string", + "minLength": 3 + }, + "p2": {"type": "boolean"} + }, + "required": ["p1", "p2"], + "additionalProperties": false + }, + "type": { + "enum": ["Foo", "Bar", "Baz"] + } + }, + "required": ["name", "hasValue", "age", "type"], + "additionalProperties": false +} +JSON; + return json_decode($schema, true); + } + + private function validData(): array + { + return [ + 'name' => 'Tester', + 'hasValue' => true, + 'age' => 40, + 'type' => 'Bar', + ]; + } + + /** + * @test + */ + public function it_validates_json_schema(): void + { + $data = $this->validData(); + + $cut = new OpisJsonSchema(); + $cut->assert('myObject', $data, $this->schema()); + $this->assertTrue(true); + } + + /** + * @test + */ + public function it_throws_json_validation_error_exception(): void + { + $data = $this->validData(); + $data['unknown'] = 'set'; + + $expectedMessage = <<<'Msg' +Validation of "myObject" failed: [additionalProperties] [] +field "unknown" [$schema] { + "schema": false +} +Msg; + + $cut = new OpisJsonSchema(); + try { + $cut->assert('myObject', $data, $this->schema()); + } catch (JsonValidationError $e) { + $this->assertSame(400, $e->getCode()); + $this->assertStringStartsWith($expectedMessage, $e->getMessage()); + } + } + + /** + * @test + */ + public function it_throws_justin_rainbow_json_validation_error_exception(): void + { + $data = $this->validData(); + $data['subObject']['unknown'] = 'set'; + + $expectedMessage = <<<'Msg' +Validation of "myObject" failed: field "subObject" [required] { + "missing": "p1" +} +Msg; + + $cut = new OpisJsonSchema(); + try { + $cut->assert('myObject', $data, $this->schema()); + } catch (OpisJsonValidationError $e) { + $this->assertSame(400, $e->getCode()); + $this->assertCount(1, $e->errors()); + $this->assertStringStartsWith($expectedMessage, $e->getMessage()); + } + } +}