Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/Schema/Keywords/ConstValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace League\OpenAPIValidation\Schema\Keywords;

use League\OpenAPIValidation\Schema\Exception\KeywordMismatch;

use function is_array;
use function is_bool;
use function is_object;
use function is_string;
use function json_encode;
use function sprintf;

use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;

class ConstValue extends BaseKeyword
{
/**
* The value of this keyword MAY be of any type, including null.
*
* An instance validates successfully against this keyword if its value
* is equal to the value of the keyword.
*
* @param mixed $data
* @param mixed $constValue
*
* @throws KeywordMismatch
*/
public function validate($data, $constValue): void
{
// Use strict comparison (===) to match JSON Schema const behavior
if ($data !== $constValue) {
throw KeywordMismatch::fromKeyword(
'const',
$data,
sprintf('Value must be equal to constant %s', $this->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;
}
}
14 changes: 13 additions & 1 deletion src/Schema/Keywords/Nullable.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}
14 changes: 13 additions & 1 deletion src/Schema/SchemaValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}
Expand Down
110 changes: 110 additions & 0 deletions tests/Schema/Keywords/ConstValueTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace League\OpenAPIValidation\Tests\Schema\Keywords;

use League\OpenAPIValidation\Schema\Exception\KeywordMismatch;
use League\OpenAPIValidation\Schema\SchemaValidator;
use League\OpenAPIValidation\Tests\Schema\SchemaValidatorTest;

final class ConstValueTest extends SchemaValidatorTest
{
public function testItValidatesConstStringGreen(): void
{
$spec = <<<SPEC
schema:
type: string
const: "doc"
SPEC;

$schema = $this->loadRawSchema($spec);
$data = 'doc';

(new SchemaValidator())->validate($data, $schema);
$this->addToAssertionCount(1);
}

public function testItValidatesConstStringRed(): void
{
$spec = <<<SPEC
schema:
type: string
const: "doc"
SPEC;

$schema = $this->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 = <<<SPEC
schema:
type: integer
const: 42
SPEC;

$schema = $this->loadRawSchema($spec);
$data = 42;

(new SchemaValidator())->validate($data, $schema);
$this->addToAssertionCount(1);
}

public function testItValidatesConstIntegerRed(): void
{
$spec = <<<SPEC
schema:
type: integer
const: 42
SPEC;

$schema = $this->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 = <<<SPEC
schema:
type: boolean
const: true
SPEC;

$schema = $this->loadRawSchema($spec);
$data = true;

(new SchemaValidator())->validate($data, $schema);
$this->addToAssertionCount(1);
}

public function testItValidatesConstNullGreen(): void
{
$spec = <<<SPEC
schema:
type: "null"
const: null
SPEC;

$schema = $this->loadRawSchema($spec);
$data = null;

(new SchemaValidator())->validate($data, $schema);
$this->addToAssertionCount(1);
}
}