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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ if ($schema instanceof ReferenceSchema) {
}
```

This makes valid recursive schemas safe to parse.
This makes valid recursive schemas safe to parse. Resolve a schema's local component reference explicitly when its concrete type is needed:

```php
$resolved = $specification->resolveSchema($schema);
```

`resolveSchema()` follows chained local schema references and leaves missing, external, or cyclic references unresolved.

OpenAPI 3.1 annotated enumerations (`oneOf` or `anyOf` of `const` + `title`)
are mapped onto `StringSchema` fields. The type name is the composite `title`.
Expand Down
31 changes: 31 additions & 0 deletions src/Specification.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Utopia\OpenAPI\Model\Info;
use Utopia\OpenAPI\Model\Operation;
use Utopia\OpenAPI\Model\PathItem;
use Utopia\OpenAPI\Model\ReferenceSchema;
use Utopia\OpenAPI\Model\Schema;
use Utopia\OpenAPI\Model\SecurityRequirement;
use Utopia\OpenAPI\Model\SecurityScheme;
Expand Down Expand Up @@ -40,6 +41,36 @@ public function __construct(
public ?ExternalDocumentation $externalDocumentation = null,
) {}

/**
* Resolve chained local component references without expanding recursive schema graphs.
*/
public function resolveSchema(Schema $schema): Schema
{
$visited = [];
while ($schema instanceof ReferenceSchema) {
if (! str_starts_with($schema->reference, '#')) {
break;
}

$name = null;
$reference = rawurldecode($schema->reference);
Comment thread
ChiragAgg5k marked this conversation as resolved.
foreach (['#/components/schemas/', '#/definitions/'] as $prefix) {
if (str_starts_with($reference, $prefix)) {
$name = str_replace(['~1', '~0'], ['/', '~'], substr($reference, strlen($prefix)));
break;
}
}

if ($name === null || isset($visited[$name]) || ! isset($this->schemas[$name])) {
break;
}
$visited[$name] = true;
$schema = $this->schemas[$name];
}

return $schema;
}

/** @return list<Operation> */
public function operations(): array
{
Expand Down
34 changes: 33 additions & 1 deletion tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ public function test_parses_open_api2_directly(): void
$operation = $spec->paths['/pets']->operation(HttpMethod::POST);
self::assertTrue($operation?->requestBody?->required);
self::assertInstanceOf(ReferenceSchema::class, $operation?->requestBody?->content['application/json']->schema);
self::assertInstanceOf(ReferenceSchema::class, $operation?->responses['200']->content['application/json']->schema);
$responseSchema = $operation?->responses['200']->content['application/json']->schema;
self::assertInstanceOf(ReferenceSchema::class, $responseSchema);
self::assertSame($spec->schemas['Pet'], $spec->resolveSchema($responseSchema));
}

public function test_parses_open_api2_form_data_as_request_body(): void
Expand All @@ -153,6 +155,36 @@ public function test_parses_open_api2_form_data_as_request_body(): void
self::assertSame('binary', $body?->content['multipart/form-data']->schema->properties['file']->format);
}

public function test_resolves_schema_references_explicitly(): void
{
$spec = Parser::parse([
'openapi' => '3.1.0',
'info' => ['title' => 'References', 'version' => '1'],
'paths' => [],
'components' => ['schemas' => [
'Binary' => ['type' => 'string', 'format' => 'binary'],
'Alias' => ['$ref' => '#/components/schemas/Binary'],
'Chained' => ['$ref' => '#/components/schemas/Alias'],
'Encoded Name' => ['type' => 'string'],
'Encoded' => ['$ref' => '#/components/schemas/Encoded%20Name'],
'Missing' => ['$ref' => '#/components/schemas/Unknown'],
'External' => ['$ref' => 'schemas.json#/Binary'],
'EncodedExternal' => ['$ref' => '%23/components/schemas/Binary'],
'CycleA' => ['$ref' => '#/components/schemas/CycleB'],
'CycleB' => ['$ref' => '#/components/schemas/CycleA'],
]],
]);

self::assertSame($spec->schemas['Binary'], $spec->resolveSchema($spec->schemas['Binary']));
self::assertSame($spec->schemas['Binary'], $spec->resolveSchema($spec->schemas['Alias']));
self::assertSame($spec->schemas['Binary'], $spec->resolveSchema($spec->schemas['Chained']));
self::assertSame($spec->schemas['Encoded Name'], $spec->resolveSchema($spec->schemas['Encoded']));
self::assertSame($spec->schemas['Missing'], $spec->resolveSchema($spec->schemas['Missing']));
self::assertSame($spec->schemas['External'], $spec->resolveSchema($spec->schemas['External']));
self::assertSame($spec->schemas['EncodedExternal'], $spec->resolveSchema($spec->schemas['EncodedExternal']));
self::assertSame($spec->schemas['CycleA'], $spec->resolveSchema($spec->schemas['CycleA']));
}

public function test_resolves_escaped_local_json_pointer_and_detects_reference_cycles(): void
{
$resolver = new LocalResolver([
Expand Down
Loading