From 8b9923b903b101f7547eb7301ecc046996e40836 Mon Sep 17 00:00:00 2001 From: Mateusz Charytoniuk <mateusz.charytoniuk@protonmail.com> Date: Tue, 6 Feb 2024 10:58:44 +0100 Subject: [PATCH] feat: switch to constraints --- composer.json | 1 - composer.lock | 397 ++++-------------- config.ini.example | 3 + docs/pages/docs/changelog/index.md | 4 + .../docs/features/configuration/index.md | 36 +- docs/pages/docs/features/validation/index.md | 31 +- .../docs/features/websockets/protocols.md | 9 +- .../index.md | 20 +- .../session-based-authentication/index.md | 33 +- src/Attribute/RespondsWith.php | 4 +- src/Command/TestHttpResponders.php | 20 +- src/Constraint.php | 5 + src/Constraint/AnyOfConstraint.php | 101 +++++ src/Constraint/AnyOfConstraintTest.php | 73 ++++ src/Constraint/ListConstraint.php | 125 ++++++ src/Constraint/ListConstraintTest.php | 91 ++++ src/Constraint/MapConstraint.php | 2 +- src/Constraint/ObjectConstraint.php | 63 ++- src/Constraint/ObjectConstraintTest.php | 126 +++--- src/ConstraintReason.php | 1 + src/ConstraintResultErrorMessage.php | 37 ++ src/ConstraintStringFormat.php | 1 + src/ConstraintValidationException.php | 14 +- .../ValidatedRequestResolver.php | 2 +- src/InputValidationResult.php | 21 +- src/InputValidator.php | 2 +- src/InputValidator/FrontMatterValidator.php | 124 ++---- src/InputValidator/RPCMessageValidator.php | 30 +- src/InputValidatorController.php | 32 +- src/JsonSchema.php | 20 - src/JsonSchemaSourceInterface.php | 10 - src/JsonSchemaValidationErrorMessage.php | 37 -- src/JsonSchemaValidationException.php | 19 - src/JsonSchemaValidationResult.php | 22 - src/JsonSchemaValidator.php | 177 -------- src/JsonSchemableInterface.php | 4 +- .../RespondsWithExtractor.php | 2 +- src/OpenAPIReusableSchemaCollection.php | 18 +- .../DoctrineEntityRouteParameterExtractor.php | 25 +- .../ValidatedRequestExtractor.php | 4 +- src/OpenAPISchemaComponents.php | 6 +- src/OpenAPISchemaParameter.php | 4 +- src/OpenAPISchemaRequestBodyContent.php | 6 +- src/OpenAPISchemaResponse.php | 6 +- .../ApplicationConfigurationProvider.php | 14 +- .../DatabaseConfigurationProvider.php | 26 +- .../LlamaCppConfigurationProvider.php | 16 +- .../MailerConfigurationProvider.php | 18 +- .../OAuth2ConfigurationProvider.php | 20 +- .../OpenAPIConfigurationProvider.php | 12 +- .../RedisConfigurationProvider.php | 22 +- .../SQLiteVSSConfigurationProvider.php | 10 +- .../SessionConfigurationProvider.php | 14 +- .../StaticPageConfigurationProvider.php | 16 +- .../SwooleConfigurationProvider.php | 20 +- .../TranslatorConfigurationProvider.php | 10 +- .../WebSocketConfigurationProvider.php | 13 +- .../RPCProtocolController.php | 11 +- src/WebSocketRPCConnectionHandle.php | 19 +- src/WebSocketRPCResponderInterface.php | 2 +- 60 files changed, 928 insertions(+), 1083 deletions(-) create mode 100644 src/Constraint/AnyOfConstraint.php create mode 100644 src/Constraint/AnyOfConstraintTest.php create mode 100644 src/Constraint/ListConstraint.php create mode 100644 src/Constraint/ListConstraintTest.php create mode 100644 src/ConstraintResultErrorMessage.php delete mode 100644 src/JsonSchema.php delete mode 100644 src/JsonSchemaSourceInterface.php delete mode 100644 src/JsonSchemaValidationErrorMessage.php delete mode 100644 src/JsonSchemaValidationException.php delete mode 100644 src/JsonSchemaValidationResult.php delete mode 100644 src/JsonSchemaValidator.php diff --git a/composer.json b/composer.json index 913aca54..78bab0aa 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,6 @@ "webonyx/graphql-php": "^15.6", "dragonmantank/cron-expression": "^3.3", "league/oauth2-client": "^2.7", - "opis/json-schema": "^2.3", "symfony/mailer": "^7.0", "symfony/messenger": "^7.0", "symfony/http-client": "^7.0" diff --git a/composer.lock b/composer.lock index d2f62758..646bb09b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6086fb3c49229272d9af22c93b741be1", + "content-hash": "4d012e211f64a4200dd92f58b584976a", "packages": [ { "name": "defuse/php-encryption", @@ -492,16 +492,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.0", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9" + "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/d244f2e6e6bf32bff5174e6729b57214923ecec9", - "reference": "d244f2e6e6bf32bff5174e6729b57214923ecec9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c9ea252cdce4da324ede3d6c5913dd89f769afd2", + "reference": "c9ea252cdce4da324ede3d6c5913dd89f769afd2", "shasum": "" }, "require": { @@ -517,9 +517,9 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.56", + "phpstan/phpstan": "1.10.57", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.15", + "phpunit/phpunit": "9.6.16", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", "squizlabs/php_codesniffer": "3.8.1", @@ -585,7 +585,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.0" + "source": "https://github.com/doctrine/dbal/tree/3.8.1" }, "funding": [ { @@ -601,7 +601,7 @@ "type": "tidelift" } ], - "time": "2024-01-25T21:44:02+00:00" + "time": "2024-02-03T17:33:49+00:00" }, { "name": "doctrine/deprecations", @@ -904,28 +904,27 @@ }, { "name": "doctrine/lexer", - "version": "2.1.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { @@ -962,7 +961,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.0" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -978,7 +977,7 @@ "type": "tidelift" } ], - "time": "2022-12-14T08:49:07+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "doctrine/migrations", @@ -1084,16 +1083,16 @@ }, { "name": "doctrine/orm", - "version": "2.17.4", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/doctrine/orm.git", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d" + "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/orm/zipball/ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", - "reference": "ccfc97c32f63aaa0988ac6aa42e71c5590bb794d", + "url": "https://api.github.com/repos/doctrine/orm/zipball/f2176a9ce56cafdfd1624d54bfdb076819083d5b", + "reference": "f2176a9ce56cafdfd1624d54bfdb076819083d5b", "shasum": "" }, "require": { @@ -1106,7 +1105,7 @@ "doctrine/event-manager": "^1.2 || ^2", "doctrine/inflector": "^1.4 || ^2.0", "doctrine/instantiator": "^1.3 || ^2", - "doctrine/lexer": "^2", + "doctrine/lexer": "^2 || ^3", "doctrine/persistence": "^2.4 || ^3", "ext-ctype": "*", "php": "^7.1 || ^8.0", @@ -1142,7 +1141,7 @@ "type": "library", "autoload": { "psr-4": { - "Doctrine\\ORM\\": "lib/Doctrine/ORM" + "Doctrine\\ORM\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1179,9 +1178,9 @@ ], "support": { "issues": "https://github.com/doctrine/orm/issues", - "source": "https://github.com/doctrine/orm/tree/2.17.4" + "source": "https://github.com/doctrine/orm/tree/2.18.0" }, - "time": "2024-01-26T19:41:16+00:00" + "time": "2024-01-31T15:53:12+00:00" }, { "name": "doctrine/persistence", @@ -1986,16 +1985,16 @@ }, { "name": "league/commonmark", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5" + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/3669d6d5f7a47a93c08ddff335e6d945481a1dd5", - "reference": "3669d6d5f7a47a93c08ddff335e6d945481a1dd5", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", "shasum": "" }, "require": { @@ -2008,7 +2007,7 @@ }, "require-dev": { "cebe/markdown": "^1.0", - "commonmark/cmark": "0.30.0", + "commonmark/cmark": "0.30.3", "commonmark/commonmark.js": "0.30.0", "composer/package-versions-deprecated": "^1.8", "embed/embed": "^4.4", @@ -2018,10 +2017,10 @@ "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", "scrutinizer/ocular": "^1.8.1", - "symfony/finder": "^5.3 | ^6.0", - "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", "unleashedtech/php-coding-standard": "^3.1.1", "vimeo/psalm": "^4.24.0 || ^5.0.0" }, @@ -2088,7 +2087,7 @@ "type": "tidelift" } ], - "time": "2023-08-30T16:55:00+00:00" + "time": "2024-02-02T11:59:32+00:00" }, { "name": "league/config", @@ -2973,196 +2972,6 @@ ], "time": "2023-11-08T09:30:43+00:00" }, - { - "name": "opis/json-schema", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/opis/json-schema.git", - "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/json-schema/zipball/c48df6d7089a45f01e1c82432348f2d5976f9bfb", - "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb", - "shasum": "" - }, - "require": { - "ext-json": "*", - "opis/string": "^2.0", - "opis/uri": "^1.0", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "ext-bcmath": "*", - "ext-intl": "*", - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\JsonSchema\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - }, - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - } - ], - "description": "Json Schema Validator for PHP", - "homepage": "https://opis.io/json-schema", - "keywords": [ - "json", - "json-schema", - "schema", - "validation", - "validator" - ], - "support": { - "issues": "https://github.com/opis/json-schema/issues", - "source": "https://github.com/opis/json-schema/tree/2.3.0" - }, - "time": "2022-01-08T20:38:03+00:00" - }, - { - "name": "opis/string", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/opis/string.git", - "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/string/zipball/9ebf1a1f873f502f6859d11210b25a4bf5d141e7", - "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "ext-json": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\String\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "Multibyte strings as objects", - "homepage": "https://opis.io/string", - "keywords": [ - "multi-byte", - "opis", - "string", - "string manipulation", - "utf-8" - ], - "support": { - "issues": "https://github.com/opis/string/issues", - "source": "https://github.com/opis/string/tree/2.0.1" - }, - "time": "2022-01-14T15:42:23+00:00" - }, - { - "name": "opis/uri", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/opis/uri.git", - "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", - "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", - "shasum": "" - }, - "require": { - "opis/string": "^2.0", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\Uri\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "Build, parse and validate URIs and URI-templates", - "homepage": "https://opis.io", - "keywords": [ - "URI Template", - "parse url", - "punycode", - "uri", - "uri components", - "url", - "validate uri" - ], - "support": { - "issues": "https://github.com/opis/uri/issues", - "source": "https://github.com/opis/uri/tree/1.1.0" - }, - "time": "2021-05-22T15:57:08+00:00" - }, { "name": "paragonie/random_compat", "version": "v9.99.100", @@ -4833,16 +4642,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", - "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { @@ -4856,9 +4665,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4895,7 +4701,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -4911,20 +4717,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "875e90aeea2777b6f135677f618529449334a612" + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", - "reference": "875e90aeea2777b6f135677f618529449334a612", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", "shasum": "" }, "require": { @@ -4935,9 +4741,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -4976,7 +4779,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" }, "funding": [ { @@ -4992,20 +4795,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d" + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/ecaafce9f77234a6a449d29e49267ba10499116d", - "reference": "ecaafce9f77234a6a449d29e49267ba10499116d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", "shasum": "" }, "require": { @@ -5018,9 +4821,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5063,7 +4863,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" }, "funding": [ { @@ -5079,20 +4879,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:30:37+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", - "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", "shasum": "" }, "require": { @@ -5103,9 +4903,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5147,7 +4944,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" }, "funding": [ { @@ -5163,20 +4960,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "42292d99c55abe617799667f454222c54c60e229" + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", - "reference": "42292d99c55abe617799667f454222c54c60e229", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { @@ -5190,9 +4987,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5230,7 +5024,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -5246,20 +5040,20 @@ "type": "tidelift" } ], - "time": "2023-07-28T09:04:16+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179" + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/70f4aebd92afca2f865444d30a4d2151c13c3179", - "reference": "70f4aebd92afca2f865444d30a4d2151c13c3179", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", "shasum": "" }, "require": { @@ -5267,9 +5061,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5306,7 +5097,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" }, "funding": [ { @@ -5322,20 +5113,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5" + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/6caa57379c4aec19c0a12a38b59b26487dcfe4b5", - "reference": "6caa57379c4aec19c0a12a38b59b26487dcfe4b5", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { @@ -5343,9 +5134,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5389,7 +5177,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -5405,20 +5193,20 @@ "type": "tidelift" } ], - "time": "2023-01-26T09:26:14+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11" + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", - "reference": "b0f46ebbeeeda3e9d2faebdfbf4b4eae9b59fa11", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", "shasum": "" }, "require": { @@ -5427,9 +5215,6 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "1.28-dev" - }, "thanks": { "name": "symfony/polyfill", "url": "https://github.com/symfony/polyfill" @@ -5469,7 +5254,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.28.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" }, "funding": [ { @@ -5485,7 +5270,7 @@ "type": "tidelift" } ], - "time": "2023-08-16T06:22:46+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { "name": "symfony/routing", @@ -6899,16 +6684,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.9", + "version": "10.5.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe" + "reference": "50b8e314b6d0dd06521dc31d1abffa73f25f850c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", - "reference": "0bd663704f0165c9e76fe4f06ffa6a1ca727fdbe", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50b8e314b6d0dd06521dc31d1abffa73f25f850c", + "reference": "50b8e314b6d0dd06521dc31d1abffa73f25f850c", "shasum": "" }, "require": { @@ -6980,7 +6765,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.9" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.10" }, "funding": [ { @@ -6996,7 +6781,7 @@ "type": "tidelift" } ], - "time": "2024-01-22T14:35:40+00:00" + "time": "2024-02-04T09:07:51+00:00" }, { "name": "sebastian/cli-parser", diff --git a/config.ini.example b/config.ini.example index abd506bb..c9be3544 100644 --- a/config.ini.example +++ b/config.ini.example @@ -81,3 +81,6 @@ ssl_key_file = ssl/origin.key [translator] base_directory = app/lang default_primary_language = en + +[websocket] +max_connections = 10000 diff --git a/docs/pages/docs/changelog/index.md b/docs/pages/docs/changelog/index.md index a8f562ef..949a7642 100644 --- a/docs/pages/docs/changelog/index.md +++ b/docs/pages/docs/changelog/index.md @@ -10,6 +10,10 @@ title: Changelog # Changelog +## v0.19.0 + +- Use Json-Schema serializable constraints + ## v0.18.0 - Feature: add {{docs/features/mail/index}} diff --git a/docs/pages/docs/features/configuration/index.md b/docs/pages/docs/features/configuration/index.md index e79f7dc8..97e54d83 100644 --- a/docs/pages/docs/features/configuration/index.md +++ b/docs/pages/docs/features/configuration/index.md @@ -196,7 +196,7 @@ readonly class ManifestConfiguration ``` Then you need to define the configuration provider. -[JSON Schema](https://json-schema.org/) is used for config validation: +Constraints schema is used for config validation: ```php <?php @@ -205,11 +205,13 @@ namespace App\SingletonProvider\ConfigurationProvider; use App\ManifestConfiguration; use Distantmagic\Resonance\Attribute\Singleton; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\ObjectConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; /** - * @template-extends ConfigurationProvider<ManifestConfiguration, object{ + * @template-extends ConfigurationProvider<ManifestConfiguration, array{ * background_color: string, * theme_color: string, * }> @@ -217,34 +219,24 @@ use Distantmagic\Resonance\SingletonProvider\ConfigurationProvider; #[Singleton(provides: ManifestConfiguration::class)] final readonly class ManifestConfigurationProvider extends ConfigurationProvider { - protected function getConfigurationKey(): string + public function getConstraint(): Constraint { - return 'manifest'; + return new ObjectConstraint([ + 'background_color' => new StringConstraint(), + 'theme_color' => new StringConstraint(), + ]); } - protected function getSchema(): JsonSchema + protected function getConfigurationKey(): string { - return new JsonSchema([ - 'type' => 'object', - 'properties' => [ - 'background_color' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'theme_color' => [ - 'type' => 'string', - 'minLength' => 1, - ] - ], - 'required' => ['background_color', 'theme_color'] - ]); + return 'manifest'; } protected function provideConfiguration($validatedData): ManifestConfiguration { return new ManifestConfiguration( - backgroundColor: $validatedData->background_color, - themeColor: $validatedData->theme_color, + backgroundColor: $validatedData['background_color'], + themeColor: $validatedData['theme_color'], ); } } diff --git a/docs/pages/docs/features/validation/index.md b/docs/pages/docs/features/validation/index.md index 5f8df33b..f8b57462 100644 --- a/docs/pages/docs/features/validation/index.md +++ b/docs/pages/docs/features/validation/index.md @@ -47,8 +47,7 @@ readonly class BlogPostForm extends InputValidatedData ## Validators Validators take in any data and check if it adheres to the configuration -schema. The `getSchema()` method must return a -[JSON Schema](https://json-schema.org/) object. +schema. The `getConstraint()` method must return a constraints object. ```php <?php @@ -57,8 +56,10 @@ namespace App\InputValidator; use App\InputValidatedData\BlogPostForm; use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\ObjectConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\InputValidator; -use Distantmagic\Resonance\JsonSchema; use Distantmagic\Resonance\SingletonCollection; /** @@ -78,21 +79,11 @@ readonly class BlogPostFormValidator extends InputValidator ); } - public function getSchema(): JsonSchema + public function getConstraint(): Constraint { - return new JsonSchema([ - 'type' => 'object', - 'properties' => [ - 'content' => [ - 'type' => 'string', - 'minLength' => 1 - ], - 'title' => [ - 'type' => 'string', - 'minLength' => 1 - ] - ], - 'required' => ['content', 'title'] + return new ObjectConstraint([ + 'content' => new StringConstraint(), + 'title' => new StringConstraint() ]); } } @@ -106,11 +97,9 @@ parameters manually. Then you can call their `validateData()` method. <?php use Distantmagic\Resonance\InputValidatorController; -use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\InputValidationResult; -$jsonSchemaValidator = new JsonSchemaValidator(); -$inputValidatorController = new InputValidatorController($jsonSchemaValidator); +$inputValidatorController = new InputValidatorController(); /** * @var InputValidationResult $validationResult @@ -122,7 +111,7 @@ $validationResult = $inputValidatorController->validateData($blogPostFormValidat // If validation is successful, the errors list is empty and validation data // is set. -assert($validationResult->errors->isEmpty()); +assert($validationResult->constraintResult->getErrors()->isEmpty()); /** * It's null if validation failed. diff --git a/docs/pages/docs/features/websockets/protocols.md b/docs/pages/docs/features/websockets/protocols.md index 438f3c88..887353e0 100644 --- a/docs/pages/docs/features/websockets/protocols.md +++ b/docs/pages/docs/features/websockets/protocols.md @@ -65,7 +65,8 @@ use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Attribute\WantsFeature; use Distantmagic\Resonance\Feature; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\RPCRequest; use Distantmagic\Resonance\RPCResponse; use Distantmagic\Resonance\SingletonCollection; @@ -78,11 +79,9 @@ use Distantmagic\Resonance\WebSocketRPCResponder; #[WantsFeature(Feature::WebSocket)] final readonly class EchoResponder extends WebSocketRPCResponder { - public function getSchema(): JsonSchema + public function getConstraint(): Constraint { - return new JsonSchema([ - 'type' => 'string', - ]); + return new StringConstraint(); } public function onRequest( 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 61583877..0c2c4b42 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 @@ -202,7 +202,9 @@ use Distantmagic\Resonance\Attribute\RespondsToWebSocketRPC; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\Attribute\WantsFeature; use Distantmagic\Resonance\Feature; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\ObjectConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\LlamaCppClient; use Distantmagic\Resonance\LlamaCppCompletionRequest; use Distantmagic\Resonance\RPCNotification; @@ -222,20 +224,10 @@ final readonly class LlmChatPromptResponder extends WebSocketRPCResponder private LlamaCppClient $llamaCppClient, ) {} - public function getSchema(): JsonSchema + public function getConstraint(): Constraint { - return new JsonSchema([ - 'type' => 'object', - 'additionalProperties' => false, - 'properties' => [ - 'prompt' => [ - 'minLength' => 1, - 'type' => 'string', - ], - ], - 'required' => [ - 'prompt', - ], + return new ObjectConstraint([ + 'prompt' => new StringConstraint(), ]); } diff --git a/docs/pages/tutorials/session-based-authentication/index.md b/docs/pages/tutorials/session-based-authentication/index.md index 0725b59f..ed1aaa76 100644 --- a/docs/pages/tutorials/session-based-authentication/index.md +++ b/docs/pages/tutorials/session-based-authentication/index.md @@ -346,8 +346,7 @@ readonly class UsernamePassword extends InputValidatedData ``` We will use this input validator in the HTTP Responder. It uses -[JSON Schema](https://json-schema.org/) -to validate the incoming data: +constraints to validate the incoming data: ```php file:app/InputValidator/UsernamePasswordValidator.php <?php @@ -357,11 +356,13 @@ namespace App\InputValidator; use App\InputValidatedData\UsernamePassword; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\InputValidator; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\ObjectConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\SingletonCollection; /** - * @extends InputValidator<UsernamePassword, object{ + * @extends InputValidator<UsernamePassword, array{ * csrf: string, * username: string, * password: string, @@ -372,27 +373,15 @@ readonly class UsernamePasswordValidator extends InputValidator { public function castValidatedData(mixed $data): UsernamePassword { - return new UsernamePassword($data->username, $data->password); + return new UsernamePassword($data['username'], $data['password']); } - public function getSchema(): Schema + public function getSchema(): Constraint { - return new JsonSchema([ - 'type' => 'object', - 'properties' => [ - 'csrf' => [ - 'type' => 'string', - ], - 'username' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'password' => [ - 'type' => 'string', - 'minLength' => 1, - ] - ], - 'required' => ['csrf', 'username', 'password'] + return new ObjectConstraint([ + 'csrf' => (new StringConstraint())->optional(), + 'username' => new StringConstraint(), + 'password' => new StringConstraint(), ]); } } diff --git a/src/Attribute/RespondsWith.php b/src/Attribute/RespondsWith.php index fe0a8f80..78d8505b 100644 --- a/src/Attribute/RespondsWith.php +++ b/src/Attribute/RespondsWith.php @@ -6,8 +6,8 @@ namespace Distantmagic\Resonance\Attribute; use Attribute; use Distantmagic\Resonance\Attribute as BaseAttribute; +use Distantmagic\Resonance\Constraint; use Distantmagic\Resonance\ContentType; -use Distantmagic\Resonance\JsonSchema; #[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS)] final readonly class RespondsWith extends BaseAttribute @@ -17,7 +17,7 @@ final readonly class RespondsWith extends BaseAttribute */ public function __construct( public ContentType $contentType, - public JsonSchema $jsonSchema, + public Constraint $constraint, public int $status, public ?string $description = null, ) {} diff --git a/src/Command/TestHttpResponders.php b/src/Command/TestHttpResponders.php index 0bd34c72..ef0e27a0 100644 --- a/src/Command/TestHttpResponders.php +++ b/src/Command/TestHttpResponders.php @@ -11,7 +11,6 @@ use Distantmagic\Resonance\HttpRecursiveResponder; use Distantmagic\Resonance\HttpResponderAggregate; use Distantmagic\Resonance\HttpResponderInterface; use Distantmagic\Resonance\InspectableSwooleResponse; -use Distantmagic\Resonance\JsonSchemaValidator; use Distantmagic\Resonance\SwooleCoroutineHelper; use Distantmagic\Resonance\TestableHttpResponseCollection; use Ds\Map; @@ -29,7 +28,6 @@ final class TestHttpResponders extends Command public function __construct( private readonly HttpRecursiveResponder $recursiveResponder, private readonly HttpResponderAggregate $httpResponderAggregate, - private readonly JsonSchemaValidator $jsonSchemaValidator, private readonly TestableHttpResponseCollection $testableHttpResponseCollection, ) { parent::__construct(); @@ -109,15 +107,12 @@ final class TestHttpResponders extends Command )); } - $jsonSchemaValidationResult = $this - ->jsonSchemaValidator - ->validateSchema( - $respondsWith->jsonSchema, - $response->mockGetCastedContent(), - ) + $constraintResult = $respondsWith + ->constraint + ->validate($response->mockGetCastedContent()) ; - if (empty($jsonSchemaValidationResult->errors)) { + if ($constraintResult->status->isValid()) { $output->writeln('ok'); return true; @@ -125,10 +120,9 @@ final class TestHttpResponders extends Command $output->writeln('<error>error</error>'); - foreach ($jsonSchemaValidationResult->errors as $path => $errors) { - foreach ($errors as $error) { - $output->writeln(sprintf('%s -> %s', $path, $error)); - } + foreach ($constraintResult->getErrors() as $path => $error) { + $output->writeln(sprintf('%s -> %s', $path, $error)); + $output->writeln(print_r($constraintResult->castedData, true)); } return false; diff --git a/src/Constraint.php b/src/Constraint.php index 75be005a..7cc6f036 100644 --- a/src/Constraint.php +++ b/src/Constraint.php @@ -33,6 +33,11 @@ abstract readonly class Constraint implements JsonSchemableInterface public bool $isRequired = true, ) {} + public function jsonSerialize(): array|object + { + return $this->toJsonSchema(); + } + /** * @return PJsonSchema */ diff --git a/src/Constraint/AnyOfConstraint.php b/src/Constraint/AnyOfConstraint.php new file mode 100644 index 00000000..5fb62b8f --- /dev/null +++ b/src/Constraint/AnyOfConstraint.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\ConstraintDefaultValue; +use Distantmagic\Resonance\ConstraintPath; +use Distantmagic\Resonance\ConstraintReason; +use Distantmagic\Resonance\ConstraintResult; +use Distantmagic\Resonance\ConstraintResultStatus; +use RuntimeException; + +final readonly class AnyOfConstraint extends Constraint +{ + /** + * @param array<Constraint> $anyOf + */ + public function __construct( + public array $anyOf, + ?ConstraintDefaultValue $defaultValue = null, + bool $isNullable = false, + bool $isRequired = true, + ) { + parent::__construct( + defaultValue: $defaultValue, + isNullable: $isNullable, + isRequired: $isRequired, + ); + } + + public function default(mixed $defaultValue): self + { + return new self( + anyOf: $this->anyOf, + defaultValue: new ConstraintDefaultValue($defaultValue), + isNullable: $this->isNullable, + isRequired: $this->isRequired, + ); + } + + public function nullable(): never + { + throw new RuntimeException('AnyOf constraint cannot be nullable'); + } + + public function optional(): self + { + return new self( + anyOf: $this->anyOf, + defaultValue: $this->defaultValue, + isNullable: $this->isNullable, + isRequired: false, + ); + } + + protected function doConvertToJsonSchema(): array + { + /** + * @var list<array> $anyOf + */ + $anyOf = []; + + foreach ($this->anyOf as $constraint) { + $anyOf[] = $constraint->toJsonSchema(); + } + + /** + * @var array{ + * anyOf: list<array> + * } + */ + return [ + 'anyOf' => $anyOf, + ]; + } + + protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult + { + foreach ($this->anyOf as $constraint) { + $constraintResult = $constraint->validate($notValidatedData, $path); + + if ($constraintResult->status->isValid()) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::Ok, + status: ConstraintResultStatus::Valid, + ); + } + } + + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::InvalidNestedConstraint, + status: ConstraintResultStatus::Invalid, + ); + } +} diff --git a/src/Constraint/AnyOfConstraintTest.php b/src/Constraint/AnyOfConstraintTest.php new file mode 100644 index 00000000..3e08d56e --- /dev/null +++ b/src/Constraint/AnyOfConstraintTest.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + * + * @internal + */ +final class AnyOfConstraintTest extends TestCase +{ + public function test_is_converted_optionally_to_json_schema(): void + { + $constraint = new AnyOfConstraint( + anyOf: [ + new StringConstraint(), + new IntegerConstraint(), + ] + ); + + self::assertEquals([ + 'anyOf' => [ + [ + 'type' => 'string', + 'minLength' => 1, + ], + [ + 'type' => 'integer', + ], + ], + ], $constraint->optional()->toJsonSchema()); + } + + public function test_is_converted_to_json_schema(): void + { + $constraint = new AnyOfConstraint( + anyOf: [ + new StringConstraint(), + new IntegerConstraint(), + ] + ); + + self::assertEquals([ + 'anyOf' => [ + [ + 'type' => 'string', + 'minLength' => 1, + ], + [ + 'type' => 'integer', + ], + ], + ], $constraint->optional()->toJsonSchema()); + } + + public function test_validates(): void + { + $constraint = new AnyOfConstraint( + anyOf: [ + new StringConstraint(), + new IntegerConstraint(), + ] + ); + + self::assertTrue($constraint->validate('hi')->status->isValid()); + self::assertTrue($constraint->validate(5)->status->isValid()); + self::assertFalse($constraint->validate(false)->status->isValid()); + } +} diff --git a/src/Constraint/ListConstraint.php b/src/Constraint/ListConstraint.php new file mode 100644 index 00000000..8ea6c9dd --- /dev/null +++ b/src/Constraint/ListConstraint.php @@ -0,0 +1,125 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\ConstraintDefaultValue; +use Distantmagic\Resonance\ConstraintPath; +use Distantmagic\Resonance\ConstraintReason; +use Distantmagic\Resonance\ConstraintResult; +use Distantmagic\Resonance\ConstraintResultStatus; + +final readonly class ListConstraint extends Constraint +{ + public function __construct( + public Constraint $valueConstraint, + ConstraintDefaultValue $defaultValue = new ConstraintDefaultValue([]), + bool $isNullable = false, + bool $isRequired = true, + ) { + parent::__construct( + defaultValue: $defaultValue, + isNullable: $isNullable, + isRequired: $isRequired, + ); + } + + public function default(mixed $defaultValue): self + { + return new self( + valueConstraint: $this->valueConstraint, + defaultValue: new ConstraintDefaultValue($defaultValue), + isNullable: $this->isNullable, + isRequired: $this->isRequired, + ); + } + + public function nullable(): self + { + return new self( + valueConstraint: $this->valueConstraint, + defaultValue: new ConstraintDefaultValue(null), + isNullable: true, + isRequired: $this->isRequired, + ); + } + + public function optional(): self + { + return new self( + valueConstraint: $this->valueConstraint, + defaultValue: $this->defaultValue ?? new ConstraintDefaultValue([]), + isNullable: $this->isNullable, + isRequired: false, + ); + } + + protected function doConvertToJsonSchema(): array + { + return [ + 'type' => 'array', + 'items' => $this->valueConstraint->toJsonSchema(), + ]; + } + + protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult + { + if (!is_array($notValidatedData)) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path, + reason: ConstraintReason::InvalidDataType, + status: ConstraintResultStatus::Invalid, + ); + } + + $ret = []; + + $i = 0; + + /** + * @var list<ConstraintResult> + */ + $invalidChildStatuses = []; + + /** + * @var mixed $notValidatedValue explicitly mixed for typechecks + */ + foreach ($notValidatedData as $notValidatedValue) { + $childResult = $this->valueConstraint->validate( + notValidatedData: $notValidatedValue, + path: $path->fork((string) $i) + ); + + if ($childResult->status->isValid()) { + /** + * @var mixed explicitly mixed for typechecks + */ + $ret[] = $childResult->castedData; + } else { + $invalidChildStatuses[] = $childResult; + } + + ++$i; + } + + if (!empty($invalidChildStatuses)) { + return new ConstraintResult( + castedData: $notValidatedData, + nested: $invalidChildStatuses, + path: $path, + reason: ConstraintReason::InvalidNestedConstraint, + status: ConstraintResultStatus::Invalid, + ); + } + + return new ConstraintResult( + castedData: $ret, + path: $path, + reason: ConstraintReason::Ok, + status: ConstraintResultStatus::Valid, + ); + } +} diff --git a/src/Constraint/ListConstraintTest.php b/src/Constraint/ListConstraintTest.php new file mode 100644 index 00000000..fa81178b --- /dev/null +++ b/src/Constraint/ListConstraintTest.php @@ -0,0 +1,91 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance\Constraint; + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + * + * @internal + */ +final class ListConstraintTest extends TestCase +{ + public function test_is_converted_optionally_to_json_schema(): void + { + $constraint = new ListConstraint( + valueConstraint: new StringConstraint() + ); + self::assertEquals([ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'minLength' => 1, + ], + 'default' => [], + ], $constraint->optional()->toJsonSchema()); + } + + public function test_is_converted_to_json_schema(): void + { + $constraint = new ListConstraint( + valueConstraint: new StringConstraint() + ); + self::assertEquals([ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'minLength' => 1, + ], + 'default' => [], + ], $constraint->optional()->toJsonSchema()); + } + + public function test_nullable_is_converted_to_json_schema(): void + { + $constraint = new ListConstraint( + valueConstraint: new StringConstraint() + ); + self::assertEquals([ + 'type' => ['null', 'array'], + 'items' => [ + 'type' => 'string', + 'minLength' => 1, + ], + 'default' => null, + ], $constraint->nullable()->toJsonSchema()); + } + + public function test_validates_fail(): void + { + $constraint = new ListConstraint( + valueConstraint: new StringConstraint() + ); + + $validatedResult = $constraint->validate([ + 'hi', + 5, + ]); + self::assertFalse($validatedResult->status->isValid()); + self::assertEquals([ + '' => 'invalid_nested_constraint', + '1' => 'invalid_data_type', + ], $validatedResult->getErrors()->toArray()); + } + + public function test_validates_ok(): void + { + $constraint = new ListConstraint( + valueConstraint: new StringConstraint() + ); + + $validatedResult = $constraint->validate([ + 'hi', + 'foo', + ]); + + self::assertTrue($validatedResult->status->isValid()); + } +} diff --git a/src/Constraint/MapConstraint.php b/src/Constraint/MapConstraint.php index 837067b6..cad126f2 100644 --- a/src/Constraint/MapConstraint.php +++ b/src/Constraint/MapConstraint.php @@ -66,7 +66,7 @@ final readonly class MapConstraint extends Constraint protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult { - if (!is_array($notValidatedData)) { + if (!is_array($notValidatedData) && !is_object($notValidatedData)) { return new ConstraintResult( castedData: $notValidatedData, path: $path, diff --git a/src/Constraint/ObjectConstraint.php b/src/Constraint/ObjectConstraint.php index 3a24b775..ed1c636e 100644 --- a/src/Constraint/ObjectConstraint.php +++ b/src/Constraint/ObjectConstraint.php @@ -17,7 +17,8 @@ final readonly class ObjectConstraint extends Constraint * @param array<non-empty-string,Constraint> $properties */ public function __construct( - public array $properties, + public array $properties = [], + public bool $additionalProperties = false, ?ConstraintDefaultValue $defaultValue = null, bool $isNullable = false, bool $isRequired = true, @@ -29,9 +30,21 @@ final readonly class ObjectConstraint extends Constraint ); } + public function additionalProperties(bool $allow): self + { + return new self( + additionalProperties: $allow, + properties: $this->properties, + defaultValue: $this->defaultValue, + isNullable: $this->isNullable, + isRequired: $this->isRequired, + ); + } + public function default(mixed $defaultValue): self { return new self( + additionalProperties: $this->additionalProperties, properties: $this->properties, defaultValue: new ConstraintDefaultValue($defaultValue), isNullable: $this->isNullable, @@ -42,6 +55,7 @@ final readonly class ObjectConstraint extends Constraint public function nullable(): self { return new self( + additionalProperties: $this->additionalProperties, properties: $this->properties, defaultValue: $this->defaultValue ?? new ConstraintDefaultValue(null), isNullable: true, @@ -52,6 +66,7 @@ final readonly class ObjectConstraint extends Constraint public function optional(): self { return new self( + additionalProperties: $this->additionalProperties, properties: $this->properties, defaultValue: $this->defaultValue, isNullable: $this->isNullable, @@ -76,13 +91,34 @@ final readonly class ObjectConstraint extends Constraint 'type' => 'object', 'properties' => $convertedProperties, 'required' => $requiredProperties, - 'additionalProperties' => false, + 'additionalProperties' => $this->additionalProperties, ]; } + protected function getProperty(array|object $notValidatedData, string $propertyName): mixed + { + if (is_array($notValidatedData)) { + return $notValidatedData[$propertyName]; + } + + return $notValidatedData->{$propertyName}; + } + + protected function hasProperty(array|object $notValidatedData, string $propertyName): bool + { + if (is_array($notValidatedData)) { + return array_key_exists($propertyName, $notValidatedData); + } + + return property_exists($notValidatedData, $propertyName); + } + + /** + * @psalm-suppress UnusedForeachValue we need foreach here + */ protected function doValidate(mixed $notValidatedData, ConstraintPath $path): ConstraintResult { - if (!is_array($notValidatedData)) { + if (!is_array($notValidatedData) && !is_object($notValidatedData)) { return new ConstraintResult( castedData: $notValidatedData, path: $path, @@ -99,7 +135,7 @@ final readonly class ObjectConstraint extends Constraint $invalidChildStatuses = []; foreach ($this->properties as $name => $validator) { - if (!array_key_exists($name, $notValidatedData)) { + if (!$this->hasProperty($notValidatedData, $name)) { if ($validator->defaultValue) { /** * @var mixed explicitly mixed for typechecks @@ -115,7 +151,7 @@ final readonly class ObjectConstraint extends Constraint } } else { $childResult = $validator->validate( - notValidatedData: $notValidatedData[$name], + notValidatedData: $this->getProperty($notValidatedData, $name), path: $path->fork($name) ); @@ -140,6 +176,23 @@ final readonly class ObjectConstraint extends Constraint ); } + if (!$this->additionalProperties) { + /** + * @var array-key|string $notValidatedKey + * @var mixed $notValidatedValue explicitly mixed for typechecks + */ + foreach ($notValidatedData as $notValidatedKey => $notValidatedValue) { + if (!array_key_exists($notValidatedKey, $this->properties)) { + return new ConstraintResult( + castedData: $notValidatedData, + path: $path->fork((string) $notValidatedKey), + reason: ConstraintReason::UnexpectedProperty, + status: ConstraintResultStatus::Invalid, + ); + } + } + } + return new ConstraintResult( castedData: $ret, path: $path, diff --git a/src/Constraint/ObjectConstraintTest.php b/src/Constraint/ObjectConstraintTest.php index a28960ab..bcf082ed 100644 --- a/src/Constraint/ObjectConstraintTest.php +++ b/src/Constraint/ObjectConstraintTest.php @@ -15,12 +15,10 @@ final class ObjectConstraintTest extends TestCase { public function test_is_converted_optionally_to_json_schema(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => new StringConstraint(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => new StringConstraint(), + 'bbb' => new EnumConstraint(['foo']), + ]); self::assertEquals([ 'type' => 'object', 'properties' => [ @@ -40,12 +38,10 @@ final class ObjectConstraintTest extends TestCase public function test_is_converted_to_json_schema(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => new StringConstraint(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => new StringConstraint(), + 'bbb' => new EnumConstraint(['foo']), + ]); self::assertEquals([ 'type' => 'object', 'properties' => [ @@ -65,12 +61,10 @@ final class ObjectConstraintTest extends TestCase public function test_is_converted_to_json_schema_with_optionals(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => (new StringConstraint())->optional(), - 'bbb' => (new EnumConstraint(['foo']))->default(null), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => (new StringConstraint())->optional(), + 'bbb' => (new EnumConstraint(['foo']))->default(null), + ]); self::assertEquals([ 'type' => 'object', 'properties' => [ @@ -91,12 +85,10 @@ final class ObjectConstraintTest extends TestCase public function test_nullable_is_converted_to_json_schema(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => new StringConstraint(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => new StringConstraint(), + 'bbb' => new EnumConstraint(['foo']), + ]); self::assertEquals([ 'type' => ['null', 'object'], 'properties' => [ @@ -115,14 +107,38 @@ final class ObjectConstraintTest extends TestCase ], $constraint->nullable()->toJsonSchema()); } + public function test_validates_additional_properties_failure(): void + { + $constraint = new ObjectConstraint(additionalProperties: false); + + $validatedResult = $constraint->validate([ + 'aaa' => 'hi', + 'bbb' => 'foo', + 'cc' => 'bar', + ]); + + self::assertFalse($validatedResult->status->isValid()); + } + + public function test_validates_additional_properties_ok(): void + { + $constraint = new ObjectConstraint(additionalProperties: true); + + $validatedResult = $constraint->validate([ + 'aaa' => 'hi', + 'bbb' => 'foo', + 'cc' => 'bar', + ]); + + self::assertTrue($validatedResult->status->isValid()); + } + public function test_validates_defaults(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => (new StringConstraint())->default('hi'), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => (new StringConstraint())->default('hi'), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'bbb' => 'foo', @@ -137,12 +153,10 @@ final class ObjectConstraintTest extends TestCase public function test_validates_fail(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => new StringConstraint(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => new StringConstraint(), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'aaa' => 'hi', @@ -157,12 +171,10 @@ final class ObjectConstraintTest extends TestCase public function test_validates_nullable(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => (new StringConstraint())->nullable(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => (new StringConstraint())->nullable(), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'bbb' => 'foo', @@ -177,12 +189,10 @@ final class ObjectConstraintTest extends TestCase public function test_validates_nullable_null(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => (new StringConstraint())->nullable(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => (new StringConstraint())->nullable(), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'aaa' => null, @@ -198,12 +208,10 @@ final class ObjectConstraintTest extends TestCase public function test_validates_ok(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => new StringConstraint(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => new StringConstraint(), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'aaa' => 'hi', @@ -215,12 +223,10 @@ final class ObjectConstraintTest extends TestCase public function test_validates_optional(): void { - $constraint = new ObjectConstraint( - properties: [ - 'aaa' => (new StringConstraint())->optional(), - 'bbb' => new EnumConstraint(['foo']), - ] - ); + $constraint = new ObjectConstraint([ + 'aaa' => (new StringConstraint())->optional(), + 'bbb' => new EnumConstraint(['foo']), + ]); $validatedResult = $constraint->validate([ 'bbb' => 'foo', diff --git a/src/ConstraintReason.php b/src/ConstraintReason.php index bc858543..0c990ff2 100644 --- a/src/ConstraintReason.php +++ b/src/ConstraintReason.php @@ -12,4 +12,5 @@ enum ConstraintReason: string case InvalidNestedConstraint = 'invalid_nested_constraint'; case MissingProperty = 'missing_property'; case Ok = 'ok'; + case UnexpectedProperty = 'unexpected_property'; } diff --git a/src/ConstraintResultErrorMessage.php b/src/ConstraintResultErrorMessage.php new file mode 100644 index 00000000..0b6c2d75 --- /dev/null +++ b/src/ConstraintResultErrorMessage.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Distantmagic\Resonance; + +use Stringable; + +readonly class ConstraintResultErrorMessage implements Stringable +{ + public string $message; + + public function __construct( + string $constraintId, + ConstraintResult $constraintResult, + ) { + $errors = $constraintResult->getErrors(); + $message = []; + + foreach ($errors as $name => $errorCode) { + $message[] = sprintf('"%s" -> %s', $name, $errorCode); + } + + // var_dump($constraintResult->castedData); + + $this->message = sprintf( + "%s:\n%s", + $constraintId, + implode("\n", $message), + ); + } + + public function __toString(): string + { + return $this->message; + } +} diff --git a/src/ConstraintStringFormat.php b/src/ConstraintStringFormat.php index 4df07ee7..ef7cb307 100644 --- a/src/ConstraintStringFormat.php +++ b/src/ConstraintStringFormat.php @@ -6,5 +6,6 @@ namespace Distantmagic\Resonance; enum ConstraintStringFormat { + case Mail; case Uuid; } diff --git a/src/ConstraintValidationException.php b/src/ConstraintValidationException.php index e09148d4..878b3982 100644 --- a/src/ConstraintValidationException.php +++ b/src/ConstraintValidationException.php @@ -12,19 +12,9 @@ class ConstraintValidationException extends RuntimeException string $constraintId, ConstraintResult $constraintResult, ) { - $errors = $constraintResult->getErrors(); - $message = []; - - foreach ($errors as $name => $errorCode) { - $message[] = sprintf('"%s" -> %s', $name, $errorCode); - } - - var_dump($constraintResult->castedData); - - parent::__construct(sprintf( - "%s:\n%s", + parent::__construct((string) new ConstraintResultErrorMessage( $constraintId, - implode("\n", $message), + $constraintResult, )); } } diff --git a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php index ea2e03fc..23b2bade 100644 --- a/src/HttpControllerParameterResolver/ValidatedRequestResolver.php +++ b/src/HttpControllerParameterResolver/ValidatedRequestResolver.php @@ -62,7 +62,7 @@ readonly class ValidatedRequestResolver extends HttpControllerParameterResolver return new HttpControllerParameterResolution( HttpControllerParameterResolutionStatus::ValidationErrors, - $validationResult->errors, + $validationResult->constraintResult->getErrors(), ); } } diff --git a/src/InputValidationResult.php b/src/InputValidationResult.php index 36c9ac51..2e9814af 100644 --- a/src/InputValidationResult.php +++ b/src/InputValidationResult.php @@ -4,29 +4,24 @@ declare(strict_types=1); namespace Distantmagic\Resonance; -use Ds\Map; -use Ds\Set; - /** * @template TValidatedModel of InputValidatedData */ readonly class InputValidationResult { - /** - * @var Map<non-empty-string,Set<non-empty-string>> $errors - */ - public Map $errors; - /** * @param null|TValidatedModel $inputValidatedData */ - public function __construct(public ?InputValidatedData $inputValidatedData = null) - { - $this->errors = new Map(); - } + public function __construct( + public ?InputValidatedData $inputValidatedData, + public ConstraintResult $constraintResult, + ) {} public function getErrorMessage(): string { - return $this->errors->values()->join("\n"); + return (string) (new ConstraintResultErrorMessage( + 'input_validation_result', + $this->constraintResult, + )); } } diff --git a/src/InputValidator.php b/src/InputValidator.php index 9ea2e3e6..65c2eaeb 100644 --- a/src/InputValidator.php +++ b/src/InputValidator.php @@ -10,4 +10,4 @@ namespace Distantmagic\Resonance; * * @template-implements CastsValidatedDataInterface<TCastedData,TValidatedData> */ -abstract readonly class InputValidator implements CastsValidatedDataInterface, JsonSchemaSourceInterface {} +abstract readonly class InputValidator implements CastsValidatedDataInterface, ConstraintSourceInterface {} diff --git a/src/InputValidator/FrontMatterValidator.php b/src/InputValidator/FrontMatterValidator.php index 0737d644..eb1aebeb 100644 --- a/src/InputValidator/FrontMatterValidator.php +++ b/src/InputValidator/FrontMatterValidator.php @@ -5,17 +5,23 @@ declare(strict_types=1); namespace Distantmagic\Resonance\InputValidator; use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\AnyOfConstraint; +use Distantmagic\Resonance\Constraint\BooleanConstraint; +use Distantmagic\Resonance\Constraint\EnumConstraint; +use Distantmagic\Resonance\Constraint\ListConstraint; +use Distantmagic\Resonance\Constraint\ObjectConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\FrontMatterCollectionReference; use Distantmagic\Resonance\InputValidatedData\FrontMatter; use Distantmagic\Resonance\InputValidator; -use Distantmagic\Resonance\JsonSchema; use Distantmagic\Resonance\StaticPageContentType; use Generator; use RuntimeException; /** - * @extends InputValidator<FrontMatter, object{ - * collections: array<non-empty-string|object{ name: non-empty-string, next: non-empty-string }>, + * @extends InputValidator<FrontMatter, array{ + * collections: array<non-empty-string|array{ name: non-empty-string, next: non-empty-string }>, * content_type: non-empty-string, * description: non-empty-string, * draft: bool, @@ -31,15 +37,15 @@ readonly class FrontMatterValidator extends InputValidator { public function castValidatedData(mixed $data): FrontMatter { - $collections = iterator_to_array($this->normalizeDataCollections($data->collections)); + $collections = iterator_to_array($this->normalizeDataCollections($data['collections'])); - $description = trim($data->description); + $description = trim($data['description']); if (empty($description)) { throw new RuntimeException('Description cannot be empty'); } - $title = trim($data->title); + $title = trim($data['title']); if (empty($title)) { throw new RuntimeException('Title cannot be empty'); @@ -47,94 +53,46 @@ readonly class FrontMatterValidator extends InputValidator return new FrontMatter( collections: $collections, - contentType: StaticPageContentType::from($data->content_type), + contentType: StaticPageContentType::from($data['content_type']), description: $description, - isDraft: $data->draft, - layout: $data->layout, - next: $data->next ?? null, - parent: $data->parent ?? null, - registerStylesheets: $data->register_stylesheets, + isDraft: $data['draft'], + layout: $data['layout'], + next: $data['next'] ?? null, + parent: $data['parent'] ?? null, + registerStylesheets: $data['register_stylesheets'], title: $title, ); } - public function getSchema(): JsonSchema + public function getConstraint(): Constraint { $contentTypes = StaticPageContentType::values(); - return new JsonSchema([ - 'type' => 'object', - 'properties' => [ - 'collections' => [ - 'type' => 'array', - 'items' => [ - 'anyOf' => [ - [ - 'type' => 'string', - 'minLength' => 1, - ], - [ - 'type' => 'object', - 'properties' => [ - 'name' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'next' => [ - 'type' => 'string', - 'minLength' => 1, - ], - ], - 'required' => ['name', 'next'], - ], + return new ObjectConstraint([ + 'collections' => new ListConstraint( + valueConstraint: new AnyOfConstraint([ + new StringConstraint(), + new ObjectConstraint( + properties: [ + 'name' => new StringConstraint(), + 'next' => new StringConstraint(), ], - ], - 'default' => [], - ], - 'content_type' => [ - 'type' => 'string', - 'enum' => $contentTypes, - 'default' => StaticPageContentType::Markdown->value, - ], - 'description' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'draft' => [ - 'type' => 'boolean', - 'default' => false, - ], - 'layout' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'next' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'parent' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'register_stylesheets' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'string', - 'minLength' => 1, - ], - 'default' => [], - ], - 'title' => [ - 'type' => 'string', - 'minLength' => 1, - ], - ], - 'required' => ['description', 'layout', 'title'], + ), + ]), + ), + 'content_type' => (new EnumConstraint($contentTypes))->default(StaticPageContentType::Markdown->value), + 'description' => new StringConstraint(), + 'draft' => (new BooleanConstraint())->default(false), + 'layout' => new StringConstraint(), + 'next' => (new StringConstraint())->nullable(), + 'parent' => (new StringConstraint())->nullable(), + 'register_stylesheets' => new ListConstraint(valueConstraint: new StringConstraint()), + 'title' => new StringConstraint(), ]); } /** - * @param array<non-empty-string|object{ name: non-empty-string, next: non-empty-string }> $collections + * @param array<array{ name: non-empty-string, next: non-empty-string }|non-empty-string> $collections * * @return Generator<FrontMatterCollectionReference> */ @@ -145,8 +103,8 @@ readonly class FrontMatterValidator extends InputValidator yield new FrontMatterCollectionReference($collection, null); } else { yield new FrontMatterCollectionReference( - $collection->name, - $collection->next, + $collection['name'], + $collection['next'], ); } } diff --git a/src/InputValidator/RPCMessageValidator.php b/src/InputValidator/RPCMessageValidator.php index cc421450..954d1344 100644 --- a/src/InputValidator/RPCMessageValidator.php +++ b/src/InputValidator/RPCMessageValidator.php @@ -6,12 +6,16 @@ namespace Distantmagic\Resonance\InputValidator; use Distantmagic\Resonance\Attribute\GrantsFeature; use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\Constraint; +use Distantmagic\Resonance\Constraint\AnyConstraint; +use Distantmagic\Resonance\Constraint\EnumConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; +use Distantmagic\Resonance\Constraint\TupleConstraint; +use Distantmagic\Resonance\ConstraintStringFormat; use Distantmagic\Resonance\Feature; use Distantmagic\Resonance\InputValidatedData\RPCMessage; use Distantmagic\Resonance\InputValidator; -use Distantmagic\Resonance\JsonSchema; use Distantmagic\Resonance\RPCMethodValidatorInterface; -use stdClass; /** * @extends InputValidator<RPCMessage, array{ @@ -35,22 +39,14 @@ readonly class RPCMessageValidator extends InputValidator ); } - public function getSchema(): JsonSchema + public function getConstraint(): Constraint { - return new JsonSchema([ - 'type' => 'array', - 'items' => false, - 'prefixItems' => [ - [ - 'type' => 'string', - 'enum' => $this->rpcMethodValidator->values(), - ], - new stdClass(), - [ - 'type' => ['null', 'string'], - 'format' => 'uuid', - ], + return new TupleConstraint( + items: [ + new EnumConstraint($this->rpcMethodValidator->values()), + new AnyConstraint(), + (new StringConstraint(format: ConstraintStringFormat::Uuid))->nullable(), ], - ]); + ); } } diff --git a/src/InputValidatorController.php b/src/InputValidatorController.php index 91ed354d..d02f6d09 100644 --- a/src/InputValidatorController.php +++ b/src/InputValidatorController.php @@ -9,8 +9,6 @@ use Distantmagic\Resonance\Attribute\Singleton; #[Singleton] readonly class InputValidatorController { - public function __construct(private JsonSchemaValidator $jsonSchemaValidator) {} - /** * @template TCastedData of InputValidatedData * @template TValidatedData @@ -21,9 +19,9 @@ readonly class InputValidatorController */ public function validateData(InputValidator $inputValidator, mixed $data): InputValidationResult { - $jsonSchemaValidationResult = $this->jsonSchemaValidator->validate($inputValidator, $data); + $constraintResult = $inputValidator->getConstraint()->validate($data); - return $this->castJsonSchemaValidationResult($inputValidator, $jsonSchemaValidationResult); + return $this->castJsonSchemaValidationResult($constraintResult, $inputValidator); } /** @@ -31,32 +29,26 @@ readonly class InputValidatorController * @template TValidatedData * * @param InputValidator<TCastedData,TValidatedData> $inputValidator - * @param JsonSchemaValidationResult<TValidatedData> $jsonSchemaValidationResult * * @return InputValidationResult<TCastedData> */ protected function castJsonSchemaValidationResult( + ConstraintResult $constraintResult, InputValidator $inputValidator, - JsonSchemaValidationResult $jsonSchemaValidationResult, ): InputValidationResult { - $errors = $jsonSchemaValidationResult->errors; - - if (empty($errors)) { - return new InputValidationResult($inputValidator->castValidatedData($jsonSchemaValidationResult->data)); + if ($constraintResult->status->isValid()) { + /** + * @var TValidatedData $constraintResult->castedData + */ + return new InputValidationResult( + $inputValidator->castValidatedData($constraintResult->castedData), + $constraintResult, + ); } /** * @var InputValidationResult<TCastedData> */ - $validationResult = new InputValidationResult(); - - foreach ($errors as $propertyName => $propertyErrors) { - $validationResult->errors->put( - $propertyName, - $propertyErrors, - ); - } - - return $validationResult; + return new InputValidationResult(null, $constraintResult); } } diff --git a/src/JsonSchema.php b/src/JsonSchema.php deleted file mode 100644 index 594bd587..00000000 --- a/src/JsonSchema.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use JsonSerializable; - -readonly class JsonSchema implements JsonSerializable -{ - /** - * @param non-empty-array $schema - */ - public function __construct(public array $schema) {} - - public function jsonSerialize(): array - { - return $this->schema; - } -} diff --git a/src/JsonSchemaSourceInterface.php b/src/JsonSchemaSourceInterface.php deleted file mode 100644 index 2ec9a2a3..00000000 --- a/src/JsonSchemaSourceInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -interface JsonSchemaSourceInterface -{ - public function getSchema(): JsonSchema; -} diff --git a/src/JsonSchemaValidationErrorMessage.php b/src/JsonSchemaValidationErrorMessage.php deleted file mode 100644 index 9f503624..00000000 --- a/src/JsonSchemaValidationErrorMessage.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Ds\Set; -use Stringable; - -readonly class JsonSchemaValidationErrorMessage implements Stringable -{ - public string $message; - - /** - * @param array<non-empty-string,Set<non-empty-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 deleted file mode 100644 index 9f81f837..00000000 --- a/src/JsonSchemaValidationException.php +++ /dev/null @@ -1,19 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Ds\Set; -use RuntimeException; - -class JsonSchemaValidationException extends RuntimeException -{ - /** - * @param array<non-empty-string,Set<non-empty-string>> $errors - */ - public function __construct(array $errors) - { - parent::__construct((string) new JsonSchemaValidationErrorMessage($errors)); - } -} diff --git a/src/JsonSchemaValidationResult.php b/src/JsonSchemaValidationResult.php deleted file mode 100644 index db8b14cb..00000000 --- a/src/JsonSchemaValidationResult.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Ds\Set; - -/** - * @template TValidatedData - */ -readonly class JsonSchemaValidationResult -{ - /** - * @param TValidatedData $data - * @param array<non-empty-string,Set<non-empty-string>> $errors - */ - public function __construct( - public mixed $data, - public array $errors, - ) {} -} diff --git a/src/JsonSchemaValidator.php b/src/JsonSchemaValidator.php deleted file mode 100644 index 713e64a5..00000000 --- a/src/JsonSchemaValidator.php +++ /dev/null @@ -1,177 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Distantmagic\Resonance; - -use Distantmagic\Resonance\Attribute\Singleton; -use Ds\Map; -use Ds\Set; -use Opis\JsonSchema\Errors\ErrorFormatter; -use Opis\JsonSchema\Errors\ValidationError; -use Opis\JsonSchema\Helper; -use Opis\JsonSchema\Parsers\SchemaParser; -use Opis\JsonSchema\Schema; -use Opis\JsonSchema\SchemaLoader; -use Opis\JsonSchema\ValidationResult; -use Opis\JsonSchema\Validator; -use RuntimeException; - -#[Singleton] -readonly class JsonSchemaValidator -{ - /** - * @var Map<JsonSchemaSourceInterface,Schema> - */ - private Map $convertedSchemas; - - private ErrorFormatter $errorFormatter; - private SchemaLoader $schemaLoader; - private Validator $validator; - - public function __construct() - { - $this->convertedSchemas = new Map(); - $this->errorFormatter = new ErrorFormatter(); - $this->schemaLoader = new SchemaLoader(new SchemaParser(), null, false); - $this->validator = new Validator($this->schemaLoader); - } - - public function validate(JsonSchemaSourceInterface $jsonSchemaSource, mixed $data): JsonSchemaValidationResult - { - return $this->validateConvertedSchema( - $this->convertSchemaSource($jsonSchemaSource), - $data, - ); - } - - public function validateConvertedSchema(Schema $schema, mixed $data): JsonSchemaValidationResult - { - /** - * @var bool|object|string $convertedData - */ - $convertedData = Helper::toJSON($data); - - $validationResult = $this->validator->validate($convertedData, $schema); - - return new JsonSchemaValidationResult( - data: $convertedData, - errors: $this->formatErrors($validationResult), - ); - } - - public function validateSchema(JsonSchema $jsonSchema, mixed $data): JsonSchemaValidationResult - { - return $this->validateConvertedSchema( - $this->convertSchema($jsonSchema), - $data, - ); - } - - private function convertSchema(JsonSchema $jsonSchema): Schema - { - $convertedSchema = Helper::toJSON($jsonSchema->schema); - - if (!is_object($convertedSchema)) { - throw new RuntimeException('Json Schema must be an object'); - } - - return $this->schemaLoader->loadObjectSchema($convertedSchema); - } - - private function convertSchemaSource(JsonSchemaSourceInterface $jsonSchemaSource): Schema - { - if ($this->convertedSchemas->hasKey($jsonSchemaSource)) { - return $this->convertedSchemas->get($jsonSchemaSource); - } - - $validatedSchema = $this->convertSchema($jsonSchemaSource->getSchema()); - - $this->convertedSchemas->put($jsonSchemaSource, $validatedSchema); - - return $validatedSchema; - } - - /** - * @return array<non-empty-string,Set<non-empty-string>> - */ - private function formatErrors(ValidationResult $validationResult): array - { - $error = $validationResult->error(); - - if (empty($error)) { - return []; - } - - /** - * @var list<array{ - * message: non-empty-string, - * keywords: list<non-empty-string>, - * path: non-empty-string, - * }> - */ - $nestedFormat = []; - - $this->errorFormatter->formatNested( - error: $error, - formatter: function (ValidationError $validationError) use (&$nestedFormat) { - $fullPath = implode('.', $validationError->data()->fullPath()); - - /** - * @var array<non-empty-string> $keywords - */ - $keywords = [ - $validationError->keyword(), - ]; - - $args = $validationError->args(); - - /** - * @var non-empty-string $keyword - * @var list<string> $paths - */ - foreach ($args as $keyword => $paths) { - $keywords[] = $keyword; - - foreach ($paths as $path) { - if (empty($fullPath)) { - $fullPath = $path; - } else { - throw new RuntimeException('Ambigous error path'); - } - } - } - - if (empty($fullPath)) { - throw new RuntimeException('Unable to determine error path'); - } - - $nestedFormat[] = [ - 'message' => $this->errorFormatter->formatErrorMessage($validationError), - 'keywords' => $keywords, - 'path' => $fullPath, - ]; - }, - ); - - /** - * @var array<non-empty-string,Set<non-empty-string>> - */ - $ret = []; - - foreach ($nestedFormat as $error) { - if (!isset($ret[$error['path']])) { - /** - * @var Set<non-empty-string> - */ - $ret[$error['path']] = new Set(); - } - - foreach ($error['keywords'] as $keyword) { - $ret[$error['path']]->add($keyword); - } - } - - return $ret; - } -} diff --git a/src/JsonSchemableInterface.php b/src/JsonSchemableInterface.php index 7fe349fe..994f5bdf 100644 --- a/src/JsonSchemableInterface.php +++ b/src/JsonSchemableInterface.php @@ -4,17 +4,19 @@ declare(strict_types=1); namespace Distantmagic\Resonance; +use JsonSerializable; use stdClass; /** * @psalm-type PJsonSchema = stdClass|array{ + * anyOf?: list<array>, * const?: int|float|non-empty-string, * default?: mixed, * type?: non-empty-string|list<non-empty-string>, * ... * } */ -interface JsonSchemableInterface +interface JsonSchemableInterface extends JsonSerializable { /** * @return PJsonSchema diff --git a/src/OpenAPIMetadataResponseExtractor/RespondsWithExtractor.php b/src/OpenAPIMetadataResponseExtractor/RespondsWithExtractor.php index 3e9d2870..c35c3d26 100644 --- a/src/OpenAPIMetadataResponseExtractor/RespondsWithExtractor.php +++ b/src/OpenAPIMetadataResponseExtractor/RespondsWithExtractor.php @@ -29,7 +29,7 @@ readonly class RespondsWithExtractor extends OpenAPIMetadataResponseExtractor contentType: $attribute->contentType, description: $attribute->description, status: $attribute->status, - jsonSchema: $attribute->jsonSchema, + jsonSchemable: $attribute->constraint, ), ]; } diff --git a/src/OpenAPIReusableSchemaCollection.php b/src/OpenAPIReusableSchemaCollection.php index 4a35df5e..1b67a454 100644 --- a/src/OpenAPIReusableSchemaCollection.php +++ b/src/OpenAPIReusableSchemaCollection.php @@ -15,7 +15,7 @@ readonly class OpenAPIReusableSchemaCollection public Map $hashes; /** - * @var Map<JsonSchema,non-empty-string> + * @var Map<JsonSchemableInterface,non-empty-string> */ public Map $references; @@ -25,9 +25,9 @@ readonly class OpenAPIReusableSchemaCollection $this->references = new Map(); } - public function reuse(JsonSchema $jsonSchema): JsonSchema + public function reuse(JsonSchemableInterface $jsonSchemable): array { - $hashed = $this->makeHash($jsonSchema); + $hashed = $this->makeHash($jsonSchemable); if (!$this->hashes->hasKey($hashed)) { $this->hashes->put($hashed, uniqid()); @@ -35,25 +35,25 @@ readonly class OpenAPIReusableSchemaCollection $schemaId = $this->hashes->get($hashed); - $this->references->put($jsonSchema, $schemaId); + $this->references->put($jsonSchemable, $schemaId); - return new JsonSchema([ + return [ '$ref' => sprintf( '#/components/schemas/%s', $schemaId, ), - ]); + ]; } /** * @return non-empty-string */ - private function makeHash(JsonSchema $jsonSchema): string + private function makeHash(JsonSchemableInterface $jsonSchemable): string { - $serialized = serialize($jsonSchema->schema); + $serialized = serialize($jsonSchemable->toJsonSchema()); if (empty($serialized)) { - throw new RuntimeException('Unable to serialize JsonSchema'); + throw new RuntimeException('Unable to serialize JsonSchemableInterface'); } return $serialized; diff --git a/src/OpenAPIRouteParameterExtractor/DoctrineEntityRouteParameterExtractor.php b/src/OpenAPIRouteParameterExtractor/DoctrineEntityRouteParameterExtractor.php index d1b5320d..0ba8bc53 100644 --- a/src/OpenAPIRouteParameterExtractor/DoctrineEntityRouteParameterExtractor.php +++ b/src/OpenAPIRouteParameterExtractor/DoctrineEntityRouteParameterExtractor.php @@ -8,9 +8,11 @@ use Distantmagic\Resonance\Attribute; use Distantmagic\Resonance\Attribute\DoctrineEntityRouteParameter; use Distantmagic\Resonance\Attribute\ExtractsOpenAPIRouteParameter; use Distantmagic\Resonance\Attribute\Singleton; +use Distantmagic\Resonance\Constraint\IntegerConstraint; +use Distantmagic\Resonance\Constraint\StringConstraint; use Distantmagic\Resonance\DoctrineAttributeDriver; use Distantmagic\Resonance\DoctrineEntityManagerRepository; -use Distantmagic\Resonance\JsonSchema; +use Distantmagic\Resonance\JsonSchemableInterface; use Distantmagic\Resonance\OpenAPIParameterIn; use Distantmagic\Resonance\OpenAPIRouteParameterExtractor; use Distantmagic\Resonance\OpenAPISchemaParameter; @@ -26,21 +28,10 @@ use RuntimeException; #[Singleton(collection: SingletonCollection::OpenAPIRouteParameterExtractor)] readonly class DoctrineEntityRouteParameterExtractor extends OpenAPIRouteParameterExtractor { - private JsonSchema $jsonSchemaInteger; - private JsonSchema $jsonSchemaString; - public function __construct( private DoctrineAttributeDriver $doctrineAttributeDriver, private DoctrineEntityManagerRepository $doctrineEntityManagerRepository, - ) { - $this->jsonSchemaInteger = new JsonSchema([ - 'type' => 'integer', - ]); - $this->jsonSchemaString = new JsonSchema([ - 'minLength' => 1, - 'type' => 'string', - ]); - } + ) {} /** * Unfortunately to extract the metadata factory we need an instance of @@ -84,7 +75,7 @@ readonly class DoctrineEntityRouteParameterExtractor extends OpenAPIRouteParamet in: OpenAPIParameterIn::Path, name: $attribute->from, required: true, - jsonSchema: $this->jsonSchemaFromFieldType($parameterFieldType), + jsonSchemable: $this->jsonSchemableFromFieldType($parameterFieldType), ); return [ @@ -92,11 +83,11 @@ readonly class DoctrineEntityRouteParameterExtractor extends OpenAPIRouteParamet ]; } - private function jsonSchemaFromFieldType(string $fieldType): JsonSchema + private function jsonSchemableFromFieldType(string $fieldType): JsonSchemableInterface { return match ($fieldType) { - 'integer' => $this->jsonSchemaInteger, - 'string' => $this->jsonSchemaString, + 'integer' => new IntegerConstraint(), + 'string' => new StringConstraint(), default => throw new LogicException(sprintf( 'Unsupported Doctrine field type: "%s"', $fieldType, diff --git a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php index 4bbdb52a..6fa4b0cc 100644 --- a/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php +++ b/src/OpenAPIRouteRequestBodyContentExtractor/ValidatedRequestExtractor.php @@ -32,11 +32,11 @@ readonly class ValidatedRequestExtractor extends OpenAPIRouteRequestBodyContentE return [ new OpenAPISchemaRequestBodyContent( mimeType: 'application/x-www-form-urlencoded', - jsonSchema: $this + jsonSchemable: $this ->inputValidatorCollection ->inputValidators ->get($attribute->validator) - ->getSchema(), + ->getConstraint(), ), ]; } diff --git a/src/OpenAPISchemaComponents.php b/src/OpenAPISchemaComponents.php index 2093d9c1..1985bd19 100644 --- a/src/OpenAPISchemaComponents.php +++ b/src/OpenAPISchemaComponents.php @@ -8,7 +8,7 @@ use stdClass; /** * @template-implements OpenAPISerializableFieldInterface<array{ - * schemas: object|array<non-empty-string,JsonSchema>, + * schemas: object|array<non-empty-string,JsonSchemableInterface>, * securitySchemes: OpenAPISchemaComponentsSecuritySchemes, * }> */ @@ -27,12 +27,12 @@ readonly class OpenAPISchemaComponents implements OpenAPISerializableFieldInterf } /** - * @return array<non-empty-string,JsonSchema>|object + * @return array<non-empty-string,JsonSchemableInterface>|object */ private function serializeSchemaCollection(OpenAPIReusableSchemaCollection $openAPIReusableSchemaCollection): array|object { /** - * @var array<non-empty-string,JsonSchema> $schemas + * @var array<non-empty-string,JsonSchemableInterface> $schemas */ $schemas = []; diff --git a/src/OpenAPISchemaParameter.php b/src/OpenAPISchemaParameter.php index cc231ee3..5f7ff5ae 100644 --- a/src/OpenAPISchemaParameter.php +++ b/src/OpenAPISchemaParameter.php @@ -10,7 +10,7 @@ readonly class OpenAPISchemaParameter implements JsonSerializable { public function __construct( private OpenAPIParameterIn $in, - private JsonSchema $jsonSchema, + private JsonSchemableInterface $jsonSchemable, private string $name, private bool $required, ) {} @@ -21,7 +21,7 @@ readonly class OpenAPISchemaParameter implements JsonSerializable 'in' => $this->in->value, 'name' => $this->name, 'required' => $this->required, - 'schema' => $this->jsonSchema, + 'schema' => $this->jsonSchemable, ]; } } diff --git a/src/OpenAPISchemaRequestBodyContent.php b/src/OpenAPISchemaRequestBodyContent.php index 853ff872..33afab25 100644 --- a/src/OpenAPISchemaRequestBodyContent.php +++ b/src/OpenAPISchemaRequestBodyContent.php @@ -6,7 +6,7 @@ namespace Distantmagic\Resonance; /** * @psalm-type PArraySerializedOpenAPISchemaRequestBodyContent = array{ - * schema: JsonSchema + * schema: array * } * * @template-implements OpenAPISerializableFieldInterface<PArraySerializedOpenAPISchemaRequestBodyContent> @@ -18,13 +18,13 @@ readonly class OpenAPISchemaRequestBodyContent implements OpenAPISerializableFie */ public function __construct( public string $mimeType, - public JsonSchema $jsonSchema, + public JsonSchemableInterface $jsonSchemable, ) {} public function toArray(OpenAPIReusableSchemaCollection $openAPIReusableSchemaCollection): array { return [ - 'schema' => $openAPIReusableSchemaCollection->reuse($this->jsonSchema), + 'schema' => $openAPIReusableSchemaCollection->reuse($this->jsonSchemable), ]; } } diff --git a/src/OpenAPISchemaResponse.php b/src/OpenAPISchemaResponse.php index 63a11eca..cdaf2aad 100644 --- a/src/OpenAPISchemaResponse.php +++ b/src/OpenAPISchemaResponse.php @@ -7,7 +7,7 @@ namespace Distantmagic\Resonance; /** * @psalm-type PArraySerializedOpenAPISchemaResponse = array{ * description?: non-empty-string, - * content: array<string, array{ schema: JsonSchema }> + * content: array<string, array{ schema: array }> * } * * @template-implements OpenAPISerializableFieldInterface<PArraySerializedOpenAPISchemaResponse> @@ -19,7 +19,7 @@ readonly class OpenAPISchemaResponse implements OpenAPISerializableFieldInterfac */ public function __construct( public ContentType $contentType, - public JsonSchema $jsonSchema, + public JsonSchemableInterface $jsonSchemable, public int $status, public ?string $description = null, ) {} @@ -34,7 +34,7 @@ readonly class OpenAPISchemaResponse implements OpenAPISerializableFieldInterfac $response['content'] = [ $this->contentType->value => [ - 'schema' => $openAPIReusableSchemaCollection->reuse($this->jsonSchema), + 'schema' => $openAPIReusableSchemaCollection->reuse($this->jsonSchemable), ], ]; diff --git a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php index 8fb7c2ae..a85ab7a6 100644 --- a/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/ApplicationConfigurationProvider.php @@ -27,14 +27,12 @@ final readonly class ApplicationConfigurationProvider extends ConfigurationProvi { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'env' => new EnumConstraint(Environment::values()), - 'esbuild_metafile' => (new StringConstraint())->default('esbuild-meta.json'), - 'scheme' => (new EnumConstraint(['http', 'https']))->default('https'), - 'url' => new StringConstraint(), - ], - ); + return new ObjectConstraint([ + 'env' => new EnumConstraint(Environment::values()), + 'esbuild_metafile' => (new StringConstraint())->default('esbuild-meta.json'), + 'scheme' => (new EnumConstraint(['http', 'https']))->default('https'), + 'url' => new StringConstraint(), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php index a3353715..ca419170 100644 --- a/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/DatabaseConfigurationProvider.php @@ -39,20 +39,18 @@ final readonly class DatabaseConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - $valueConstraint = new ObjectConstraint( - properties: [ - 'database' => new StringConstraint(), - 'driver' => new EnumConstraint(DatabaseConnectionPoolDriverName::values()), - 'host' => (new StringConstraint())->default(null), - 'log_queries' => new BooleanConstraint(), - 'password' => (new StringConstraint())->nullable(), - 'pool_prefill' => (new BooleanConstraint())->default(true), - 'pool_size' => new IntegerConstraint(), - 'port' => (new IntegerConstraint())->nullable()->default(3306), - 'unix_socket' => (new StringConstraint())->nullable(), - 'username' => new StringConstraint(), - ], - ); + $valueConstraint = new ObjectConstraint([ + 'database' => new StringConstraint(), + 'driver' => new EnumConstraint(DatabaseConnectionPoolDriverName::values()), + 'host' => (new StringConstraint())->default(null), + 'log_queries' => new BooleanConstraint(), + 'password' => (new StringConstraint())->nullable(), + 'pool_prefill' => (new BooleanConstraint())->default(true), + 'pool_size' => new IntegerConstraint(), + 'port' => (new IntegerConstraint())->nullable()->default(3306), + 'unix_socket' => (new StringConstraint())->nullable(), + 'username' => new StringConstraint(), + ]); return new MapConstraint(valueConstraint: $valueConstraint); } diff --git a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php index 0f7f0b4c..4ca3224e 100644 --- a/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/LlamaCppConfigurationProvider.php @@ -28,15 +28,13 @@ final readonly class LlamaCppConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'api_key' => (new StringConstraint())->default(null), - 'completion_token_timeout' => (new NumberConstraint())->default(1.0), - 'host' => new StringConstraint(), - 'port' => new IntegerConstraint(), - 'scheme' => (new EnumConstraint(['http', 'https']))->default('http'), - ], - ); + return new ObjectConstraint([ + 'api_key' => (new StringConstraint())->default(null), + 'completion_token_timeout' => (new NumberConstraint())->default(1.0), + 'host' => new StringConstraint(), + 'port' => new IntegerConstraint(), + 'scheme' => (new EnumConstraint(['http', 'https']))->default('http'), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php index 4f09ae76..0a59a230 100644 --- a/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/MailerConfigurationProvider.php @@ -31,16 +31,14 @@ final readonly class MailerConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - $valueConstraint = new ObjectConstraint( - properties: [ - 'dkim_domain_name' => (new StringConstraint())->nullable(), - 'dkim_selector' => (new StringConstraint())->nullable(), - 'dkim_signing_key_passphrase' => (new StringConstraint())->nullable(), - 'dkim_signing_key_private' => (new StringConstraint())->nullable(), - 'dkim_signing_key_public' => (new StringConstraint())->nullable(), - 'transport_dsn' => new StringConstraint(), - ], - ); + $valueConstraint = new ObjectConstraint([ + 'dkim_domain_name' => (new StringConstraint())->nullable(), + 'dkim_selector' => (new StringConstraint())->nullable(), + 'dkim_signing_key_passphrase' => (new StringConstraint())->nullable(), + 'dkim_signing_key_private' => (new StringConstraint())->nullable(), + 'dkim_signing_key_public' => (new StringConstraint())->nullable(), + 'transport_dsn' => new StringConstraint(), + ]); return new MapConstraint(valueConstraint: $valueConstraint); } diff --git a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php index 8df863e2..6590cc91 100644 --- a/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/OAuth2ConfigurationProvider.php @@ -34,17 +34,15 @@ final readonly class OAuth2ConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'encryption_key' => new StringConstraint(), - 'jwt_signing_key_passphrase' => (new StringConstraint())->nullable(), - 'jwt_signing_key_private' => new StringConstraint(), - 'jwt_signing_key_public' => new StringConstraint(), - 'session_key_authorization_request' => (new StringConstraint())->default('oauth2.authorization_request'), - 'session_key_pkce' => (new StringConstraint())->default('oauth2.pkce'), - 'session_key_state' => (new StringConstraint())->default('oauth2.state'), - ], - ); + return new ObjectConstraint([ + 'encryption_key' => new StringConstraint(), + 'jwt_signing_key_passphrase' => (new StringConstraint())->nullable(), + 'jwt_signing_key_private' => new StringConstraint(), + 'jwt_signing_key_public' => new StringConstraint(), + 'session_key_authorization_request' => (new StringConstraint())->default('oauth2.authorization_request'), + 'session_key_pkce' => (new StringConstraint())->default('oauth2.pkce'), + 'session_key_state' => (new StringConstraint())->default('oauth2.state'), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php index d779e780..e97a0690 100644 --- a/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/OpenAPIConfigurationProvider.php @@ -23,13 +23,11 @@ final readonly class OpenAPIConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'description' => new StringConstraint(), - 'title' => new StringConstraint(), - 'version' => new StringConstraint(), - ], - ); + return new ObjectConstraint([ + 'description' => new StringConstraint(), + 'title' => new StringConstraint(), + 'version' => new StringConstraint(), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php index e05c94de..98fb15d7 100644 --- a/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/RedisConfigurationProvider.php @@ -35,18 +35,16 @@ final readonly class RedisConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - $valueConstraint = new ObjectConstraint( - properties: [ - 'db_index' => new IntegerConstraint(), - 'host' => new StringConstraint(), - 'password' => (new StringConstraint())->nullable(), - 'pool_prefill' => (new BooleanConstraint())->default(true), - 'pool_size' => new IntegerConstraint(), - 'port' => new IntegerConstraint(), - 'prefix' => new StringConstraint(), - 'timeout' => new IntegerConstraint(), - ], - ); + $valueConstraint = new ObjectConstraint([ + 'db_index' => new IntegerConstraint(), + 'host' => new StringConstraint(), + 'password' => (new StringConstraint())->nullable(), + 'pool_prefill' => (new BooleanConstraint())->default(true), + 'pool_size' => new IntegerConstraint(), + 'port' => new IntegerConstraint(), + 'prefix' => new StringConstraint(), + 'timeout' => new IntegerConstraint(), + ]); return new MapConstraint(valueConstraint: $valueConstraint); } diff --git a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php index e7ab93b4..88da3f84 100644 --- a/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SQLiteVSSConfigurationProvider.php @@ -22,12 +22,10 @@ final readonly class SQLiteVSSConfigurationProvider extends ConfigurationProvide { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'extension_vector0' => new StringConstraint(), - 'extension_vss0' => new StringConstraint(), - ] - ); + return new ObjectConstraint([ + 'extension_vector0' => new StringConstraint(), + 'extension_vss0' => new StringConstraint(), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php index c96ea49b..bd8f5767 100644 --- a/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SessionConfigurationProvider.php @@ -42,14 +42,12 @@ final readonly class SessionConfigurationProvider extends ConfigurationProvider ->toArray() ; - return new ObjectConstraint( - properties: [ - 'cookie_lifespan' => new IntegerConstraint(), - 'cookie_name' => new StringConstraint(), - 'cookie_samesite' => (new EnumConstraint(['lax', 'none', 'strict']))->default('lax'), - 'redis_connection_pool' => new EnumConstraint($redisConnectionPools), - ], - ); + return new ObjectConstraint([ + 'cookie_lifespan' => new IntegerConstraint(), + 'cookie_name' => new StringConstraint(), + 'cookie_samesite' => (new EnumConstraint(['lax', 'none', 'strict']))->default('lax'), + 'redis_connection_pool' => new EnumConstraint($redisConnectionPools), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php index 5f3fca8f..90940c99 100644 --- a/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/StaticPageConfigurationProvider.php @@ -25,15 +25,13 @@ final readonly class StaticPageConfigurationProvider extends ConfigurationProvid { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'base_url' => new StringConstraint(), - 'esbuild_metafile' => new StringConstraint(), - 'input_directory' => new StringConstraint(), - 'output_directory' => new StringConstraint(), - 'sitemap' => new StringConstraint(), - ], - ); + return new ObjectConstraint([ + 'base_url' => new StringConstraint(), + 'esbuild_metafile' => new StringConstraint(), + 'input_directory' => new StringConstraint(), + 'output_directory' => new StringConstraint(), + 'sitemap' => new StringConstraint(), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php index deb95764..a819cf6e 100644 --- a/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/SwooleConfigurationProvider.php @@ -29,17 +29,15 @@ final readonly class SwooleConfigurationProvider extends ConfigurationProvider { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'host' => new StringConstraint(), - 'log_level' => new IntegerConstraint(), - 'log_requests' => (new BooleanConstraint())->default(false), - 'port' => new IntegerConstraint(), - 'ssl_cert_file' => new StringConstraint(), - 'ssl_key_file' => new StringConstraint(), - 'task_worker_num' => (new IntegerConstraint())->default(4), - ], - ); + return new ObjectConstraint([ + 'host' => new StringConstraint(), + 'log_level' => new IntegerConstraint(), + 'log_requests' => (new BooleanConstraint())->default(false), + 'port' => new IntegerConstraint(), + 'ssl_cert_file' => new StringConstraint(), + 'ssl_key_file' => new StringConstraint(), + 'task_worker_num' => (new IntegerConstraint())->default(4), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php index f090d176..fc5119bb 100644 --- a/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/TranslatorConfigurationProvider.php @@ -22,12 +22,10 @@ final readonly class TranslatorConfigurationProvider extends ConfigurationProvid { public function getConstraint(): Constraint { - return new ObjectConstraint( - properties: [ - 'base_directory' => new StringConstraint(), - 'default_primary_language' => new StringConstraint(), - ], - ); + return new ObjectConstraint([ + 'base_directory' => new StringConstraint(), + 'default_primary_language' => new StringConstraint(), + ]); } protected function getConfigurationKey(): string diff --git a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php index 40540e10..fd3bbb13 100644 --- a/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php +++ b/src/SingletonProvider/ConfigurationProvider/WebSocketConfigurationProvider.php @@ -24,19 +24,14 @@ final readonly class WebSocketConfigurationProvider extends ConfigurationProvide { public function getConstraint(): Constraint { - return new ObjectConstraint( - // 'minimum' => 1, - // 'maximum' => 65535, - // 'default' => 10000, - properties: [ - 'max_connections' => (new IntegerConstraint())->default(10000), - ], - ); + return new ObjectConstraint([ + 'max_connections' => (new IntegerConstraint())->default(10000), + ]); } protected function getConfigurationKey(): string { - return 'swoole'; + return 'websocket'; } protected function provideConfiguration($validatedData): WebSocketConfiguration diff --git a/src/WebSocketProtocolController/RPCProtocolController.php b/src/WebSocketProtocolController/RPCProtocolController.php index b47cfb0d..0510b25a 100644 --- a/src/WebSocketProtocolController/RPCProtocolController.php +++ b/src/WebSocketProtocolController/RPCProtocolController.php @@ -8,13 +8,12 @@ use Distantmagic\Resonance\Attribute\ControlsWebSocketProtocol; use Distantmagic\Resonance\Attribute\GrantsFeature; use Distantmagic\Resonance\Attribute\Singleton; use Distantmagic\Resonance\AuthenticatedUserStoreAggregate; +use Distantmagic\Resonance\ConstraintResultErrorMessage; 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; @@ -51,7 +50,6 @@ final readonly class RPCProtocolController extends WebSocketProtocolController private AuthenticatedUserStoreAggregate $authenticatedUserSourceAggregate, private Gatekeeper $gatekeeper, private InputValidatorController $inputValidatorController, - private JsonSchemaValidator $jsonSchemaValidator, private JsonSerializer $jsonSerializer, private LoggerInterface $logger, private RPCMessageValidator $rpcMessageValidator, @@ -124,7 +122,6 @@ final readonly class RPCProtocolController extends WebSocketProtocolController { $webSocketConnection = new WebSocketConnection($server, $fd); $connectionHandle = new WebSocketRPCConnectionHandle( - $this->jsonSchemaValidator, $this->webSocketRPCResponderAggregate, $webSocketAuthResolution, $webSocketConnection, @@ -168,16 +165,16 @@ final readonly class RPCProtocolController extends WebSocketProtocolController return; } - $payloadValidationResult = $this + $payloadConstraintResult = $this ->getFrameController($frame) ->onRPCMessage($rpcMessageValidationResult->inputValidatedData) ; - if (!empty($payloadValidationResult->errors)) { + if (!$payloadConstraintResult->status->isValid()) { $this->onProtocolError( $server, $frame, - (string) new JsonSchemaValidationErrorMessage($payloadValidationResult->errors), + (string) new ConstraintResultErrorMessage('rpc_protocol_controller', $payloadConstraintResult), ); } } diff --git a/src/WebSocketRPCConnectionHandle.php b/src/WebSocketRPCConnectionHandle.php index 27a2b084..022c632e 100644 --- a/src/WebSocketRPCConnectionHandle.php +++ b/src/WebSocketRPCConnectionHandle.php @@ -15,7 +15,6 @@ readonly class WebSocketRPCConnectionHandle private Set $activeResponders; public function __construct( - public JsonSchemaValidator $jsonSchemaValidator, public WebSocketRPCResponderAggregate $webSocketRPCResponderAggregate, public WebSocketAuthResolution $webSocketAuthResolution, public WebSocketConnection $webSocketConnection, @@ -37,20 +36,20 @@ readonly class WebSocketRPCConnectionHandle } } - public function onRPCMessage(RPCMessage $rpcMessage): JsonSchemaValidationResult + public function onRPCMessage(RPCMessage $rpcMessage): ConstraintResult { $responder = $this ->webSocketRPCResponderAggregate ->selectResponder($rpcMessage) ; - $jsonSchemaValidationResult = $this - ->jsonSchemaValidator - ->validate($responder, $rpcMessage->payload) + $constraintResult = $responder + ->getConstraint() + ->validate($rpcMessage->payload) ; - if (!empty($jsonSchemaValidationResult->errors)) { - return $jsonSchemaValidationResult; + if ($constraintResult->status->isValid()) { + return $constraintResult; } $this->activeResponders->add($responder); @@ -66,7 +65,7 @@ readonly class WebSocketRPCConnectionHandle $this->webSocketConnection, new RPCRequest( $rpcMessage->method, - $jsonSchemaValidationResult->data, + $constraintResult->castedData, $rpcMessage->requestId, ), ); @@ -76,11 +75,11 @@ readonly class WebSocketRPCConnectionHandle $this->webSocketConnection, new RPCNotification( $rpcMessage->method, - $jsonSchemaValidationResult->data, + $constraintResult->castedData, ) ); } - return $jsonSchemaValidationResult; + return $constraintResult; } } diff --git a/src/WebSocketRPCResponderInterface.php b/src/WebSocketRPCResponderInterface.php index e88f71b2..904a7454 100644 --- a/src/WebSocketRPCResponderInterface.php +++ b/src/WebSocketRPCResponderInterface.php @@ -7,7 +7,7 @@ namespace Distantmagic\Resonance; /** * @template TPayload */ -interface WebSocketRPCResponderInterface extends JsonSchemaSourceInterface +interface WebSocketRPCResponderInterface extends ConstraintSourceInterface { public function onBeforeMessage( WebSocketAuthResolution $webSocketAuthResolution, -- GitLab