Skip to content

Commit 890e346

Browse files
committed
🍺 initial commit
0 parents  commit 890e346

10 files changed

+516
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
vendor/
2+
coverage/
3+
composer.lock
4+
.phpunit.result.cache

LICENSE

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Copyright (c) 2020 Webtools Ltd
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of
4+
this software and associated documentation files (the "Software"), to deal in
5+
the Software without restriction, including without limitation the rights to
6+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7+
the Software, and to permit persons to whom the Software is furnished to do so,
8+
subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

composer.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "webtools/laravel-json-schema-request",
3+
"version": "1.0.0",
4+
"description": "Like FormRequests, but for validating against a json-schema",
5+
"keywords": [
6+
"webtools",
7+
"json-schema",
8+
"laravel",
9+
"FormRequest",
10+
"validation"
11+
],
12+
"homepage": "https://github.com/webtoolsnz/laravel-json-schema-request",
13+
"license": "MIT",
14+
"authors": [
15+
{
16+
"name": "Byron Adams",
17+
"email": "[email protected]",
18+
"homepage": "https://webtools.nz",
19+
"role": "Developer"
20+
}
21+
],
22+
"require-dev": {
23+
"orchestra/testbench": "^5.3",
24+
"phpunit/phpunit": "^9.2"
25+
},
26+
"require": {
27+
"justinrainbow/json-schema": "^5.2",
28+
"php": "^7.4",
29+
"illuminate/support": "^7.0"
30+
},
31+
"config": {
32+
"sort-packages": true
33+
},
34+
"autoload": {
35+
"psr-4": {
36+
"Webtools\\JsonSchemaRequest\\": "src/",
37+
"Webtools\\JsonSchemaRequest\\Tests\\": "tests/"
38+
}
39+
},
40+
"extra": {
41+
"laravel": {
42+
"providers": [
43+
"Webtools\\JsonSchemaRequest\\ServiceProvider"
44+
]
45+
}
46+
}
47+
}

phpunit.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.2/phpunit.xsd"
4+
bootstrap="vendor/autoload.php"
5+
executionOrder="depends,defects"
6+
beStrictAboutCoversAnnotation="true"
7+
beStrictAboutOutputDuringTests="true"
8+
beStrictAboutTodoAnnotatedTests="true"
9+
verbose="true">
10+
<testsuites>
11+
<testsuite name="default">
12+
<directory suffix="Test.php">tests</directory>
13+
</testsuite>
14+
</testsuites>
15+
16+
<filter>
17+
<whitelist processUncoveredFilesFromWhitelist="true">
18+
<directory suffix=".php">src</directory>
19+
</whitelist>
20+
</filter>
21+
</phpunit>

src/JsonSchemaRequest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Webtools\JsonSchemaRequest;
4+
5+
use Illuminate\Contracts\Container\Container;
6+
use Illuminate\Contracts\Validation\ValidatesWhenResolved;
7+
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
8+
use Illuminate\Http\Exceptions\HttpResponseException;
9+
use Illuminate\Http\Request;
10+
use Illuminate\Http\Response;
11+
use Illuminate\Validation\ValidatesWhenResolvedTrait;
12+
13+
class JsonSchemaRequest extends Request implements ValidatesWhenResolved
14+
{
15+
use ValidatesWhenResolvedTrait;
16+
17+
protected Container $container;
18+
19+
protected ?ValidatorContract $validator = null;
20+
21+
public function getValidatorInstance()
22+
{
23+
if (!$this->validator) {
24+
$this->validator = new JsonSchemaValidator(
25+
$this->container->make(\JsonSchema\Validator::class),
26+
$this->container->call([$this, 'schema']),
27+
$this->json()->all(),
28+
);
29+
}
30+
31+
return $this->validator;
32+
}
33+
34+
public function setContainer(Container $container)
35+
{
36+
$this->container = $container;
37+
38+
return $this;
39+
}
40+
41+
public function failedValidation(JsonSchemaValidator $validator)
42+
{
43+
throw new HttpResponseException(response()->json(
44+
['errors' => $validator->errors()],
45+
Response::HTTP_UNPROCESSABLE_ENTITY
46+
));
47+
}
48+
49+
public function validated()
50+
{
51+
return $this->validator->validated();
52+
}
53+
}

src/JsonSchemaValidator.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace Webtools\JsonSchemaRequest;
4+
5+
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
6+
use Illuminate\Support\MessageBag;
7+
use JsonSchema\Validator as SchemaValidator;
8+
use JsonSchema\Constraints\Constraint;
9+
use Illuminate\Validation\ValidationException;
10+
11+
class JsonSchemaValidator implements ValidatorContract
12+
{
13+
protected SchemaValidator $validator;
14+
protected array $schema;
15+
protected array $data;
16+
protected ?MessageBag $failedConstraints = null;
17+
protected ?MessageBag $messages = null;
18+
19+
/**
20+
* Array of callbacks to be executed after validation
21+
*
22+
* @var \Closure[]
23+
*/
24+
private array $after = [];
25+
26+
public function __construct(SchemaValidator $validator, array $schema, array $data)
27+
{
28+
$this->validator = $validator;
29+
$this->schema = $schema;
30+
$this->data = $data;
31+
}
32+
33+
public function passes(): bool
34+
{
35+
$this->messages = new MessageBag();
36+
$this->failedConstraints = new MessageBag();
37+
38+
$this->validator->validate($this->data, $this->schema, Constraint::CHECK_MODE_TYPE_CAST);
39+
40+
foreach ($this->validator->getErrors(SchemaValidator::ERROR_DOCUMENT_VALIDATION) as $error) {
41+
$this->messages->add($error['property'], $error['message']);
42+
$this->failedConstraints->add($error['property'], $error['constraint']);
43+
}
44+
45+
foreach ($this->after as $after) {
46+
$after();
47+
}
48+
49+
return $this->messages->isEmpty();
50+
}
51+
52+
public function getMessageBag()
53+
{
54+
return $this->messages;
55+
}
56+
57+
/**
58+
* @inheritDoc
59+
* @throws ValidationException
60+
*/
61+
public function validate()
62+
{
63+
if ($this->fails()) {
64+
$this->signalFailure();
65+
}
66+
67+
return $this->validated();
68+
}
69+
70+
/**
71+
* @inheritDoc
72+
*/
73+
public function validated()
74+
{
75+
if (!$this->messages) {
76+
$this->passes();
77+
}
78+
79+
if ($this->messages->isNotEmpty()) {
80+
$this->signalFailure();
81+
}
82+
83+
return $this->data;
84+
}
85+
86+
/**
87+
* @inheritDoc
88+
*/
89+
public function fails()
90+
{
91+
return !$this->passes();
92+
}
93+
94+
/**
95+
* Returns a list of the failed constraints for each property
96+
* @return array
97+
*/
98+
public function failed()
99+
{
100+
return $this->failedConstraints->messages();
101+
}
102+
103+
/**
104+
* This is a NO-OP, was only added to support the ValidatorContract,
105+
* Rules cannot be applied to a schema in this manner.
106+
*/
107+
public function sometimes($attribute, $rules, callable $callback)
108+
{
109+
return $this;
110+
}
111+
112+
/**
113+
* @inheritDoc
114+
*/
115+
public function after($callback)
116+
{
117+
$this->after[] = function () use ($callback) {
118+
return call_user_func_array($callback, [$this]);
119+
};
120+
121+
return $this;
122+
}
123+
124+
/**
125+
* @inheritDoc
126+
*/
127+
public function errors()
128+
{
129+
return $this->getMessageBag();
130+
}
131+
132+
private function signalFailure()
133+
{
134+
throw new ValidationException($this);
135+
}
136+
}

src/ServiceProvider.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Webtools\JsonSchemaRequest;
4+
5+
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
6+
7+
class ServiceProvider extends BaseServiceProvider
8+
{
9+
/**
10+
* Bootstrap the application services.
11+
*
12+
* @return void
13+
*/
14+
public function boot()
15+
{
16+
$this->app->resolving(JsonSchemaRequest::class, function ($request, $app) {
17+
$request = JsonSchemaRequest::createFrom($app['request'], $request);
18+
$request->setContainer($app);
19+
});
20+
}
21+
}

tests/JsonSchemaRequestTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace Webtools\JsonSchemaRequest\Tests;
4+
5+
use Orchestra\Testbench\TestCase;
6+
use Webtools\JsonSchemaRequest\ServiceProvider;
7+
use Webtools\JsonSchemaRequest\Tests\Support\ApiRequest;
8+
use Illuminate\Routing\Router;
9+
10+
class JsonSchemaRequestTest extends TestCase
11+
{
12+
protected function getPackageProviders($app)
13+
{
14+
return [ServiceProvider::class];
15+
}
16+
17+
/**
18+
* @test
19+
*/
20+
public function controllers_should_reject_invalid_json_resolve()
21+
{
22+
$router = $this->app->get(Router::class);
23+
$router->post('/test', fn(ApiRequest $request) => response(200));
24+
25+
$response = $this->postJson('/test', [
26+
'first_name' => 'foo',
27+
]);
28+
29+
$response
30+
->assertStatus(422)
31+
->assertJson([
32+
'errors' => [
33+
'last_name' => ['The property last_name is required'],
34+
'email' => ['The property email is required']
35+
]
36+
]);
37+
}
38+
39+
/**
40+
* @test
41+
*/
42+
public function it_should_accept_valid_json()
43+
{
44+
$router = $this->app->get(Router::class);
45+
$router->post('/test', fn(ApiRequest $request) => $request->validated());
46+
47+
$data = [
48+
'first_name' => 'foo',
49+
'last_name' => 'bar',
50+
'email' => '[email protected]',
51+
];
52+
53+
$this->postJson('/test', $data)->assertOk()->assertJson($data);
54+
}
55+
56+
public function it_should_not_resolve_the_validator_more_than_once()
57+
{
58+
$request = app(ApiRequest::class);
59+
$request->getValidatorInstance();
60+
}
61+
}

0 commit comments

Comments
 (0)