Skip to content

Commit f66289a

Browse files
authored
Map array<K, V> to type: object + additionalProperties in TypeInfoTypeResolver (#2001) (#2003)
1 parent 6f1dd23 commit f66289a

5 files changed

Lines changed: 76 additions & 16 deletions

File tree

src/Type/TypeInfoTypeResolver.php

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -148,19 +148,37 @@ protected function setSchemaType(OA\Schema $schema, Type $type, Analysis $analys
148148
} elseif ($type instanceof ExplicitType) {
149149
$schema->type = $type->getTypeIdentifier()->value;
150150
} elseif ($type instanceof CollectionType) {
151-
$schema->type = 'array';
152-
153-
if (Generator::isDefault($schema->items)) {
154-
$schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]);
155-
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
156-
$this->type2ref($schema->items, $analysis);
157-
$analysis->addAnnotation($schema->items, $schema->items->_context);
158-
} elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) {
159-
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
160-
$this->type2ref($schema->items, $analysis);
161-
}
151+
if ($type->isList() || $type->getCollectionKeyType() instanceof UnionType) {
152+
// list<T>, array<T>, T[] → ordered list
153+
$schema->type = 'array';
154+
155+
if (Generator::isDefault($schema->items)) {
156+
$schema->items = new OA\Items(['_context' => new Context(['generated' => true], $schema->_context)]);
157+
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
158+
$this->type2ref($schema->items, $analysis);
159+
$analysis->addAnnotation($schema->items, $schema->items->_context);
160+
} elseif (Generator::isDefault($schema->items->type, $schema->items->oneOf, $schema->items->allOf, $schema->items->anyOf)) {
161+
$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
162+
$this->type2ref($schema->items, $analysis);
163+
}
162164

163-
$this->mapNativeType($schema->items, $schema->items->type);
165+
$this->mapNativeType($schema->items, $schema->items->type);
166+
} else {
167+
// explicit key type (e.g. array<string, string>) → map
168+
$schema->type = 'object';
169+
170+
if (Generator::isDefault($schema->additionalProperties)) {
171+
$schema->additionalProperties = new OA\AdditionalProperties(['_context' => new Context(['generated' => true], $schema->_context)]);
172+
$this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis);
173+
$this->type2ref($schema->additionalProperties, $analysis);
174+
$analysis->addAnnotation($schema->additionalProperties, $schema->additionalProperties->_context);
175+
} elseif (Generator::isDefault($schema->additionalProperties->type, $schema->additionalProperties->oneOf, $schema->additionalProperties->allOf, $schema->additionalProperties->anyOf)) {
176+
$this->setSchemaType($schema->additionalProperties, $type->getCollectionValueType(), $analysis);
177+
$this->type2ref($schema->additionalProperties, $analysis);
178+
}
179+
180+
$this->mapNativeType($schema->additionalProperties, $schema->additionalProperties->type);
181+
}
164182
}
165183
}
166184

tests/Fixtures/PHP/DocblockAndTypehintTypes.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ class DocblockAndTypehintTypes
125125
#[OAT\Property]
126126
public array $arrayShape;
127127

128+
/**
129+
* @var array<string, string>
130+
*/
131+
#[OAT\Property]
132+
public array $stringMap;
133+
134+
/**
135+
* @var array<int, string>
136+
*/
137+
#[OAT\Property]
138+
public array $intKeyedMap;
139+
128140
/**
129141
* @var int|string
130142
*/

tests/Fixtures/TypedProperties.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,12 @@ class TypedProperties
8282
*/
8383
#[OAT\Property]
8484
public array $nativeArray;
85+
86+
/**
87+
* A map of string to string.
88+
*
89+
* @var array<string, string>
90+
*/
91+
#[OAT\Property]
92+
public array $stringMap;
8593
}

tests/Processors/AugmentPropertiesTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public function testTypedProperties(): void
177177
$staticString,
178178
$staticNullableString,
179179
$nativeArray,
180+
$stringMap,
180181
] = $analysis->openapi->components->schemas[0]->properties;
181182

182183
$this->assertName($stringType, [
@@ -255,6 +256,10 @@ public function testTypedProperties(): void
255256
'property' => Generator::UNDEFINED,
256257
'type' => Generator::UNDEFINED,
257258
]);
259+
$this->assertName($stringMap, [
260+
'property' => Generator::UNDEFINED,
261+
'type' => Generator::UNDEFINED,
262+
]);
258263

259264
$this->processorPipeline([new AugmentProperties()])->process($analysis);
260265

@@ -348,6 +353,13 @@ public function testTypedProperties(): void
348353
'string',
349354
$nativeArray->items->type
350355
);
356+
$this->assertName($stringMap, [
357+
'property' => 'stringMap',
358+
'type' => 'object',
359+
]);
360+
$this->assertFalse(Generator::isDefault($stringMap->additionalProperties));
361+
$this->assertSame('string', $stringMap->additionalProperties->type);
362+
$this->assertTrue(Generator::isDefault($stringMap->items));
351363
}
352364

353365
public function testComplexVarTypeDescription(): void

tests/Type/TypeResolverTest.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ public static function resolverAugmentCases(): iterable
4343
'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }',
4444
'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }',
4545
'nonzeroint' => '{ "type": "integer", "not": { "enum": [ 0 ] }, "property": "nonZeroInt" }',
46-
'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
46+
'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
47+
'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }',
48+
'legacy:stringmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "stringMap" }',
49+
'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }',
50+
'legacy:intkeyedmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "intKeyedMap" }',
51+
'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }',
4752
'uniontype' => '{ "property": "unionType" }',
4853
'promotedstring' => '{ "type": "string", "property": "promotedString" }',
4954
'legacy:mixedunion' => '{ "example": "My value", "property": "mixedUnion" }',
@@ -58,7 +63,7 @@ public static function resolverAugmentCases(): iterable
5863
'legacy:nullabletypedlistunion' => '{ "nullable": true, "property": "nullableTypedListUnion" }',
5964
'type-info:nullabletypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } ], "property": "nullableTypedListUnion" }',
6065
'legacy:nullablenestedtypedlistunion' => '{ "nullable": true, "property": "nullableNestedTypedListUnion" }',
61-
'type-info:nullablenestedtypedlistunion' => '{ "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } } ], "property": "nullableNestedTypedListUnion" }',
66+
'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" }',
6267
'reflectionvalue' => '{ "example": true, "nullable": true, "property": "reflectionValue" }',
6368
'legacy:intersectionvar' => '{ "property": "intersectionVar" }',
6469
'type-info:intersectionvar' => '{ "allOf": [ { "$ref": "#/components/schemas/FirstInterface" }, { "$ref": "#/components/schemas/SecondInterface" } ], "property": "intersectionVar" }',
@@ -87,7 +92,12 @@ public static function resolverAugmentCases(): iterable
8792
'intrange' => '{ "type": "integer", "maximum": 10, "minimum": -9223372036854775808, "property": "intRange" }',
8893
'positiveint' => '{ "type": "integer", "maximum": 9223372036854775807, "minimum": 1, "property": "positiveInt" }',
8994
'nonzeroint' => '{ "type": "integer", "not": { "const": 0 }, "property": "nonZeroInt" } ',
90-
'arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
95+
'legacy:arrayshape' => '{ "type": "array", "items": { "type": "boolean" }, "property": "arrayShape" }',
96+
'type-info:arrayshape' => '{ "type": "object", "additionalProperties": { "type": "boolean" }, "property": "arrayShape" }',
97+
'legacy:stringmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "stringMap" }',
98+
'type-info:stringmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "stringMap" }',
99+
'legacy:intkeyedmap' => '{ "type": "array", "items": { "type": "mixed" }, "property": "intKeyedMap" }',
100+
'type-info:intkeyedmap' => '{ "type": "object", "additionalProperties": { "type": "string" }, "property": "intKeyedMap" }',
91101
'legacy:uniontype' => '{ "property": "unionType" }',
92102
'type-info:uniontype' => '{ "type": [ "integer", "string" ], "property": "unionType" }',
93103
'promotedstring' => '{ "type": "string", "property": "promotedString" }',
@@ -103,7 +113,7 @@ public static function resolverAugmentCases(): iterable
103113
'legacy:nullabletypedlistunion' => '{ "property": "nullableTypedListUnion" }',
104114
'type-info:nullabletypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "null" } ], "property": "nullableTypedListUnion" }',
105115
'legacy:nullablenestedtypedlistunion' => '{ "property": "nullableNestedTypedListUnion" }',
106-
'type-info:nullablenestedtypedlistunion' => '{ "oneOf": [ { "$ref": "#/components/schemas/DocblockAndTypehintTypes" }, { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } }, { "type": "array", "items": { "type": "array", "items": { "$ref": "#/components/schemas/DocblockAndTypehintTypes" } } }, { "type": "null" } ], "property": "nullableNestedTypedListUnion" }',
116+
'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" }',
107117
'legacy:reflectionvalue' => '{ "example": true, "property": "reflectionValue" }',
108118
'type-info:reflectionvalue' => '{ "type": [ "boolean", "integer", "null" ], "example": true, "property": "reflectionValue" }',
109119
'legacy:intersectionvar' => '{ "property": "intersectionVar" }',

0 commit comments

Comments
 (0)