diff --git a/src/Routing/RouteLoader.php b/src/Routing/RouteLoader.php index 152120f..0f34eab 100755 --- a/src/Routing/RouteLoader.php +++ b/src/Routing/RouteLoader.php @@ -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; @@ -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); } @@ -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]|(? '[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(? '[^/@]+@[^/]+\.[^/]+', + $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. */ diff --git a/tests/Functional/App/Controller/SuccessController.php b/tests/Functional/App/Controller/SuccessController.php new file mode 100644 index 0000000..54fc480 --- /dev/null +++ b/tests/Functional/App/Controller/SuccessController.php @@ -0,0 +1,29 @@ + + * + * 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 + */ +class SuccessController +{ + public function __invoke(): Response + { + return new Response('', Response::HTTP_OK); + } +} diff --git a/tests/Functional/App/config.yaml b/tests/Functional/App/config.yaml index 794dde7..1e98fbb 100644 --- a/tests/Functional/App/config.yaml +++ b/tests/Functional/App/config.yaml @@ -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' diff --git a/tests/Functional/App/openapi.yaml b/tests/Functional/App/openapi.yaml index c6e3ee2..6512b22 100644 --- a/tests/Functional/App/openapi.yaml +++ b/tests/Functional/App/openapi.yaml @@ -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: diff --git a/tests/Functional/RoutePathParameterValidationTest.php b/tests/Functional/RoutePathParameterValidationTest.php new file mode 100644 index 0000000..5175112 --- /dev/null +++ b/tests/Functional/RoutePathParameterValidationTest.php @@ -0,0 +1,73 @@ + + * + * 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], + ]; + } +}