diff --git a/src/Schema/Keywords/ConstValue.php b/src/Schema/Keywords/ConstValue.php new file mode 100644 index 00000000..28aa7c54 --- /dev/null +++ b/src/Schema/Keywords/ConstValue.php @@ -0,0 +1,69 @@ +formatValue($constValue)) + ); + } + } + + /** + * Format a value for error message display + * + * @param mixed $value + */ + private function formatValue($value): string + { + if (is_string($value)) { + return sprintf("'%s'", $value); + } + + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if ($value === null) { + return 'null'; + } + + if (is_array($value) || is_object($value)) { + return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + return (string) $value; + } +} diff --git a/src/Schema/Keywords/Nullable.php b/src/Schema/Keywords/Nullable.php index 9569ac27..6cc22a5b 100644 --- a/src/Schema/Keywords/Nullable.php +++ b/src/Schema/Keywords/Nullable.php @@ -4,9 +4,11 @@ namespace League\OpenAPIValidation\Schema\Keywords; +use cebe\openapi\spec\Type as CebeType; use League\OpenAPIValidation\Schema\Exception\KeywordMismatch; use function in_array; +use function is_array; use function is_string; class Nullable extends BaseKeyword @@ -27,6 +29,16 @@ public function validate($data, bool $nullable): void public function nullableByType(): bool { - return ! is_string($this->parentSchema->type) && in_array('null', $this->parentSchema->type); + if (is_string($this->parentSchema->type)) { + // If type is the string "null", then null values are allowed + return $this->parentSchema->type === CebeType::NULL; + } + + if (is_array($this->parentSchema->type)) { + // If type is an array containing 'null', then null values are allowed + return in_array(CebeType::NULL, $this->parentSchema->type); + } + + return false; } } diff --git a/src/Schema/SchemaValidator.php b/src/Schema/SchemaValidator.php index f1f75937..828205cf 100644 --- a/src/Schema/SchemaValidator.php +++ b/src/Schema/SchemaValidator.php @@ -10,6 +10,7 @@ use League\OpenAPIValidation\Schema\Exception\SchemaMismatch; use League\OpenAPIValidation\Schema\Keywords\AllOf; use League\OpenAPIValidation\Schema\Keywords\AnyOf; +use League\OpenAPIValidation\Schema\Keywords\ConstValue; use League\OpenAPIValidation\Schema\Keywords\Enum; use League\OpenAPIValidation\Schema\Keywords\Items; use League\OpenAPIValidation\Schema\Keywords\Maximum; @@ -59,8 +60,17 @@ public function validate($data, CebeSchema $schema, ?BreadCrumb $breadCrumb = nu // These keywords are not part of the JSON Schema at all (new to OAS) (new Nullable($schema))->validate($data, $schema->nullable ?? true); + // Validate const keyword (JSON Schema draft 6+) before early return for null + // This is necessary because const might be null, and we need to validate it + $schemaData = $schema->getSerializableData(); + $hasConst = isset($schemaData->const); + if ($hasConst) { + (new ConstValue($schema))->validate($data, $schemaData->const); + } + // We don't want to validate any more if the value is a valid Null - if ($data === null) { + // But only if const was not specified, as const validation already happened above + if ($data === null && ! $hasConst) { return; } @@ -121,6 +131,8 @@ public function validate($data, CebeSchema $schema, ?BreadCrumb $breadCrumb = nu (new Required($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->required); } + // Note: const validation is done earlier (before early return for null) + if (isset($schema->enum)) { (new Enum($schema))->validate($data, $schema->enum); } diff --git a/tests/Schema/Keywords/ConstValueTest.php b/tests/Schema/Keywords/ConstValueTest.php new file mode 100644 index 00000000..349cb8c2 --- /dev/null +++ b/tests/Schema/Keywords/ConstValueTest.php @@ -0,0 +1,110 @@ +loadRawSchema($spec); + $data = 'doc'; + + (new SchemaValidator())->validate($data, $schema); + $this->addToAssertionCount(1); + } + + public function testItValidatesConstStringRed(): void + { + $spec = <<loadRawSchema($spec); + $data = 'invalid'; + + try { + (new SchemaValidator())->validate($data, $schema); + $this->fail('Validation did not expected to pass'); + } catch (KeywordMismatch $e) { + $this->assertEquals('const', $e->keyword()); + } + } + + public function testItValidatesConstIntegerGreen(): void + { + $spec = <<loadRawSchema($spec); + $data = 42; + + (new SchemaValidator())->validate($data, $schema); + $this->addToAssertionCount(1); + } + + public function testItValidatesConstIntegerRed(): void + { + $spec = <<loadRawSchema($spec); + $data = 43; + + try { + (new SchemaValidator())->validate($data, $schema); + $this->fail('Validation did not expected to pass'); + } catch (KeywordMismatch $e) { + $this->assertEquals('const', $e->keyword()); + } + } + + public function testItValidatesConstBooleanGreen(): void + { + $spec = <<loadRawSchema($spec); + $data = true; + + (new SchemaValidator())->validate($data, $schema); + $this->addToAssertionCount(1); + } + + public function testItValidatesConstNullGreen(): void + { + $spec = <<loadRawSchema($spec); + $data = null; + + (new SchemaValidator())->validate($data, $schema); + $this->addToAssertionCount(1); + } +}