diff --git a/composer.json b/composer.json index 94e96de9..c42595e3 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "require": { "php": "^8.1", "ext-fileinfo": "*", - "opis/json-schema": "^2.4", "php-http/discovery": "^1.20", "phpdocumentor/reflection-docblock": "^5.6", "psr/clock": "^1.0", diff --git a/src/Capability/Discovery/SchemaValidator.php b/src/Capability/Discovery/SchemaValidator.php deleted file mode 100644 index cc6be11d..00000000 --- a/src/Capability/Discovery/SchemaValidator.php +++ /dev/null @@ -1,336 +0,0 @@ - - */ -class SchemaValidator -{ - private ?Validator $jsonSchemaValidator = null; - - public function __construct( - private LoggerInterface $logger = new NullLogger(), - ) { - } - - /** - * Validates data against a JSON schema. - * - * @param mixed $data the data to validate (should generally be decoded JSON) - * @param array|object $schema the JSON Schema definition (as PHP array or object) - * - * @return list array of validation errors, empty if valid - */ - public function validateAgainstJsonSchema(mixed $data, array|object $schema): array - { - if (\is_array($data) && empty($data)) { - $data = new \stdClass(); - } - - try { - // --- Schema Preparation --- - if (\is_array($schema)) { - $schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); - $schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR); - } elseif (\is_object($schema)) { - // This might be overly cautious but safer against varied inputs. - $schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); - $schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR); - } else { - throw new InvalidArgumentException('Schema must be an array or object.'); - } - - // --- Data Preparation --- - // Opis Validator generally prefers objects for object validation - $dataToValidate = $this->convertDataForValidator($data); - } catch (\JsonException $e) { - $this->logger->error('MCP SDK: Invalid schema structure provided for validation (JSON conversion failed).', ['exception' => $e]); - - return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Invalid schema definition provided (JSON error).']]; - } catch (InvalidArgumentException $e) { - $this->logger->error('MCP SDK: Invalid schema structure provided for validation.', ['exception' => $e]); - - return [['pointer' => '', 'keyword' => 'internal', 'message' => $e->getMessage()]]; - } catch (\Throwable $e) { - $this->logger->error('MCP SDK: Error preparing data/schema for validation.', ['exception' => $e]); - - return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Internal validation preparation error.']]; - } - - $validator = $this->getJsonSchemaValidator(); - - try { - $result = $validator->validate($dataToValidate, $schemaObject); - } catch (\Throwable $e) { - $this->logger->error('MCP SDK: JSON Schema validation failed internally.', [ - 'exception_message' => $e->getMessage(), - 'exception_trace' => $e->getTraceAsString(), - 'data' => json_encode($dataToValidate), - 'schema' => json_encode($schemaObject), - ]); - - return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Schema validation process failed: '.$e->getMessage()]]; - } - - if ($result->isValid()) { - return []; - } - - $formattedErrors = []; - $topError = $result->error(); - - if ($topError) { - $this->collectSubErrors($topError, $formattedErrors); - } - - if (empty($formattedErrors) && $topError) { // Fallback - $formattedErrors[] = [ - 'pointer' => $this->formatJsonPointerPath($topError->data()->path()), - 'keyword' => $topError->keyword(), - 'message' => $this->formatValidationError($topError), - ]; - } - - return $formattedErrors; - } - - /** - * Get or create the JSON Schema validator instance. - */ - private function getJsonSchemaValidator(): Validator - { - if (null === $this->jsonSchemaValidator) { - $this->jsonSchemaValidator = new Validator(); - // Potentially configure resolver here if needed later - } - - return $this->jsonSchemaValidator; - } - - /** - * Recursively converts associative arrays to stdClass objects for validator compatibility. - */ - private function convertDataForValidator(mixed $data): mixed - { - if (\is_array($data)) { - // Check if it's an associative array (keys are not sequential numbers 0..N-1) - if (!empty($data) && array_keys($data) !== range(0, \count($data) - 1)) { - $obj = new \stdClass(); - foreach ($data as $key => $value) { - $obj->{$key} = $this->convertDataForValidator($value); - } - - return $obj; - } else { - // It's a list (sequential array), convert items recursively - return array_map([$this, 'convertDataForValidator'], $data); - } - } elseif (\is_object($data) && $data instanceof \stdClass) { - // Deep copy/convert stdClass objects as well - $obj = new \stdClass(); - foreach (get_object_vars($data) as $key => $value) { - $obj->{$key} = $this->convertDataForValidator($value); - } - - return $obj; - } - - // Leave other objects and scalar types as they are - return $data; - } - - /** - * Recursively collects leaf validation errors. - * - * @param Error[] $collectedErrors - */ - private function collectSubErrors(ValidationError $error, array &$collectedErrors): void - { - $subErrors = $error->subErrors(); - if (empty($subErrors)) { - $collectedErrors[] = [ - 'pointer' => $this->formatJsonPointerPath($error->data()->path()), - 'keyword' => $error->keyword(), - 'message' => $this->formatValidationError($error), - ]; - } else { - foreach ($subErrors as $subError) { - $this->collectSubErrors($subError, $collectedErrors); - } - } - } - - /** - * Formats the path array into a JSON Pointer string. - * - * @param string[]|int[]|null $pathComponents - */ - private function formatJsonPointerPath(?array $pathComponents): string - { - if (empty($pathComponents)) { - return '/'; - } - $escapedComponents = array_map(function ($component) { - $componentStr = (string) $component; - - return str_replace(['~', '/'], ['~0', '~1'], $componentStr); - }, $pathComponents); - - return '/'.implode('/', $escapedComponents); - } - - /** - * Formats an Opis SchemaValidationError into a user-friendly message. - */ - private function formatValidationError(ValidationError $error): string - { - $keyword = $error->keyword(); - $args = $error->args(); - $message = "Constraint `{$keyword}` failed."; - - switch (strtolower($keyword)) { - case 'required': - $missing = $args['missing'] ?? []; - $formattedMissing = implode(', ', array_map(fn ($p) => "`{$p}`", $missing)); - $message = "Missing required properties: {$formattedMissing}."; - break; - case 'type': - $expected = implode('|', (array) ($args['expected'] ?? [])); - $used = $args['used'] ?? 'unknown'; - $message = "Invalid type. Expected `{$expected}`, but received `{$used}`."; - break; - case 'enum': - $schemaData = $error->schema()->info()->data(); - $allowedValues = []; - if (\is_object($schemaData) && property_exists($schemaData, 'enum') && \is_array($schemaData->enum)) { - $allowedValues = $schemaData->enum; - } elseif (\is_array($schemaData) && isset($schemaData['enum']) && \is_array($schemaData['enum'])) { - $allowedValues = $schemaData['enum']; - } else { - $this->logger->warning("MCP SDK: Could not retrieve 'enum' values from schema info for error.", ['error_args' => $args]); - } - if (empty($allowedValues)) { - $message = 'Value does not match the allowed enumeration.'; - } else { - $formattedAllowed = array_map(function ($v) { /* ... formatting logic ... */ - if (\is_string($v)) { - return '"'.$v.'"'; - } - if (\is_bool($v)) { - return $v ? 'true' : 'false'; - } - if (null === $v) { - return 'null'; - } - - return (string) $v; - }, $allowedValues); - $message = 'Value must be one of the allowed values: '.implode(', ', $formattedAllowed).'.'; - } - break; - case 'const': - $expected = json_encode($args['expected'] ?? 'null', \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE); - $message = "Value must be equal to the constant value: {$expected}."; - break; - case 'minLength': // Corrected casing - $min = $args['min'] ?? '?'; - $message = "String must be at least {$min} characters long."; - break; - case 'maxLength': // Corrected casing - $max = $args['max'] ?? '?'; - $message = "String must not be longer than {$max} characters."; - break; - case 'pattern': - $pattern = $args['pattern'] ?? '?'; - $message = "String does not match the required pattern: `{$pattern}`."; - break; - case 'minimum': - $min = $args['min'] ?? '?'; - $message = "Number must be greater than or equal to {$min}."; - break; - case 'maximum': - $max = $args['max'] ?? '?'; - $message = "Number must be less than or equal to {$max}."; - break; - case 'exclusiveMinimum': // Corrected casing - $min = $args['min'] ?? '?'; - $message = "Number must be strictly greater than {$min}."; - break; - case 'exclusiveMaximum': // Corrected casing - $max = $args['max'] ?? '?'; - $message = "Number must be strictly less than {$max}."; - break; - case 'multipleOf': // Corrected casing - $value = $args['value'] ?? '?'; - $message = "Number must be a multiple of {$value}."; - break; - case 'minItems': // Corrected casing - $min = $args['min'] ?? '?'; - $message = "Array must contain at least {$min} items."; - break; - case 'maxItems': // Corrected casing - $max = $args['max'] ?? '?'; - $message = "Array must contain no more than {$max} items."; - break; - case 'uniqueItems': // Corrected casing - $message = 'Array items must be unique.'; - break; - case 'minProperties': // Corrected casing - $min = $args['min'] ?? '?'; - $message = "Object must have at least {$min} properties."; - break; - case 'maxProperties': // Corrected casing - $max = $args['max'] ?? '?'; - $message = "Object must have no more than {$max} properties."; - break; - case 'additionalProperties': // Corrected casing - $unexpected = $args['properties'] ?? []; - $formattedUnexpected = implode(', ', array_map(fn ($p) => "`{$p}`", $unexpected)); - $message = "Object contains unexpected additional properties: {$formattedUnexpected}."; - break; - case 'format': - $format = $args['format'] ?? 'unknown'; - $message = "Value does not match the required format: `{$format}`."; - break; - default: - $builtInMessage = $error->message(); - if ($builtInMessage && 'The data must match the schema' !== $builtInMessage) { - $placeholders = $args; - $builtInMessage = preg_replace_callback('/\{(\w+)\}/', function ($match) use ($placeholders) { - $key = $match[1]; - $value = $placeholders[$key] ?? '{'.$key.'}'; - - return \is_array($value) ? json_encode($value) : (string) $value; - }, $builtInMessage); - $message = $builtInMessage; - } - break; - } - - return $message; - } -} diff --git a/tests/Unit/Capability/Discovery/SchemaValidatorTest.php b/tests/Unit/Capability/Discovery/SchemaValidatorTest.php deleted file mode 100644 index 417ddbde..00000000 --- a/tests/Unit/Capability/Discovery/SchemaValidatorTest.php +++ /dev/null @@ -1,505 +0,0 @@ -validator = new SchemaValidator(); - } - - // --- Basic Validation Tests --- - - public function testValidDataPassesValidation() - { - $schema = $this->getSimpleSchema(); - $data = $this->getValidData(); - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - - $this->assertEmpty($errors); - } - - public function testInvalidTypeGeneratesTypeError() - { - $schema = $this->getSimpleSchema(); - $data = $this->getValidData(); - $data['age'] = 'thirty'; // Invalid type - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - - $this->assertCount(1, $errors); - $this->assertEquals('/age', $errors[0]['pointer']); - $this->assertEquals('type', $errors[0]['keyword']); - $this->assertStringContainsString('Expected `integer`', $errors[0]['message']); - } - - public function testMissingRequiredPropertyGeneratesRequiredError() - { - $schema = $this->getSimpleSchema(); - $data = $this->getValidData(); - unset($data['name']); // Missing required - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('required', $errors[0]['keyword']); - $this->assertStringContainsString('Missing required properties: `name`', $errors[0]['message']); - } - - public function testAdditionalPropertyGeneratesAdditionalPropertiesError() - { - $schema = $this->getSimpleSchema(); - $data = $this->getValidData(); - $data['extra'] = 'not allowed'; // Additional property - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('/', $errors[0]['pointer']); // Error reported at the object root - $this->assertEquals('additionalProperties', $errors[0]['keyword']); - $this->assertStringContainsString('Additional object properties are not allowed: ["extra"]', $errors[0]['message']); - } - - // --- Keyword Constraint Tests --- - - public function testEnumConstraintViolation() - { - $schema = ['type' => 'string', 'enum' => ['A', 'B']]; - $data = 'C'; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('enum', $errors[0]['keyword']); - $this->assertStringContainsString('must be one of the allowed values: "A", "B"', $errors[0]['message']); - } - - public function testMinimumConstraintViolation() - { - $schema = ['type' => 'integer', 'minimum' => 10]; - $data = 5; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('minimum', $errors[0]['keyword']); - $this->assertStringContainsString('must be greater than or equal to 10', $errors[0]['message']); - } - - public function testMaxLengthConstraintViolation() - { - $schema = ['type' => 'string', 'maxLength' => 5]; - $data = 'toolong'; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('maxLength', $errors[0]['keyword']); - $this->assertStringContainsString('Maximum string length is 5, found 7', $errors[0]['message']); - } - - public function testPatternConstraintViolation() - { - $schema = ['type' => 'string', 'pattern' => '^[a-z]+$']; - $data = '123'; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('pattern', $errors[0]['keyword']); - $this->assertStringContainsString('does not match the required pattern: `^[a-z]+$`', $errors[0]['message']); - } - - public function testMinItemsConstraintViolation() - { - $schema = ['type' => 'array', 'minItems' => 2]; - $data = ['one']; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('minItems', $errors[0]['keyword']); - $this->assertStringContainsString('Array should have at least 2 items, 1 found', $errors[0]['message']); - } - - public function testUniqueItemsConstraintViolation() - { - $schema = ['type' => 'array', 'uniqueItems' => true]; - $data = ['a', 'b', 'a']; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('uniqueItems', $errors[0]['keyword']); - $this->assertStringContainsString('Array must have unique items', $errors[0]['message']); - } - - // --- Nested Structures and Pointers --- - public function testNestedObjectValidationErrorPointer() - { - $schema = [ - 'type' => 'object', - 'properties' => [ - 'user' => [ - 'type' => 'object', - 'properties' => ['id' => ['type' => 'integer']], - 'required' => ['id'], - ], - ], - 'required' => ['user'], - ]; - $data = ['user' => ['id' => 'abc']]; // Invalid nested type - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('/user/id', $errors[0]['pointer']); - } - - public function testArrayItemValidationErrorPointer() - { - $schema = [ - 'type' => 'array', - 'items' => ['type' => 'integer'], - ]; - $data = [1, 2, 'three', 4]; // Invalid item type - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('/2', $errors[0]['pointer']); // Pointer to the index of the invalid item - } - - // --- Data Conversion Tests --- - public function testValidatesDataPassedAsStdClassObject() - { - $schema = $this->getSimpleSchema(); - $dataObj = json_decode(json_encode($this->getValidData())); // Convert to stdClass - - $errors = $this->validator->validateAgainstJsonSchema($dataObj, $schema); - $this->assertEmpty($errors); - } - - public function testValidatesDataWithNestedAssociativeArraysCorrectly() - { - $schema = [ - 'type' => 'object', - 'properties' => [ - 'nested' => [ - 'type' => 'object', - 'properties' => ['key' => ['type' => 'string']], - 'required' => ['key'], - ], - ], - 'required' => ['nested'], - ]; - $data = ['nested' => ['key' => 'value']]; // Nested assoc array - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertEmpty($errors); - } - - // --- Edge Cases --- - public function testHandlesInvalidSchemaStructureGracefully() - { - $schema = ['type' => 'object', 'properties' => ['name' => ['type' => 123]]]; // Invalid type value - $data = ['name' => 'test']; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - $this->assertCount(1, $errors); - $this->assertEquals('internal', $errors[0]['keyword']); - $this->assertStringContainsString('Schema validation process failed', $errors[0]['message']); - } - - public function testHandlesEmptyDataObjectAgainstSchemaRequiringProperties() - { - $schema = $this->getSimpleSchema(); // Requires name, age etc. - $data = []; // Empty data - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - - $this->assertNotEmpty($errors); - $this->assertEquals('required', $errors[0]['keyword']); - } - - public function testHandlesEmptySchemaAllowsAnything() - { - $schema = []; // Empty schema object/array implies no constraints - $data = ['anything' => [1, 2], 'goes' => true]; - - $errors = $this->validator->validateAgainstJsonSchema($data, $schema); - - $this->assertNotEmpty($errors); - $this->assertEquals('internal', $errors[0]['keyword']); - $this->assertStringContainsString('Invalid schema', $errors[0]['message']); - } - - public function testValidatesSchemaWithStringFormatConstraintsFromSchemaAttribute() - { - $emailSchema = (new Schema(format: 'email'))->toArray(); - - // Valid email - $validErrors = $this->validator->validateAgainstJsonSchema('user@example.com', $emailSchema); - $this->assertEmpty($validErrors); - - // Invalid email - $invalidErrors = $this->validator->validateAgainstJsonSchema('not-an-email', $emailSchema); - $this->assertNotEmpty($invalidErrors); - $this->assertEquals('format', $invalidErrors[0]['keyword']); - $this->assertStringContainsString('email', $invalidErrors[0]['message']); - } - - public function testValidatesSchemaWithStringLengthConstraintsFromSchemaAttribute() - { - $passwordSchema = (new Schema(minLength: 8, pattern: '^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$'))->toArray(); - - // Valid password (meets length and pattern) - $validErrors = $this->validator->validateAgainstJsonSchema('Password123', $passwordSchema); - $this->assertEmpty($validErrors); - - // Invalid - too short - $shortErrors = $this->validator->validateAgainstJsonSchema('Pass1', $passwordSchema); - $this->assertNotEmpty($shortErrors); - $this->assertEquals('minLength', $shortErrors[0]['keyword']); - - // Invalid - no digit - $noDigitErrors = $this->validator->validateAgainstJsonSchema('PasswordXYZ', $passwordSchema); - $this->assertNotEmpty($noDigitErrors); - $this->assertEquals('pattern', $noDigitErrors[0]['keyword']); - } - - public function testValidatesSchemaWithNumericConstraintsFromSchemaAttribute() - { - $ageSchema = (new Schema(minimum: 18, maximum: 120))->toArray(); - - // Valid age - $validErrors = $this->validator->validateAgainstJsonSchema(25, $ageSchema); - $this->assertEmpty($validErrors); - - // Invalid - too low - $tooLowErrors = $this->validator->validateAgainstJsonSchema(15, $ageSchema); - $this->assertNotEmpty($tooLowErrors); - $this->assertEquals('minimum', $tooLowErrors[0]['keyword']); - - // Invalid - too high - $tooHighErrors = $this->validator->validateAgainstJsonSchema(150, $ageSchema); - $this->assertNotEmpty($tooHighErrors); - $this->assertEquals('maximum', $tooHighErrors[0]['keyword']); - } - - public function testValidatesSchemaWithArrayConstraintsFromSchemaAttribute() - { - $tagsSchema = (new Schema(uniqueItems: true, minItems: 2))->toArray(); - - // Valid tags array - $validErrors = $this->validator->validateAgainstJsonSchema(['php', 'javascript', 'python'], $tagsSchema); - $this->assertEmpty($validErrors); - - // Invalid - duplicate items - $duplicateErrors = $this->validator->validateAgainstJsonSchema(['php', 'php', 'javascript'], $tagsSchema); - $this->assertNotEmpty($duplicateErrors); - $this->assertEquals('uniqueItems', $duplicateErrors[0]['keyword']); - - // Invalid - too few items - $tooFewErrors = $this->validator->validateAgainstJsonSchema(['php'], $tagsSchema); - $this->assertNotEmpty($tooFewErrors); - $this->assertEquals('minItems', $tooFewErrors[0]['keyword']); - } - - public function testValidatesSchemaWithObjectConstraintsFromSchemaAttribute() - { - $userSchema = (new Schema( - properties: [ - 'name' => ['type' => 'string', 'minLength' => 2], - 'email' => ['type' => 'string', 'format' => 'email'], - 'age' => ['type' => 'integer', 'minimum' => 18], - ], - required: ['name', 'email'] - ))->toArray(); - - // Valid user object - $validUser = [ - 'name' => 'John', - 'email' => 'john@example.com', - 'age' => 25, - ]; - $validErrors = $this->validator->validateAgainstJsonSchema($validUser, $userSchema); - $this->assertEmpty($validErrors); - - // Invalid - missing required email - $missingEmailUser = [ - 'name' => 'John', - 'age' => 25, - ]; - $missingErrors = $this->validator->validateAgainstJsonSchema($missingEmailUser, $userSchema); - $this->assertNotEmpty($missingErrors); - $this->assertEquals('required', $missingErrors[0]['keyword']); - - // Invalid - name too short - $shortNameUser = [ - 'name' => 'J', - 'email' => 'john@example.com', - 'age' => 25, - ]; - $nameErrors = $this->validator->validateAgainstJsonSchema($shortNameUser, $userSchema); - $this->assertNotEmpty($nameErrors); - $this->assertEquals('minLength', $nameErrors[0]['keyword']); - - // Invalid - age too low - $youngUser = [ - 'name' => 'John', - 'email' => 'john@example.com', - 'age' => 15, - ]; - $ageErrors = $this->validator->validateAgainstJsonSchema($youngUser, $userSchema); - $this->assertNotEmpty($ageErrors); - $this->assertEquals('minimum', $ageErrors[0]['keyword']); - } - - public function testValidatesSchemaWithNestedConstraintsFromSchemaAttribute() - { - $orderSchema = (new Schema( - properties: [ - 'customer' => [ - 'type' => 'object', - 'properties' => [ - 'id' => ['type' => 'string', 'pattern' => '^CUS-[0-9]{6}$'], - 'name' => ['type' => 'string', 'minLength' => 2], - ], - ], - 'items' => [ - 'type' => 'array', - 'minItems' => 1, - 'items' => [ - 'type' => 'object', - 'properties' => [ - 'product_id' => ['type' => 'string', 'pattern' => '^PRD-[0-9]{4}$'], - 'quantity' => ['type' => 'integer', 'minimum' => 1], - ], - 'required' => ['product_id', 'quantity'], - ], - ], - ], - required: ['customer', 'items'] - ))->toArray(); - - // Valid order - $validOrder = [ - 'customer' => [ - 'id' => 'CUS-123456', - 'name' => 'John', - ], - 'items' => [ - [ - 'product_id' => 'PRD-1234', - 'quantity' => 2, - ], - ], - ]; - $validErrors = $this->validator->validateAgainstJsonSchema($validOrder, $orderSchema); - $this->assertEmpty($validErrors); - - // Invalid - bad customer ID format - $badCustomerIdOrder = [ - 'customer' => [ - 'id' => 'CUST-123', // Wrong format - 'name' => 'John', - ], - 'items' => [ - [ - 'product_id' => 'PRD-1234', - 'quantity' => 2, - ], - ], - ]; - $customerIdErrors = $this->validator->validateAgainstJsonSchema($badCustomerIdOrder, $orderSchema); - $this->assertNotEmpty($customerIdErrors); - $this->assertEquals('pattern', $customerIdErrors[0]['keyword']); - - // Invalid - empty items array - $emptyItemsOrder = [ - 'customer' => [ - 'id' => 'CUS-123456', - 'name' => 'John', - ], - 'items' => [], - ]; - $emptyItemsErrors = $this->validator->validateAgainstJsonSchema($emptyItemsOrder, $orderSchema); - $this->assertNotEmpty($emptyItemsErrors); - $this->assertEquals('minItems', $emptyItemsErrors[0]['keyword']); - - // Invalid - missing required property in items - $missingProductIdOrder = [ - 'customer' => [ - 'id' => 'CUS-123456', - 'name' => 'John', - ], - 'items' => [ - [ - // Missing product_id - 'quantity' => 2, - ], - ], - ]; - $missingProductIdErrors = $this->validator->validateAgainstJsonSchema($missingProductIdOrder, $orderSchema); - $this->assertNotEmpty($missingProductIdErrors); - $this->assertEquals('required', $missingProductIdErrors[0]['keyword']); - } - - /** - * @return array{ - * type: 'object', - * properties: array>, - * required: string[], - * additionalProperties: false, - * } - */ - private function getSimpleSchema(): array - { - return [ - 'type' => 'object', - 'properties' => [ - 'name' => ['type' => 'string', 'description' => 'The name'], - 'age' => ['type' => 'integer', 'minimum' => 0], - 'active' => ['type' => 'boolean'], - 'score' => ['type' => 'number'], - 'items' => ['type' => 'array', 'items' => ['type' => 'string']], - 'nullableValue' => ['type' => ['string', 'null']], - 'optionalValue' => ['type' => 'string'], - ], - 'required' => ['name', 'age', 'active', 'score', 'items', 'nullableValue'], - 'additionalProperties' => false, - ]; - } - - /** - * @return array{ - * name: string, - * age: int, - * active: bool, - * score: float, - * items: string[], - * nullableValue: null, - * optionalValue: string - * } - */ - private function getValidData(): array - { - return [ - 'name' => 'Tester', - 'age' => 30, - 'active' => true, - 'score' => 99.5, - 'items' => ['a', 'b'], - 'nullableValue' => null, - 'optionalValue' => 'present', - ]; - } -}