Skip to content

Commit f17a1dd

Browse files
authored
Add support for style=form explode=false to serialized parameter (#72)
1 parent 2a1a62c commit f17a1dd

File tree

2 files changed

+175
-9
lines changed

2 files changed

+175
-9
lines changed

src/PSR7/Validators/SerializedParameter.php

+63-9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use Respect\Validation\Exceptions\ExceptionInterface;
1515
use Respect\Validation\Validator;
1616
use const JSON_ERROR_NONE;
17+
use function explode;
18+
use function in_array;
1719
use function is_float;
1820
use function is_int;
1921
use function is_numeric;
@@ -24,18 +26,34 @@
2426
use function key;
2527
use function preg_match;
2628
use function reset;
29+
use function strtolower;
2730

2831
final class SerializedParameter
2932
{
33+
private const STYLE_FORM = 'form';
34+
private const STYLE_SPACE_DELIMITED = 'spaceDelimited';
35+
private const STYLE_PIPE_DELIMITED = 'pipeDelimited';
36+
private const STYLE_DELIMITER_MAP = [
37+
self::STYLE_FORM => ',',
38+
self::STYLE_SPACE_DELIMITED => ' ',
39+
self::STYLE_PIPE_DELIMITED => '|',
40+
];
41+
3042
/** @var CebeSchema */
3143
private $schema;
3244
/** @var string|null */
3345
private $contentType;
46+
/** @var string|null */
47+
private $style;
48+
/** @var bool|null */
49+
private $explode;
3450

35-
public function __construct(CebeSchema $schema, ?string $contentType = null)
51+
public function __construct(CebeSchema $schema, ?string $contentType = null, ?string $style = null, ?bool $explode = null)
3652
{
3753
$this->schema = $schema;
3854
$this->contentType = $contentType;
55+
$this->style = $style;
56+
$this->explode = $explode;
3957
}
4058

4159
public static function fromSpec(CebeParameter $parameter) : self
@@ -45,7 +63,7 @@ public static function fromSpec(CebeParameter $parameter) : self
4563
if ($parameter->schema !== null) {
4664
Validator::not(Validator::notEmpty())->assert($content);
4765

48-
return new self($parameter->schema);
66+
return new self($parameter->schema, null, $parameter->style, $parameter->explode);
4967
}
5068

5169
Validator::length(1, 1)->assert($content);
@@ -59,7 +77,7 @@ public static function fromSpec(CebeParameter $parameter) : self
5977
$schema = reset($content)->schema;
6078
$contentType = key($content);
6179

62-
return new self($schema, $contentType);
80+
return new self($schema, $contentType, $parameter->style, $parameter->explode);
6381
}
6482

6583
/**
@@ -85,26 +103,62 @@ public function deserialize($value)
85103
return $decodedValue;
86104
}
87105

88-
if (($this->schema->type === CebeType::BOOLEAN) && is_scalar($value) && preg_match('#^(true|false)$#i', (string) $value)) {
89-
return (bool) $value;
106+
$value = $this->castToSchemaType($value, $this->schema->type);
107+
108+
return $value;
109+
}
110+
111+
private function isJsonContentType() : bool
112+
{
113+
return $this->contentType !== null && preg_match('#^application/.*json$#', $this->contentType) !== false;
114+
}
115+
116+
/**
117+
* @param mixed $value
118+
*
119+
* @return mixed
120+
*/
121+
private function castToSchemaType($value, ?string $type)
122+
{
123+
if (($type === CebeType::BOOLEAN) && is_scalar($value) && preg_match('#^(true|false)$#i', (string) $value)) {
124+
return is_string($value) ? strtolower($value) === 'true' : (bool) $value;
90125
}
91126

92-
if (($this->schema->type === CebeType::NUMBER)
127+
if (($type === CebeType::NUMBER)
93128
&& is_scalar($value) && is_numeric($value)) {
94129
return is_int($value) ? (int) $value : (float) $value;
95130
}
96131

97-
if (($this->schema->type === CebeType::INTEGER)
132+
if (($type === CebeType::INTEGER)
98133
&& is_scalar($value) && ! is_float($value) && preg_match('#^[-+]?\d+$#', (string) $value)) {
99134
return (int) $value;
100135
}
101136

137+
if (($type === CebeType::ARRAY) && is_string($value)) {
138+
return $this->convertToSerializationStyle($value);
139+
}
140+
102141
return $value;
103142
}
104143

105-
private function isJsonContentType() : bool
144+
/**
145+
* @param mixed $value
146+
*
147+
* @return mixed
148+
*/
149+
protected function convertToSerializationStyle($value)
106150
{
107-
return $this->contentType !== null && preg_match('#^application/.*json$#', $this->contentType) !== false;
151+
if ($this->explode === false
152+
&& in_array($this->style, [self::STYLE_FORM, self::STYLE_SPACE_DELIMITED, self::STYLE_PIPE_DELIMITED], true)) {
153+
$value = explode(self::STYLE_DELIMITER_MAP[$this->style], $value);
154+
foreach ($value as &$val) {
155+
$val = $this->castToSchemaType($val, $this->schema->items->type ?? null);
156+
}
157+
158+
return $value;
159+
}
160+
161+
return $value;
108162
}
109163

110164
public function getSchema() : CebeSchema
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace League\OpenAPIValidation\Tests\FromCommunity;
6+
7+
use GuzzleHttp\Psr7\ServerRequest;
8+
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
9+
use PHPUnit\Framework\TestCase;
10+
11+
final class IssueWithQueryArrayTest extends TestCase
12+
{
13+
public function testConvertFormIntegerArray() : void
14+
{
15+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'integer', 'int32'))->getServerRequestValidator();
16+
$validator->validate($this->makeRequest('form', 'integer'));
17+
$this->addToAssertionCount(1);
18+
}
19+
20+
public function testConvertFormNumberArray() : void
21+
{
22+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'number', 'float'))->getServerRequestValidator();
23+
$validator->validate($this->makeRequest('form', 'number'));
24+
$this->addToAssertionCount(1);
25+
}
26+
27+
public function testConvertFormIntegerArrayToStringArray() : void
28+
{
29+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'string', 'int32'))->getServerRequestValidator();
30+
$validator->validate($this->makeRequest('form', 'integer'));
31+
$this->addToAssertionCount(1);
32+
}
33+
34+
public function testConvertFormStringArray() : void
35+
{
36+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'string', 'int32'))->getServerRequestValidator();
37+
$validator->validate($this->makeRequest('form', 'string'));
38+
$this->addToAssertionCount(1);
39+
}
40+
41+
public function testConvertFormBooleanArray() : void
42+
{
43+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'boolean', 'int32'))->getServerRequestValidator();
44+
$validator->validate($this->makeRequest('form', 'boolean'));
45+
$this->addToAssertionCount(1);
46+
}
47+
48+
public function testConvertFormIntegerArrayError() : void
49+
{
50+
$this->expectExceptionMessage('Value "id1,id2,id3" for argument "id" is invalid for Request [get /users]');
51+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('form', 'integer', 'int32'))->getServerRequestValidator();
52+
$validator->validate($this->makeRequest('form', 'string'));
53+
$this->addToAssertionCount(1);
54+
}
55+
56+
public function testConvertSpaceIntegerArray() : void
57+
{
58+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('spaceDelimited', 'integer', 'int32'))->getServerRequestValidator();
59+
$validator->validate($this->makeRequest('spaceDelimited', 'integer'));
60+
$this->addToAssertionCount(1);
61+
}
62+
63+
public function testConvertPipeIntegerArray() : void
64+
{
65+
$validator = (new ValidatorBuilder())->fromYaml($this->makeYaml('pipeDelimited', 'integer', 'int32'))->getServerRequestValidator();
66+
$validator->validate($this->makeRequest('pipeDelimited', 'integer'));
67+
$this->addToAssertionCount(1);
68+
}
69+
70+
protected function makeYaml(string $style, string $type, string $format) : string
71+
{
72+
return $yaml = /** @lang yaml */
73+
<<<YAML
74+
openapi: 3.0.0
75+
info:
76+
title: Product import API
77+
version: '1.0'
78+
servers:
79+
- url: 'http://localhost:8000/api/v1'
80+
paths:
81+
/users:
82+
get:
83+
parameters:
84+
- in: query
85+
name: id
86+
required: true
87+
style: $style
88+
explode: false
89+
schema:
90+
type: array
91+
items:
92+
type: $type
93+
format: $format
94+
responses:
95+
'200':
96+
description: A list of users
97+
YAML;
98+
}
99+
100+
protected function makeRequest(string $style, string $type) : ServerRequest
101+
{
102+
$map = [
103+
'form' => ['integer' => '1,2,3', 'string' => 'id1,id2,id3', 'boolean' => 'true,false', 'number' => '1.00,2.00,3.00'],
104+
'spaceDelimited' => ['integer' => '1 2 3', 'string' => 'id1 id2 id3', 'boolean' => 'true false', 'number' => '1.00 2.00 3.00'],
105+
'pipeDelimited' => ['integer' => '1|2|3', 'string' => 'id1|id2|id3', 'boolean' => 'true|false', 'number' => '1.00|2.00|3.00'],
106+
];
107+
$request = new ServerRequest('GET', 'http://localhost:8000/api/v1/users');
108+
$request = $request->withQueryParams(['id' => $map[$style][$type]]);
109+
110+
return $request;
111+
}
112+
}

0 commit comments

Comments
 (0)