Skip to content

Commit 2284946

Browse files
m8nkyjosephHolger Hahn
authored
Feature/handle path parameters (#76)
* using the pathfinder to find the path instead of pure string matching - but only when required * test to reproduce issue and fix * reproduces issue * adds exception, throws it * phpcbf ran * phpcbf ran * realising that guzzle must have been a dev dependency so we need to ensure we have a UriFactory * realising that we were requiring a whole URL interface when all we needed was the path which can safely just be a string * cs fixes * should only check for empty specs if we have at least one path with parameters * DEL: Remove orphans. * FIX: Naming for collecting operational and path level specs. * CHG: Refactored path spec finder. Co-authored-by: joseph <joseph@joseph> Co-authored-by: Holger Hahn <[email protected]>
1 parent 3ba8dc7 commit 2284946

7 files changed

+147
-60
lines changed

src/PSR7/PathFinder.php

+28-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
namespace League\OpenAPIValidation\PSR7;
66

77
use cebe\openapi\spec\OpenApi;
8+
use cebe\openapi\spec\PathItem;
89
use cebe\openapi\spec\Server;
9-
use Psr\Http\Message\UriInterface;
1010
use const PHP_URL_PATH;
1111
use function array_key_exists;
1212
use function count;
@@ -29,18 +29,39 @@ class PathFinder
2929
{
3030
/** @var OpenApi */
3131
protected $openApiSpec;
32-
/** @var UriInterface */
33-
protected $uri;
32+
/** @var string */
33+
protected $path;
3434
/** @var string $method like "get" */
3535
protected $method;
3636

37-
public function __construct(OpenApi $openApiSpec, UriInterface $uri, string $method)
37+
public function __construct(OpenApi $openApiSpec, string $uri, string $method)
3838
{
3939
$this->openApiSpec = $openApiSpec;
40-
$this->uri = $uri;
40+
$this->path = (string) parse_url($uri, PHP_URL_PATH);
4141
$this->method = strtolower($method);
4242
}
4343

44+
/**
45+
* Determine matching paths.
46+
*
47+
* @return PathItem[]
48+
*/
49+
public function getPathMatches() : array
50+
{
51+
// Determine if path matches exactly.
52+
$match = $this->openApiSpec->paths->getPath($this->path);
53+
if ($match !== null) {
54+
return [$match];
55+
}
56+
// Probably path is parametrized or matches partially. Determine candidates and try to match path.
57+
$matches = [];
58+
foreach ($this->search() as $result) {
59+
$matches[] = $this->openApiSpec->paths->getPath($result->path());
60+
}
61+
62+
return $matches;
63+
}
64+
4465
/**
4566
* Make search
4667
*
@@ -72,8 +93,7 @@ public function search() : array
7293
);
7394

7495
// 3.1 Compare this path against the real/given path
75-
$searchPath = (string) parse_url((string) $this->uri, PHP_URL_PATH);
76-
if (! OperationAddress::isPathMatchesSpec($candidatePath, $searchPath)) {
96+
if (! OperationAddress::isPathMatchesSpec($candidatePath, $this->path)) {
7797
continue;
7898
}
7999

@@ -105,7 +125,7 @@ private function searchForCandidates() : array
105125
// servers: /v1
106126
$pattern = '#' . preg_replace('#{[^}]+}#', '[^/]+', $specPath) . '/?$#';
107127

108-
if (! (bool) preg_match($pattern, $this->uri->getPath())) {
128+
if (! (bool) preg_match($pattern, $this->path)) {
109129
continue;
110130
}
111131

src/PSR7/RequestValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function validate(RequestInterface $request) : OperationAddress
9898
*/
9999
private function findMatchingOperations(RequestInterface $request) : array
100100
{
101-
$pathFinder = new PathFinder($this->openApi, $request->getUri(), $request->getMethod());
101+
$pathFinder = new PathFinder($this->openApi, (string) $request->getUri(), $request->getMethod());
102102

103103
return $pathFinder->search();
104104
}

src/PSR7/ServerRequestValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public function validate(ServerRequestInterface $serverRequest) : OperationAddre
9898
*/
9999
private function findMatchingOperations(ServerRequestInterface $request) : array
100100
{
101-
$pathFinder = new PathFinder($this->openApi, $request->getUri(), $request->getMethod());
101+
$pathFinder = new PathFinder($this->openApi, (string) $request->getUri(), $request->getMethod());
102102

103103
return $pathFinder->search();
104104
}

src/PSR7/SpecFinder.php

+58-43
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,39 @@ public function __construct(OpenApi $openApi)
3737
$this->openApi = $openApi;
3838
}
3939

40+
/**
41+
* @return Parameter[]
42+
*
43+
* @throws NoPath
44+
*/
45+
public function findOperationAndPathLevelSpecs(OperationAddress $addr) : array
46+
{
47+
$spec = $this->findOperationSpec($addr);
48+
49+
// 1. Collect operation-level params
50+
$pathSpecs = [];
51+
52+
foreach ($spec->parameters as $p) {
53+
if ($p->in !== 'path') {
54+
continue;
55+
}
56+
57+
$pathSpecs[$p->name] = $p;
58+
}
59+
60+
// 2. Collect path-level params
61+
$pathSpec = $this->findPathSpec($addr);
62+
foreach ($pathSpec->parameters as $p) {
63+
if ($p->in !== 'path') {
64+
continue;
65+
}
66+
67+
$pathSpecs += [$p->name => $p]; // union won't override
68+
}
69+
70+
return $pathSpecs;
71+
}
72+
4073
/**
4174
* Find a particular operation (path + method) in the spec
4275
*
@@ -65,46 +98,14 @@ public function findOperationSpec(OperationAddress $addr) : Operation
6598
*/
6699
public function findPathSpec(OperationAddress $addr) : PathItem
67100
{
68-
$pathSpec = $this->openApi->paths->getPath($addr->path());
101+
$finder = new PathFinder($this->openApi, $addr->path(), $addr->method());
102+
$pathSpecs = $finder->getPathMatches();
69103

70-
if (! $pathSpec) {
104+
if (empty($pathSpecs) === true) {
71105
throw NoPath::fromPath($addr->path());
72106
}
73107

74-
return $pathSpec;
75-
}
76-
77-
/**
78-
* @return Parameter[]
79-
*
80-
* @throws NoPath
81-
*/
82-
public function findPathSpecs(OperationAddress $addr) : array
83-
{
84-
$spec = $this->findOperationSpec($addr);
85-
86-
// 1. Collect operation-level params
87-
$pathSpecs = [];
88-
89-
foreach ($spec->parameters as $p) {
90-
if ($p->in !== 'path') {
91-
continue;
92-
}
93-
94-
$pathSpecs[$p->name] = $p;
95-
}
96-
97-
// 2. Collect path-level params
98-
$pathSpec = $this->findPathSpec($addr);
99-
foreach ($pathSpec->parameters as $p) {
100-
if ($p->in !== 'path') {
101-
continue;
102-
}
103-
104-
$pathSpecs += [$p->name => $p]; // union won't override
105-
}
106-
107-
return $pathSpecs;
108+
return $pathSpecs[0];
108109
}
109110

110111
/**
@@ -198,10 +199,13 @@ public function findBodySpec(OperationAddress $addr) : array
198199
*/
199200
public function findResponseSpec($addr) : ResponseSpec
200201
{
201-
Assert::isInstanceOfAny($addr, [
202-
ResponseAddress::class,
203-
CallbackResponseAddress::class,
204-
]);
202+
Assert::isInstanceOfAny(
203+
$addr,
204+
[
205+
ResponseAddress::class,
206+
CallbackResponseAddress::class,
207+
]
208+
);
205209

206210
$operation = $this->findOperationSpec($addr);
207211

@@ -237,7 +241,8 @@ public function findHeaderSpecs(OperationAddress $addr) : array
237241
$spec = $this->findOperationSpec($addr);
238242

239243
// 1. Collect operation level headers from "parameters" keyword
240-
// An API call may require that custom headers be sent with an HTTP request. OpenAPI lets you define custom request headers as in: header parameters.
244+
// An API call may require that custom headers be sent with an HTTP request. OpenAPI lets you define custom
245+
// request headers as in: header parameters.
241246
$headerSpecs = [];
242247
foreach ($spec->parameters as $p) {
243248
if ($p->in !== 'header') {
@@ -307,13 +312,23 @@ private function findCallbackInOperation(CallbackAddress $addr, Operation $opera
307312
{
308313
$callbacks = $operation->callbacks;
309314
if (! isset($callbacks[$addr->callbackName()])) {
310-
throw NoCallback::fromCallbackPath($addr->path(), $addr->method(), $addr->callbackName(), $addr->callbackMethod());
315+
throw NoCallback::fromCallbackPath(
316+
$addr->path(),
317+
$addr->method(),
318+
$addr->callbackName(),
319+
$addr->callbackMethod()
320+
);
311321
}
312322

313323
/** @var Callback $callback */
314324
$callback = $callbacks[$addr->callbackName()];
315325
if (! isset($callback->getRequest()->getOperations()[$addr->callbackMethod()])) {
316-
throw NoCallback::fromCallbackPath($addr->path(), $addr->method(), $addr->callbackName(), $addr->callbackMethod());
326+
throw NoCallback::fromCallbackPath(
327+
$addr->path(),
328+
$addr->method(),
329+
$addr->callbackName(),
330+
$addr->callbackMethod()
331+
);
317332
}
318333

319334
return $callback->getRequest()->getOperations()[$addr->callbackMethod()];

src/PSR7/Validators/PathValidator.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function validate(OperationAddress $addr, MessageInterface $message) : vo
4343
*/
4444
private function validateRequest(OperationAddress $addr, RequestInterface $message) : void
4545
{
46-
$validator = new ArrayValidator($this->finder->findPathSpecs($addr));
46+
$validator = new ArrayValidator($this->finder->findOperationAndPathLevelSpecs($addr));
4747
$path = $message->getUri()->getPath();
4848
$pathParsedParams = $addr->parseParams($path); // ['id'=>12]
4949

tests/PSR7/PathFinderTest.php

+4-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace League\OpenAPIValidation\Tests\PSR7;
66

77
use cebe\openapi\Reader;
8-
use GuzzleHttp\Psr7\Uri;
98
use League\OpenAPIValidation\PSR7\PathFinder;
109
use PHPUnit\Framework\TestCase;
1110

@@ -30,7 +29,7 @@ public function testItFindsMatchingOperation() : void
3029
summary: Product Types
3130
SPEC;
3231

33-
$pathFinder = new PathFinder(Reader::readFromYaml($spec), new Uri('/v1/products/10'), 'get');
32+
$pathFinder = new PathFinder(Reader::readFromYaml($spec), '/v1/products/10', 'get');
3433
$opAddrs = $pathFinder->search();
3534

3635
$this->assertCount(1, $opAddrs);
@@ -56,7 +55,7 @@ public function testItFindsMatchingOperationWithParametrizedServer() : void
5655
summary: Product Types
5756
SPEC;
5857

59-
$pathFinder = new PathFinder(Reader::readFromYaml($spec), new Uri('/v1/2019-05-07/products/20'), 'get');
58+
$pathFinder = new PathFinder(Reader::readFromYaml($spec), '/v1/2019-05-07/products/20', 'get');
6059
$opAddrs = $pathFinder->search();
6160

6261
$this->assertCount(1, $opAddrs);
@@ -83,7 +82,7 @@ public function testItFindsMatchingOperationForFullUrl() : void
8382
summary: Product Types
8483
SPEC;
8584

86-
$pathFinder = new PathFinder(Reader::readFromYaml($spec), new Uri('https://localhost/v1/products/10'), 'get');
85+
$pathFinder = new PathFinder(Reader::readFromYaml($spec), 'https://localhost/v1/products/10', 'get');
8786
$opAddrs = $pathFinder->search();
8887

8988
$this->assertCount(1, $opAddrs);
@@ -111,7 +110,7 @@ public function testItFindsMatchingOperationForMultipleServersWithSamePath() : v
111110
summary: Product Types
112111
SPEC;
113112

114-
$pathFinder = new PathFinder(Reader::readFromYaml($spec), new Uri('https://localhost/v1/products/10'), 'get');
113+
$pathFinder = new PathFinder(Reader::readFromYaml($spec), 'https://localhost/v1/products/10', 'get');
115114
$opAddrs = $pathFinder->search();
116115

117116
$this->assertCount(1, $opAddrs);

tests/PSR7/SpecFinderTest.php

+54-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace League\OpenAPIValidation\Tests\PSR7;
66

77
use League\OpenAPIValidation\PSR7\CallbackAddress;
8+
use League\OpenAPIValidation\PSR7\OperationAddress;
89
use League\OpenAPIValidation\PSR7\SpecFinder;
910
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
1011
use PHPUnit\Framework\TestCase;
@@ -65,7 +66,59 @@ public function testFindCallbackSpecs() : void
6566
$operation = $specFinder->findOperationSpec($address);
6667

6768
// Some assertions to ensure we have the right operation
68-
$this->assertEquals('boolean', $operation->requestBody->content['application/json']->schema->properties['success']->type);
69+
$this->assertEquals(
70+
'boolean',
71+
$operation->requestBody->content['application/json']->schema->properties['success']->type
72+
);
6973
$this->assertEquals(['200'], array_keys(iterator_to_array($operation->responses->getIterator())));
7074
}
75+
76+
public function testHandleParameters() : void
77+
{
78+
$json = /** @lang JSON */
79+
<<<'JSON'
80+
{
81+
"openapi": "3.0.0",
82+
"info": {
83+
"title": "API",
84+
"version": "1.0"
85+
},
86+
"paths": {
87+
"/api/1.0/order/{orderId}": {
88+
"get": {
89+
"operationId": "get_order",
90+
"parameters": [
91+
{
92+
"name": "orderId",
93+
"in": "path",
94+
"description": "The order ID",
95+
"required": true,
96+
"schema": {
97+
"type": "integer",
98+
"minimum": "1"
99+
}
100+
}
101+
],
102+
"responses": {
103+
"200": {
104+
"description": "The order object",
105+
"content": {
106+
"application/json": {
107+
"properties": {
108+
"value": true
109+
}
110+
}
111+
}
112+
}
113+
}
114+
}
115+
}
116+
}
117+
}
118+
JSON;
119+
$schema = (new ValidatorBuilder())->fromJson($json)->getServerRequestValidator()->getSchema();
120+
$specFinder = new SpecFinder($schema);
121+
$pathItem = $specFinder->findPathSpec(new OperationAddress('/api/1.0/order/123', 'get'));
122+
self::assertSame('The order object', $pathItem->get->responses[200]->description);
123+
}
71124
}

0 commit comments

Comments
 (0)