Skip to content
Snippets Groups Projects
Commit 91781b3a authored by Travis Carden's avatar Travis Carden Committed by Wim Leers
Browse files

Issue #3471928 by traviscarden, wim leers: Decompose ApiMessageValidator into...

Issue #3471928 by traviscarden, wim leers: Decompose ApiMessageValidator into separate classes again
parent 8bd833e7
No related branches found
No related tags found
1 merge request!250#3471928: Decompose ApiMessageValidator into separate classes for requests and responses again.
Pipeline #274445 passed
......@@ -21,7 +21,9 @@
[OpenAPI] @traviscarden
/openapi.yml
/src/EventSubscriber/ApiMessageValidator.php
/src/EventSubscriber/ApiMessageValidatorBase.php
/src/EventSubscriber/ApiRequestValidator.php
/src/EventSubscriber/ApiResponseValidator.php
/tests/src/Traits/OpenApiSpecTrait.php
/tests/src/Unit/OpenApiSpecValidationTest.php
/tests/src/Unit/UiFixturesValidationTest.php
......
......@@ -34,8 +34,17 @@ services:
arguments: ['experience_builder']
# Event subscribers.
experience_builder.openapi.http_message_validator.subscriber:
class: Drupal\experience_builder\EventSubscriber\ApiMessageValidator
experience_builder.openapi.http_request_validator.subscriber:
class: Drupal\experience_builder\EventSubscriber\ApiRequestValidator
arguments:
$logger: '@logger.channel.experience_builder'
$appRoot: '%app.root%'
calls:
- [setValidatorBuilder, []]
tags:
- { name: event_subscriber }
experience_builder.openapi.http_response_validator.subscriber:
class: Drupal\experience_builder\EventSubscriber\ApiResponseValidator
arguments:
$logger: '@logger.channel.experience_builder'
$appRoot: '%app.root%'
......
......@@ -9,21 +9,15 @@ use Drupal\Core\Routing\RouteMatchInterface;
use League\OpenAPIValidation\PSR7\Exception\NoPath;
use League\OpenAPIValidation\PSR7\Exception\Validation\AddressValidationFailed;
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator;
use League\OpenAPIValidation\PSR7\SpecFinder;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* HTTP message subscriber that validates an Experience Builder API message.
* Event subscriber base class for validating Experience Builder API messages.
*
* This functionality only takes effect in the presence of the
* league/openapi-psr7-validator Composer library with PHP assertions enabled
......@@ -33,47 +27,31 @@ use Symfony\Component\HttpKernel\KernelEvents;
*
* @internal
*/
final class ApiMessageValidator implements EventSubscriberInterface {
abstract class ApiMessageValidatorBase implements EventSubscriberInterface {
/**
* The OpenAPI validator.
* The OpenAPI validator builder.
*
* This property will only be set if the validator library is available.
* Don't access it directly. Use {@see self::getConfiguredValidatorBuilder}
* instead.
*
* @var \League\OpenAPIValidation\PSR7\ValidatorBuilder|null
*/
private ?ValidatorBuilder $validatorBuilder = NULL;
/**
* Constructs an API Message Validator object.
*
* @param \Psr\Log\LoggerInterface $logger
* The Experience Builder logger channel.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* The module handler.
* @param \Drupal\Core\Routing\RouteMatchInterface $currentRouteMatch
* The current route match.
* @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $httpMessageFactory
* The PSR7 HTTP message factory.
* @param string $appRoot
* The application's root file path.
*/
public function __construct(
private readonly LoggerInterface $logger,
private readonly ModuleHandlerInterface $moduleHandler,
private readonly RouteMatchInterface $currentRouteMatch,
private readonly HttpMessageFactoryInterface $httpMessageFactory,
protected readonly HttpMessageFactoryInterface $httpMessageFactory,
private readonly string $appRoot,
) {}
/**
* Sets the OpenAPI validator builder service if available.
*/
public function setValidatorBuilder(?ValidatorBuilder $validator = NULL): void {
if ($validator instanceof ValidatorBuilder) {
$this->validatorBuilder = $validator;
public function setValidatorBuilder(?ValidatorBuilder $validatorBuilder = NULL): void {
if ($validatorBuilder instanceof ValidatorBuilder) {
$this->validatorBuilder = $validatorBuilder;
}
elseif (class_exists(ValidatorBuilder::class)) {
$this->validatorBuilder = new ValidatorBuilder();
......@@ -83,9 +61,6 @@ final class ApiMessageValidator implements EventSubscriberInterface {
/**
* Validates Experience Builder API messages.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent|\Symfony\Component\HttpKernel\Event\ResponseEvent $event
* The event to process.
*
* @throws \League\OpenAPIValidation\PSR7\Exception\ValidationFailed
* See docblock on {@see self::validate()}.
*/
......@@ -96,11 +71,7 @@ final class ApiMessageValidator implements EventSubscriberInterface {
try {
$validatorBuilder = $this->getConfiguredValidatorBuilder();
// @phpstan-ignore match.unhandled
match (get_class($event)) {
RequestEvent::class => $this->validateRequest($validatorBuilder, $event),
ResponseEvent::class => $this->validateResponse($validatorBuilder, $event),
};
$this->validate($validatorBuilder, $event);
}
catch (NoPath $e) {
// @todo Temporarily log and ignore missing paths. Once 'openapi.yml' is
......@@ -139,7 +110,7 @@ final class ApiMessageValidator implements EventSubscriberInterface {
}
/**
* Determines whether a given message is from this module.
* Determines whether the message is from this module.
*/
private function isExperienceBuilderMessage(): bool {
return str_starts_with(
......@@ -166,72 +137,18 @@ final class ApiMessageValidator implements EventSubscriberInterface {
}
/**
* Validates a request message.
* Validates the message.
*
* @throws \League\OpenAPIValidation\PSR7\Exception\ValidationFailed
* If validation fails.
*/
protected function validateRequest(ValidatorBuilder $validatorBuilder, RequestEvent $event): void {
$validator = $validatorBuilder->getRequestValidator();
$psr7_request = $this->httpMessageFactory
->createRequest($event->getRequest());
$validator->validate($psr7_request);
}
/**
* Validates a response message.
*
* @throws \League\OpenAPIValidation\PSR7\Exception\ValidationFailed
* If validation fails.
*/
protected function validateResponse(ValidatorBuilder $validatorBuilder, ResponseEvent $event): void {
$request = $event->getRequest();
$response = $event->getResponse();
if (!$response instanceof JsonResponse) {
return;
}
$validator = $validatorBuilder->getResponseValidator();
$operation = new OperationAddress(
$request->getPathInfo(),
strtolower($request->getMethod()),
);
$psr7_response = $this->httpMessageFactory
->createResponse($response);
$this->performXbValidation($validator, $operation, $response);
$validator->validate($operation, $psr7_response);
}
private function performXbValidation(ResponseValidator $validator, OperationAddress $operation, Response $response): void {
$schema = $validator->getSchema();
$spec_finder = new SpecFinder($schema);
$path = $spec_finder->findPathSpec($operation);
if ($operation->method() === 'get' && isset($path->get) && isset($path->get->responses[$response->getStatusCode()])) {
$extensions = $path->get->responses[$response->getStatusCode()]->getExtensions();
if (isset($extensions['x-xb-validation'])) {
assert(isset($extensions['x-xb-validation']['method']), 'Method not found in x-xb-validation extension.');
assert(method_exists(static::class, $extensions['x-xb-validation']['method']));
$content = $response->getContent();
assert(is_string($content));
$args = array_merge([json_decode($content, TRUE)], $extensions['x-xb-validation']['arguments'] ?? []);
$callback = [$this, $extensions['x-xb-validation']['method']];
assert(is_callable($callback));
call_user_func_array($callback, $args);
}
}
}
abstract protected function validate(
ValidatorBuilder $validatorBuilder,
RequestEvent|ResponseEvent $event,
): void;
/**
* Gets the validator builder configured with the module's OpenAPI schema.
*
* @return \League\OpenAPIValidation\PSR7\ValidatorBuilder
* The validator builder configured with the module's OpenAPI schema.
*/
private function getConfiguredValidatorBuilder(): ValidatorBuilder {
$openapi_spec_file = sprintf(
......@@ -248,7 +165,10 @@ final class ApiMessageValidator implements EventSubscriberInterface {
->fromYamlFile($openapi_spec_file);
}
public function logFailure(ValidationFailed $e): void {
/**
* Logs a validation failure.
*/
protected function logFailure(ValidationFailed $e): void {
// AddressValidationFailed provides additional helpful details.
// @see https://github.com/thephpleague/openapi-psr7-validator/pull/184
$message = $e instanceof AddressValidationFailed
......@@ -257,14 +177,4 @@ final class ApiMessageValidator implements EventSubscriberInterface {
$this->logger->debug($message);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
KernelEvents::REQUEST => 'onMessage',
KernelEvents::RESPONSE => 'onMessage',
];
}
}
<?php
declare(strict_types=1);
namespace Drupal\experience_builder\EventSubscriber;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Request subscriber that validates an Experience Builder API request.
*
* @internal
*/
final class ApiRequestValidator extends ApiMessageValidatorBase {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [KernelEvents::REQUEST => 'onMessage'];
}
/**
* {@inheritdoc}
*/
protected function validate(
ValidatorBuilder $validatorBuilder,
RequestEvent|ResponseEvent $event,
): void {
assert($event instanceof RequestEvent);
$validator = $validatorBuilder->getRequestValidator();
$psr7_request = $this->httpMessageFactory
->createRequest($event->getRequest());
$validator->validate($psr7_request);
}
}
<?php
declare(strict_types=1);
namespace Drupal\experience_builder\EventSubscriber;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator;
use League\OpenAPIValidation\PSR7\SpecFinder;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* Response subscriber that validates an Experience Builder API response.
*
* @internal
*/
final class ApiResponseValidator extends ApiMessageValidatorBase {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [KernelEvents::RESPONSE => 'onMessage'];
}
/**
* {@inheritdoc}
*/
protected function validate(
ValidatorBuilder $validatorBuilder,
RequestEvent|ResponseEvent $event,
): void {
assert($event instanceof ResponseEvent);
$request = $event->getRequest();
$response = $event->getResponse();
if (!$response instanceof JsonResponse) {
return;
}
$validator = $validatorBuilder->getResponseValidator();
$operation = new OperationAddress(
$request->getPathInfo(),
strtolower($request->getMethod()),
);
$psr7_response = $this->httpMessageFactory
->createResponse($response);
$this->performXbValidation($validator, $operation, $response);
$validator->validate($operation, $psr7_response);
}
private function performXbValidation(ResponseValidator $validator, OperationAddress $operation, Response $response): void {
$schema = $validator->getSchema();
$spec_finder = new SpecFinder($schema);
$path = $spec_finder->findPathSpec($operation);
if ($operation->method() === 'get' && isset($path->get) && isset($path->get->responses[$response->getStatusCode()])) {
$extensions = $path->get->responses[$response->getStatusCode()]->getExtensions();
if (isset($extensions['x-xb-validation'])) {
assert(isset($extensions['x-xb-validation']['method']), 'Method not found in x-xb-validation extension.');
assert(method_exists(static::class, $extensions['x-xb-validation']['method']));
$content = $response->getContent();
assert(is_string($content));
$args = array_merge([json_decode($content, TRUE)], $extensions['x-xb-validation']['arguments'] ?? []);
$callback = [$this, $extensions['x-xb-validation']['method']];
assert(is_callable($callback));
call_user_func_array($callback, $args);
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment