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
21 changes: 16 additions & 5 deletions src/Model/CompositeSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function __construct(
mixed $example = null,
array $extensions = [],
) {
$openEnumIndex = self::openStringEnumBranchIndex($composition, $schemas);
$openEnumIndex = self::openStringEnumBranchIndex($composition, $schemas, $not, $enum);
if ($openEnumIndex !== null) {
/** @var StringSchema $enumBranch */
$enumBranch = $schemas[$openEnumIndex];
Expand Down Expand Up @@ -64,15 +64,18 @@ enumKeys: $enumBranch->enumKeys,
*/
public function openStringEnumBranch(): ?StringSchema
{
$index = self::openStringEnumBranchIndex($this->composition, $this->schemas);
$index = self::openStringEnumBranchIndex($this->composition, $this->schemas, $this->not, $this->enum);

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

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

Expand All @@ -85,6 +88,14 @@ private static function openStringEnumBranchIndex(?Composition $composition, arr
}

if ($schema->enum === []) {
if (
$schema->minLength !== null
|| $schema->maxLength !== null
|| $schema->pattern !== null
|| $schema->format !== null
) {
return null;
}
$hasOpenBranch = true;

continue;
Expand Down
22 changes: 21 additions & 1 deletion src/Parser/Schema/Reader.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'type', 'format', 'items', 'default', 'enum', 'maximum', 'exclusiveMaximum',
'minimum', 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern',
'maxItems', 'minItems', 'uniqueItems', 'multipleOf', 'description',
'x-enum-name', 'x-enum-keys',
];

public function __construct(private Dialect $dialect) {}
Expand Down Expand Up @@ -117,7 +118,7 @@ public function read(mixed $raw, string $location): Schema
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") : [],
enumKeys: $this->enumKeys($data, $location, $common['enum']),
),
'integer' => $this->integer($data, $location, $common),
'number' => $this->number($data, $location, $common),
Expand Down Expand Up @@ -149,6 +150,25 @@ public function readParameterFields(array $data, string $location): Schema
return $this->read(array_intersect_key($data, array_flip(self::PARAMETER_FIELDS)), $location);
}

/**
* @param array<string, mixed> $data
* @param list<mixed> $enum
* @return list<string>
*/
private function enumKeys(array $data, string $location, array $enum): array
{
if (! isset($data['x-enum-keys'])) {
return [];
}

$keys = Value::stringList($data['x-enum-keys'], "{$location}/x-enum-keys");
if (count($keys) !== count($enum)) {
throw new InvalidSpecification("Expected x-enum-keys to match enum length at {$location}");
}

return $keys;
}

/**
* @param array<string, mixed> $data
* @return array<string, mixed>
Expand Down
52 changes: 52 additions & 0 deletions tests/Schema/ReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,32 @@ public function test_open_string_enum_branch_is_exposed_regardless_of_branch_ord
}
}

public function test_enum_metadata_is_preserved_for_openapi_two_inline_parameters(): void
{
$schema = $this->reader(Version::V2)->readParameterFields([
'type' => 'string',
'enum' => ['network.requests'],
'x-enum-name' => 'UsageEventMetric',
'x-enum-keys' => ['NetworkRequests'],
], '#/parameters/metric');

self::assertInstanceOf(StringSchema::class, $schema);
self::assertSame('UsageEventMetric', $schema->enumName);
self::assertSame(['NetworkRequests'], $schema->enumKeys);
}

public function test_enum_keys_must_match_enum_length(): void
{
$this->expectException(InvalidSpecification::class);
$this->expectExceptionMessage('Expected x-enum-keys to match enum length at #/x');

$this->reader(Version::V3_0)->read([
'type' => 'string',
'enum' => ['first', 'second'],
'x-enum-keys' => ['First'],
], '#/x');
}

public function test_open_string_enum_requires_any_of(): void
{
$reader = $this->reader(Version::V3_0);
Expand Down Expand Up @@ -175,6 +201,32 @@ public function test_open_string_enum_requires_one_enum_and_an_open_string_branc
}
}

public function test_open_string_enum_requires_an_unrestricted_string_branch(): void
{
$reader = $this->reader(Version::V3_0);
$enum = ['type' => 'string', 'enum' => ['known']];
$constraints = [
['minLength' => 1],
['maxLength' => 10],
['pattern' => '^known$'],
['format' => 'uuid'],
];

foreach ($constraints as $constraint) {
$schema = $reader->read(['anyOf' => [$enum, ['type' => 'string', ...$constraint]]], '#/x');

self::assertInstanceOf(CompositeSchema::class, $schema);
self::assertNull($schema->openStringEnumBranch());
}

foreach ([['enum' => ['known']], ['not' => ['type' => 'string', 'enum' => ['blocked']]]] as $constraint) {
$schema = $reader->read(['anyOf' => [$enum, ['type' => 'string']], ...$constraint], '#/x');

self::assertInstanceOf(CompositeSchema::class, $schema);
self::assertNull($schema->openStringEnumBranch());
}
}

public function test_discriminator_is_read_from_both_the_string_and_object_forms(): void
{
$reader = $this->reader(Version::V3_0);
Expand Down
Loading