diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml index 04dd958..93c0eed 100644 --- a/.github/workflows/cs.yml +++ b/.github/workflows/cs.yml @@ -13,4 +13,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.2'] + ['8.3'] diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 2ac1a4f..010777d 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -13,6 +13,6 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.3'] stability: >- - ['prefer-lowest', 'prefer-stable'] + ['prefer-stable'] diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index e1ac5f0..e09d784 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,4 +13,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.2'] + ['8.3'] diff --git a/.gitignore b/.gitignore index ccf3d50..8e5a45d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ docs vendor node_modules .php-cs-fixer.cache -runtime \ No newline at end of file +runtime +.context +mcp-*.log \ No newline at end of file diff --git a/context.yaml b/context.yaml new file mode 100644 index 0000000..522b1a8 --- /dev/null +++ b/context.yaml @@ -0,0 +1,56 @@ +$schema: 'https://raw.githubusercontent.com/context-hub/generator/refs/heads/main/json-schema.json' + +documents: + - description: 'Project structure overview' + outputPath: project-structure.md + overwrite: true + sources: + - type: tree + sourcePaths: + - src + filePattern: '*' + renderFormat: ascii + enabled: true + showCharCount: true + + - description: 'Code base' + outputPath: code-base.md + sources: + - type: file + sourcePaths: + - src + + - description: Unit tests + outputPath: unit-tests.md + sources: + - type: file + sourcePaths: + - tests + +tools: + - id: run-all-tests + description: "Run all PHPUnit tests for the json-schema-generator" + type: run + commands: + - cmd: composer + args: + - install + workingDir: "./" + - cmd: vendor/bin/phpunit + args: + - "--color=always" + workingDir: "./" + + - id: run-union-tests + description: "Run only the union type tests" + type: run + commands: + - cmd: composer + args: + - install + workingDir: "./" + - cmd: vendor/bin/phpunit + args: + - "--color=always" + - "--filter=UnionType" + workingDir: "./" diff --git a/examples/DefinitionExample.php b/examples/DefinitionExample.php new file mode 100644 index 0000000..9edc4a1 --- /dev/null +++ b/examples/DefinitionExample.php @@ -0,0 +1,65 @@ + 123, + 'name' => 'Sample Product', + 'price' => 99.99, + 'tags' => ['new', 'featured'], + 'status' => 'Active', + ], +])] +#[AdditionalProperty(name: 'maxProperties', value: 5)] +class Product +{ + public function __construct( + #[Field(title: 'Product ID', description: 'Unique identifier for the product')] + public readonly int $id, + #[Field(title: 'Product Name', description: 'Name of the product')] + public readonly string $name, + #[Field(title: 'Product Price', description: 'Current price of the product')] + public readonly float $price, + + /** + * @var array + */ + #[Field(title: 'Product Tags', description: 'List of tags associated with the product')] + public readonly array $tags = [], + #[Field(title: 'Product Status', description: 'Current status of the product')] + public readonly ?ProductStatus $status = null, + ) {} +} + +#[Definition(title: 'Product Status')] +#[AdditionalProperty(name: 'deprecated', value: ['Discontinued'])] +enum ProductStatus: string +{ + case Active = 'Active'; + case Inactive = 'Inactive'; + case Discontinued = 'Discontinued'; + case OutOfStock = 'Out of Stock'; +} + +// Generate the schema +$generator = new Generator(); +$schema = $generator->generate(Product::class); + +// Output the schema as JSON +\header('Content-Type: application/json'); +echo \json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); diff --git a/examples/UnionTypeExample.php b/examples/UnionTypeExample.php new file mode 100644 index 0000000..a137d85 --- /dev/null +++ b/examples/UnionTypeExample.php @@ -0,0 +1,45 @@ +findAttribute(ClassDefinition::class); + if ($classDefinition !== null) { + if (!empty($classDefinition->title)) { + $schema->setTitle($classDefinition->title); + } else { + // Use class short name as default title + $schema->setTitle($class->getShortName()); + } + + if (!empty($classDefinition->description)) { + $schema->setDescription($classDefinition->description); + } + + if ($classDefinition->id !== null) { + $schema->setId($classDefinition->id); + } + + if ($classDefinition->schemaVersion !== null) { + $schema->setSchemaVersion($classDefinition->schemaVersion); + } + } else { + // Set title to class name by default if no definition attribute + $schema->setTitle($class->getShortName()); + } + + // Process additional properties attributes if present + $this->processAdditionalProperties($class, $schema); + $dependencies = []; // Generating properties foreach ($class->getProperties() as $property) { @@ -80,14 +112,50 @@ public function generate(string|\ReflectionClass $class): Schema return $schema; } + /** + * Process AdditionalProperty attributes on a class + */ + protected function processAdditionalProperties(ClassParserInterface $class, Schema $schema): void + { + // Get reflection class to extract attributes with \ReflectionClass::getAttributes() + try { + $reflectionClass = new \ReflectionClass($class->getName()); + $additionalProperties = $reflectionClass->getAttributes(AdditionalProperty::class); + + foreach ($additionalProperties as $additionalProperty) { + $instance = $additionalProperty->newInstance(); + $schema->addAdditionalProperty($instance->name, $instance->value); + } + } catch (\ReflectionException) { + // Silently fail, we'll just not have additional properties + } + } + protected function generateDefinition(ClassParserInterface $class, array &$dependencies = []): ?Definition { $properties = []; + + // Process class-level Definition attribute if present + $title = $class->getShortName(); + $description = ''; + + $classDefinition = $class->findAttribute(ClassDefinition::class); + if ($classDefinition !== null) { + if (!empty($classDefinition->title)) { + $title = $classDefinition->title; + } + + if (!empty($classDefinition->description)) { + $description = $classDefinition->description; + } + } + if ($class->isEnum()) { return new Definition( type: $class->getName(), options: $class->getEnumValues(), - title: $class->getShortName(), + title: $title, + description: $description, ); } @@ -102,7 +170,12 @@ protected function generateDefinition(ClassParserInterface $class, array &$depen $properties[$property->getName()] = $psc; } - return new Definition(type: $class->getName(), title: $class->getShortName(), properties: $properties); + return new Definition( + type: $class->getName(), + title: $title, + description: $description, + properties: $properties, + ); } protected function generateProperty(PropertyInterface $property): ?Property @@ -125,6 +198,12 @@ protected function generateProperty(PropertyInterface $property): ?Property $type = $property->getType(); + // Handle union types (e.g., string|int|bool) + if ($type instanceof UnionType) { + $required = $default === null && !$type->allowsNull(); + return new Property($type, [], $title, $description, $required, $default); + } + $options = []; if ($property->isCollection()) { $options = \array_map( diff --git a/src/Parser/ClassParser.php b/src/Parser/ClassParser.php index e290880..89dd213 100644 --- a/src/Parser/ClassParser.php +++ b/src/Parser/ClassParser.php @@ -78,17 +78,12 @@ public function getProperties(): array continue; } - /** - * @var \ReflectionNamedType|null $type - */ - $type = $property->getType(); - if (!$type instanceof \ReflectionNamedType) { - continue; - } + // Parse the type using TypeParser + $typeInterface = TypeParser::fromReflectionType($property->getType()); $properties[] = new Property( property: $property, - type: new Type(name: $type->getName(), builtin: $type->isBuiltin(), nullable: $type->allowsNull()), + type: $typeInterface, hasDefaultValue: $this->hasPropertyDefaultValue($property), defaultValue: $this->getPropertyDefaultValue($property), collectionValueTypes: $this->getPropertyCollectionTypes($property->getName()), @@ -98,6 +93,17 @@ public function getProperties(): array return $properties; } + public function findAttribute(string $name): ?object + { + $attributes = $this->class->getAttributes($name); + + if ($attributes === []) { + return null; + } + + return $attributes[0]->newInstance(); + } + public function isEnum(): bool { return $this->class->isEnum(); diff --git a/src/Parser/ClassParserInterface.php b/src/Parser/ClassParserInterface.php index ccb5f55..ff54d60 100644 --- a/src/Parser/ClassParserInterface.php +++ b/src/Parser/ClassParserInterface.php @@ -24,4 +24,15 @@ public function getProperties(): array; public function isEnum(): bool; public function getEnumValues(): array; + + /** + * Find a class-level attribute. + * + * @template T + * + * @param class-string $name The class name of the attribute. + * + * @return T|null The attribute or {@see null}, if the requested attribute does not exist. + */ + public function findAttribute(string $name): ?object; } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php new file mode 100644 index 0000000..8dc5b75 --- /dev/null +++ b/src/Parser/TypeParser.php @@ -0,0 +1,65 @@ +getTypes() as $type) { + if ($type instanceof \ReflectionNamedType) { + $types[] = self::parseNamedType($type); + } else { + throw new InvalidTypeException('Nested union or intersection types are not supported.'); + } + } + + return new UnionType($types); + } + + /** + * Parse a named type into a Type. + */ + private static function parseNamedType(\ReflectionNamedType $namedType): Type + { + return new Type( + name: $namedType->getName(), + builtin: $namedType->isBuiltin(), + nullable: $namedType->allowsNull(), + ); + } +} diff --git a/src/Parser/UnionType.php b/src/Parser/UnionType.php new file mode 100644 index 0000000..41a3c39 --- /dev/null +++ b/src/Parser/UnionType.php @@ -0,0 +1,58 @@ + $types + */ + public function __construct(private array $types) {} + + /** + * Always returns SchemaType::Union for union types. + */ + public function getName(): string|SchemaType + { + return SchemaType::Union; + } + + /** + * @return array + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * Union types are not built-in types. + */ + public function isBuiltin(): bool + { + return false; + } + + /** + * Checks if any of the types in the union allows null. + */ + public function allowsNull(): bool + { + foreach ($this->types as $type) { + if ($type->allowsNull()) { + return true; + } + } + + return false; + } +} diff --git a/src/Schema.php b/src/Schema.php index 529912e..1c954e2 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -9,6 +9,11 @@ final class Schema extends AbstractDefinition { private array $definitions = []; + private string $title = ''; + private string $description = ''; + private ?string $id = null; + private ?string $schemaVersion = null; + private array $additionalProperties = []; public function addDefinition(string $name, Definition $definition): self { @@ -16,10 +21,61 @@ public function addDefinition(string $name, Definition $definition): self return $this; } + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } + + public function setDescription(string $description): self + { + $this->description = $description; + return $this; + } + + public function setId(?string $id): self + { + $this->id = $id; + return $this; + } + + public function setSchemaVersion(?string $schemaVersion): self + { + $this->schemaVersion = $schemaVersion; + return $this; + } + + public function addAdditionalProperty(string $name, mixed $value): self + { + $this->additionalProperties[$name] = $value; + return $this; + } + public function jsonSerialize(): array { $schema = $this->renderProperties([]); + if ($this->title !== '') { + $schema['title'] = $this->title; + } + + if ($this->description !== '') { + $schema['description'] = $this->description; + } + + if ($this->id !== null) { + $schema['$id'] = $this->id; + } + + if ($this->schemaVersion !== null) { + $schema['$schema'] = $this->schemaVersion; + } + + // Add any additional properties that were specified + foreach ($this->additionalProperties as $key => $value) { + $schema[$key] = $value; + } + if ($this->definitions !== []) { $schema['definitions'] = []; diff --git a/src/Schema/Definition.php b/src/Schema/Definition.php index da2db35..4f19653 100644 --- a/src/Schema/Definition.php +++ b/src/Schema/Definition.php @@ -70,7 +70,6 @@ private function renderType(array $schema): array $rf = new \ReflectionEnum($this->type); - /** @var \ReflectionEnum $rf */ if (!$rf->isBacked()) { throw new DefinitionException(\sprintf( 'Type `%s` is not a backed enum.', diff --git a/src/Schema/Property.php b/src/Schema/Property.php index 30c7930..1d3915c 100644 --- a/src/Schema/Property.php +++ b/src/Schema/Property.php @@ -5,17 +5,18 @@ namespace Spiral\JsonSchemaGenerator\Schema; use Spiral\JsonSchemaGenerator\Exception\InvalidTypeException; +use Spiral\JsonSchemaGenerator\Parser\UnionType; final readonly class Property implements \JsonSerializable { public PropertyOptions $options; /** - * @param Type|class-string $type + * @param Type|class-string|UnionType $type * @param array $options */ public function __construct( - public Type|string $type, + public Type|string|UnionType $type, array $options = [], public string $title = '', public string $description = '', @@ -44,8 +45,25 @@ public function jsonSerialize(): array $property['default'] = $this->default; } + // Handle UnionType instance + if ($this->type instanceof UnionType) { + $unionOptions = []; + foreach ($this->type->getTypes() as $unionType) { + $typeName = $unionType->getName(); + if (\is_string($typeName) && !$unionType->isBuiltin()) { + // Class reference + $unionOptions[] = ['$ref' => (new Reference($typeName))->jsonSerialize()]; + } else { + // Primitive type + $unionOptions[] = ['type' => $typeName instanceof Type ? $typeName->value : $typeName]; + } + } + $property['oneOf'] = $unionOptions; + return $property; + } + if ($this->type === Type::Union) { - $property['anyOf'] = $this->options->jsonSerialize(); + $property['oneOf'] = $this->options->jsonSerialize(); return $property; } @@ -67,7 +85,7 @@ public function jsonSerialize(): array $property['items']['type'] = $this->options[0]->value->value; } else { - $property['items']['anyOf'] = $this->options->jsonSerialize(); + $property['items']['oneOf'] = $this->options->jsonSerialize(); } } @@ -77,6 +95,17 @@ public function jsonSerialize(): array public function getDependencies(): array { $dependencies = []; + + // Extract dependencies from union types + if ($this->type instanceof UnionType) { + foreach ($this->type->getTypes() as $unionType) { + $typeName = $unionType->getName(); + if (!$unionType->isBuiltin() && \is_string($typeName)) { + $dependencies[] = $typeName; + } + } + } + foreach ($this->options->getOptions() as $option) { if (\is_string($option->value)) { $dependencies[] = $option->value; diff --git a/src/Schema/PropertyOptions.php b/src/Schema/PropertyOptions.php index 5eda459..4b16c26 100644 --- a/src/Schema/PropertyOptions.php +++ b/src/Schema/PropertyOptions.php @@ -21,7 +21,7 @@ final class PropertyOptions implements \Countable, \ArrayAccess, \JsonSerializab public function __construct(array $options = []) { foreach ($options as $option) { - $this->options[] = new PropertyOption($option); + $this->options[] = $option instanceof PropertyOption ? $option : new PropertyOption($option); } } diff --git a/tests/Unit/Attribute/AdditionalPropertyTest.php b/tests/Unit/Attribute/AdditionalPropertyTest.php new file mode 100644 index 0000000..1f393f1 --- /dev/null +++ b/tests/Unit/Attribute/AdditionalPropertyTest.php @@ -0,0 +1,44 @@ + 'Example', 'value' => 123]])] +final class AdditionalPropertyTest extends TestCase +{ + public function testAdditionalPropertyBooleanValue(): void + { + $ref = new \ReflectionClass(self::class); + $attrs = $ref->getAttributes(AdditionalProperty::class); + $attr = $attrs[0]->newInstance(); + + $this->assertSame('additionalProperties', $attr->name); + $this->assertFalse($attr->value); + } + + public function testAdditionalPropertyIntegerValue(): void + { + $ref = new \ReflectionClass(self::class); + $attrs = $ref->getAttributes(AdditionalProperty::class); + $attr = $attrs[1]->newInstance(); + + $this->assertSame('maxProperties', $attr->name); + $this->assertSame(10, $attr->value); + } + + public function testAdditionalPropertyArrayValue(): void + { + $ref = new \ReflectionClass(self::class); + $attrs = $ref->getAttributes(AdditionalProperty::class); + $attr = $attrs[2]->newInstance(); + + $this->assertSame('examples', $attr->name); + $this->assertEquals([['name' => 'Example', 'value' => 123]], $attr->value); + } +} diff --git a/tests/Unit/Attribute/DefinitionTest.php b/tests/Unit/Attribute/DefinitionTest.php new file mode 100644 index 0000000..b7fe2a6 --- /dev/null +++ b/tests/Unit/Attribute/DefinitionTest.php @@ -0,0 +1,29 @@ +getAttributes(Definition::class); + $attr = $attrs[0]->newInstance(); + + $this->assertSame('Test Title', $attr->title); + $this->assertSame('Test Description', $attr->description); + $this->assertSame('https://example.com/schema.json', $attr->id); + $this->assertSame('http://json-schema.org/draft-07/schema#', $attr->schemaVersion); + } +} diff --git a/tests/Unit/Fixture/ComplexUnionTypes.php b/tests/Unit/Fixture/ComplexUnionTypes.php new file mode 100644 index 0000000..524c6b6 --- /dev/null +++ b/tests/Unit/Fixture/ComplexUnionTypes.php @@ -0,0 +1,36 @@ + + */ + #[Field(title: 'Array of Union Types', description: 'An array that can contain strings or integers')] + public array $arrayOfUnionTypes = []; + + /** + * @var array + */ + #[Field(title: 'Array of Objects', description: 'An array that can contain different object types')] + public array $arrayOfObjects = []; + + public function __construct( + #[Field(title: 'Nullable Union', description: 'A nullable union of primitive types')] + public readonly string|int|null $nullableUnion = null, + #[Field(title: 'Complex Property', description: 'Union of primitive, array, and object types')] + public readonly string|array|Movie|null $complexProperty = null, + #[Field(title: 'Default Value', description: 'Union type with default value')] + public readonly string|int $defaultValue = 42, + ) {} +} diff --git a/tests/Unit/Fixture/Product.php b/tests/Unit/Fixture/Product.php new file mode 100644 index 0000000..2100e88 --- /dev/null +++ b/tests/Unit/Fixture/Product.php @@ -0,0 +1,40 @@ + 123, + 'name' => 'Sample Product', + 'price' => 99.99, + 'inStock' => true, + ], +])] +final readonly class Product +{ + public function __construct( + #[Field(title: 'Product ID', description: 'Unique identifier for the product')] + public int $id, + #[Field(title: 'Product Name', description: 'Name of the product')] + public string $name, + #[Field(title: 'Product Price', description: 'Current price of the product')] + public float $price, + #[Field(title: 'In Stock', description: 'Whether the product is in stock')] + public bool $inStock = true, + #[Field(title: 'Product Status', description: 'Current status of the product')] + public ?ProductStatus $status = null, + ) {} +} diff --git a/tests/Unit/Fixture/ProductStatus.php b/tests/Unit/Fixture/ProductStatus.php new file mode 100644 index 0000000..64f1b5c --- /dev/null +++ b/tests/Unit/Fixture/ProductStatus.php @@ -0,0 +1,27 @@ +generate(ComplexUnionTypes::class); + $jsonSchema = $schema->jsonSerialize(); + + // Check class-level attributes are applied + $this->assertEquals('Complex Union Types', $jsonSchema['title']); + $this->assertEquals('Class with complex union type combinations', $jsonSchema['description']); + + // Check properties exist + $this->assertArrayHasKey('properties', $jsonSchema); + $this->assertArrayHasKey('nullableUnion', $jsonSchema['properties']); + $this->assertArrayHasKey('complexProperty', $jsonSchema['properties']); + $this->assertArrayHasKey('defaultValue', $jsonSchema['properties']); + $this->assertArrayHasKey('arrayOfUnionTypes', $jsonSchema['properties']); + $this->assertArrayHasKey('arrayOfObjects', $jsonSchema['properties']); + + // Test nullable union (string|int|null) + $nullableUnion = $jsonSchema['properties']['nullableUnion']; + $this->assertEquals('Nullable Union', $nullableUnion['title']); + $this->assertArrayHasKey('oneOf', $nullableUnion); + $this->assertCount(3, $nullableUnion['oneOf']); + + // Test complex property (string|array|Movie|null) + $complexProperty = $jsonSchema['properties']['complexProperty']; + $this->assertEquals('Complex Property', $complexProperty['title']); + $this->assertArrayHasKey('oneOf', $complexProperty); + $this->assertCount(4, $complexProperty['oneOf']); + + // Verify that array, string, and object ref are in the oneOf + $hasString = false; + $hasArray = false; + $hasObjectRef = false; + $hasNull = false; + + foreach ($complexProperty['oneOf'] as $type) { + if (isset($type['type']) && $type['type'] === 'string') { + $hasString = true; + } elseif (isset($type['type']) && $type['type'] === 'array') { + $hasArray = true; + } elseif (isset($type['type']) && $type['type'] === 'null') { + $hasNull = true; + } elseif (isset($type['$ref']) && $type['$ref'] === '#/definitions/Movie') { + $hasObjectRef = true; + } + } + + $this->assertTrue($hasString, 'String type not found in complexProperty oneOf'); + $this->assertTrue($hasArray, 'Array type not found in complexProperty oneOf'); + $this->assertTrue($hasObjectRef, 'Movie reference not found in complexProperty oneOf'); + $this->assertTrue($hasNull, 'Null type not found in complexProperty oneOf'); + + // Test union type with default value + $defaultValue = $jsonSchema['properties']['defaultValue']; + $this->assertEquals('Default Value', $defaultValue['title']); + $this->assertEquals('Union type with default value', $defaultValue['description']); + $this->assertArrayHasKey('oneOf', $defaultValue); + $this->assertCount(2, $defaultValue['oneOf']); + $this->assertEquals(42, $defaultValue['default']); + + // Test array of union types + $arrayOfUnionTypes = $jsonSchema['properties']['arrayOfUnionTypes']; + $this->assertEquals('Array of Union Types', $arrayOfUnionTypes['title']); + $this->assertEquals('array', $arrayOfUnionTypes['type']); + $this->assertArrayHasKey('items', $arrayOfUnionTypes); + $this->assertArrayHasKey('oneOf', $arrayOfUnionTypes['items']); + $this->assertCount(2, $arrayOfUnionTypes['items']['oneOf']); + + // Test array of objects + $arrayOfObjects = $jsonSchema['properties']['arrayOfObjects']; + $this->assertEquals('Array of Objects', $arrayOfObjects['title']); + $this->assertEquals('array', $arrayOfObjects['type']); + $this->assertArrayHasKey('items', $arrayOfObjects); + $this->assertArrayHasKey('oneOf', $arrayOfObjects['items']); + $this->assertCount(2, $arrayOfObjects['items']['oneOf']); + + // Make sure definitions are included + $this->assertArrayHasKey('definitions', $jsonSchema); + $this->assertArrayHasKey('Movie', $jsonSchema['definitions']); + $this->assertArrayHasKey('Actor', $jsonSchema['definitions']); + } + + public function testRequiredPropertiesWithDefaultValues(): void + { + $generator = new Generator(); + $schema = $generator->generate(ComplexUnionTypes::class); + $jsonSchema = $schema->jsonSerialize(); + + // Check if there are any required properties + // If all properties have default values, the 'required' key might not exist + // Only verify that array properties without default values aren't required + if (isset($jsonSchema['required'])) { + $this->assertNotContains('nullableUnion', $jsonSchema['required']); + $this->assertNotContains('complexProperty', $jsonSchema['required']); + $this->assertNotContains('defaultValue', $jsonSchema['required']); + $this->assertNotContains('arrayOfUnionTypes', $jsonSchema['required']); + $this->assertNotContains('arrayOfObjects', $jsonSchema['required']); + } else { + // If 'required' key doesn't exist, the test passes as no properties are required + $this->assertTrue(true); + } + } +} diff --git a/tests/Unit/GeneratorExtendedTest.php b/tests/Unit/GeneratorExtendedTest.php new file mode 100644 index 0000000..0b80098 --- /dev/null +++ b/tests/Unit/GeneratorExtendedTest.php @@ -0,0 +1,131 @@ +generate(Product::class); + + $expectedSchema = [ + 'title' => 'Product Schema', + 'description' => 'A product in the catalog', + '$id' => 'https://example.com/schemas/product.json', + '$schema' => 'http://json-schema.org/draft-07/schema#', + 'additionalProperties' => false, + 'examples' => [ + [ + 'id' => 123, + 'name' => 'Sample Product', + 'price' => 99.99, + 'inStock' => true, + ], + ], + 'properties' => [ + 'id' => [ + 'title' => 'Product ID', + 'description' => 'Unique identifier for the product', + 'type' => 'integer', + ], + 'name' => [ + 'title' => 'Product Name', + 'description' => 'Name of the product', + 'type' => 'string', + ], + 'price' => [ + 'title' => 'Product Price', + 'description' => 'Current price of the product', + 'type' => 'number', + ], + 'inStock' => [ + 'title' => 'In Stock', + 'description' => 'Whether the product is in stock', + 'type' => 'boolean', + 'default' => true, + ], + 'status' => [ + 'title' => 'Product Status', + 'description' => 'Current status of the product', + 'allOf' => [ + [ + '$ref' => '#/definitions/ProductStatus', + ], + ], + ], + ], + 'required' => [ + 'id', + 'name', + 'price', + ], + 'definitions' => [ + 'ProductStatus' => [ + 'title' => 'Product Status', + 'description' => 'The status of a product in the catalog', + 'type' => 'string', + 'enum' => [ + 'Released', + 'Rumored', + 'Post Production', + 'In Production', + 'Planned', + 'Canceled', + ], + ], + ], + ]; + + $this->assertEquals($expectedSchema, $schema->jsonSerialize()); + } + + public function testGenerateEnumDefinition(): void + { + // Test Definition attributes on nested enum definitions + $generator = new Generator(); + $schema = $generator->generate(Product::class); + + // Extract the ProductStatus definition from the generated schema + $definitions = $schema->jsonSerialize()['definitions']; + $this->assertArrayHasKey('ProductStatus', $definitions); + + $productStatusDefinition = $definitions['ProductStatus']; + + // Verify Definition attribute values are applied + $this->assertEquals('Product Status', $productStatusDefinition['title']); + $this->assertEquals('The status of a product in the catalog', $productStatusDefinition['description']); + + // Verify the enum values are correct + $this->assertEquals('string', $productStatusDefinition['type']); + $this->assertEquals([ + 'Released', + 'Rumored', + 'Post Production', + 'In Production', + 'Planned', + 'Canceled', + ], $productStatusDefinition['enum']); + + // Note: AdditionalProperty attributes on nested definitions aren't currently + // processed by the Generator class, so we're not testing for them here. + // Adding support for this would require further modifications to the Generator class. + } + + public function testGenerateWithNoDefinitionAttributes(): void + { + // Test that the generator still works properly with classes that don't have Definition attributes + $generator = new Generator(); + $schema = $generator->generate(\stdClass::class); + + $this->assertArrayHasKey('title', $schema->jsonSerialize()); + $this->assertEquals('stdClass', $schema->jsonSerialize()['title']); + } +} diff --git a/tests/Unit/GeneratorTest.php b/tests/Unit/GeneratorTest.php index 499cb7c..a2bc4bb 100644 --- a/tests/Unit/GeneratorTest.php +++ b/tests/Unit/GeneratorTest.php @@ -18,44 +18,44 @@ public function testGenerateMovie(): void $this->assertEquals( [ - 'properties' => [ - 'title' => [ - 'title' => 'Title', + 'properties' => [ + 'title' => [ + 'title' => 'Title', 'description' => 'The title of the movie', - 'type' => 'string', + 'type' => 'string', ], - 'year' => [ - 'title' => 'Year', + 'year' => [ + 'title' => 'Year', 'description' => 'The year of the movie', - 'type' => 'integer', + 'type' => 'integer', ], - 'description' => [ - 'title' => 'Description', + 'description' => [ + 'title' => 'Description', 'description' => 'The description of the movie', - 'type' => 'string', + 'type' => 'string', ], - 'director' => [ + 'director' => [ 'type' => 'string', ], 'releaseStatus' => [ - 'title' => 'Release Status', + 'title' => 'Release Status', 'description' => 'The release status of the movie', - 'allOf' => [ + 'allOf' => [ [ '$ref' => '#/definitions/ReleaseStatus', ], ], ], ], - 'required' => [ + 'required' => [ 'title', 'year', ], 'definitions' => [ 'ReleaseStatus' => [ 'title' => 'ReleaseStatus', - 'type' => 'string', - 'enum' => [ + 'type' => 'string', + 'enum' => [ 'Released', 'Rumored', 'Post Production', @@ -65,6 +65,7 @@ public function testGenerateMovie(): void ], ], ], + 'title' => 'Movie', ], $schema->jsonSerialize(), ); @@ -77,81 +78,82 @@ public function testGenerateActor(): void $this->assertEquals( [ - 'properties' => [ - 'name' => [ + 'properties' => [ + 'name' => [ 'type' => 'string', ], - 'age' => [ + 'age' => [ 'type' => 'integer', ], - 'bio' => [ - 'title' => 'Biography', + 'bio' => [ + 'title' => 'Biography', 'description' => 'The biography of the actor', - 'type' => 'string', + 'type' => 'string', ], - 'movies' => [ - 'type' => 'array', + 'movies' => [ + 'type' => 'array', 'items' => [ '$ref' => '#/definitions/Movie', ], 'default' => [], ], 'bestMovie' => [ - 'title' => 'Best Movie', + 'title' => 'Best Movie', 'description' => 'The best movie of the actor', - 'allOf' => [ + 'allOf' => [ [ '$ref' => '#/definitions/Movie', ], ], ], ], - 'required' => [ + 'required' => [ 'name', 'age', ], + 'title' => 'Actor', 'definitions' => [ - 'Movie' => [ - 'title' => 'Movie', - 'type' => 'object', + 'Movie' => [ + 'title' => 'Movie', + 'type' => 'object', 'properties' => [ - 'title' => [ - 'title' => 'Title', + 'title' => [ + 'title' => 'Title', 'description' => 'The title of the movie', - 'type' => 'string', + 'type' => 'string', ], - 'year' => [ - 'title' => 'Year', + 'year' => [ + 'title' => 'Year', 'description' => 'The year of the movie', - 'type' => 'integer', + 'type' => 'integer', ], - 'description' => [ - 'title' => 'Description', + 'description' => [ + 'title' => 'Description', 'description' => 'The description of the movie', - 'type' => 'string', + 'type' => 'string', ], - 'director' => [ + 'director' => [ 'type' => 'string', ], 'releaseStatus' => [ - 'title' => 'Release Status', + 'title' => 'Release Status', 'description' => 'The release status of the movie', - 'allOf' => [ + 'allOf' => [ [ '$ref' => '#/definitions/ReleaseStatus', ], ], ], ], - 'required' => [ + 'required' => [ 'title', 'year', ], ], 'ReleaseStatus' => [ 'title' => 'ReleaseStatus', - 'type' => 'string', - 'enum' => [ + 'type' => 'string', + 'enum' => [ 'Released', 'Rumored', 'Post Production', diff --git a/tests/Unit/GeneratorUnionTypeTest.php b/tests/Unit/GeneratorUnionTypeTest.php new file mode 100644 index 0000000..71a53eb --- /dev/null +++ b/tests/Unit/GeneratorUnionTypeTest.php @@ -0,0 +1,90 @@ +generate(UnionTypeExample::class); + $jsonSchema = $schema->jsonSerialize(); + + // Check title is set properly + $this->assertEquals('UnionTypeExample', $jsonSchema['title']); + + // Check properties exist + $this->assertArrayHasKey('properties', $jsonSchema); + $this->assertArrayHasKey('stringOrInt', $jsonSchema['properties']); + $this->assertArrayHasKey('multiType', $jsonSchema['properties']); + $this->assertArrayHasKey('objectUnion', $jsonSchema['properties']); + + // Check primitive union type (string|int) + $stringOrInt = $jsonSchema['properties']['stringOrInt']; + $this->assertEquals('String or Integer', $stringOrInt['title']); + $this->assertEquals('A value that can be either a string or an integer', $stringOrInt['description']); + $this->assertArrayHasKey('oneOf', $stringOrInt); + $this->assertCount(2, $stringOrInt['oneOf']); + + // The order of types in oneOf can vary, so check both options + $stringIntTypes = [ + ['type' => 'string'], + ['type' => 'integer'], + ]; + foreach ($stringOrInt['oneOf'] as $type) { + $this->assertTrue(\in_array($type, $stringIntTypes)); + } + + // Check multiple types union with null (string|int|bool|null) + $multiType = $jsonSchema['properties']['multiType']; + $this->assertEquals('Multiple Types', $multiType['title']); + $this->assertEquals('A value that can be one of multiple types', $multiType['description']); + $this->assertArrayHasKey('oneOf', $multiType); + $this->assertCount(4, $multiType['oneOf']); + + // Check object union type (Movie|Actor|null) + $objectUnion = $jsonSchema['properties']['objectUnion']; + $this->assertEquals('Object Union', $objectUnion['title']); + $this->assertEquals('A value that can be one of multiple object types', $objectUnion['description']); + $this->assertArrayHasKey('oneOf', $objectUnion); + $this->assertCount(3, $objectUnion['oneOf']); + + // Check that there are references to Movie and Actor in the oneOf + $hasMovieRef = false; + $hasActorRef = false; + $hasNullType = false; + + foreach ($objectUnion['oneOf'] as $option) { + if (isset($option['$ref']) && $option['$ref'] === '#/definitions/Movie') { + $hasMovieRef = true; + } + if (isset($option['$ref']) && $option['$ref'] === '#/definitions/Actor') { + $hasActorRef = true; + } + if (isset($option['type']) && $option['type'] === 'null') { + $hasNullType = true; + } + } + + $this->assertTrue($hasMovieRef, 'Movie reference not found in objectUnion oneOf'); + $this->assertTrue($hasActorRef, 'Actor reference not found in objectUnion oneOf'); + $this->assertTrue($hasNullType, 'Null type not found in objectUnion oneOf'); + + // Check that definitions are included + $this->assertArrayHasKey('definitions', $jsonSchema); + $this->assertArrayHasKey('Movie', $jsonSchema['definitions']); + $this->assertArrayHasKey('Actor', $jsonSchema['definitions']); + + // Check that required properties are set correctly + $this->assertArrayHasKey('required', $jsonSchema); + $this->assertContains('stringOrInt', $jsonSchema['required']); + $this->assertNotContains('multiType', $jsonSchema['required']); + $this->assertNotContains('objectUnion', $jsonSchema['required']); + } +} diff --git a/tests/Unit/Parser/ClassParserTest.php b/tests/Unit/Parser/ClassParserTest.php index 32cc10a..6110878 100644 --- a/tests/Unit/Parser/ClassParserTest.php +++ b/tests/Unit/Parser/ClassParserTest.php @@ -7,8 +7,11 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Spiral\JsonSchemaGenerator\Exception\GeneratorException; +use Spiral\JsonSchemaGenerator\Attribute\Definition; +use Spiral\JsonSchemaGenerator\Attribute\AdditionalProperty; use Spiral\JsonSchemaGenerator\Parser\ClassParser; use Spiral\JsonSchemaGenerator\Schema\Type; +use Spiral\JsonSchemaGenerator\Tests\Unit\Fixture\Product; use Spiral\JsonSchemaGenerator\Tests\Unit\Fixture\Movie; use Spiral\JsonSchemaGenerator\Tests\Unit\Fixture\ReleaseStatus; @@ -242,4 +245,37 @@ public function testGetPropertyCollectionTypes(object $class, array $expected): $parser = new ClassParser($class::class); $this->assertEquals($expected, $parser->getProperties()[0]->getCollectionValueTypes()); } + + public function testFindAttribute(): void + { + // Test with a class that has the Definition attribute + $parser = new ClassParser(Product::class); + + $definition = $parser->findAttribute(Definition::class); + $this->assertInstanceOf(Definition::class, $definition); + $this->assertEquals('Product Schema', $definition->title); + $this->assertEquals('A product in the catalog', $definition->description); + $this->assertEquals('https://example.com/schemas/product.json', $definition->id); + $this->assertEquals('http://json-schema.org/draft-07/schema#', $definition->schemaVersion); + + // Test with a class that doesn't have a sought attribute + $parser = new ClassParser(Movie::class); + $this->assertNull($parser->findAttribute(Definition::class)); + } + + public function testFindAttributeReturnsFirstInstance(): void + { + // The findAttribute method should return the first instance of a repeatable attribute + // but we can't directly test this through ClassParser because it uses ReflectionClass::getAttributes + // which only returns the first attribute by default + + $ref = new \ReflectionClass(Product::class); + $additionalPropAttrs = $ref->getAttributes(AdditionalProperty::class); + + $this->assertGreaterThan( + 1, + \count($additionalPropAttrs), + 'Product class should have multiple AdditionalProperty attributes for this test to be valid', + ); + } } diff --git a/tests/Unit/Parser/TypeParserTest.php b/tests/Unit/Parser/TypeParserTest.php new file mode 100644 index 0000000..51e75c2 --- /dev/null +++ b/tests/Unit/Parser/TypeParserTest.php @@ -0,0 +1,111 @@ +createNamedType('string'); + $type = TypeParser::fromReflectionType($reflectionType); + + $this->assertInstanceOf(Type::class, $type); + $this->assertSame(SchemaType::String, $type->getName()); + $this->assertTrue($type->isBuiltin()); + $this->assertFalse($type->allowsNull()); + } + + public function testFromReflectionTypeWithNullableNamedType(): void + { + $reflectionType = $this->createNamedType('string', true); + $type = TypeParser::fromReflectionType($reflectionType); + + $this->assertInstanceOf(Type::class, $type); + $this->assertSame(SchemaType::String, $type->getName()); + $this->assertTrue($type->isBuiltin()); + $this->assertTrue($type->allowsNull()); + } + + public function testFromReflectionTypeWithUnionType(): void + { + $reflectionType = $this->createUnionType(['string', 'int']); + $type = TypeParser::fromReflectionType($reflectionType); + + $this->assertInstanceOf(UnionType::class, $type); + $this->assertSame(SchemaType::Union, $type->getName()); + $this->assertFalse($type->isBuiltin()); + $this->assertFalse($type->allowsNull()); + + $types = $type->getTypes(); + $this->assertCount(2, $types); + $this->assertSame(SchemaType::String, $types[0]->getName()); + $this->assertSame(SchemaType::Integer, $types[1]->getName()); + } + + public function testFromReflectionTypeWithUnionTypeIncludingClass(): void + { + $reflectionType = $this->createUnionType(['string', \stdClass::class]); + $type = TypeParser::fromReflectionType($reflectionType); + + $this->assertInstanceOf(UnionType::class, $type); + + $types = $type->getTypes(); + $this->assertCount(2, $types); + $this->assertSame(SchemaType::String, $types[0]->getName()); + $this->assertSame(\stdClass::class, $types[1]->getName()); + $this->assertTrue($types[0]->isBuiltin()); + $this->assertFalse($types[1]->isBuiltin()); + } + + public function testFromReflectionTypeWithIntersectionType(): void + { + $intersectionType = $this->createMock(\ReflectionIntersectionType::class); + + $this->expectException(InvalidTypeException::class); + $this->expectExceptionMessage('Intersection types are not supported in JSON Schema.'); + + TypeParser::fromReflectionType($intersectionType); + } + + public function testFromReflectionTypeWithUnsupportedType(): void + { + $unsupportedType = $this->createMock(\ReflectionType::class); + + $this->expectException(InvalidTypeException::class); + $this->expectExceptionMessage('Unsupported reflection type:'); + + TypeParser::fromReflectionType($unsupportedType); + } + + private function createNamedType(string $typeName, bool $allowsNull = false): \ReflectionNamedType + { + $namedType = $this->createMock(\ReflectionNamedType::class); + $namedType->method('getName')->willReturn($typeName); + $namedType->method('isBuiltin')->willReturn(\in_array($typeName, ['string', 'int', 'bool', 'float', 'array', 'null'])); + $namedType->method('allowsNull')->willReturn($allowsNull); + + return $namedType; + } + + private function createUnionType(array $typeNames): \ReflectionUnionType + { + $types = []; + foreach ($typeNames as $typeName) { + $types[] = $this->createNamedType($typeName); + } + + $unionType = $this->createMock(\ReflectionUnionType::class); + $unionType->method('getTypes')->willReturn($types); + + return $unionType; + } +} diff --git a/tests/Unit/Parser/UnionTypeTest.php b/tests/Unit/Parser/UnionTypeTest.php new file mode 100644 index 0000000..e3d7312 --- /dev/null +++ b/tests/Unit/Parser/UnionTypeTest.php @@ -0,0 +1,76 @@ +assertSame(SchemaType::Union, $unionType->getName()); + } + + public function testGetTypes(): void + { + $types = [ + new Type('string', true, false), + new Type('int', true, false), + new Type(Movie::class, false, false), + ]; + + $unionType = new UnionType($types); + + $this->assertSame($types, $unionType->getTypes()); + $this->assertCount(3, $unionType->getTypes()); + } + + public function testIsBuiltin(): void + { + $unionType = new UnionType([ + new Type('string', true, false), + new Type('int', true, false), + ]); + + // Union types are not built-in types + $this->assertFalse($unionType->isBuiltin()); + } + + public function testAllowsNull(): void + { + // Union without null type + $unionType = new UnionType([ + new Type('string', true, false), + new Type('int', true, false), + ]); + + $this->assertFalse($unionType->allowsNull()); + + // Union with one nullable type + $unionType = new UnionType([ + new Type('string', true, false), + new Type('int', true, true), + ]); + + $this->assertTrue($unionType->allowsNull()); + + // Union with null type + $unionType = new UnionType([ + new Type('string', true, false), + new Type('null', true, false), + ]); + + $this->assertFalse($unionType->allowsNull(), 'Having a null Type does not make it nullable'); + } +} diff --git a/tests/Unit/Schema/PropertyTest.php b/tests/Unit/Schema/PropertyTest.php index cb31af1..44f6e95 100644 --- a/tests/Unit/Schema/PropertyTest.php +++ b/tests/Unit/Schema/PropertyTest.php @@ -58,7 +58,7 @@ public function testPropertyWithUnionType(): void ); $this->assertEquals([ - 'anyOf' => [ + 'oneOf' => [ [ '$ref' => '#/definitions/Movie', ], @@ -130,7 +130,7 @@ public function testPropertyWithArrayTypeMultipleElems(): void 'type' => 'array', 'title' => 'Some movie', 'items' => [ - 'anyOf' => [ + 'oneOf' => [ [ '$ref' => '#/definitions/Movie', ], diff --git a/tests/Unit/Schema/PropertyUnionTypeTest.php b/tests/Unit/Schema/PropertyUnionTypeTest.php new file mode 100644 index 0000000..f5a8dd8 --- /dev/null +++ b/tests/Unit/Schema/PropertyUnionTypeTest.php @@ -0,0 +1,120 @@ +jsonSerialize(); + + $this->assertArrayHasKey('title', $serialized); + $this->assertArrayHasKey('description', $serialized); + $this->assertArrayHasKey('oneOf', $serialized); + $this->assertCount(2, $serialized['oneOf']); + + // Check that oneOf contains the correct types + $this->assertEquals(['type' => 'string'], $serialized['oneOf'][0]); + $this->assertEquals(['type' => 'integer'], $serialized['oneOf'][1]); + } + + public function testObjectUnionTypePropertySerialization(): void + { + $unionTypes = [ + new ParserType(Movie::class, false, false), + new ParserType(Actor::class, false, false), + ]; + + $unionType = new UnionType($unionTypes); + + $property = new Property( + type: $unionType, + title: 'Movie or Actor', + description: 'A value that can be either a movie or an actor', + required: true, + ); + + $serialized = $property->jsonSerialize(); + + $this->assertArrayHasKey('title', $serialized); + $this->assertArrayHasKey('description', $serialized); + $this->assertArrayHasKey('oneOf', $serialized); + $this->assertCount(2, $serialized['oneOf']); + + // Check that oneOf contains the correct references + $this->assertEquals(['$ref' => '#/definitions/Movie'], $serialized['oneOf'][0]); + $this->assertEquals(['$ref' => '#/definitions/Actor'], $serialized['oneOf'][1]); + } + + public function testMixedUnionTypePropertySerialization(): void + { + $unionTypes = [ + new ParserType('string', true, false), + new ParserType(Movie::class, false, false), + ]; + + $unionType = new UnionType($unionTypes); + + $property = new Property( + type: $unionType, + title: 'String or Movie', + description: 'A value that can be either a string or a movie', + required: true, + ); + + $serialized = $property->jsonSerialize(); + + $this->assertArrayHasKey('oneOf', $serialized); + $this->assertCount(2, $serialized['oneOf']); + + // Check that oneOf contains the correct types and references + $this->assertEquals(['type' => 'string'], $serialized['oneOf'][0]); + $this->assertEquals(['$ref' => '#/definitions/Movie'], $serialized['oneOf'][1]); + } + + public function testGetDependenciesForUnionType(): void + { + $unionTypes = [ + new ParserType('string', true, false), + new ParserType(Movie::class, false, false), + new ParserType(Actor::class, false, false), + ]; + + $unionType = new UnionType($unionTypes); + + $property = new Property( + type: $unionType, + title: 'String or Object', + required: true, + ); + + $dependencies = $property->getDependencies(); + + $this->assertContains(Movie::class, $dependencies); + $this->assertContains(Actor::class, $dependencies); + $this->assertCount(2, $dependencies); + } +} diff --git a/tests/Unit/SchemaExtendedTest.php b/tests/Unit/SchemaExtendedTest.php new file mode 100644 index 0000000..5ef11f3 --- /dev/null +++ b/tests/Unit/SchemaExtendedTest.php @@ -0,0 +1,81 @@ +setTitle('Test Schema'); + $schema->setDescription('A test schema for unit testing'); + $schema->setId('https://example.com/schemas/test.json'); + $schema->setSchemaVersion('http://json-schema.org/draft-07/schema#'); + + $this->assertEquals([ + 'title' => 'Test Schema', + 'description' => 'A test schema for unit testing', + '$id' => 'https://example.com/schemas/test.json', + '$schema' => 'http://json-schema.org/draft-07/schema#', + ], $schema->jsonSerialize()); + } + + public function testSchemaWithAdditionalProperties(): void + { + $schema = new Schema(); + $schema->setTitle('Test Schema'); + $schema->addAdditionalProperty('additionalProperties', false); + $schema->addAdditionalProperty('maxProperties', 10); + $schema->addAdditionalProperty('examples', [ + ['name' => 'Example', 'value' => 123], + ]); + + $this->assertEquals([ + 'title' => 'Test Schema', + 'additionalProperties' => false, + 'maxProperties' => 10, + 'examples' => [ + ['name' => 'Example', 'value' => 123], + ], + ], $schema->jsonSerialize()); + } + + public function testSchemaWithMetadataAndProperties(): void + { + $schema = new Schema(); + $schema->setTitle('Test Schema'); + $schema->setDescription('A test schema for unit testing'); + $schema->addAdditionalProperty('additionalProperties', false); + + $schema->addProperty( + 'name', + new Schema\Property( + type: Schema\Type::String, + title: 'Name', + description: 'Name of the entity', + required: true, + ), + ); + + $this->assertEquals([ + 'title' => 'Test Schema', + 'description' => 'A test schema for unit testing', + 'additionalProperties' => false, + 'properties' => [ + 'name' => [ + 'title' => 'Name', + 'description' => 'Name of the entity', + 'type' => 'string', + ], + ], + 'required' => [ + 'name', + ], + ], $schema->jsonSerialize()); + } +} diff --git a/tests/Unit/SchemaTest.php b/tests/Unit/SchemaTest.php index a7adf2f..135d22e 100644 --- a/tests/Unit/SchemaTest.php +++ b/tests/Unit/SchemaTest.php @@ -39,7 +39,7 @@ public function testStringProperty(): void 'type' => 'string', ], ], - 'required' => [ + 'required' => [ 'name', ], ], @@ -93,28 +93,28 @@ public function testScalarProperties(): void $this->assertEquals( [ 'properties' => [ - 'name' => [ - 'title' => 'Name', + 'name' => [ + 'title' => 'Name', 'description' => 'Name of the user', - 'type' => 'string', + 'type' => 'string', ], - 'age' => [ - 'title' => 'Age', + 'age' => [ + 'title' => 'Age', 'description' => 'Age of the user', - 'type' => 'integer', + 'type' => 'integer', ], - 'height' => [ - 'title' => 'Height', + 'height' => [ + 'title' => 'Height', 'description' => 'Height of the user', - 'type' => 'number', + 'type' => 'number', ], 'is_active' => [ - 'title' => 'Is Active', + 'title' => 'Is Active', 'description' => 'Is the user active', - 'type' => 'boolean', + 'type' => 'boolean', ], ], - 'required' => [ + 'required' => [ 'name', 'age', 'height', @@ -142,15 +142,15 @@ public function testArrayProperty(): void [ 'properties' => [ 'hobbies' => [ - 'type' => 'array', - 'items' => [ + 'type' => 'array', + 'items' => [ 'type' => 'string', ], - 'title' => 'Hobbies', + 'title' => 'Hobbies', 'description' => 'Hobbies of the user', ], ], - 'required' => [ + 'required' => [ 'hobbies', ], ], @@ -176,18 +176,18 @@ public function testArrayPropertyWithMultipleTypes(): void [ 'properties' => [ 'hobbies' => [ - 'type' => 'array', - 'items' => [ - 'anyOf' => [ + 'type' => 'array', + 'items' => [ + 'oneOf' => [ ['type' => 'string'], ['type' => 'number'], ], ], - 'title' => 'Hobbies', + 'title' => 'Hobbies', 'description' => 'Hobbies of the user', ], ], - 'required' => [ + 'required' => [ 'hobbies', ], ], @@ -213,16 +213,16 @@ public function testMixedProperty(): void [ 'properties' => [ 'hobbies' => [ - 'title' => 'Some value', + 'title' => 'Some value', 'description' => 'Some random user value', - 'anyOf' => [ + 'oneOf' => [ ['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ], ], ], - 'required' => [ + 'required' => [ 'hobbies', ], ], @@ -277,7 +277,7 @@ public function testClassArrayProperty(): void 'properties' => [ 'movie' => [ 'title' => 'Some movie', - 'type' => 'array', + 'type' => 'array', 'items' => [ '$ref' => '#/definitions/Movie', ], @@ -306,9 +306,9 @@ public function testArrayOfClassesAndStrings(): void 'properties' => [ 'movie' => [ 'title' => 'Some movie', - 'type' => 'array', + 'type' => 'array', 'items' => [ - 'anyOf' => [ + 'oneOf' => [ [ '$ref' => '#/definitions/Movie', ], @@ -353,12 +353,12 @@ public function testNestedDefinition(): void $this->assertEquals( [ - 'properties' => [ + 'properties' => [ 'movie' => [ 'title' => 'Some movie', - 'type' => 'array', + 'type' => 'array', 'items' => [ - 'anyOf' => [ + 'oneOf' => [ [ '$ref' => '#/definitions/Movie', ], @@ -371,15 +371,15 @@ public function testNestedDefinition(): void ], 'definitions' => [ 'Movie' => [ - 'type' => 'object', + 'type' => 'object', 'properties' => [ 'title' => [ - 'type' => 'string', - 'title' => 'Title', + 'type' => 'string', + 'title' => 'Title', 'description' => 'Title of the movie', ], ], - 'required' => [ + 'required' => [ 'title', ], ],