Skip to content

Commit a836231

Browse files
authored
Merge pull request #33 from studio-design/refactor/schema-converter-in-place
refactor: reduce array copies in OpenApiSchemaConverter
2 parents 953abde + f6bbd36 commit a836231

2 files changed

Lines changed: 89 additions & 46 deletions

File tree

src/OpenApiSchemaConverter.php

Lines changed: 33 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Studio\OpenApiContractTesting;
66

77
use function array_is_list;
8-
use function array_map;
98
use function is_array;
109
use function is_string;
1110

@@ -44,92 +43,92 @@ final class OpenApiSchemaConverter
4443
*/
4544
public static function convert(array $schema, OpenApiVersion $version = OpenApiVersion::V3_0): array
4645
{
47-
return self::convertRecursive($schema, $version);
46+
self::convertInPlace($schema, $version);
47+
48+
return $schema;
4849
}
4950

5051
/**
5152
* @param array<string, mixed> $schema
52-
*
53-
* @return array<string, mixed>
5453
*/
55-
private static function convertRecursive(array $schema, OpenApiVersion $version): array
54+
private static function convertInPlace(array &$schema, OpenApiVersion $version): void
5655
{
5756
if ($version === OpenApiVersion::V3_0) {
58-
$schema = self::handleNullable($schema);
59-
$schema = self::removeKeys($schema, self::OPENAPI_3_0_KEYS);
57+
self::handleNullable($schema);
58+
self::removeKeys($schema, self::OPENAPI_3_0_KEYS);
6059
} else {
61-
$schema = self::handlePrefixItems($schema);
62-
$schema = self::removeKeys($schema, self::DRAFT_2020_12_KEYS);
60+
self::handlePrefixItems($schema);
61+
self::removeKeys($schema, self::DRAFT_2020_12_KEYS);
6362
}
6463

65-
$schema = self::removeKeys($schema, self::OPENAPI_COMMON_KEYS);
64+
self::removeKeys($schema, self::OPENAPI_COMMON_KEYS);
6665

6766
if (isset($schema['properties']) && is_array($schema['properties'])) {
68-
foreach ($schema['properties'] as $key => $property) {
67+
foreach ($schema['properties'] as &$property) {
6968
if (is_array($property)) {
70-
$schema['properties'][$key] = self::convertRecursive($property, $version);
69+
self::convertInPlace($property, $version);
7170
}
7271
}
72+
unset($property);
7373
}
7474

7575
if (isset($schema['items']) && is_array($schema['items'])) {
76-
// items can be an array (tuple from prefixItems conversion) or an object schema
7776
if (array_is_list($schema['items'])) {
78-
$schema['items'] = array_map(
79-
static fn(mixed $item): mixed => is_array($item) ? self::convertRecursive($item, $version) : $item,
80-
$schema['items'],
81-
);
77+
foreach ($schema['items'] as &$item) {
78+
if (is_array($item)) {
79+
self::convertInPlace($item, $version);
80+
}
81+
}
82+
unset($item);
8283
} else {
83-
$schema['items'] = self::convertRecursive($schema['items'], $version);
84+
self::convertInPlace($schema['items'], $version);
8485
}
8586
}
8687

8788
foreach (['allOf', 'oneOf', 'anyOf'] as $combiner) {
8889
if (isset($schema[$combiner]) && is_array($schema[$combiner])) {
89-
$schema[$combiner] = array_map(
90-
static fn(mixed $item): mixed => is_array($item) ? self::convertRecursive($item, $version) : $item,
91-
$schema[$combiner],
92-
);
90+
foreach ($schema[$combiner] as &$item) {
91+
if (is_array($item)) {
92+
self::convertInPlace($item, $version);
93+
}
94+
}
95+
unset($item);
9396
}
9497
}
9598

9699
if (isset($schema['additionalProperties']) && is_array($schema['additionalProperties'])) {
97-
$schema['additionalProperties'] = self::convertRecursive($schema['additionalProperties'], $version);
100+
self::convertInPlace($schema['additionalProperties'], $version);
98101
}
99102

100103
if (isset($schema['not']) && is_array($schema['not'])) {
101-
$schema['not'] = self::convertRecursive($schema['not'], $version);
104+
self::convertInPlace($schema['not'], $version);
102105
}
103-
104-
return $schema;
105106
}
106107

107108
/**
108109
* Convert OpenAPI 3.0 nullable to JSON Schema compatible type.
109110
*
110111
* @param array<string, mixed> $schema
111-
*
112-
* @return array<string, mixed>
113112
*/
114-
private static function handleNullable(array $schema): array
113+
private static function handleNullable(array &$schema): void
115114
{
116115
if (!isset($schema['nullable']) || $schema['nullable'] !== true) {
117-
return $schema;
116+
return;
118117
}
119118

120119
unset($schema['nullable']);
121120

122121
if (isset($schema['type']) && is_string($schema['type'])) {
123122
$schema['type'] = [$schema['type'], 'null'];
124123

125-
return $schema;
124+
return;
126125
}
127126

128127
foreach (['oneOf', 'anyOf'] as $combiner) {
129128
if (isset($schema[$combiner]) && is_array($schema[$combiner])) {
130129
$schema[$combiner][] = ['type' => 'null'];
131130

132-
return $schema;
131+
return;
133132
}
134133
}
135134

@@ -140,42 +139,30 @@ private static function handleNullable(array $schema): array
140139
['allOf' => $allOf],
141140
['type' => 'null'],
142141
];
143-
144-
return $schema;
145142
}
146-
147-
return $schema;
148143
}
149144

150145
/**
151146
* Convert Draft 2020-12 prefixItems to Draft 07 items array (tuple validation).
152147
*
153148
* @param array<string, mixed> $schema
154-
*
155-
* @return array<string, mixed>
156149
*/
157-
private static function handlePrefixItems(array $schema): array
150+
private static function handlePrefixItems(array &$schema): void
158151
{
159152
if (isset($schema['prefixItems']) && is_array($schema['prefixItems'])) {
160153
$schema['items'] = $schema['prefixItems'];
161154
unset($schema['prefixItems']);
162155
}
163-
164-
return $schema;
165156
}
166157

167158
/**
168159
* @param array<string, mixed> $schema
169160
* @param string[] $keys
170-
*
171-
* @return array<string, mixed>
172161
*/
173-
private static function removeKeys(array $schema, array $keys): array
162+
private static function removeKeys(array &$schema, array $keys): void
174163
{
175164
foreach ($keys as $key) {
176165
unset($schema[$key]);
177166
}
178-
179-
return $schema;
180167
}
181168
}

tests/Unit/OpenApiSchemaConverterTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,60 @@ public function v31_read_only_write_only_preserved(): void
308308
$this->assertTrue($result['readOnly']);
309309
$this->assertFalse($result['writeOnly']);
310310
}
311+
312+
// ========================================
313+
// Input immutability tests
314+
// ========================================
315+
316+
#[Test]
317+
public function convert_does_not_mutate_input_schema(): void
318+
{
319+
$schema = [
320+
'type' => 'object',
321+
'nullable' => true,
322+
'example' => 'test',
323+
'properties' => [
324+
'name' => [
325+
'type' => 'string',
326+
'nullable' => true,
327+
'example' => 'John',
328+
],
329+
'tags' => [
330+
'type' => 'array',
331+
'items' => [
332+
'type' => 'string',
333+
'deprecated' => true,
334+
],
335+
],
336+
],
337+
];
338+
$original = $schema;
339+
340+
OpenApiSchemaConverter::convert($schema, OpenApiVersion::V3_0);
341+
342+
$this->assertSame($original, $schema);
343+
}
344+
345+
#[Test]
346+
public function convert_does_not_mutate_input_schema_v31(): void
347+
{
348+
$schema = [
349+
'type' => 'object',
350+
'prefixItems' => [
351+
['type' => 'string'],
352+
],
353+
'examples' => [['key' => 'value']],
354+
'properties' => [
355+
'data' => [
356+
'type' => 'object',
357+
'$dynamicRef' => '#meta',
358+
],
359+
],
360+
];
361+
$original = $schema;
362+
363+
OpenApiSchemaConverter::convert($schema, OpenApiVersion::V3_1);
364+
365+
$this->assertSame($original, $schema);
366+
}
311367
}

0 commit comments

Comments
 (0)