diff --git a/src/Type/TypeInfoTypeResolver.php b/src/Type/TypeInfoTypeResolver.php index a14db2218..2bb3d9683 100644 --- a/src/Type/TypeInfoTypeResolver.php +++ b/src/Type/TypeInfoTypeResolver.php @@ -103,7 +103,121 @@ protected function doAugment(Analysis $analysis, OA\Schema $schema, \Reflector $ } } +<<<<<<< HEAD /** +======= + protected function setSchemaType(OA\Schema $schema, Type $type, Analysis $analysis, string $sourceClass = OA\Schema::class): OA\Schema + { + if ($type instanceof CompositeTypeInterface) { + $types = $type->getTypes(); + + $isNonZeroInt = 2 === count($types) && $types[0] instanceof IntRangeType && $types[1] instanceof IntRangeType; + + if ($isNonZeroInt) { + $schema->type = 'int'; + $schema->not = $schema->_context->isVersion('3.0.x') + ? ['enum' => [0]] + : ['const' => 0]; + } else { + $allBuiltin = array_reduce($types, static fn ($carry, $t): bool => $carry && $t instanceof BuiltinType, true); + + if ($type instanceof UnionType) { + if ($allBuiltin) { + $schema->type = array_map(static fn (Type $t): string => (string) $t, $types); + } else { + $builtinTypes = array_filter($types, static fn (Type $t): bool => $t instanceof BuiltinType); + $otherTypes = array_filter($types, static fn (Type $t): bool => !$t instanceof BuiltinType); + + if ($schema->items instanceof OA\Items) { + // nothing more we can do here + return $schema; + } + + $schema->type = Generator::UNDEFINED; + $schema->oneOf = []; + + if ($builtinTypes) { + $schema->oneOf[] = $builtinSchema = new OA\Schema([ + 'type' => array_values(array_map(static fn (Type $t): string => (string) $t, $builtinTypes)), + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $this->type2ref($builtinSchema, $analysis); + $analysis->addAnnotation($builtinSchema, $builtinSchema->_context); + } + + foreach ($otherTypes as $otherType) { + $otherSchema = new OA\Schema([ + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $schema->oneOf[] = $this->setSchemaType($otherSchema, $otherType, $analysis); + $this->type2ref($otherSchema, $analysis); + $analysis->addAnnotation($otherSchema, $otherSchema->_context); + } + } + } elseif ($type instanceof IntersectionType) { + $schema->type = Generator::UNDEFINED; + $schema->allOf = []; + + foreach ($types as $intersectionType) { + $intersectionSchema = new OA\Schema([ + '_context' => new Context(['generated' => true], $schema->_context), + ]); + $schema->allOf[] = $this->setSchemaType($intersectionSchema, $intersectionType, $analysis); + $this->type2ref($intersectionSchema, $analysis); + $analysis->addAnnotation($intersectionSchema, $intersectionSchema->_context); + } + } + } + } else { + if ($type instanceof BuiltinType || $type instanceof ObjectType) { + $schema->type = (string) $type; + } elseif ($type instanceof IntRangeType) { + $schema->type = $type->getTypeIdentifier()->value; + + $schema->minimum = $type->getFrom(); + $schema->maximum = $type->getTo(); + } elseif ($type instanceof ExplicitType) { + $schema->type = $type->getTypeIdentifier()->value; + } elseif ($type instanceof CollectionType) { + if ($type->isList() || $type->getCollectionKeyType() instanceof UnionType) { + // list, array, T[] → ordered list + $schema->type = 'array'; + + if (Generator::isDefault($schema->items)) { + $schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]); + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + $analysis->addAnnotation($schema->items, $schema->items->_context); + } elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) { + $this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->items, $analysis); + } + + $this->mapNativeType($schema->items, $schema->items->type); + } else { + // explicit key type (e.g. array) → map + $schema->type = 'object'; + + if (Generator::isDefault($schema->additionalProperties)) { + $schema->additionalProperties = new OA\AdditionalProperties(['_context' => new Context(['generated' => true], $schema->_context)]); + $this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->additionalProperties, $analysis); + $analysis->addAnnotation($schema->additionalProperties, $schema->additionalProperties->_context); + } elseif (Generator::isDefault($schema->additionalProperties->type, $schema->additionalProperties->oneOf, $schema->additionalProperties->allOf, $schema->additionalProperties->anyOf)) { + $this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis); + $this->type2ref($schema->additionalProperties, $analysis); + } + + $this->mapNativeType($schema->additionalProperties, $schema->additionalProperties->type); + } + } + } + + return $schema; + } + + /**645 1050272 02 1268 0026220 00 +>>>>>>> f66289a (Map `array` to `type: object` + `additionalProperties` in TypeInfoTypeResolver (#2001) (#2003)) * @param \ReflectionParameter|\ReflectionProperty|\ReflectionMethod $reflector */ protected function getReflectionType(\Reflector $reflector): ?Type diff --git a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php index dd3c8a81e..c070ef066 100644 --- a/tests/Fixtures/PHP/DocblockAndTypehintTypes.php +++ b/tests/Fixtures/PHP/DocblockAndTypehintTypes.php @@ -125,6 +125,18 @@ class DocblockAndTypehintTypes #[OAT\Property] public array $arrayShape; + /** + * @var array + */ + #[OAT\Property] + public array $stringMap; + + /** + * @var array + */ + #[OAT\Property] + public array $intKeyedMap; + /** * @var int|string */ diff --git a/tests/Fixtures/TypedProperties.php b/tests/Fixtures/TypedProperties.php index f47304575..b406c96f2 100644 --- a/tests/Fixtures/TypedProperties.php +++ b/tests/Fixtures/TypedProperties.php @@ -123,4 +123,12 @@ class TypedProperties * @OA\Property() */ public array $nativeArray; + + /** + * A map of string to string. + * + * @var array + */ + #[OAT\Property] + public array $stringMap; } diff --git a/tests/Processors/AugmentPropertiesTest.php b/tests/Processors/AugmentPropertiesTest.php index de796eb68..7cee1d95b 100644 --- a/tests/Processors/AugmentPropertiesTest.php +++ b/tests/Processors/AugmentPropertiesTest.php @@ -180,6 +180,7 @@ public function testTypedProperties(): void $staticString, $staticNullableString, $nativeArray, + $stringMap, ] = $analysis->openapi->components->schemas[0]->properties; $this->assertName($stringType, [ @@ -258,6 +259,10 @@ public function testTypedProperties(): void 'property' => Generator::UNDEFINED, 'type' => Generator::UNDEFINED, ]); + $this->assertName($stringMap, [ + 'property' => Generator::UNDEFINED, + 'type' => Generator::UNDEFINED, + ]); $analysis->process($this->initializeProcessors([new AugmentProperties()])); @@ -351,6 +356,13 @@ public function testTypedProperties(): void 'string', $nativeArray->items->type ); + $this->assertName($stringMap, [ + 'property' => 'stringMap', + 'type' => 'object', + ]); + $this->assertFalse(Generator::isDefault($stringMap->additionalProperties)); + $this->assertSame('string', $stringMap->additionalProperties->type); + $this->assertTrue(Generator::isDefault($stringMap->items)); } protected function assertName(OA\Property $property, array $expectedValues): void diff --git a/tests/Type/TypeResolverTest.php b/tests/Type/TypeResolverTest.php index bbffde572..c1bc19eae 100644 --- a/tests/Type/TypeResolverTest.php +++ b/tests/Type/TypeResolverTest.php @@ -29,6 +29,7 @@ public static function resolverAugmentCases(): iterable } $expectations = [ +<<<<<<< HEAD 'nothing' => '{ "property": "nothing" }', 'string' => '{ "type": "string", "property": "string" }', 'nullablestring' => '{ "type": "string", "nullable": true, "property": "nullableString" }', @@ -62,6 +63,108 @@ public static function resolverAugmentCases(): iterable 'intersectionvar' => '{ "property": "intersectionVar" }', 'nestedoneof' => '{ "property": "nestedOneOf" }', 'nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', +======= + OA\OpenApi::VERSION_3_0_0 => [ + 'nothing' => '{ "property": "nothing" }', + 'string' => '{ "type": "string", "property": "string" }', + 'nullablestring' => '{ "type": "string", "nullable": true, "property": "nullableString" }', + 'nullablestringexplicit' => '{ "type": "string", "nullable": false, "property": "nullableStringExplicit" }', + 'nullablestringdocblock' => '{ "type": "string", "nullable": true, "property": "nullableStringDocblock" }', + 'nullablestringnative' => '{ "type": "string", "nullable": true, "property": "nullableStringNative" }', + 'stringarray' => '{ "type": "array", "items": { "type": "string" }, "property": "stringArray" }', + 'stringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "stringList" }', + 'stringlistexplicit' => '{ "type": "array", "items": { "type": "string", "example": "foo" }, "property": "stringListExplicit" }', + 'nullablestringlist' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringList" }', + 'nullablestringlistunion' => '{ "type": "array", "items": { "type": "string" }, "nullable": true, "property": "nullableStringListUnion" }', + 'class' => '{ "$ref": "#/components/schemas/DocblockAndTypehintTypes" }', + 'nullableclass' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } ], "nullable": true, "property": "nullableClass" }', + 'namespacedglobalclass' => '{ "type": "string", "format": "date-time", "property": "namespacedGlobalClass" }', + 'nullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "nullableNamespacedGlobalClass" }', + 'alsonullablenamespacedglobalclass' => '{ "type": "string", "format": "date-time", "nullable": true, "property": "alsoNullableNamespacedGlobalClass" }', + 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', + 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', + 'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }', + 'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }', + 'legacy:stringmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "stringMap" }', + 'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }', + 'legacy:intkeyedmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "intKeyedMap" }', + 'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }', + 'uniontype' => '{ "property": "unionType" }', + 'promotedstring' => '{ "type": "string", "property": "promotedString" }', + 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', + 'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }', + 'getstring' => '{ "type": "string", "property": "getString" }', + 'paramdatetimelist' => '{ "type": "array", "items": { "type": "string", "format": "date-time" }, "property": "paramDateTimeList" }', + 'paramstringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "paramStringList" }', + 'blah' => '{ "type": "string", "example": "My blah", "nullable": true, "property": "blah" }', + 'blah_values' => '{ "type": "array", "items": { "type": "string", "example": "hello" }, "nullable": true, "property": "blah_values" }', + 'oneofvar' => '{ "oneOf": [ { "type": "string" }, { "type": "bool" } ], "property": "oneOfVar" }', + 'oneoflist' => '{ "type": "array", "items": { "oneOf": [ { "type": "string" }, { "type": "bool" } ] }, "property": "oneOfList" }', + 'legacy:nullabletypedlistunion' => '{ "nullable": true, "property": "nullableTypedListUnion" }', + 'type-info:nullabletypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } ], "property": "nullableTypedListUnion" }', + 'legacy:nullablenestedtypedlistunion' => '{ "nullable": true, "property": "nullableNestedTypedListUnion" }', + 'type-info:nullablenestedtypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } } ], "property": "nullableNestedTypedListUnion" }', + 'reflectionvalue' => '{ "example": true, "nullable": true, "property": "reflectionValue" }', + 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', + 'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }', + 'legacy:nestedoneof' => '{ "property": "nestedOneOf" }', + 'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }', + 'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + 'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + ], + OA\OpenApi::VERSION_3_1_0 => [ + 'nothing' => '{ "property": "nothing" }', + 'string' => '{ "type": "string", "property": "string" }', + 'nullablestring' => '{ "type": [ "string", "null" ], "property": "nullableString" }', + 'nullablestringexplicit' => '{ "type": "string", "property": "nullableStringExplicit" }', + 'nullablestringdocblock' => '{ "type": [ "string", "null" ], "property": "nullableStringDocblock" }', + 'nullablestringnative' => '{ "type": [ "string", "null" ], "property": "nullableStringNative" }', + 'stringarray' => '{ "type": "array", "items": { "type": "string" }, "property": "stringArray" }', + 'stringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "stringList" }', + 'stringlistexplicit' => '{ "type": "array", "items": { "type": "string", "example": "foo" }, "property": "stringListExplicit" }', + 'nullablestringlist' => '{ "type": [ "array", "null" ], "items": { "type": "string" }, "property": "nullableStringList" }', + 'nullablestringlistunion' => '{ "type": [ "array", "null" ], "items": { "type": "string" }, "property": "nullableStringListUnion" }', + 'class' => '{ "$ref": "#/components/schemas/DocblockAndTypehintTypes" }', + 'nullableclass' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "null" } ], "property": "nullableClass" }', + 'namespacedglobalclass' => '{ "type": "string", "format": "date-time", "property": "namespacedGlobalClass" }', + 'nullablenamespacedglobalclass' => '{ "type": [ "string", "null" ], "format": "date-time", "property": "nullableNamespacedGlobalClass" }', + 'alsonullablenamespacedglobalclass' => '{ "type": [ "string", "null" ], "format": "date-time", "property": "alsoNullableNamespacedGlobalClass" }', + 'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }', + 'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }', + 'nonzeroint' => '{ "type": "integer", "not": { "const": 0 }, "property": "nonZeroInt" } ', + 'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }', + 'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }', + 'legacy:stringmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "stringMap" }', + 'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }', + 'legacy:intkeyedmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "intKeyedMap" }', + 'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }', + 'legacy:uniontype' => '{ "property": "unionType" }', + 'type-info:uniontype' => '{ "type": [ "integer", "string" ], "property": "unionType" }', + 'promotedstring' => '{ "type": "string", "property": "promotedString" }', + 'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }', + 'type-info:mixedunion' => '{ "example": "My value", "oneOf": [ { "type": [ "string" ] }, { "type": "array", "items": { "type": "mixed" } } ], "property": "mixedUnion" }', + 'getstring' => '{ "type": "string", "property": "getString" }', + 'paramdatetimelist' => '{ "type": "array", "items": { "type": "string", "format": "date-time" }, "property": "paramDateTimeList" }', + 'paramstringlist' => '{ "type": "array", "items": { "type": "string" }, "property": "paramStringList" }', + 'blah' => '{ "type": [ "string", "null" ], "example": "My blah", "property": "blah" }', + 'blah_values' => '{ "type": [ "array", "null" ], "items": { "type": "string", "example": "hello" }, "property": "blah_values" }', + 'oneofvar' => '{ "oneOf": [ { "type": "string" }, { "type": "bool" } ], "property": "oneOfVar" }', + 'oneoflist' => '{ "type": "array", "items": { "oneOf": [ { "type": "string" }, { "type": "bool" } ] }, "property": "oneOfList" }', + 'legacy:nullabletypedlistunion' => '{ "property": "nullableTypedListUnion" }', + 'type-info:nullabletypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "null" } ], "property": "nullableTypedListUnion" }', + 'legacy:nullablenestedtypedlistunion' => '{ "property": "nullableNestedTypedListUnion" }', + 'type-info:nullablenestedtypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } }, { "type": "null" } ], "property": "nullableNestedTypedListUnion" }', + 'legacy:reflectionvalue' => '{ "example": true, "property": "reflectionValue" }', + 'type-info:reflectionvalue' => '{ "type": [ "boolean", "integer", "null" ], "example": true, "property": "reflectionValue" }', + 'legacy:intersectionvar' => '{ "property": "intersectionVar" }', + 'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }', + 'legacy:nestedoneof' => '{ "property": "nestedOneOf" }', + 'type-info:nestedoneof' => '{ "oneOf": [ { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "string" } } ], "property": "nestedOneOf" }', + 'legacy:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + 'type-info:nestedoneofwithitems' => '{ "type": "array", "items": { "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "string" } ] }, "property": "nestedOneOfWithItems" }', + ], +>>>>>>> f66289a (Map `array` to `type: object` + `additionalProperties` in TypeInfoTypeResolver (#2001) (#2003)) ]; $rc = new \ReflectionClass(DocblockAndTypehintTypes::class);