Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ enum-bearing branch without requiring consumers to inspect the union shape:
use Utopia\OpenAPI\Model\CompositeSchema;

if ($schema instanceof CompositeSchema) {
$suggestedValues = $schema->openStringEnumBranch()?->enum ?? [];
$enumBranch = $schema->openStringEnumBranch();
$suggestedValues = $enumBranch?->enum ?? [];
}
```

Expand Down
51 changes: 44 additions & 7 deletions src/Model/CompositeSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

final readonly class CompositeSchema extends Schema
{
/** @var list<Schema> */
public array $schemas;

/** @param list<Schema> $schemas */
public function __construct(
public ?Composition $composition,
public array $schemas = [],
array $schemas = [],
public ?Schema $not = null,
public ?Discriminator $discriminator = null,
?string $title = null,
Expand All @@ -24,6 +27,32 @@ public function __construct(
mixed $example = null,
array $extensions = [],
) {
$openEnumIndex = self::openStringEnumBranchIndex($composition, $schemas);
if ($openEnumIndex !== null) {
/** @var StringSchema $enumBranch */
$enumBranch = $schemas[$openEnumIndex];
$schemas[$openEnumIndex] = new StringSchema(
minLength: $enumBranch->minLength,
maxLength: $enumBranch->maxLength,
pattern: $enumBranch->pattern,
title: $enumBranch->title,
description: $enumBranch->description,
nullable: $enumBranch->nullable,
default: $enumBranch->default,
enum: $enumBranch->enum,
format: $enumBranch->format,
readOnly: $enumBranch->readOnly,
writeOnly: $enumBranch->writeOnly,
deprecated: $enumBranch->deprecated,
example: $enumBranch->example,
extensions: $enumBranch->extensions,
enumName: $enumBranch->enumName,
enumKeys: $enumBranch->enumKeys,
open: true,
);
}
$this->schemas = $schemas;

parent::__construct($title, $description, $nullable, $default, $enum, $format, $readOnly, $writeOnly, $deprecated, $example, $extensions);
}

Expand All @@ -35,14 +64,22 @@ public function __construct(
*/
public function openStringEnumBranch(): ?StringSchema
{
if ($this->composition !== Composition::ANY_OF) {
$index = self::openStringEnumBranchIndex($this->composition, $this->schemas);

return $index === null ? null : $this->schemas[$index];
}

/** @param list<Schema> $schemas */
private static function openStringEnumBranchIndex(?Composition $composition, array $schemas): ?int
{
if ($composition !== Composition::ANY_OF) {
return null;
}

$enumBranch = null;
$enumBranchIndex = null;
$hasOpenBranch = false;

foreach ($this->schemas as $schema) {
foreach ($schemas as $index => $schema) {
if (! $schema instanceof StringSchema) {
return null;
}
Expand All @@ -53,13 +90,13 @@ public function openStringEnumBranch(): ?StringSchema
continue;
}

if ($enumBranch !== null) {
if ($enumBranchIndex !== null) {
return null;
}

$enumBranch = $schema;
$enumBranchIndex = $index;
}

return $hasOpenBranch ? $enumBranch : null;
return $hasOpenBranch ? $enumBranchIndex : null;
}
}
4 changes: 4 additions & 0 deletions src/Model/StringSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

final readonly class StringSchema extends Schema
{
/** @param list<string> $enumKeys */
public function __construct(
public ?int $minLength = null,
public ?int $maxLength = null,
Expand All @@ -21,6 +22,9 @@ public function __construct(
bool $deprecated = false,
mixed $example = null,
array $extensions = [],
public ?string $enumName = null,
public array $enumKeys = [],
public bool $open = false,
) {
parent::__construct($title, $description, $nullable, $default, $enum, $format, $readOnly, $writeOnly, $deprecated, $example, $extensions);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Parser/Schema/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public function read(mixed $raw, string $location): Schema
default: $common['default'], enum: $common['enum'], readOnly: $common['readOnly'],
writeOnly: $common['writeOnly'], deprecated: $common['deprecated'], example: $common['example'],
extensions: $common['extensions'],
enumName: Value::optionalString($data, 'x-enum-name'),
enumKeys: isset($data['x-enum-keys']) ? Value::stringList($data['x-enum-keys'], "{$location}/x-enum-keys") : [],
),
'integer' => $this->integer($data, $location, $common),
'number' => $this->number($data, $location, $common),
Expand Down
13 changes: 13 additions & 0 deletions src/Parser/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ public static function list(mixed $value, string $location): array
return $value;
}

/** @return list<string> */
public static function stringList(mixed $value, string $location): array
{
$values = self::list($value, $location);
foreach ($values as $index => $item) {
if (! is_string($item)) {
throw new InvalidSpecification("Expected string at {$location}/{$index}");
}
}

return $values;
}

/** @param array<string, mixed> $data */
public static function requiredString(array $data, string $key, string $location): string
{
Expand Down
14 changes: 12 additions & 2 deletions tests/Schema/ReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,24 @@ public function test_composition_and_not(): void
public function test_open_string_enum_branch_is_exposed_regardless_of_branch_order(): void
{
$reader = $this->reader(Version::V3_0);
$enum = ['type' => 'string', 'enum' => ['network.requests', 'network.inbound']];
$enum = [
'type' => 'string',
'enum' => ['network.requests', 'network.inbound'],
'x-enum-name' => 'UsageEventMetric',
'x-enum-keys' => ['NetworkRequests', 'NetworkInbound'],
];
$open = ['type' => 'string'];

foreach ([[$enum, $open], [$open, $enum]] as $branches) {
$schema = $reader->read(['anyOf' => $branches], '#/x');

self::assertInstanceOf(CompositeSchema::class, $schema);
self::assertSame(['network.requests', 'network.inbound'], $schema->openStringEnumBranch()?->enum);
$enumBranch = $schema->openStringEnumBranch();
self::assertSame(['network.requests', 'network.inbound'], $enumBranch?->enum);
self::assertSame('UsageEventMetric', $enumBranch?->enumName);
self::assertSame(['NetworkRequests', 'NetworkInbound'], $enumBranch?->enumKeys);
self::assertTrue($enumBranch?->open);
self::assertFalse($schema->schemas[$enumBranch === $schema->schemas[0] ? 1 : 0]->open);
}
}

Expand Down
9 changes: 9 additions & 0 deletions tests/ValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ public function test_list_rejects_maps_and_scalars(): void
}
}

public function test_string_list_rejects_non_string_items(): void
{
self::assertSame(['first', 'second'], Value::stringList(['first', 'second'], '#/x-enum-keys'));

$this->expectException(InvalidSpecification::class);
$this->expectExceptionMessage('Expected string at #/x-enum-keys/1');
Value::stringList(['first', 2], '#/x-enum-keys');
}

public function test_required_string_names_the_missing_key(): void
{
self::assertSame('Pets', Value::requiredString(['title' => 'Pets'], 'title', '#/info'));
Expand Down
Loading