Skip to content

Commit

Permalink
Handle validation of nested query paramaters e.g. ?filter[groupId]=
Browse files Browse the repository at this point in the history
  • Loading branch information
HughbertD committed Sep 9, 2022
1 parent 0f0980a commit f44b459
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 4 deletions.
24 changes: 21 additions & 3 deletions src/Validation/RequestValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace Spectator\Validation;

use cebe\openapi\spec\Operation;
use cebe\openapi\spec\Parameter;
use cebe\openapi\spec\PathItem;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationException;
use Opis\JsonSchema\Validator;
use Spectator\Exceptions\RequestValidationException;
use Spectator\Exceptions\SchemaValidationException;
Expand Down Expand Up @@ -94,7 +96,7 @@ protected function validateParameters()
// Verify presence, if required.
if ($parameter->in === 'path' && ! $route->hasParameter($parameter->name)) {
throw new RequestValidationException("Missing required parameter {$parameter->name} in URL path.");
} elseif ($parameter->in === 'query' && ! $this->request->query->has($parameter->name)) {
} elseif ($parameter->in === 'query' && ! $this->hasQueryParam($parameter->name)) {
throw new RequestValidationException("Missing required query parameter [?{$parameter->name}=].");
} elseif ($parameter->in === 'header' && ! $this->request->headers->has($parameter->name)) {
throw new RequestValidationException("Missing required header [{$parameter->name}].");
Expand All @@ -117,8 +119,8 @@ protected function validateParameters()
if ($parameter_value instanceof Model) {
$parameter_value = $route->originalParameters()[$parameter->name];
}
} elseif ($parameter->in === 'query' && $this->request->query->has($parameter->name)) {
$parameter_value = $this->request->query->get($parameter->name);
} elseif ($parameter->in === 'query' && $this->hasQueryParam($parameter->name)) {
$parameter_value = $this->getQueryParam($parameter->name);
} elseif ($parameter->in === 'header' && $this->request->headers->has($parameter->name)) {
$parameter_value = $this->request->headers->get($parameter->name);
} elseif ($parameter->in === 'cookie' && $this->request->cookies->has($parameter->name)) {
Expand Down Expand Up @@ -221,4 +223,20 @@ private function toObject($data)
return array_map([$this, 'toObject'], $data);
}
}

private function hasQueryParam(string $parameterName): bool
{
return Arr::has($this->request->query->all(), $this->convertQueryParameterToDotted($parameterName));
}

private function getQueryParam(string $parameterName)
{
return Arr::get($this->request->query->all(), $this->convertQueryParameterToDotted($parameterName));
}

private function convertQueryParameterToDotted(string $parameterName): string
{
parse_str($parameterName, $parsedParameterName);
return key(Arr::dot($parsedParameterName));
}
}
44 changes: 43 additions & 1 deletion tests/Fixtures/Test.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
],
"get": {
"summary": "Get Order",
"description": "Orders are made by organizations to fund tree planting, and when trees are planted those orders will be marked as fullfilled. Orders may be partially fulfilled, and it is up to you if you wish to import those. Make sure to deduplicate the trees if you do.",
"description": "Orders are made by organizations to fund tree planting, and when trees are planted those orders will be marked as fulfilled. Orders may be partially fulfilled, and it is up to you if you wish to import those. Make sure to deduplicate the trees if you do.",
"tags": [],
"responses": {
"200": {
Expand Down Expand Up @@ -414,6 +414,48 @@
},
"operationId": "get-orders-uuid"
}
},
"/orders": {
"parameters": [
{
"schema": {
"type": "string",
"format": "uuid",
"example": "53f7bd7b-2ee2-4be4-a15b-e09c76a150f9"
},
"name": "filter[groupId]",
"in": "query",
"required": true
}
],
"get": {
"summary": "Filter Orders",
"description": "Orders are made by organizations to fund tree planting, and when trees are planted those orders will be marked as fulfilled. Orders may be partially fulfilled, and it is up to you if you wish to import those. Make sure to deduplicate the trees if you do.",
"tags": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Order"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/Generic_Problem"
}
}
}
}
},
"operationId": "filter-orders-by-group"
}
}
},
"components": {
Expand Down
17 changes: 17 additions & 0 deletions tests/RequestValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,25 @@ public function test_handles_query_parameters(): void
$this->get('/users?order=email,name')
->assertValidationMessage('The data should match one item from enum')
->assertInvalidRequest();

// Test it handles nested query parameters
Route::get('/orders', function () {
return [];
})->middleware(Middleware::class);

$this->get('/orders')
->assertValidationMessage('Missing required query parameter [?filter[groupId]=].')
->assertInvalidRequest();

$this->get('/orders?filter[groupId]=1')
->assertValidationMessage('The data must match the \'uuid\' format')
->assertInvalidRequest();

$this->get('/orders?filter[groupId]=cc8936c7-d681-4c42-9410-c50488f43736')
->assertValid();
}


public function test_handles_query_parameters_int(): void
{
Spectator::using('Test.v1.json');
Expand Down

0 comments on commit f44b459

Please sign in to comment.