Skip to content

Commit a4f39c6

Browse files
committed
initial commit
0 parents  commit a4f39c6

26 files changed

+1479
-0
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
._*
2+
.~lock.*
3+
.DS_Store
4+
.idea
5+
nbproject
6+
.project
7+
.settings
8+
composer.lock
9+
/vendor
10+
.phpunit.result.cache
11+
/dist

README.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# openapi-http-foundation-validator
2+
OpenAPI(v3) Validators for Symfony http-foundation, using `league/openapi-psr7-validator` and `symfony/psr-http-message-bridge`.
3+
4+
## Requirements
5+
- PHP >= 8.0
6+
7+
## Installation
8+
9+
```shell
10+
composer require n1215/openapi-http-foundation-validator
11+
```
12+
13+
## Usage
14+
15+
### 1. install PSR-17 HTTP Factory implementation.
16+
- You can use any implementation of PSR-17 HTTP Factory.
17+
- ex. `nyholm/psr7`
18+
19+
```shell
20+
composer require nyholm/psr7
21+
```
22+
23+
### 2. create http message factory
24+
25+
```php
26+
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
27+
/** @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $httpMessageFactory */
28+
$httpMessageFactory = new \Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory(
29+
serverRequestFactory: $psr17Factory,
30+
streamFactory: $psr17Factory,
31+
uploadedFileFactory: $psr17Factory,
32+
responseFactory: $psr17Factory
33+
);
34+
```
35+
36+
### 3. create validator builder
37+
38+
- A builder can be created from YAML file, YAML string, JSON file, or JSON string.
39+
- You can use PSR-16 simple cache instead of PSR-6 Cache.
40+
41+
#### example1
42+
```php
43+
/** @var \N1215\OpenApiValidation\HttpFoundation\ValidatorBuilder $validatorBuilder */
44+
$validatorBuilder = (new \N1215\OpenApiValidation\HttpFoundation\ValidatorBuilder($psr17Factory))
45+
->fromYamlFile('/path/to/openapi.yaml')
46+
->setCache(new YourPsr6Cache(), 86400);
47+
```
48+
49+
50+
#### example2
51+
```php
52+
/** @var \N1215\OpenApiValidation\HttpFoundation\ValidatorBuilder $validatorBuilder */
53+
$validatorBuilder = (new \N1215\OpenApiValidation\HttpFoundation\ValidatorBuilder($psr17Factory))
54+
->fromJsonFile('/path/to/openapi.json')
55+
->setSimpleCache(new YourPsr16Cache(), 3600);
56+
```
57+
58+
### 4. get validators from builder
59+
60+
```php
61+
/** @var \N1215\OpenApiValidation\HttpFoundation\Validators $validators */
62+
$validators = $validatorBuilder->getValidators();
63+
```
64+
65+
### 5. validate request
66+
67+
```php
68+
/** @var \Symfony\Component\HttpFoundation\Request $request */
69+
/** @var \N1215\OpenApiValidation\HttpFoundation\RequestValidatorInterface $requestValidator */
70+
$requestValidator = $validators->getRequestValidator();
71+
$responseValidator->validate($request);
72+
```
73+
74+
### 6. validate response
75+
76+
```php
77+
/** @var \Symfony\Component\HttpFoundation\Response $response */
78+
/** @var \N1215\OpenApiValidation\HttpFoundation\ResponseValidatorInterface $responseValidator */
79+
$responseValidator = $validators->getResponseValidator();
80+
$responseValidator->validate(
81+
new \N1215\OpenApiValidation\OperationAddress('/path', 'GET'),
82+
$response
83+
);
84+
```
85+
86+
## Usage for Laravel HTTP testing
87+
88+
### 1. create a OpenAPI definition file
89+
./tests/Utils/openapi.yaml
90+
91+
```yaml
92+
openapi: 3.0.1
93+
info:
94+
description: test
95+
version: 1.0.0
96+
title: Test API
97+
servers:
98+
- url: https://example.com
99+
paths:
100+
/hello:
101+
get:
102+
summary: hello
103+
tags:
104+
- hello
105+
description: say hello
106+
operationId: hello
107+
parameters:
108+
- name: name
109+
in: query
110+
description: name
111+
schema:
112+
type: string
113+
description: name
114+
example: Taro
115+
required: true
116+
responses:
117+
200:
118+
description: success
119+
content:
120+
application/json:
121+
schema:
122+
type: object
123+
required:
124+
- message
125+
properties:
126+
message:
127+
type: string
128+
description: message
129+
example: "Hello, Taro!"
130+
422:
131+
description: validation failed
132+
content:
133+
application/json:
134+
schema:
135+
type: object
136+
required:
137+
- errors
138+
properties:
139+
errors:
140+
type: object
141+
additionalProperties:
142+
type: array
143+
items:
144+
type: string
145+
```
146+
147+
### 2. create a trait
148+
149+
./tests/Utils/OpenApiAssertion.php
150+
```php
151+
<?php
152+
153+
declare(strict_types=1);
154+
155+
namespace Tests\Utils;
156+
157+
use Illuminate\Foundation\Http\Events\RequestHandled;
158+
use Illuminate\Support\Facades\Cache;
159+
use Illuminate\Support\Facades\Event;
160+
use N1215\OpenApiValidation\OperationAddress;
161+
use Nyholm\Psr7\Factory\Psr17Factory;
162+
use N1215\OpenApiValidation\HttpFoundation\Validators;
163+
use N1215\OpenApiValidation\HttpFoundation\ValidatorBuilder;
164+
use N1215\OpenApiValidation\RequestValidationFailed;
165+
use N1215\OpenApiValidation\ResponseValidationFailed;
166+
use PHPUnit\Framework\TestCase;
167+
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
168+
169+
trait OpenApiAssertion
170+
{
171+
protected ?Validators $validators;
172+
173+
protected function makeHttpFoundationValidators(): Validators
174+
{
175+
$psr17Factory = new Psr17Factory();
176+
$httpMessageFactory = new PsrHttpFactory(
177+
$psr17Factory,
178+
$psr17Factory,
179+
$psr17Factory,
180+
$psr17Factory
181+
);
182+
return (new ValidatorBuilder($httpMessageFactory))
183+
->fromYamlFile(__DIR__ . '/openapi.yaml')
184+
->setSimpleCache(Cache::store(), 3600)
185+
->getValidators();
186+
}
187+
188+
protected function setOpenApiAssertion(string $method, string $path): void
189+
{
190+
if (!$this instanceof TestCase) {
191+
throw new \BadMethodCallException('trait '. OpenApiAssertion::class . ' should be used by a subclass of' . TestCase::class);
192+
}
193+
194+
$this->validators = $this->makeHttpFoundationValidators();
195+
Event::listen(
196+
RequestHandled::class,
197+
function (RequestHandled $event) use ($method, $path) {
198+
try {
199+
$this->validators->getRequestValidator()->validate($event->request);
200+
} catch (RequestValidationFailed $e) {
201+
$this->fail((string) $e);
202+
}
203+
204+
if ($event->response->getStatusCode() >= 500) {
205+
return;
206+
}
207+
208+
try {
209+
$this->validators->getResponseValidator()->validate(
210+
new OperationAddress($path, $method),
211+
$event->response
212+
);
213+
$this->assertTrue(true);
214+
} catch (ResponseValidationFailed $e) {
215+
$this->fail((string) $e);
216+
}
217+
}
218+
);
219+
}
220+
221+
protected function disableRequestAssertion(): void
222+
{
223+
$this->validators->getRequestValidator()->enable(false);
224+
}
225+
226+
protected function disableResponseAssertion(): void
227+
{
228+
$this->validators->getResponseValidator()->enable(false);
229+
}
230+
}
231+
```
232+
233+
### 3. use the trait in test class
234+
235+
```php
236+
<?php
237+
238+
declare(strict_types=1);
239+
240+
namespace Tests\Feature;
241+
242+
use Tests\TestCase;
243+
use Tests\Utils\OpenApiAssertion;
244+
245+
class GetHelloTest extends TestCase
246+
{
247+
use OpenApiAssertion;
248+
249+
public function testSuccess(): void
250+
{
251+
$this->setOpenApiAssertion(
252+
'get',
253+
'/hello'
254+
);
255+
256+
$response = $this->json(
257+
'get',
258+
'/hello?name=Taro'
259+
);
260+
261+
$response->assertOk();
262+
$response->assertJson(['message' => 'Hello, Taro']);
263+
}
264+
265+
public function testValidationFailed(): void
266+
{
267+
$this->setOpenApiAssertion(
268+
'get',
269+
'/hello'
270+
);
271+
272+
// disable request validation for invalid request parameters
273+
$this->disableRequestAssertion();
274+
275+
$response = $this->json(
276+
'get',
277+
'/hello'
278+
);
279+
280+
$response->assertStatus(422);
281+
$response->assertJsonValidationErrors(['name' => 'The name field is required']);
282+
}
283+
}
284+
```

composer.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "n1215/openapi-http-foundation-validator",
3+
"type": "library",
4+
"require": {
5+
"php": ">=7.4",
6+
"league/openapi-psr7-validator": "^0.15.2",
7+
"psr/cache": "^1.0",
8+
"psr/simple-cache": "^1.0",
9+
"symfony/psr-http-message-bridge": "^2.1"
10+
},
11+
"require-dev": {
12+
"nyholm/psr7": "^1.4",
13+
"phpstan/phpstan": "^0.12.98",
14+
"phpunit/phpunit": "^9.5",
15+
"squizlabs/php_codesniffer": "^3.6",
16+
"yiisoft/cache": "^1.0"
17+
},
18+
"autoload": {
19+
"psr-4": {
20+
"N1215\\OpenApiValidation\\": "src"
21+
}
22+
},
23+
"autoload-dev": {
24+
"psr-4": {
25+
"N1215\\OpenApiValidation\\": "tests"
26+
}
27+
},
28+
"license": "MIT",
29+
"authors": [
30+
{
31+
"name": "n1215",
32+
"email": "[email protected]"
33+
}
34+
],
35+
"config": {
36+
"optimize-autoloader": true,
37+
"preferred-install": "dist",
38+
"sort-packages": true
39+
},
40+
"prefer-stable": true,
41+
"scripts": {
42+
"analyse": [
43+
"./vendor/bin/phpstan analyse --memory-limit=1024M"
44+
],
45+
"lint": [
46+
"./vendor/bin/phpcs --standard=phpcs.xml ./"
47+
],
48+
"lint:fix": [
49+
"./vendor/bin/phpcbf --standard=phpcs.xml ./"
50+
],
51+
"test": [
52+
"./vendor/bin/phpunit"
53+
],
54+
"coverage": [
55+
"rm -rf ./dist",
56+
"mkdir ./dist",
57+
"php -d pcov.enabled=1 ./vendor/bin/phpunit --coverage-html ./dist/coverage"
58+
]
59+
}
60+
}

phpcs.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<ruleset name="PHP_CodeSniffer"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd">
5+
<description>Custom ruleset Based on PSR12</description>
6+
<rule ref="PSR12" />
7+
<arg name="extensions" value="php" />
8+
<arg name="colors" />
9+
<arg value="ps" />
10+
<exclude-pattern>/dist/</exclude-pattern>
11+
<exclude-pattern>/vendor/</exclude-pattern>
12+
</ruleset>

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
parameters:
2+
paths:
3+
- src
4+
- tests
5+
# The level 8 is the highest level
6+
level: 8

phpunit.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" failOnWarning="true" stopOnFailure="true">
3+
<coverage processUncoveredFiles="true">
4+
<include>
5+
<directory suffix=".php">./src</directory>
6+
</include>
7+
</coverage>
8+
<testsuites>
9+
<testsuite name="Tests">
10+
<directory suffix="Test.php">./tests</directory>
11+
</testsuite>
12+
</testsuites>
13+
</phpunit>

0 commit comments

Comments
 (0)