Skip to content

Commit 75f8f8f

Browse files
committed
fix(OpenApiType): Prevent making misktakes with empty JSON objects
Signed-off-by: provokateurin <[email protected]>
1 parent bb21dad commit 75f8f8f

File tree

1 file changed

+23
-19
lines changed

1 file changed

+23
-19
lines changed

src/OpenApiType.php

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ enum: [0, 1],
113113
if ($this->description !== null && $this->description !== '' && !$isParameter) {
114114
$values['description'] = Helpers::cleanDocComment($this->description);
115115
}
116-
if ($this->items instanceof \OpenAPIExtractor\OpenApiType) {
116+
if ($this->items instanceof OpenApiType) {
117117
$values['items'] = $this->items->toArray();
118118
}
119119
if ($this->minLength !== null) {
@@ -139,7 +139,7 @@ enum: [0, 1],
139139
}
140140
if ($this->properties !== null && $this->properties !== []) {
141141
$values['properties'] = array_combine(array_keys($this->properties),
142-
array_map(static fn (OpenApiType $property): array|\stdClass => $property->toArray(), array_values($this->properties)),
142+
array_map(static fn(OpenApiType $property): array|stdClass => $property->toArray(), array_values($this->properties)),
143143
);
144144
}
145145
if ($this->additionalProperties !== null) {
@@ -150,13 +150,13 @@ enum: [0, 1],
150150
}
151151
}
152152
if ($this->oneOf !== null) {
153-
$values['oneOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->oneOf);
153+
$values['oneOf'] = array_map(fn(OpenApiType $type): array|stdClass => $type->toArray(), $this->oneOf);
154154
}
155155
if ($this->anyOf !== null) {
156-
$values['anyOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->anyOf);
156+
$values['anyOf'] = array_map(fn(OpenApiType $type): array|stdClass => $type->toArray(), $this->anyOf);
157157
}
158158
if ($this->allOf !== null) {
159-
$values['allOf'] = array_map(fn (OpenApiType $type): array|\stdClass => $type->toArray(), $this->allOf);
159+
$values['allOf'] = array_map(fn(OpenApiType $type): array|stdClass => $type->toArray(), $this->allOf);
160160
}
161161

162162
return $values !== [] ? $values : new stdClass();
@@ -184,9 +184,9 @@ public static function resolve(string $context, array $definitions, ParamTagValu
184184
items: self::resolve($context . ': items', $definitions, $node->type),
185185
);
186186
}
187-
if ($node instanceof GenericTypeNode && ($node->type->name === 'array' || $node->type->name === 'list' || $node->type->name === 'non-empty-list') && count($node->genericTypes) === 1) {
188-
if ($node->type->name === 'array') {
189-
Logger::error($context, "The 'array<TYPE>' syntax for arrays is forbidden due to ambiguities. Use 'list<TYPE>' for JSON arrays or 'array<string, TYPE>' for JSON objects instead.");
187+
if ($node instanceof GenericTypeNode && ($node->type->name === 'array' || $node->type->name === 'non-empty-array' || $node->type->name === 'list' || $node->type->name === 'non-empty-list') && count($node->genericTypes) === 1) {
188+
if ($node->type->name === 'array' || $node->type->name === 'non-empty-array') {
189+
Logger::error($context, "The 'array<TYPE>' and 'non-empty-array<TYPE>' syntax for arrays is forbidden due to ambiguities. Use 'list<TYPE>' for JSON arrays or 'non-empty-array<string, TYPE>' for JSON objects instead.");
190190
}
191191

192192
if ($node->genericTypes[0] instanceof IdentifierTypeNode && $node->genericTypes[0]->name === 'empty') {
@@ -213,7 +213,7 @@ public static function resolve(string $context, array $definitions, ParamTagValu
213213
foreach ($node->items as $item) {
214214
$name = $item->keyName instanceof ConstExprStringNode ? $item->keyName->value : $item->keyName->name;
215215
$type = self::resolve($context . ': ' . $name, $definitions, $item->valueType);
216-
$comments = array_map(static fn (Comment $comment): ?string => preg_replace('/^\/\/\s*/', '', $comment->text), $item->keyName->getAttribute(Attribute::COMMENTS) ?? []);
216+
$comments = array_map(static fn(Comment $comment): ?string => preg_replace('/^\/\/\s*/', '', $comment->text), $item->keyName->getAttribute(Attribute::COMMENTS) ?? []);
217217
if ($comments !== []) {
218218
$type->description = implode("\n", $comments);
219219
}
@@ -232,7 +232,11 @@ public static function resolve(string $context, array $definitions, ParamTagValu
232232
);
233233
}
234234

235-
if ($node instanceof GenericTypeNode && $node->type->name === 'array' && count($node->genericTypes) === 2 && $node->genericTypes[0] instanceof IdentifierTypeNode) {
235+
if ($node instanceof GenericTypeNode && in_array($node->type->name, ['array', 'non-empty-array']) && count($node->genericTypes) === 2 && $node->genericTypes[0] instanceof IdentifierTypeNode) {
236+
if ($node->type->name !== 'non-empty-array') {
237+
Logger::error($context, 'You must ensure JSON objects are not empty using the "non-empty-array" type. To allow return empty JSON objects your code must manually check if the array is empty in order to return "new \\stdClass()" and use "non-empty-array|\\stdClass" as the type.');
238+
}
239+
236240
$allowedTypes = ['string', 'lowercase-string', 'non-empty-string', 'non-empty-lowercase-string'];
237241
if (in_array($node->genericTypes[0]->name, $allowedTypes, true)) {
238242
return new OpenApiType(
@@ -271,14 +275,14 @@ public static function resolve(string $context, array $definitions, ParamTagValu
271275

272276
$isUnion = $node instanceof UnionTypeNode || $node instanceof UnionType;
273277
$isIntersection = $node instanceof IntersectionTypeNode || $node instanceof IntersectionType;
274-
if ($isUnion && count($node->types) === count(array_filter($node->types, fn ($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprStringNode))) {
278+
if ($isUnion && count($node->types) === count(array_filter($node->types, fn($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprStringNode))) {
275279
$values = [];
276280
/** @var ConstTypeNode $type */
277281
foreach ($node->types as $type) {
278282
$values[] = $type->constExpr->value;
279283
}
280284

281-
if (array_filter($values, fn (string $value): bool => $value === '') !== []) {
285+
if (array_filter($values, fn(string $value): bool => $value === '') !== []) {
282286
// Not a valid enum
283287
return new OpenApiType(
284288
context: $context,
@@ -292,14 +296,14 @@ public static function resolve(string $context, array $definitions, ParamTagValu
292296
enum: $values,
293297
);
294298
}
295-
if ($isUnion && count($node->types) === count(array_filter($node->types, fn ($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprIntegerNode))) {
299+
if ($isUnion && count($node->types) === count(array_filter($node->types, fn($type): bool => $type instanceof ConstTypeNode && $type->constExpr instanceof ConstExprIntegerNode))) {
296300
$values = [];
297301
/** @var ConstTypeNode $type */
298302
foreach ($node->types as $type) {
299303
$values[] = (int)$type->constExpr->value;
300304
}
301305

302-
if (array_filter($values, fn (string $value): bool => $value === '') !== []) {
306+
if (array_filter($values, fn(string $value): bool => $value === '') !== []) {
303307
// Not a valid enum
304308
return new OpenApiType(
305309
context: $context,
@@ -355,7 +359,7 @@ enum: $values,
355359
return $item->type;
356360
}, $items);
357361

358-
if (array_filter($itemTypes, static fn (?string $type): bool => $type === null) !== [] || count($itemTypes) !== count(array_unique($itemTypes))) {
362+
if (array_filter($itemTypes, static fn(?string $type): bool => $type === null) !== [] || count($itemTypes) !== count(array_unique($itemTypes))) {
359363
return new OpenApiType(
360364
context: $context,
361365
nullable: $nullable,
@@ -422,21 +426,21 @@ private static function mergeEnums(string $context, array $types): array {
422426
}
423427
}
424428

425-
foreach (array_map(static fn (OpenApiType $type): ?string => $type->type, $nonEnums) as $type) {
429+
foreach (array_map(static fn(OpenApiType $type): ?string => $type->type, $nonEnums) as $type) {
426430
if (array_key_exists($type, $enums)) {
427431
unset($enums[$type]);
428432
}
429433
}
430434

431-
return array_merge($nonEnums, array_map(static fn (string $type): \OpenAPIExtractor\OpenApiType => new OpenApiType(
435+
return array_merge($nonEnums, array_map(static fn(string $type): OpenApiType => new OpenApiType(
432436
context: $context,
433437
type: $type, enum: $enums[$type],
434438
), array_keys($enums)));
435439
}
436440

437441
private static function resolveIdentifier(string $context, array $definitions, string $name): OpenApiType {
438442
if ($name === 'array') {
439-
Logger::error($context, "Instead of 'array' use:\n'new stdClass()' for empty objects\n'array<string, mixed>' for non-empty objects\n'array<emtpy>' for empty lists\n'array<YourTypeHere>' for lists");
443+
Logger::error($context, "Instead of 'array' use:\n'new stdClass()' for empty objects\n'non-empty-array<string, mixed>' for non-empty objects\n'list<emtpy>' for empty lists\n'list<YourTypeHere>' for lists");
440444
}
441445
if (str_starts_with($name, '\\')) {
442446
$name = substr($name, 1);
@@ -455,7 +459,7 @@ private static function resolveIdentifier(string $context, array $definitions, s
455459
'numeric' => new OpenApiType(context: $context, type: 'number'),
456460
// https://www.php.net/manual/en/language.types.float.php: Both float and double are always stored with double precision
457461
'float', 'double' => new OpenApiType(context: $context, type: 'number', format: 'double'),
458-
'mixed', 'empty', 'array' => new OpenApiType(context: $context, type: 'object'),
462+
'mixed', 'empty' => new OpenApiType(context: $context, type: 'object'),
459463
'object', 'stdClass' => new OpenApiType(context: $context, type: 'object', additionalProperties: true),
460464
'null' => new OpenApiType(context: $context, nullable: true),
461465
default => (function () use ($context, $definitions, $name) {

0 commit comments

Comments
 (0)