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
88 changes: 75 additions & 13 deletions src/Routing/RouteLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ private function parseOperation(

$defaults[RouteContext::REQUEST_ATTRIBUTE] = $openapiRouteContext;

$route = new Route($path, $defaults, []);
$route = new Route($path, $defaults, $this->getRouteRequirementsForPathParameters($operation, $pathItem));
$route->setMethods($requestMethod);

$routeName = null;
Expand Down Expand Up @@ -198,19 +198,8 @@ private function addRouteContextForValidation(
}

$openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS] = [];
$parameters = array_merge(
$pathItem->parameters ?? [],
$operation->parameters ?? []
);
$parameters = $this->getParameters('query', $operation, $pathItem);
foreach ($parameters as $parameter) {
if ($parameter->in !== 'query') {
continue;
}

if ($parameter instanceof Reference) {
$parameter = $parameter->resolve();
}

$openapiRouteContext[RouteContext::REQUEST_VALIDATE_QUERY_PARAMETERS][$parameter->name] = json_encode($parameter);
}

Expand All @@ -233,6 +222,79 @@ private function addRouteContextForValidation(
}
}

private function getRouteRequirementsForPathParameters(stdClass $operation, stdClass $pathItem): array
{
$requirements = [];
$parameters = $this->getParameters('path', $operation, $pathItem);
foreach ($parameters as $parameter) {
$parameterSchema = $parameter->schema;

if (empty($parameterSchema->pattern ?? '') === false) {
$requirements[$parameter->name] = $parameterSchema->pattern;

continue;
}

if (empty($parameterSchema->enum ?? []) === false) {
$requirements[$parameter->name] = sprintf(
'(%s)',
implode(
'|',
array_map(function ($value) {
return preg_quote($value, '/');
}, $parameter->enum)
)
);

continue;
}

$type = $parameterSchema->type ?? 'string';
$format = $parameterSchema->format ?? null;
$pattern = $parameterSchema->pattern ?? null;

$requirements[$parameter->name] = match ($type) {
'boolean' => '(?:true|false)',
'integer' => '-?\d+',
'number' => '-?(?:\d+)(?:\.\d+)?(?:[eE][+-]?\d+)?',
'string' => match (true) {
$format === 'date' => '[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(?<!02-)3[01])',
$format === 'date-time' => '[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(?<!02-)3[01])T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})',
$format === 'email' => '[^/@]+@[^/]+\.[^/]+',
$format === 'uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[13-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}',
!empty($pattern) => $pattern,
default => '[^/]+',
},
default => '[^/]+',
};
}

return $requirements;
}

private function getParameters(?string $in, stdClass $operation, stdClass $pathItem): array
{
$matchingParameters = [];
$parameters = array_merge(
$pathItem->parameters ?? [],
$operation->parameters ?? []
);

foreach ($parameters as $parameter) {
if ($in !== null && $parameter->in !== $in) {
continue;
}

if ($parameter instanceof Reference) {
$parameter = $parameter->resolve();
}

$matchingParameters[$parameter->name] = $parameter;
}

return $matchingParameters;
}

/**
* Returns true when the provided request method is a valid request method in the OpenAPI specification.
*/
Expand Down
29 changes: 29 additions & 0 deletions tests/Functional/App/Controller/SuccessController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

/*
* This file is part of the OpenapiBundle package.
*
* (c) Niels Nijens <nijens.niels@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nijens\OpenapiBundle\Tests\Functional\App\Controller;

use Symfony\Component\HttpFoundation\Response;

/**
* Controller for functional testing that always returns a 200 OK response.
*
* @author Niels Nijens <nijens.niels@gmail.com>
*/
class SuccessController
{
public function __invoke(): Response
{
return new Response('', Response::HTTP_OK);
}
}
4 changes: 4 additions & 0 deletions tests/Functional/App/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ services:
tags:
- 'controller.service_arguments'

Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController:
tags:
- 'controller.service_arguments'

Nijens\OpenapiBundle\Tests\Functional\App\Controller\UpdatePetController:
arguments:
- '@Symfony\Component\Serializer\SerializerInterface'
Expand Down
159 changes: 159 additions & 0 deletions tests/Functional/App/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,165 @@ paths:
schema:
type: integer
format: int64
x-openapi-bundle-deserialize-as: 'id'

/validate-path/boolean/{boolean}:
get:
operationId: validatePathBoolean
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: boolean
in: path
required: true
schema:
type: boolean

/validate-path/integer/{integer}:
get:
operationId: validatePathInteger
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: integer
in: path
required: true
schema:
type: integer

/validate-path/number/{number}:
get:
operationId: validatePathNumber
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: number
in: path
required: true
schema:
type: number

/validate-path/string/date/{date}:
get:
operationId: validatePathStringDate
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: date
in: path
required: true
schema:
type: string
format: date

/validate-path/string/date-time/{dateTime}:
get:
operationId: validatePathStringDateTime
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: dateTime
in: path
required: true
schema:
type: string
format: date-time

/validate-path/string/email/{email}:
get:
operationId: validatePathStringEmail
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: email
in: path
required: true
schema:
type: string
format: email

/validate-path/string/uuid/{uuid}:
get:
operationId: validatePathStringUuid
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: uuid
in: path
required: true
schema:
type: string
format: uuid

/validate-path/string/pattern/{stringPattern}:
get:
operationId: validatePathStringPattern
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: stringPattern
in: path
required: true
schema:
type: string
pattern: '[A-Z]{2}'

/validate-path/string/{string}:
get:
operationId: validatePathString
summary: Endpoint to validate path parameter requirement.
x-openapi-bundle:
controller: 'Nijens\OpenapiBundle\Tests\Functional\App\Controller\SuccessController'
responses:
'200':
description: Success!

parameters:
- name: string
in: path
required: true
schema:
type: string

/authenticated/pets:
post:
Expand Down
73 changes: 73 additions & 0 deletions tests/Functional/RoutePathParameterValidationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

/*
* This file is part of the OpenapiBundle package.
*
* (c) Niels Nijens <nijens.niels@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Nijens\OpenapiBundle\Tests\Functional;

use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class RoutePathParameterValidationTest extends WebTestCase
{
/**
* @var KernelBrowser
*/
private $client;

protected function setUp(): void
{
$this->client = static::createClient();
}

/**
* @dataProvider provideTestCases
*/
public function testCanValidateString(string $path, string $parameter, int $expectedStatusCode): void
{
$this->client->request(Request::METHOD_GET, sprintf($path, $parameter));

static::assertResponseStatusCodeSame($expectedStatusCode);
}

public static function provideTestCases(): array
{
return [
['/api/validate-path/boolean/%s', 'true', Response::HTTP_OK],
['/api/validate-path/boolean/%s', 'false', Response::HTTP_OK],
['/api/validate-path/boolean/%s', 'no', Response::HTTP_NOT_FOUND],
['/api/validate-path/integer/%s', '1', Response::HTTP_OK],
['/api/validate-path/integer/%s', '1.0', Response::HTTP_NOT_FOUND],
['/api/validate-path/integer/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/number/%s', '1', Response::HTTP_OK],
['/api/validate-path/number/%s', '1.0', Response::HTTP_OK],
['/api/validate-path/number/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/date/%s', '2026-01-19', Response::HTTP_OK],
['/api/validate-path/string/date/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/date-time/%s', '2026-01-19T12:34:56Z', Response::HTTP_OK],
['/api/validate-path/string/date-time/%s', '1', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/date-time/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/email/%s', 'john@doe.com', Response::HTTP_OK],
['/api/validate-path/string/email/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/uuid/%s', '5f89d86d-4ead-4bc6-ba3e-61726d22fe13', Response::HTTP_OK],
['/api/validate-path/string/uuid/%s', '1', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/uuid/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/pattern/%s', 'NL', Response::HTTP_OK],
['/api/validate-path/string/pattern/%s', 'ab', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/pattern/%s', 'abc', Response::HTTP_NOT_FOUND],
['/api/validate-path/string/%s', '1', Response::HTTP_OK],
['/api/validate-path/string/%s', 'abc', Response::HTTP_OK],
['/api/validate-path/string/%s', 'abc/abc', Response::HTTP_NOT_FOUND],
];
}
}
Loading