diff --git a/docs/pages/docs/features/validation/index.md b/docs/pages/docs/features/validation/index.md index f6dc6f989c9c5c0f613bd9058dd75b32d17245ff..ca43f77217890ac7b4fa854272f9997065a5f219 100644 --- a/docs/pages/docs/features/validation/index.md +++ b/docs/pages/docs/features/validation/index.md @@ -47,7 +47,7 @@ readonly class BlogPostForm extends InputValidatedData ## Validators Validators take in any data and check if it adheres to the configuration -schema. The `makeSchema()` method must return a +schema. The `getSchema()` method must return a [JSON Schema](https://json-schema.org/) object. ```php @@ -70,7 +70,7 @@ use Distantmagic\Resonance\SingletonCollection; #[Singleton(collection: SingletonCollection::InputValidator)] readonly class BlogPostFormValidator extends InputValidator { - protected function castValidatedData(mixed $data): BlogPostForm + public function castValidatedData(mixed $data): BlogPostForm { return new BlogPostForm( $data['content'], @@ -78,7 +78,7 @@ readonly class BlogPostFormValidator extends InputValidator ); } - protected function makeSchema(): Schema + public function getSchema(): Schema { return new JsonSchema([ 'type' => 'object', @@ -100,18 +100,22 @@ readonly class BlogPostFormValidator extends InputValidator Preferably validators should be injected somewhere by the {{docs/features/dependency-injection/index}}, so you don't have to set up their -parameters manually. Then you can call their `validateData()` or -`validateRequest()` methods. +parameters manually. Then you can call their `validateData()` method. ```php <?php +use Distantmagic\Resonance\InputValidatorController; +use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\InputValidationResult; +$jsonSchemaValidator = new JsonSchemaValidator(); +$inputValidatorController = new InputValidatorController($jsonSchemaValidator); + /** * @var InputValidationResult $validationResult */ -$validationResult = $blogPostFormValidator->validateData([ +$validationResult = $inputValidatorController->validateData($blogPostFormValidator, [ 'content' => 'test', 'title' => 'test', ]); diff --git a/docs/pages/docs/features/websockets/protocols.md b/docs/pages/docs/features/websockets/protocols.md index 4af04e3689b22c14be67e428fed45b9a4e6061e3..37116bd556d6cf4587399d464aaf858288c828a1 100644 --- a/docs/pages/docs/features/websockets/protocols.md +++ b/docs/pages/docs/features/websockets/protocols.md @@ -64,6 +64,7 @@ use App\RPCMethod; use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Feature; +use Distantmagic\Resonance\JsonSchema; use Distantmagic\Resonance\RPCRequest; use Distantmagic\Resonance\RPCResponse; use Distantmagic\Resonance\SingletonCollection; @@ -78,14 +79,21 @@ use Distantmagic\Resonance\WebSocketRPCResponder; )] final readonly class EchoResponder extends WebSocketRPCResponder { - protected function onRequest( + public function getSchema(): JsonSchema + { + return new JsonSchema([ + 'type' => 'string', + ]); + } + + public function onRequest( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, RPCRequest $rpcRequest, ): void { $webSocketConnection->push(new RPCResponse( $rpcRequest->requestId, - (string) $rpcRequest->payload, + $rpcRequest->payload, )); } } diff --git a/docs/pages/index.md b/docs/pages/index.md index 837817b6e02286b82dfeef3ae45ef90a4d4b9d44..625b00ff96088b99c946616f11062b229920ca01 100644 --- a/docs/pages/index.md +++ b/docs/pages/index.md @@ -106,14 +106,21 @@ description: > #[Singleton(collection: SingletonCollection::WebSocketRPCResponder)] final readonly class EchoResponder extends WebSocketRPCResponder { - protected function onRequest( + public function getSchema(): JsonSchema + { + return new JsonSchema([ + 'type' => 'string', + ]); + } + + public function onRequest( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, RPCRequest $rpcRequest, ): void { $webSocketConnection->push(new RPCResponse( $rpcRequest->requestId, - (string) $rpcRequest->payload, + $rpcRequest->payload, )); } }</code></pre> diff --git a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md index a263fd977bf0e3ee086bcdc84fdfe2cc950f7c5d..fbb52d35312ef5b8c6b4e6c137a8db4434c35c1e 100644 --- a/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md +++ b/docs/pages/tutorials/how-to-create-llm-websocket-chat-with-llama-cpp/index.md @@ -203,6 +203,7 @@ use App\RPCMethod; use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Feature; +use Distantmagic\Resonance\JsonSchema; use Distantmagic\Resonance\LlamaCppClient; use Distantmagic\Resonance\LlamaCppCompletionRequest; use Distantmagic\Resonance\RPCNotification; @@ -224,7 +225,24 @@ final readonly class LlmChatPromptResponder extends WebSocketRPCResponder private LlamaCppClient $llamaCppClient, ) {} - protected function onNotification( + public function getSchema(): JsonSchema + { + return new JsonSchema([ + 'type' => 'object', + 'properties' => [ + 'prompt' => [ + 'minLength' => 1, + 'type' => 'string', + ], + ], + 'required' => [ + 'prompt', + ], + 'additionalProperties' => false, + ]); + } + + public function onNotification( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, RPCNotification $rpcNotification, diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md index 0559023dedaa3692485c7a42cc448aa796482729..9dec331ff17e63e7625bf148875256b018e758d4 100644 --- a/docs/pages/tutorials/session-based-authentication/index.md +++ b/docs/pages/tutorials/session-based-authentication/index.md @@ -370,12 +370,12 @@ use Distantmagic\Resonance\SingletonCollection; #[Singleton(collection: SingletonCollection::InputValidator)] readonly class UsernamePasswordValidator extends InputValidator { - protected function castValidatedData(mixed $data): UsernamePassword + public function castValidatedData(mixed $data): UsernamePassword { return new UsernamePassword($data->username, $data->password); } - protected function makeSchema(): Schema + public function getSchema(): Schema { return new JsonSchema([ 'type' => 'object', diff --git a/src/CastsValidatedDataInterface.php b/src/CastsValidatedDataInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ff92e50181734dfcb88c1803a7a728a1c9133da2 --- /dev/null +++ b/src/CastsValidatedDataInterface.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +/** + * @template TCastedData of InputValidatedData + * @template TValidatedData + */ +interface CastsValidatedDataInterface +{ + /** + * @param TValidatedData $data + * + * @return TCastedData + */ + public function castValidatedData(mixed $data): InputValidatedData; +} diff --git a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php index 42cb0af577b7df5411919539b1825586b182e89b..ea2e03fc1b7e3ea02b6b2248081935a06d1c73f4 100644 --- a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php +++ b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php @@ -13,6 +13,7 @@ use Distantmagic\Resonance\HttpControllerParameterResolution; use Distantmagic\Resonance\HttpControllerParameterResolutionStatus; use Distantmagic\Resonance\HttpControllerParameterResolver; use Distantmagic\Resonance\InputValidatorCollection; +use Distantmagic\Resonance\InputValidatorController; use Distantmagic\Resonance\SingletonCollection; use LogicException; use Swoole\Http\Request; @@ -27,6 +28,7 @@ readonly class ValidatedRequestResolver extends HttpControllerParameterResolver { public function __construct( private InputValidatorCollection $inputValidatorCollection, + private InputValidatorController $inputValidatorController, ) {} public function resolve( @@ -42,7 +44,10 @@ readonly class ValidatedRequestResolver extends HttpControllerParameterResolver } $validator = $this->inputValidatorCollection->inputValidators->get($validatorClassName); - $validationResult = $validator->validateRequest($request); + $validationResult = $this + ->inputValidatorController + ->validateData($validator, $request->post) + ; if ($validationResult->inputValidatedData) { if ($validationResult->inputValidatedData instanceof $parameter->className) { diff --git a/src/InputValidator.php b/src/InputValidator.php index 30127d8d85133f144dc0f1e7779e243d4b060852..9ea2e3e62a7ccb103e9478e7fb59b98ccca4f24f 100644 --- a/src/InputValidator.php +++ b/src/InputValidator.php @@ -4,80 +4,10 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -use LogicException; -use Opis\JsonSchema\Exceptions\ParseException; -use Swoole\Http\Request; - /** - * @template TValidatedModel of InputValidatedData + * @template TCastedData of InputValidatedData * @template TValidatedData + * + * @template-implements CastsValidatedDataInterface<TCastedData,TValidatedData> */ -abstract readonly class InputValidator -{ - public JsonSchema $jsonSchema; - - /** - * @param TValidatedData $data - * - * @return TValidatedModel - */ - abstract protected function castValidatedData(mixed $data): InputValidatedData; - - abstract protected function makeSchema(): JsonSchema; - - public function __construct(private JsonSchemaValidator $jsonSchemaValidator) - { - $this->jsonSchema = $this->makeSchema(); - } - - public function getSchema(): JsonSchema - { - return $this->jsonSchema; - } - - /** - * @return InputValidationResult<TValidatedModel> - */ - public function validateData(mixed $data): InputValidationResult - { - try { - /** - * @var JsonSchemaValidationResult<TValidatedData> - */ - $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this->jsonSchema, $data); - } catch (ParseException $parseException) { - throw new LogicException(sprintf( - 'JSON schema is invalid in: "%s"', - $this::class, - ), 0, $parseException); - } - - $errors = $jsonSchemaValidationResult->errors; - - if (empty($errors)) { - return new InputValidationResult($this->castValidatedData($jsonSchemaValidationResult->data)); - } - - /** - * @var InputValidationResult<TValidatedModel> - */ - $validationResult = new InputValidationResult(); - - foreach ($errors as $propertyName => $propertyErrors) { - $validationResult->errors->put( - $propertyName, - implode("\n", $propertyErrors), - ); - } - - return $validationResult; - } - - /** - * @return InputValidationResult<TValidatedModel> - */ - public function validateRequest(Request $request): InputValidationResult - { - return $this->validateData($request->post); - } -} +abstract readonly class InputValidator implements CastsValidatedDataInterface, JsonSchemaSourceInterface {} diff --git a/src/InputValidator/FrontMatterValidator.php b/src/InputValidator/FrontMatterValidator.php index bd250518afb5174548059c9d37115613364b8bc3..627f9977fab41267575e9cfba78ed0f0ad084620 100644 --- a/src/InputValidator/FrontMatterValidator.php +++ b/src/InputValidator/FrontMatterValidator.php @@ -28,7 +28,7 @@ use Generator; #[Singleton] readonly class FrontMatterValidator extends InputValidator { - protected function castValidatedData(mixed $data): FrontMatter + public function castValidatedData(mixed $data): FrontMatter { $collections = iterator_to_array($this->normalizeDataCollections($data->collections)); @@ -45,7 +45,7 @@ readonly class FrontMatterValidator extends InputValidator ); } - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { $contentTypes = StaticPageContentType::values(); diff --git a/src/InputValidator/RPCMessageValidator.php b/src/InputValidator/RPCMessageValidator.php index 7716c6870de02f7324c9a4aa49b081e3d521280e..b7cb9d680cb0bac124603c99571a474587c4e564 100644 --- a/src/InputValidator/RPCMessageValidator.php +++ b/src/InputValidator/RPCMessageValidator.php @@ -9,7 +9,6 @@ use Distantmagic\Resonance\Feature; use Distantmagic\Resonance\InputValidatedData\RPCMessage; use Distantmagic\Resonance\InputValidator; use Distantmagic\Resonance\JsonSchema; -use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\RPCMethodValidatorInterface; use stdClass; @@ -23,14 +22,9 @@ use stdClass; #[Singleton(grantsFeature: Feature::WebSocket)] readonly class RPCMessageValidator extends InputValidator { - public function __construct( - JsonSchemaValidator $jsonSchemaValidator, - private RPCMethodValidatorInterface $rpcMethodValidator, - ) { - parent::__construct($jsonSchemaValidator); - } + public function __construct(private RPCMethodValidatorInterface $rpcMethodValidator) {} - protected function castValidatedData(mixed $data): RPCMessage + public function castValidatedData(mixed $data): RPCMessage { return new RPCMessage( $this->rpcMethodValidator->castToRPCMethod($data[0]), @@ -39,7 +33,7 @@ readonly class RPCMessageValidator extends InputValidator ); } - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'array', diff --git a/src/InputValidatorController.php b/src/InputValidatorController.php new file mode 100644 index 0000000000000000000000000000000000000000..8793da2b8190917e442e7755fc7b05811af44679 --- /dev/null +++ b/src/InputValidatorController.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Distantmagic\Resonance\Attribute\Singleton; + +#[Singleton] +readonly class InputValidatorController +{ + public function __construct(private JsonSchemaValidator $jsonSchemaValidator) {} + + /** + * @template TCastedData of InputValidatedData + * @template TValidatedData + * + * @param InputValidator<TCastedData,TValidatedData> $inputValidator + * + * @return InputValidationResult<TCastedData> + */ + public function validateData(InputValidator $inputValidator, mixed $data): InputValidationResult + { + $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($inputValidator, $data); + + return $this->castJsonSchemaValidationResult($inputValidator, $jsonSchemaValidationResult); + } + + /** + * @template TCastedData of InputValidatedData + * @template TValidatedData + * + * @param InputValidator<TCastedData,TValidatedData> $inputValidator + * @param JsonSchemaValidationResult<TValidatedData> $jsonSchemaValidationResult + * + * @return InputValidationResult<TCastedData> + */ + protected function castJsonSchemaValidationResult( + InputValidator $inputValidator, + JsonSchemaValidationResult $jsonSchemaValidationResult, + ): InputValidationResult { + $errors = $jsonSchemaValidationResult->errors; + + if (empty($errors)) { + return new InputValidationResult($inputValidator->castValidatedData($jsonSchemaValidationResult->data)); + } + + /** + * @var InputValidationResult<TCastedData> + */ + $validationResult = new InputValidationResult(); + + foreach ($errors as $propertyName => $propertyErrors) { + $validationResult->errors->put( + $propertyName, + implode("\n", $propertyErrors), + ); + } + + return $validationResult; + } +} diff --git a/src/JsonSchemaSourceInterface.php b/src/JsonSchemaSourceInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2ec9a2a3d8bc559e428e0e5597ceba9848aff80e --- /dev/null +++ b/src/JsonSchemaSourceInterface.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +interface JsonSchemaSourceInterface +{ + public function getSchema(): JsonSchema; +} diff --git a/src/JsonSchemaValidationErrorMessage.php b/src/JsonSchemaValidationErrorMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..994ba6f4b975ba169be6c382d7a869cd94156eda --- /dev/null +++ b/src/JsonSchemaValidationErrorMessage.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Stringable; + +readonly class JsonSchemaValidationErrorMessage implements Stringable +{ + public string $message; + + /** + * @param array<string,array<string>> $errors + */ + public function __construct(array $errors) + { + $messages = []; + + foreach ($errors as $propertyName => $propertyErrors) { + foreach ($propertyErrors as $propertyError) { + $messages[] = sprintf('"%s": "%s"', $propertyName, $propertyError); + } + } + + $this->message = sprintf( + "Encountered validation errors:\n-> %s", + implode("\n-> ", $messages) + ); + } + + public function __toString(): string + { + return $this->message; + } +} diff --git a/src/JsonSchemaValidationException.php b/src/JsonSchemaValidationException.php new file mode 100644 index 0000000000000000000000000000000000000000..3c2be17594ddcf501028f3d6e3c0b8b863bdbbde --- /dev/null +++ b/src/JsonSchemaValidationException.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use RuntimeException; + +class JsonSchemaValidationException extends RuntimeException +{ + /** + * @param array<string,array<string>> $errors + */ + public function __construct(array $errors) + { + parent::__construct((string) new JsonSchemaValidationErrorMessage($errors)); + } +} diff --git a/src/JsonSchemaValidator.php b/src/JsonSchemaValidator.php index ed0a080d49768e2fb2fa2f3f28f3961665db950b..8abcef8c0d264ad166ca65ca5d9d06d4814251ed 100644 --- a/src/JsonSchemaValidator.php +++ b/src/JsonSchemaValidator.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Distantmagic\Resonance; use Distantmagic\Resonance\Attribute\Singleton; +use Ds\Map; use Opis\JsonSchema\Errors\ErrorFormatter; use Opis\JsonSchema\Helper; use Opis\JsonSchema\ValidationResult; @@ -13,21 +14,24 @@ use Opis\JsonSchema\Validator; #[Singleton] readonly class JsonSchemaValidator { + /** + * @var Map<JsonSchemaSourceInterface,bool|object|string> + */ + private Map $convertedSchemas; + private ErrorFormatter $errorFormatter; private Validator $validator; public function __construct() { + $this->convertedSchemas = new Map(); $this->errorFormatter = new ErrorFormatter(); $this->validator = new Validator(); } - public function validate(JsonSchema $jsonSchema, mixed $data): JsonSchemaValidationResult + public function validate(JsonSchemaSourceInterface $jsonSchemaSource, mixed $data): JsonSchemaValidationResult { - /** - * @var bool|object|string $convertedSchema - */ - $convertedSchema = Helper::toJSON($jsonSchema->schema); + $convertedSchema = $this->convertSchema($jsonSchemaSource); /** * @var bool|object|string $convertedData @@ -42,6 +46,22 @@ readonly class JsonSchemaValidator ); } + private function convertSchema(JsonSchemaSourceInterface $jsonSchemaSource): bool|object|string + { + if ($this->convertedSchemas->hasKey($jsonSchemaSource)) { + return $this->convertedSchemas->get($jsonSchemaSource); + } + + /** + * @var bool|object|string $convertedSchema + */ + $convertedSchema = Helper::toJSON($jsonSchemaSource->getSchema()->schema); + + $this->convertedSchemas->put($jsonSchemaSource, $convertedSchema); + + return $convertedSchema; + } + /** * @return array<string,array<string>> */ diff --git a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php index 2e6c2ba235ee6f2d2721429437b5385116891f36..4bbdb52a7f5d5d8511dc531d0752a517705b17dd 100644 --- a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php +++ b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php @@ -36,7 +36,7 @@ readonly class ValidatedRequestExtractor extends OpenAPIRouteRequestBodyContentE ->inputValidatorCollection ->inputValidators ->get($attribute->validator) - ->jsonSchema, + ->getSchema(), ), ]; } diff --git a/src/RPCNotification.php b/src/RPCNotification.php index 7ca2c4aa0d4f4c587a7816cf2056d502e0ca0ee8..e99f1eb611dc233637d49428fbb8fe25f210f766 100644 --- a/src/RPCNotification.php +++ b/src/RPCNotification.php @@ -13,6 +13,9 @@ use Stringable; */ readonly class RPCNotification implements Stringable { + /** + * @param TPayload $payload + */ public function __construct( public RPCMethodInterface $method, public mixed $payload, diff --git a/src/SingletonProvider/ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider.php index 6a2fc300da1d4f718c3bef4f9dd9139babcf5b89..180e202d6f965ca4daf617df1a6d7e1f7fe17882 100644 --- a/src/SingletonProvider/ConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider.php @@ -5,13 +5,13 @@ declare(strict_types=1); namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\ConfigurationFile; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\JsonSchemaSourceInterface; +use Distantmagic\Resonance\JsonSchemaValidationException; use Distantmagic\Resonance\JsonSchemaValidationResult; use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\PHPProjectFiles; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; -use LogicException; /** * @template TObject of object @@ -19,14 +19,10 @@ use LogicException; * * @template-extends SingletonProvider<TObject> */ -abstract readonly class ConfigurationProvider extends SingletonProvider +abstract readonly class ConfigurationProvider extends SingletonProvider implements JsonSchemaSourceInterface { - private JsonSchema $jsonSchema; - abstract protected function getConfigurationKey(): string; - abstract protected function makeSchema(): JsonSchema; - /** * @param TSchema $validatedData * @@ -37,9 +33,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider public function __construct( private ConfigurationFile $configurationFile, private JsonSchemaValidator $jsonSchemaValidator, - ) { - $this->jsonSchema = $this->makeSchema(); - } + ) {} /** * @return TObject @@ -54,7 +48,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider /** * @var JsonSchemaValidationResult<TSchema> */ - $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this->jsonSchema, $data); + $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($this, $data); $errors = $jsonSchemaValidationResult->errors; @@ -62,18 +56,7 @@ abstract readonly class ConfigurationProvider extends SingletonProvider return $this->provideConfiguration($jsonSchemaValidationResult->data); } - $messages = []; - - foreach ($errors as $propertyName => $propertyErrors) { - foreach ($propertyErrors as $propertyError) { - $messages[] = sprintf('"%s": "%s"', $propertyName, $propertyError); - } - } - - throw new LogicException(sprintf( - "Encountered validation errors:\n-> %s", - implode("\n-> ", $messages) - )); + throw new JsonSchemaValidationException($errors); } public function shouldRegister(): bool diff --git a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php index 2ce54d5a49ea2883d15ab3e584e65fdfa2286372..0495bcc1e747195e264a875acea866c88f4bf7c0 100644 --- a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php @@ -21,12 +21,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: ApplicationConfiguration::class)] final readonly class ApplicationConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'app'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -55,6 +50,11 @@ final readonly class ApplicationConfigurationProvider extends ConfigurationProvi ]); } + protected function getConfigurationKey(): string + { + return 'app'; + } + protected function provideConfiguration($validatedData): ApplicationConfiguration { return new ApplicationConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php index b04319d15fdce8e47fc631426e097564127fbff8..cceea18dffc86945d10f9d1d0b95bd19f273d468 100644 --- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php @@ -31,12 +31,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: DatabaseConfiguration::class)] final readonly class DatabaseConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'database'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { $valueSchema = [ 'type' => 'object', @@ -99,6 +94,11 @@ final readonly class DatabaseConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'database'; + } + protected function provideConfiguration($validatedData): DatabaseConfiguration { $databaseconfiguration = new DatabaseConfiguration(); diff --git a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php index 9ba541cf38a7dc161167b1950cea17a9078e310d..8964da359829808f194363d8e95eefc6df3cd014 100644 --- a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php @@ -21,12 +21,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: LlamaCppConfiguration::class)] final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'llamacpp'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -59,6 +54,11 @@ final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'llamacpp'; + } + protected function provideConfiguration($validatedData): LlamaCppConfiguration { return new LlamaCppConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php index 58fab3f8a6128f37c09fec642114600c83af025d..f5b45cd6de5448a376c3ec82b7529aab7cd0bac1 100644 --- a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php @@ -31,12 +31,7 @@ use Swoole\Coroutine; )] final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'oauth2'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -77,6 +72,11 @@ final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'oauth2'; + } + protected function provideConfiguration($validatedData): OAuth2Configuration { $encryptionKeyContent = Coroutine::readFile($validatedData->encryption_key); diff --git a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php index 3014813cd2292b79205a9f518e34d1657960318f..9cf75ada3103738babc6331f147e8f1b830e6b72 100644 --- a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php @@ -19,12 +19,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: OpenAPIConfiguration::class)] final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'openapi'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -46,6 +41,11 @@ final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'openapi'; + } + protected function provideConfiguration($validatedData): OpenAPIConfiguration { return new OpenAPIConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php index 3f8c1e7aa56bbff7d8620dd62ddb206fa04906c4..9a93a5143c063c4f935b230ee730fb231ae991ad 100644 --- a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php @@ -28,12 +28,7 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: RedisConfiguration::class)] final readonly class RedisConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'redis'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { $valueSchema = [ 'type' => 'object', @@ -88,6 +83,11 @@ final readonly class RedisConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'redis'; + } + protected function provideConfiguration($validatedData): RedisConfiguration { $databaseconfiguration = new RedisConfiguration(); diff --git a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php index 665109350e0340b875aa11ab6fc01ea530a802f0..7041902c71d8ca1fef640a53f2bc77ac9b3b99fe 100644 --- a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php @@ -31,12 +31,7 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider parent::__construct($configurationFile, $jsonSchemaValidator); } - protected function getConfigurationKey(): string - { - return 'session'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { $redisConnectionPools = $this ->redisConfiguration @@ -70,6 +65,11 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'session'; + } + protected function provideConfiguration($validatedData): SessionConfiguration { return new SessionConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php index 02c121c54d2daec13d0902807bbbfec58f95c7fc..e1c354fe122f805dd2bd7623390da665027e18e1 100644 --- a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php @@ -21,12 +21,7 @@ use Distantmagic\Resonance\StaticPageConfiguration; #[Singleton(provides: StaticPageConfiguration::class)] final readonly class StaticPageConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'static'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -56,6 +51,11 @@ final readonly class StaticPageConfigurationProvider extends ConfigurationProvid ]); } + protected function getConfigurationKey(): string + { + return 'static'; + } + protected function provideConfiguration($validatedData): StaticPageConfiguration { return new StaticPageConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php index ade316341eaf772262b4d7415b7c8ce46a346ebe..83abf24376cdd4fbd56a9093d63cebafc4fa7d9e 100644 --- a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php @@ -22,12 +22,7 @@ use Distantmagic\Resonance\SwooleConfiguration; #[Singleton(provides: SwooleConfiguration::class)] final readonly class SwooleConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'swoole'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -62,6 +57,11 @@ final readonly class SwooleConfigurationProvider extends ConfigurationProvider ]); } + protected function getConfigurationKey(): string + { + return 'swoole'; + } + protected function provideConfiguration($validatedData): SwooleConfiguration { return new SwooleConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php index 01519a55d38795ceb974aefcb1fa8f1d87a9b7aa..a3d15f896ef6827595a6c59404d56f1c0f792cbe 100644 --- a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php @@ -18,12 +18,7 @@ use Distantmagic\Resonance\TranslatorConfiguration; #[Singleton(provides: TranslatorConfiguration::class)] final readonly class TranslatorConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'translator'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -41,6 +36,11 @@ final readonly class TranslatorConfigurationProvider extends ConfigurationProvid ]); } + protected function getConfigurationKey(): string + { + return 'translator'; + } + protected function provideConfiguration($validatedData): TranslatorConfiguration { return new TranslatorConfiguration( diff --git a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php index a49ed0651644b30f02c5e37c2924e7489270d1bb..0dd57855936f52d8f223ca698640cb5aab5f11c9 100644 --- a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php @@ -21,12 +21,7 @@ use Distantmagic\Resonance\WebSocketConfiguration; )] final readonly class WebSocketConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string - { - return 'swoole'; - } - - protected function makeSchema(): JsonSchema + public function getSchema(): JsonSchema { return new JsonSchema([ 'type' => 'object', @@ -41,6 +36,11 @@ final readonly class WebSocketConfigurationProvider extends ConfigurationProvide ]); } + protected function getConfigurationKey(): string + { + return 'swoole'; + } + protected function provideConfiguration($validatedData): WebSocketConfiguration { return new WebSocketConfiguration( diff --git a/src/SingletonProvider/StaticPageAggregateProvider.php b/src/SingletonProvider/StaticPageAggregateProvider.php index 075a03fcd3942179d6b9572e4eb8a4e0dca791c6..d297c8df49a1dacddcc6dc9073dcbf702383735d 100644 --- a/src/SingletonProvider/StaticPageAggregateProvider.php +++ b/src/SingletonProvider/StaticPageAggregateProvider.php @@ -6,6 +6,7 @@ namespace Distantmagic\Resonance\SingletonProvider; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\InputValidator\FrontMatterValidator; +use Distantmagic\Resonance\InputValidatorController; use Distantmagic\Resonance\PHPProjectFiles; use Distantmagic\Resonance\SingletonContainer; use Distantmagic\Resonance\SingletonProvider; @@ -22,6 +23,7 @@ final readonly class StaticPageAggregateProvider extends SingletonProvider { public function __construct( private FrontMatterValidator $frontMatterValidator, + private InputValidatorController $inputValidatorController, private StaticPageConfiguration $staticPageConfiguration, ) {} @@ -30,6 +32,7 @@ final readonly class StaticPageAggregateProvider extends SingletonProvider $fileIterator = new StaticPageFileIterator($this->staticPageConfiguration->inputDirectory); $staticPageIterator = new StaticPageIterator( $this->frontMatterValidator, + $this->inputValidatorController, $fileIterator, $this->staticPageConfiguration->outputDirectory, ); diff --git a/src/StaticPageIterator.php b/src/StaticPageIterator.php index 9311cc8e09d4e732cf883d75233021998cc1d572..30cb72ce950c25be947284d95023dd1ec16583d3 100644 --- a/src/StaticPageIterator.php +++ b/src/StaticPageIterator.php @@ -24,6 +24,7 @@ readonly class StaticPageIterator implements IteratorAggregate public function __construct( private FrontMatterValidator $frontMatterValidator, + private InputValidatorController $inputValidatorController, private StaticPageFileIterator $fileIterator, private string $staticPagesOutputDirectory, ) { @@ -75,7 +76,10 @@ readonly class StaticPageIterator implements IteratorAggregate throw new StaticPageFileException($file, 'File does not have a front matter'); } - $inputValidationResult = $this->frontMatterValidator->validateData($frontMatter); + $inputValidationResult = $this->inputValidatorController->validateData( + $this->frontMatterValidator, + $frontMatter, + ); if ($inputValidationResult->inputValidatedData) { return $inputValidationResult->inputValidatedData; diff --git a/src/WebSocketProtocolController/RPCProtocolController.php b/src/WebSocketProtocolController/RPCProtocolController.php index c6000eef9431c8659e2bc405c57b8b7c6035a8cf..6e7336ec7908ebc19ee231a86f1630dc821c84fd 100644 --- a/src/WebSocketProtocolController/RPCProtocolController.php +++ b/src/WebSocketProtocolController/RPCProtocolController.php @@ -11,6 +11,9 @@ use Distantmagic\Resonance\CSRFManager; use Distantmagic\Resonance\Feature; use Distantmagic\Resonance\Gatekeeper; use Distantmagic\Resonance\InputValidator\RPCMessageValidator; +use Distantmagic\Resonance\InputValidatorController; +use Distantmagic\Resonance\JsonSchemaValidationErrorMessage; +use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\JsonSerializer; use Distantmagic\Resonance\SingletonCollection; use Distantmagic\Resonance\SiteAction; @@ -48,6 +51,8 @@ final readonly class RPCProtocolController extends WebSocketProtocolController private CSRFManager $csrfManager, private AuthenticatedUserStoreAggregate $authenticatedUserSourceAggregate, private Gatekeeper $gatekeeper, + private InputValidatorController $inputValidatorController, + private JsonSchemaValidator $jsonSchemaValidator, private JsonSerializer $jsonSerializer, private LoggerInterface $logger, private RPCMessageValidator $rpcMessageValidator, @@ -120,6 +125,7 @@ final readonly class RPCProtocolController extends WebSocketProtocolController { $webSocketConnection = new WebSocketConnection($server, $fd); $connectionHandle = new WebSocketRPCConnectionHandle( + $this->jsonSchemaValidator, $this->webSocketRPCResponderAggregate, $webSocketAuthResolution, $webSocketConnection, @@ -152,15 +158,28 @@ final readonly class RPCProtocolController extends WebSocketProtocolController private function onJsonMessage(Server $server, Frame $frame, mixed $jsonMessage): void { - $inputValidationResult = $this->rpcMessageValidator->validateData($jsonMessage); - - if ($inputValidationResult->inputValidatedData) { - $this - ->getFrameController($frame) - ->onRPCMessage($inputValidationResult->inputValidatedData) - ; - } else { + $inputValidationResult = $this->inputValidatorController->validateData( + $this->rpcMessageValidator, + $jsonMessage + ); + + if (!$inputValidationResult->inputValidatedData) { $this->onProtocolError($server, $frame, $inputValidationResult->getErrorMessage()); + + return; + } + + $payloadValidationResult = $this + ->getFrameController($frame) + ->onRPCMessage($inputValidationResult->inputValidatedData) + ; + + if (!empty($payloadValidationResult->errors)) { + $this->onProtocolError( + $server, + $frame, + (string) new JsonSchemaValidationErrorMessage($payloadValidationResult->errors), + ); } } diff --git a/src/WebSocketRPCConnectionHandle.php b/src/WebSocketRPCConnectionHandle.php index c001426eb3bcb3e69bb858098a9f38016daca457..27a2b084380f092ffddb682bbb80e4af1aebf205 100644 --- a/src/WebSocketRPCConnectionHandle.php +++ b/src/WebSocketRPCConnectionHandle.php @@ -15,6 +15,7 @@ readonly class WebSocketRPCConnectionHandle private Set $activeResponders; public function __construct( + public JsonSchemaValidator $jsonSchemaValidator, public WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate, public WebSocketAuthResolution $webSocketAuthResolution, public WebSocketConnection $webSocketConnection, @@ -25,6 +26,10 @@ readonly class WebSocketRPCConnectionHandle public function onClose(): void { foreach ($this->activeResponders as $responder) { + $responder->onBeforeMessage( + $this->webSocketAuthResolution, + $this->webSocketConnection, + ); $responder->onClose( $this->webSocketAuthResolution, $this->webSocketConnection, @@ -32,19 +37,50 @@ readonly class WebSocketRPCConnectionHandle } } - public function onRPCMessage(RPCMessage $rpcMessage): void + public function onRPCMessage(RPCMessage $rpcMessage): JsonSchemaValidationResult { $responder = $this ->webSocketRPCResponderAggregate ->selectResponder($rpcMessage) ; + $jsonSchemaValidationResult = $this + ->jsonSchemaValidator + ->validate($responder, $rpcMessage->payload) + ; + + if (!empty($jsonSchemaValidationResult->errors)) { + return $jsonSchemaValidationResult; + } + $this->activeResponders->add($responder); - $responder->respond( + $responder->onBeforeMessage( $this->webSocketAuthResolution, $this->webSocketConnection, - $rpcMessage ); + + if (is_string($rpcMessage->requestId)) { + $responder->onRequest( + $this->webSocketAuthResolution, + $this->webSocketConnection, + new RPCRequest( + $rpcMessage->method, + $jsonSchemaValidationResult->data, + $rpcMessage->requestId, + ), + ); + } else { + $responder->onNotification( + $this->webSocketAuthResolution, + $this->webSocketConnection, + new RPCNotification( + $rpcMessage->method, + $jsonSchemaValidationResult->data, + ) + ); + } + + return $jsonSchemaValidationResult; } } diff --git a/src/WebSocketRPCResponder.php b/src/WebSocketRPCResponder.php index 25c74bfc1a11d5d849a83927cb02268d7badc7a3..995d6295dd646d07b38ab5b10059efa48e19b6b5 100644 --- a/src/WebSocketRPCResponder.php +++ b/src/WebSocketRPCResponder.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -use Distantmagic\Resonance\InputValidatedData\RPCMessage; use Distantmagic\Resonance\WebSocketProtocolException\UnexpectedNotification; use Distantmagic\Resonance\WebSocketProtocolException\UnexpectedRequest; +/** + * @template TPayload + * + * @template-implements WebSocketRPCResponderInterface<TPayload> + */ abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderInterface { public function onClose( @@ -15,29 +19,7 @@ abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderIn WebSocketConnection $webSocketConnection, ): void {} - public function respond( - WebSocketAuthResolution $webSocketAuthResolution, - WebSocketConnection $webSocketConnection, - RPCMessage $rpcMessage, - ): void { - if (is_string($rpcMessage->requestId)) { - $this->onRequest( - $webSocketAuthResolution, - $webSocketConnection, - new RPCRequest($rpcMessage->method, $rpcMessage->payload, $rpcMessage->requestId), - ); - - return; - } - - $this->onNotification( - $webSocketAuthResolution, - $webSocketConnection, - new RPCNotification($rpcMessage->method, $rpcMessage->payload) - ); - } - - protected function onNotification( + public function onNotification( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, RPCNotification $rpcNotification, @@ -45,7 +27,7 @@ abstract readonly class WebSocketRPCResponder implements WebSocketRPCResponderIn throw new UnexpectedNotification($rpcNotification->method); } - protected function onRequest( + public function onRequest( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, RPCRequest $rpcRequest, diff --git a/src/WebSocketRPCResponderInterface.php b/src/WebSocketRPCResponderInterface.php index afca1afdce1e6acb4f54fa299f1b7c7d956d938e..e88f71b2b58b1870f642965cdebf18ddfd63576d 100644 --- a/src/WebSocketRPCResponderInterface.php +++ b/src/WebSocketRPCResponderInterface.php @@ -4,18 +4,36 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -use Distantmagic\Resonance\InputValidatedData\RPCMessage; - -interface WebSocketRPCResponderInterface +/** + * @template TPayload + */ +interface WebSocketRPCResponderInterface extends JsonSchemaSourceInterface { + public function onBeforeMessage( + WebSocketAuthResolution $webSocketAuthResolution, + WebSocketConnection $webSocketConnection, + ): void; + public function onClose( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, ): void; - public function respond( + /** + * @param RPCNotification<TPayload> $rpcNotification + */ + public function onNotification( + WebSocketAuthResolution $webSocketAuthResolution, + WebSocketConnection $webSocketConnection, + RPCNotification $rpcNotification, + ): void; + + /** + * @param RPCRequest<TPayload> $rpcRequest + */ + public function onRequest( WebSocketAuthResolution $webSocketAuthResolution, WebSocketConnection $webSocketConnection, - RPCMessage $rpcMessage, + RPCRequest $rpcRequest, ): void; }