Skip to content
This repository has been archived by the owner on Jun 30, 2020. It is now read-only.

split a simpler json-schema validator from the route-based-validator #46

Merged
merged 3 commits into from
Nov 15, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,27 @@ $middlewares = [
];
```

### JsonValidator

Uses [justinrainbow/json-schema](https://github.com/justinrainbow/json-schema) to validate an `application/json` request body with a JSON schema:

```php
use Psr7Middlewares\Middleware;

$middlewares = [

// Transform `application/json` into an object, which is a requirement of `justinrainbow/json-schema`.
Middleware::payload([
'forceArray' => false,
]),

// Specify a JSON file (publicly-accessible in this example), or a JSON string decoded into object-notation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment does not match the method signature... the case of specifying a file is not supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the underlying lib accepts the ref as an object, e.g.

(object) [
  "$ref" => "path/to/schema.json",
]

Middleware::jsonValidator((object) [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're pushing quite a bit of implementation detail to the middleware definition here... I'd prefer to just push a reference to a file (either SplFileObject or a string to the path), rather than deal with the specifics of how the chosen Schema Validator needs to receive the schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great point. the implementation uses justinrainbow/json-schema, which can accept a file ref or a decoded schema. so your validator factory could create a schema inline, without using any files or file ops. do we really want to enforce file only?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i could create a couple of named factories, one for a decoded object, and another that accepts SplFileObject (great suggestion).

'$ref' => WEB_ROOT . '/json-schema/en.v1.users.json',
])
];
```

### JsonSchema

Uses [justinrainbow/json-schema](https://github.com/justinrainbow/json-schema) to validate an `application/json` request body using route-matched JSON schemas:
Expand Down
54 changes: 3 additions & 51 deletions src/Middleware/JsonSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Psr7Middlewares\Middleware;

use JsonSchema\Validator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

Expand Down Expand Up @@ -35,58 +34,11 @@ public function __invoke(ServerRequestInterface $request, ResponseInterface $res
$schema = $this->getSchema($request);

if (is_object($schema)) {
$value = $request->getParsedBody();
if (!is_object($value)) {
return $this->invalidateResponse(
$response,
sprintf('Parsed body must be an object. Type %s is invalid.', gettype($value))
);
}

$validator = new Validator();
$validator->check($value, $schema);

if (!$validator->isValid()) {
return $this->invalidateResponse(
$response,
'Unprocessable Entity',
[
'Content-Type' => 'application/json',
],
json_encode($validator->getErrors(), JSON_UNESCAPED_SLASHES)
);
}
}

if ($next) {
return $next($request, $response);
}

return $response;
}

/**
* @param ResponseInterface $response
* @param string $reason
* @param string[] $headers
* @param string|null $body
*
* @return ResponseInterface
*/
private function invalidateResponse(ResponseInterface $response, $reason, array $headers = [], $body = null)
{
$response = $response->withStatus(422, $reason);

foreach ($headers as $name => $value) {
$response = $response->withHeader($name, $value);
}

if ($body !== null) {
$stream = $response->getBody();
$stream->write($body);
$validator = new JsonValidator($schema);
return $validator($request, $response, $next);
}

return $response;
return $next($request, $response);
}

/**
Expand Down
88 changes: 88 additions & 0 deletions src/Middleware/JsonValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Psr7Middlewares\Middleware;

use JsonSchema\Validator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

class JsonValidator
{
/** @var \stdClass */
private $schema;

/**
* JsonSchema constructor.
*
* @param \stdClass $schema A JSON-decoded object-representation of the schema.
*/
public function __construct(\stdClass $schema)
{
$this->schema = $schema;
}

/**
* Execute the middleware.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
*
* @return ResponseInterface
* @throws \RuntimeException
* @throws \InvalidArgumentException
* @throws \JsonSchema\Exception\ExceptionInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
$value = $request->getParsedBody();
if (!is_object($value)) {
return $this->invalidateResponse(
$response,
sprintf('Parsed body must be an object. Type %s is invalid.', gettype($value))
);
}

$validator = new Validator();
$validator->check($value, $this->schema);

if (!$validator->isValid()) {
return $this->invalidateResponse(
$response,
'Unprocessable Entity',
[
'Content-Type' => 'application/json',
],
json_encode($validator->getErrors(), JSON_UNESCAPED_SLASHES)
Copy link
Contributor

@sander-bol sander-bol Nov 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're assuming the API will be providing a JSON response here, which is not necessarily true for all cases, even if we're in the business of receiving JSON. I'd be very surprised as a developer if a middleware layer suddenly decided to switch the content-type around. Think for example about people using JSON-API, which specifies a content type of Content-Type: application/vnd.api+json.

@oscarotero Does the project have a "best practice" way of dealing with generating responses to match the Accept headers provided by the user?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the response generation can be configured in a similar way than errorHandler or shutdown, providing a callable handler:

$errorResponse = function ($request, $response) {
    $validator = JsonValidator::getValidator($request);
    $response = $response->withHeader('Content-Type', 'application/json');
    $response->getBody()->write(json_encode($validator->getErrors(), JSON_UNESCAPED_SLASHES));
};

$middlewares[] = Middlewares::JsonValidator()->errorHandler($errorResponse);

And if no custom errorHandler is provided, use the default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok thanks, i'll try this out. great suggestions @sbol-coolblue and @oscarotero .

);
}

return $next($request, $response);
}

/**
* @param ResponseInterface $response
* @param string $reason
* @param string[] $headers
* @param string|null $body
*
* @return ResponseInterface
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
private function invalidateResponse(ResponseInterface $response, $reason, array $headers = [], $body = null)
{
$response = $response->withStatus(422, $reason);

foreach ($headers as $name => $value) {
$response = $response->withHeader($name, $value);
}

if ($body !== null) {
$stream = $response->getBody();
$stream->write($body);
}

return $response;
}
}
7 changes: 5 additions & 2 deletions tests/JsonSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public function testValidJson()

self::assertInstanceOf(ResponseInterface::class, $response);
self::assertGreaterThanOrEqual(200, $response->getStatusCode(), $response->getBody());
self::assertLessThan(300, $response->getStatusCode(), $response->getBody());
}

public function testUnmatchedRouteBypassesValidation()
Expand All @@ -140,6 +141,7 @@ public function testUnmatchedRouteBypassesValidation()

self::assertInstanceOf(ResponseInterface::class, $response);
self::assertGreaterThanOrEqual(200, $response->getStatusCode(), $response->getBody());
self::assertLessThan(300, $response->getStatusCode(), $response->getBody());
}

public function testSubRouteMatchesValidator()
Expand Down Expand Up @@ -171,7 +173,7 @@ public function testPayloadCollaborationWithValidJson()
$response = $this->dispatch(
[
new \Psr7Middlewares\Middleware\Payload([
'associative' => false,
'forceArray' => false,
]),
$this->validator,
],
Expand All @@ -181,6 +183,7 @@ public function testPayloadCollaborationWithValidJson()

self::assertInstanceOf(ResponseInterface::class, $response);
self::assertGreaterThanOrEqual(200, $response->getStatusCode(), $response->getReasonPhrase());
self::assertLessThan(300, $response->getStatusCode(), $response->getBody());
}

public function testPayloadCollaborationWithInvalidJson()
Expand All @@ -192,7 +195,7 @@ public function testPayloadCollaborationWithInvalidJson()
$response = $this->dispatch(
[
new \Psr7Middlewares\Middleware\Payload([
'associative' => false,
'forceArray' => false,
]),
$this->validator,
],
Expand Down
Loading