Skip to content

Commit

Permalink
in case of computer dies
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Apr 25, 2024
1 parent c47a189 commit ef38527
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 47 deletions.
Expand Up @@ -19,9 +19,9 @@
use ApiPlatform\Metadata\Parameters;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\OpenApi;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\Validator\Constraints\All;
use Symfony\Component\Validator\Constraints\Choice;
use Symfony\Component\Validator\Constraints\Count;
use Symfony\Component\Validator\Constraints\DivisibleBy;
Expand All @@ -31,6 +31,7 @@
use Symfony\Component\Validator\Constraints\LessThan;
use Symfony\Component\Validator\Constraints\LessThanOrEqual;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
use Symfony\Component\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints\Unique;

Expand Down Expand Up @@ -112,18 +113,18 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
$parameter = $parameter->withProperty($property);
}

if (null === $parameter->getRequired() && ($required = $description[$key]['required'])) {
if (null === $parameter->getRequired() && ($required = $description[$key]['required'] ?? null)) {
$parameter = $parameter->withRequired($required);
}

if (null === $parameter->getOpenApi() && $openApi = $description[$key]['openapi'] ?? null) {
if ($openApi instanceof OpenApi\Model\Parameter) {
if ($openApi instanceof OpenApiParameter) {
$parameter = $parameter->withOpenApi($openApi);
}

if (\is_array($openApi)) {
$schema = $schema ?? $openapi['schema'] ?? [];

Check failure on line 126 in src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan (PHP 8.3)

Variable $openapi on left side of ?? is never defined.
$parameter = $parameter->withOpenApi(new OpenApi\Model\Parameter(
$parameter = $parameter->withOpenApi(new OpenApiParameter(
$key,
$parameter instanceof HeaderParameterInterface ? 'header' : 'query',
$description[$key]['description'] ?? '',
Expand All @@ -142,69 +143,74 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
}
}

$schema = $parameter->getSchema() ?? $parameter->getOpenApi()?->getSchema();
if (!$parameter->getConstraint()) {
$parameter = $this->addSchemaValidation($parameter, $schema, $description['required'] ?? false, $openApi);
$parameter = $this->addSchemaValidation($parameter, $schema, $parameter->getRequired() ?? $description['required'] ?? false, $parameter->getOpenApi());
}

return $parameter;
}

private function addSchemaValidation(Parameter $parameter, ?array $schema = null, bool $required = false, ?array $openApi = null): Parameter
private function addSchemaValidation(Parameter $parameter, ?array $schema = null, bool $required = false, ?OpenApiParameter $openApi = null): Parameter
{
$assertions = [];

if ($required) {
$assertions[] = new NotNull(message: sprintf('The parameter "%s" is required.', $parameter->getKey()));
}

if (isset($schema['exclusiveMinimum'])) {
$assertions[] = new GreaterThan(value: $schema['exclusiveMinimum'], propertyPath: $parameter->getProperty() ?? $parameter->getKey());
$assertions[] = new GreaterThan(value: $schema['exclusiveMinimum']);
}

if (isset($schema['exclusiveMaximum'])) {
$assertions[] = new LessThan(value: $schema['exclusiveMaximum'], propertyPath: $parameter->getProperty() ?? $parameter->getKey());
$assertions[] = new LessThan(value: $schema['exclusiveMaximum']);
}

if (isset($schema['minimum'])) {
$assertions[] = new GreaterThanOrEqual(value: $schema['minimum'], propertyPath: $parameter->getProperty() ?? $parameter->getKey());
$assertions[] = new GreaterThanOrEqual(value: $schema['minimum']);
}

if (isset($schema['maximum'])) {
$assertions[] = new LessThanOrEqual(value: $schema['maximum'], propertyPath: $parameter->getProperty() ?? $parameter->getKey());
$assertions[] = new LessThanOrEqual(value: $schema['maximum']);
}

if ($required && !($openApi['allowEmptyValue'] ?? false)) {
$assertions[] = new NotBlank();
if (isset($schema['pattern'])) {
$assertions[] = new Regex($schema['pattern']);
}

if (isset($openApi['pattern'])) {
$assertions[] = new Regex($openApi['pattern'], message: sprintf('Query parameter "%s" must match pattern %s', $parameter->getKey(), $openApi['pattern']));
if (isset($schema['maxLength']) || isset($schema['minLength'])) {
$assertions[] = new Length(min: $schema['minLength'] ?? null, max: $schema['maxLength'] ?? null);
}

if (isset($openApi['maxLength']) || isset($openApi['minLength'])) {
$assertions[] = new Length(min: $openApi['minLength'] ?? null, max: $openApi['maxLength'] ?? null);
if (isset($schema['minItems']) || isset($schema['maxItems'])) {
$assertions[] = new Count(min: $schema['minItems'] ?? null, max: $schema['maxItems'] ?? null);
}

if (isset($openApi['maxLength']) || isset($openApi['minLength'])) {
$assertions[] = new Length(min: $openApi['minLength'] ?? null, max: $openApi['maxLength'] ?? null);
if (isset($schema['multipleOf'])) {
$assertions[] = new DivisibleBy(value: $schema['multipleOf']);
}

if (isset($openApi['minItems']) || isset($openApi['maxItems'])) {
$assertions[] = new Count(min: $openApi['minItems'] ?? null, max: $openApi['maxItems'] ?? null);
if ($schema['uniqueItems'] ?? false) {
$assertions[] = new Unique();
}

if ($openApi['uniqueItems'] ?? false) {
$assertions[] = new Unique();
if (isset($schema['enum'])) {
$assertions[] = new Choice(choices: $schema['enum']);
}

if (isset($openApi['enum'])) {
$assertions[] = new Choice(choices: $openApi['enum']);
if (false === $openApi?->getAllowEmptyValue()) {
$assertions[] = new NotBlank(allowNull: !$required);
}

if (isset($openApi['enum'])) {
$assertions[] = new Choice(choices: $openApi['enum']);
if (!$assertions) {
return $parameter;
}

if (isset($openApi['multipleOf'])) {
$assertions[] = new DivisibleBy(value: $openApi['multipleOf']);
if (1 === \count($assertions)) {
return $parameter->withConstraint($assertions[0]);
}

return $parameter->withConstraint(new All($assertions));
return $parameter->withConstraint($assertions);
}
}
21 changes: 7 additions & 14 deletions src/Symfony/Validator/State/ParameterValidatorProvider.php
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Validator\Exception\ValidationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;
Expand All @@ -25,37 +26,29 @@
*
* @experimental
*/
final class ParameterValidatorProvider implements ProviderInterface
final readonly class ParameterValidatorProvider implements ProviderInterface
{
public function __construct(
private readonly ProviderInterface $decorated,
private readonly ValidatorInterface $validator
private ProviderInterface $decorated,
private ValidatorInterface $validator
) {
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$body = $this->decorated->provide($operation, $uriVariables, $context);
if (!$context['request'] ?? null) {
if (!$context['request'] instanceof Request) {
return $body;
}

$operation = $context['request']->attributes->get('_api_operation');
foreach ($operation->getParameters() as $parameter) {
if ($parameter->getRequired() && !\array_key_exists('_api_values', $parameter->getExtraProperties())) {
throw new ValidationException(new ConstraintViolationList([new ConstraintViolation(sprintf('Parameter "%s" is required.', $parameter->getKey()), null, [], null, null, null)]));
}

if (!$constraints = $parameter->getConstraint()) {
continue;
}

// This is computed in @see ApiPlatform\State\Provider\ParameterProvider
if (!\array_key_exists('_api_values', $parameter->getExtraProperties())) {
continue;
}

$violations = $this->validator->validate(current($parameter->getExtraProperties()['_api_values']), $constraints);
$value = $parameter->getExtraProperties()['_api_values'][$parameter->getKey()] ?? null;
$violations = $this->validator->validate($value, $constraints);
if (0 !== \count($violations)) {
$constraintViolationList = new ConstraintViolationList();
foreach ($violations as $violation) {
Expand Down
18 changes: 16 additions & 2 deletions tests/Fixtures/TestBundle/ApiResource/WithParameter.php
Expand Up @@ -19,11 +19,11 @@
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\QueryParameter;
use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter;
use ApiPlatform\Serializer\Filter\GroupFilter;
use ApiPlatform\Tests\Fixtures\TestBundle\Parameter\CustomGroupParameterProvider;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints\NotBlank;

#[Get(
uriTemplate: 'with_parameters/{id}{._format}',
Expand All @@ -45,7 +45,21 @@
#[GetCollection(
uriTemplate: 'with_parameters_collection{._format}',
parameters: [
'hydra' => new QueryParameter(property: 'a', required: true, constraint: new NotBlank()),
'hydra' => new QueryParameter(property: 'a', required: true),
],
provider: [self::class, 'collectionProvider']
)]
#[GetCollection(
uriTemplate: 'validate_parameters{._format}',
parameters: [
'enum' => new QueryParameter(schema: ['enum' => ['a', 'b'], 'uniqueItems' => true]),
'num' => new QueryParameter(schema: ['minimum' => 1, 'maximum' => 3]),
'exclusiveNum' => new QueryParameter(schema: ['exclusiveMinimum' => 1, 'exclusiveMaximum' => 3]),
'blank' => new QueryParameter(openApi: new OpenApiParameter(name: 'blank', in: 'query', allowEmptyValue: false)),
'length' => new QueryParameter(schema: ['maxLength' => 1, 'minLength' => 3]),
'array' => new QueryParameter(schema: ['minItems' => 2, 'maxItems' => 3]),
'multipleOf' => new QueryParameter(schema: ['multipleOf' => 2]),
'pattern' => new QueryParameter(schema: ['pattern' => '/\d/']),
],
provider: [self::class, 'collectionProvider']
)]
Expand Down
86 changes: 84 additions & 2 deletions tests/Functional/Parameters/ValidationTests.php
Expand Up @@ -19,7 +19,89 @@ final class ValidationTests extends ApiTestCase
{
public function testWithGroupFilter(): void
{
$response = self::createClient()->request('GET', 'with_parameters_collection?hydra=');
$this->assertArraySubset(['violations' => [['propertyPath' => 'a', 'message' => 'This value should not be blank.']]], $response->toArray(false));
$response = self::createClient()->request('GET', 'with_parameters_collection');
$this->assertArraySubset(['violations' => [['propertyPath' => 'a', 'message' => 'The parameter "hydra" is required.']]], $response->toArray(false));
$response = self::createClient()->request('GET', 'with_parameters_collection?hydra');
$this->assertResponseIsSuccessful();
}

/**
* @dataProvider provideQueryStrings
*
* @param array<int,array{propertyPath: string, message: string}> $expectedViolations
*/
public function testValidation(string $queryString, array $expectedViolations): void
{
$response = self::createClient()->request('GET', 'validate_parameters?'.$queryString);
$this->assertArraySubset([
'violations' => $expectedViolations,
], $response->toArray(false));
}

public function provideQueryStrings(): array
{
return [
[
'enum[]=c&enum[]=c',
[
[
'propertyPath' => 'enum', 'message' => 'This collection should contain only unique elements.',
],
[
'propertyPath' => 'enum', 'message' => 'The value you selected is not a valid choice.',
],
],
],
[
'blank=',
[
[
'propertyPath' => 'blank', 'message' => 'This value should not be blank.',
],
],
],
[
'length=toolong',
[
['propertyPath' => 'length', 'message' => 'This value is too long. It should have 1 character or less.'],
],
],
[
'multipleOf=3',
[
['propertyPath' => 'multipleOf', 'message' => 'This value should be a multiple of 2.'],
],
],
[
'pattern=no',
[
['propertyPath' => 'pattern', 'message' => 'This value is not valid.'],
],
],
[
'array[]=1',
[
['propertyPath' => 'array', 'message' => 'This collection should contain 2 elements or more.'],
],
],
[
'num=5',
[
['propertyPath' => 'num', 'message' => 'This value should be less than or equal to 3.'],
],
],
[
'exclusiveNum=5',
[
['propertyPath' => 'exclusiveNum', 'message' => 'This value should be less than 3.'],
],
],
];
}

public function testBlank(): void
{
$response = self::createClient()->request('GET', 'validate_parameters?blank=f');
$this->assertResponseIsSuccessful();
}
}

0 comments on commit ef38527

Please sign in to comment.