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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,21 @@ if ($schema instanceof ReferenceSchema) {

This makes valid recursive schemas safe to parse.

An `anyOf` that combines one string enum with another string branch documents
suggested values without closing the set. The canonical composite exposes the
enum-bearing branch without requiring consumers to inspect the union shape:

```php
use Utopia\OpenAPI\Model\CompositeSchema;

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

`oneOf`, unions with non-string branches, and unions with multiple enum branches
are not treated as open string enums.

### Parameters and request bodies

Path-level parameters are inherited by operations. An operation-level parameter with the same case-sensitive `name` and `in` value replaces the inherited parameter.
Expand Down
36 changes: 36 additions & 0 deletions src/Model/CompositeSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,40 @@ public function __construct(
) {
parent::__construct($title, $description, $nullable, $default, $enum, $format, $readOnly, $writeOnly, $deprecated, $example, $extensions);
}

/**
* Return the documented values from an open string enum.
*
* An open string enum uses anyOf to combine one string enum with one or
* more string branches that accept values outside the documented set.
*/
public function openStringEnumBranch(): ?StringSchema
{
if ($this->composition !== Composition::ANY_OF) {
return null;
}

$enumBranch = null;
$hasOpenBranch = false;

foreach ($this->schemas as $schema) {
if (! $schema instanceof StringSchema) {
return null;
}

if ($schema->enum === []) {
$hasOpenBranch = true;

continue;
}

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

$enumBranch = $schema;
}

return $hasOpenBranch ? $enumBranch : null;
}
}
60 changes: 60 additions & 0 deletions tests/Schema/ReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,66 @@ public function test_composition_and_not(): void
self::assertInstanceOf(StringSchema::class, $negated->not);
}

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']];
$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);
}
}

public function test_open_string_enum_requires_any_of(): void
{
$reader = $this->reader(Version::V3_0);
$branches = [
['type' => 'string', 'enum' => ['known']],
['type' => 'string'],
];

foreach ([Composition::ONE_OF, Composition::ALL_OF] as $composition) {
$schema = $reader->read([$composition->value => $branches], '#/x');

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

public function test_open_string_enum_requires_one_enum_and_an_open_string_branch(): void
{
$reader = $this->reader(Version::V3_0);
$invalidUnions = [
[['type' => 'string', 'enum' => ['known']]],
[['type' => 'string'], ['type' => 'string']],
[
['type' => 'string', 'enum' => ['first']],
['type' => 'string', 'enum' => ['second']],
['type' => 'string'],
],
[
['type' => 'integer', 'enum' => [1]],
['type' => 'string'],
],
[
['type' => 'string', 'enum' => ['known']],
['type' => 'string'],
['type' => 'integer'],
],
];

foreach ($invalidUnions as $branches) {
$schema = $reader->read(['anyOf' => $branches], '#/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