Skip to content
Open
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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Fixed

- A `requestBody` that is a Reference Object is now resolved instead of
silently discarded, so the referenced body is actually validated. The
parser dropped `$ref` — it read only `description`, `content` and
`required` — which produced an empty `RequestBody`, and
`RequestBodyValidatorWithContext` then returned at its
`null === $requestBody->content` early exit. Every request body behind a
`$ref` was therefore unvalidated: malformed payloads passed and
`required: true` was not enforced, with no exception or warning. Reference
support is now wired through all five layers that `Parameter` and
`Response` already used — `RequestBody` gains `ref`/`refSummary`/
`refDescription`, `ComponentTreeBuilder::buildRequestBody()` branches on
`$ref`, `DocumentNavigator` accepts `#/components/requestBodies/*` targets,
and `RefResolverInterface` gains `resolveRequestBody()` and
`resolveRequestBodyWithOverride()`. Chained and dangling references behave
as they do for responses: chains follow through, and an unresolvable or
wrongly-typed pointer throws `UnresolvableRefException` rather than
failing open. Webhooks and callbacks are covered too, since both validate
through `RequestValidator`. Per OAS 3.1+, a `description` sibling of
`$ref` overrides the referenced description (#56).

## [0.7.0]

Preparation for the 1.0.0 stable release. This section tracks work that
Expand Down
17 changes: 17 additions & 0 deletions src/Schema/Model/RequestBody.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
final readonly class RequestBody implements JsonSerializable
{
public function __construct(
public ?string $ref = null,
public ?string $refSummary = null,
public ?string $refDescription = null,
public ?string $description = null,
public ?Content $content = null,
public bool $required = false,
Expand All @@ -18,6 +21,20 @@ public function __construct(
#[Override]
public function jsonSerialize(): array
{
if (null !== $this->ref) {
$data = ['$ref' => $this->ref];

if (null !== $this->refSummary) {
$data['summary'] = $this->refSummary;
}

if (null !== $this->refDescription) {
$data['description'] = $this->refDescription;
}

return $data;
}

$data = [];

if (null !== $this->description) {
Expand Down
8 changes: 8 additions & 0 deletions src/Schema/Parser/Internal/ComponentTreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public function __construct(private OpenApiBuildContext $context) {}

public function buildRequestBody(array $data): RequestBody
{
if (isset($data['$ref'])) {
return new RequestBody(
ref: TypeHelper::asString($data['$ref']),
refSummary: TypeHelper::asStringOrNull($data['summary'] ?? null),
refDescription: TypeHelper::asStringOrNull($data['description'] ?? null),
);
}

return new RequestBody(
description: TypeHelper::asStringOrNull($data['description'] ?? null),
content: $this->nullable($data, 'content', $this->buildContent(...)),
Expand Down
5 changes: 5 additions & 0 deletions src/Validator/Request/RequestBodyValidatorWithContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public function validate(
return;
}

$requestBody = $this->dependencies->refResolver->resolveRequestBodyWithOverride(
$requestBody,
$this->document,
);

if ($requestBody->required && '' === trim($body)) {
throw new MissingRequestBodyException();
}
Expand Down
16 changes: 9 additions & 7 deletions src/Validator/Schema/Internal/DocumentNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Duyler\OpenApi\Validator\Schema\Internal;

use Duyler\OpenApi\Schema\Model\Parameter;
use Duyler\OpenApi\Schema\Model\RequestBody;
use Duyler\OpenApi\Schema\Model\Response;
use Duyler\OpenApi\Schema\Model\Schema;
use Duyler\OpenApi\Schema\OpenApiDocument;
Expand Down Expand Up @@ -44,7 +45,7 @@ public function __construct(
* @throws SchemaDepthExceededException
* @throws UnresolvableRefException
*
* @return array{Schema|Parameter|Response, array<string, bool>}
* @return array{Schema|Parameter|RequestBody|Response, array<string, bool>}
*/
public function resolveRef(
string $ref,
Expand Down Expand Up @@ -92,7 +93,7 @@ public function navigate(
array $parts,
int $depth = 0,
int $maxDepth = ValidationContext::MAX_DEPTH,
): Schema|Parameter|Response {
): Schema|Parameter|RequestBody|Response {
$count = count($parts);

for ($i = 0; $i < $count; ++$i) {
Expand All @@ -107,14 +108,15 @@ public function navigate(
if (
$current instanceof Schema
|| $current instanceof Parameter
|| $current instanceof RequestBody
|| $current instanceof Response
) {
return $current;
}

throw new UnresolvableRefException(
'',
'Target is not a Schema, Parameter, or Response',
'Target is not a Schema, Parameter, RequestBody, or Response',
);
}

Expand Down Expand Up @@ -166,7 +168,7 @@ private function assertNotCircular(string $ref, array $visited): void
}

/** @param array<int, string> $parts */
private function navigateThrowing(string $ref, OpenApiDocument $document, array $parts): Schema|Parameter|Response
private function navigateThrowing(string $ref, OpenApiDocument $document, array $parts): Schema|Parameter|RequestBody|Response
{
try {
return $this->navigate($document, $parts);
Expand All @@ -176,7 +178,7 @@ private function navigateThrowing(string $ref, OpenApiDocument $document, array
}

/** @param WeakMap<OpenApiDocument, RefCache> $cache */
private function lookupCached(OpenApiDocument $document, string $ref, WeakMap $cache): Schema|Parameter|Response|null
private function lookupCached(OpenApiDocument $document, string $ref, WeakMap $cache): Schema|Parameter|RequestBody|Response|null
{
if (false === isset($cache[$document])) {
return null;
Expand All @@ -189,7 +191,7 @@ private function lookupCached(OpenApiDocument $document, string $ref, WeakMap $c
}

/** @param WeakMap<OpenApiDocument, RefCache> $cache */
private function storeCached(OpenApiDocument $document, string $ref, Schema|Parameter|Response $result, WeakMap $cache): void
private function storeCached(OpenApiDocument $document, string $ref, Schema|Parameter|RequestBody|Response $result, WeakMap $cache): void
{
/** @var RefCache $refCache */
$refCache = $cache[$document] ?? new RefCache();
Expand All @@ -200,7 +202,7 @@ private function storeCached(OpenApiDocument $document, string $ref, Schema|Para
/**
* @param array<string, bool> $visited
*
* @return array{Schema|Parameter|Response, array<string, bool>}
* @return array{Schema|Parameter|RequestBody|Response, array<string, bool>}
*/
private function resolveExternalRef(string $ref, array $visited): array
{
Expand Down
3 changes: 2 additions & 1 deletion src/Validator/Schema/RefCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Duyler\OpenApi\Validator\Schema;

use Duyler\OpenApi\Schema\Model\Parameter;
use Duyler\OpenApi\Schema\Model\RequestBody;
use Duyler\OpenApi\Schema\Model\Response;
use Duyler\OpenApi\Schema\Model\Schema;

/** @internal */
final class RefCache
{
/** @var array<string, Schema|Parameter|Response> */
/** @var array<string, Schema|Parameter|RequestBody|Response> */
public array $map = [];
}
32 changes: 32 additions & 0 deletions src/Validator/Schema/RefResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Duyler\OpenApi\Validator\Schema;

use Duyler\OpenApi\Schema\Model\Parameter;
use Duyler\OpenApi\Schema\Model\RequestBody;
use Duyler\OpenApi\Schema\Model\Response;
use Duyler\OpenApi\Schema\Model\Schema;
use Duyler\OpenApi\Schema\OpenApiDocument;
Expand Down Expand Up @@ -124,6 +125,17 @@ public function resolveParameter(string $ref, OpenApiDocument $document, int $de
return $result;
}

#[Override]
public function resolveRequestBody(string $ref, OpenApiDocument $document, int $depth = 0): RequestBody
{
[$result,] = $this->navigator->resolveRef($ref, $document, [], $this->cache, $depth);
if (false === $result instanceof RequestBody) {
throw new UnresolvableRefException($ref, 'Expected RequestBody but got ' . $result::class);
}

return $result;
}

#[Override]
public function resolveResponse(string $ref, OpenApiDocument $document, int $depth = 0): Response
{
Expand Down Expand Up @@ -224,6 +236,26 @@ public function resolveParameterWithOverride(
);
}

#[Override]
public function resolveRequestBodyWithOverride(
RequestBody $requestBody,
OpenApiDocument $document,
): RequestBody {
if (null === $requestBody->ref) {
return $requestBody;
}
$resolved = $this->resolveRequestBody($requestBody->ref, $document);

return new RequestBody(
ref: null,
refSummary: null,
refDescription: null,
description: $requestBody->refDescription ?? $resolved->description,
content: $resolved->content,
required: $resolved->required,
);
}

#[Override]
public function resolveResponseWithOverride(
Response $response,
Expand Down
24 changes: 24 additions & 0 deletions src/Validator/Schema/RefResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Duyler\OpenApi\Validator\Exception\RefResolutionException;
use Duyler\OpenApi\Schema\Model\Parameter;
use Duyler\OpenApi\Schema\Model\RequestBody;
use Duyler\OpenApi\Schema\Model\Response;
use Duyler\OpenApi\Schema\Model\Schema;
use Duyler\OpenApi\Schema\OpenApiDocument;
Expand Down Expand Up @@ -33,6 +34,18 @@ public function resolveParameter(
int $depth = 0,
): Parameter;

/**
* @param string $ref JSON Pointer reference (e.g., '#/components/requestBodies/UserBody')
* @param int $depth Current recursion depth
* @throws Exception\UnresolvableRefException
* @throws SchemaDepthExceededException
*/
public function resolveRequestBody(
string $ref,
OpenApiDocument $document,
int $depth = 0,
): RequestBody;

/**
* @param string $ref JSON Pointer reference (e.g., '#/components/responses/SuccessResponse')
* @param int $depth Current recursion depth
Expand Down Expand Up @@ -113,6 +126,17 @@ public function resolveParameterWithOverride(
OpenApiDocument $document,
): Parameter;

/**
* Resolve request body reference with summary/description override
*
* @param RequestBody $requestBody Request body with potential $ref and override values
* @throws Exception\UnresolvableRefException
*/
public function resolveRequestBodyWithOverride(
RequestBody $requestBody,
OpenApiDocument $document,
): RequestBody;

/**
* Resolve response reference with summary/description override
*
Expand Down
Loading