diff --git a/composer.json b/composer.json index 1e8c01e34..e71a725aa 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "pear/archive_tar": "^1.4.9", "pear/pear-core-minimal": "^1.10", "php-http/guzzle7-adapter": "^1.1.0", - "php-opencloud/openstack": "^3.14", + "php-opencloud/openstack": "^3.17", "phpseclib/phpseclib": "^3.0.55", "pimple/pimple": "^3.6.0", "predis/predis": "^3.4.2", diff --git a/composer.lock b/composer.lock index 218ad28ff..9aa0aa381 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": "b47e22e2def372440b2f2044e1c016d2", + "content-hash": "488de1d2900a42d9be68d1957569d281", "packages": [ { "name": "aws/aws-crt-php", @@ -1107,21 +1107,21 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.2", + "version": "7.15.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "744101956d78b7c1384d0cbf379db13e859167bf" + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", - "reference": "744101956d78b7c1384d0cbf379db13e859167bf", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/promises": "^2.5.2", "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", @@ -1215,7 +1215,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.2" + "source": "https://github.com/guzzle/guzzle/tree/7.15.3" }, "funding": [ { @@ -1231,20 +1231,20 @@ "type": "tidelift" } ], - "time": "2026-07-26T23:23:20+00:00" + "time": "2026-08-05T19:48:21+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.1", + "version": "2.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", "shasum": "" }, "require": { @@ -1299,7 +1299,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.1" + "source": "https://github.com/guzzle/promises/tree/2.5.2" }, "funding": [ { @@ -1315,7 +1315,7 @@ "type": "tidelift" } ], - "time": "2026-07-08T15:48:39+00:00" + "time": "2026-08-05T19:30:54+00:00" }, { "name": "guzzlehttp/psr7", @@ -1438,25 +1438,25 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", + "version": "v1.0.10", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/f6c24c21f42b990e9a58912b332d0874df6ba839", + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1504,7 +1504,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.10" }, "funding": [ { @@ -1520,7 +1520,7 @@ "type": "tidelift" } ], - "time": "2025-02-03T10:55:03+00:00" + "time": "2026-07-17T13:53:03+00:00" }, { "name": "icewind/searchdav", @@ -1654,16 +1654,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.4", + "version": "6.10.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", "shasum": "" }, "require": { @@ -1673,7 +1673,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "^23.2", + "json-schema/json-schema-test-suite": "dev-main", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -1723,9 +1723,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" }, - "time": "2025-12-19T15:01:32+00:00" + "time": "2026-06-16T20:50:26+00:00" }, { "name": "kornrunner/blurhash", @@ -1902,16 +1902,16 @@ }, { "name": "marc-mabe/php-enum", - "version": "v4.7.1", + "version": "v4.7.2", "source": { "type": "git", "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", "shasum": "" }, "require": { @@ -1969,9 +1969,9 @@ ], "support": { "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2024-11-28T04:54:44+00:00" + "time": "2025-09-14T11:18:39+00:00" }, { "name": "masterminds/html5", @@ -2857,21 +2857,21 @@ }, { "name": "php-opencloud/openstack", - "version": "v3.14.0", + "version": "v3.17.0", "source": { "type": "git", "url": "https://github.com/php-opencloud/openstack.git", - "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" + "reference": "8b0aa946dc89f942c69d2968caedaa4d58201aa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", - "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/8b0aa946dc89f942c69d2968caedaa4d58201aa7", + "reference": "8b0aa946dc89f942c69d2968caedaa4d58201aa7", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.0", - "guzzlehttp/psr7": ">=1.7", + "guzzlehttp/guzzle": "^7.5.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "guzzlehttp/uri-template": "^0.2 || ^1.0", "justinrainbow/json-schema": "^5.2 || ^6.0", "php": "^7.2.5 || ^8.0" @@ -2928,9 +2928,9 @@ ], "support": { "issues": "https://github.com/php-opencloud/openstack/issues", - "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" + "source": "https://github.com/php-opencloud/openstack/tree/v3.17.0" }, - "time": "2025-05-30T09:26:42+00:00" + "time": "2026-06-01T14:33:34+00:00" }, { "name": "phpseclib/phpseclib", diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index 2d0b3124c..c9c90f3d5 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -1236,6 +1236,77 @@ 'JsonSchema\\Constraints\\Drafts\\Draft06\\RequiredConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RequiredConstraint.php', 'JsonSchema\\Constraints\\Drafts\\Draft06\\TypeConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/TypeConstraint.php', 'JsonSchema\\Constraints\\Drafts\\Draft06\\UniqueItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/UniqueItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AdditionalItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AdditionalPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AllOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AllOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AnyOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AnyOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ConstConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ConstConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ContainsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContainsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ContentConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContentConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\DependenciesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/DependenciesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\Draft07Constraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\EnumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/EnumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ExclusiveMaximumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ExclusiveMinimumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\Factory' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Factory.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\FormatConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\IfThenElseConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/IfThenElseConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxLengthConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaximumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinLengthConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinimumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MultipleOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MultipleOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\NotConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/NotConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\OneOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/OneOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PatternConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PatternPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PropertiesNamesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesNamesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\RefConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\RequiredConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\TypeConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/TypeConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\UniqueItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/UniqueItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AdditionalItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AdditionalPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AllOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AllOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AnyOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AnyOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ConstConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ConstConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ContainsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContainsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ContentConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContentConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\DependentRequiredConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentRequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\DependentSchemasConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentSchemasConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\Draft2019Constraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\EnumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/EnumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ExclusiveMaximumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ExclusiveMinimumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\Factory' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\FormatConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\IfThenElseConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/IfThenElseConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxLengthConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaximumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinLengthConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinimumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MultipleOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MultipleOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\NotConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/NotConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\OneOfConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/OneOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PatternConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PatternPropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PropertiesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PropertiesNamesConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesNamesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\RefConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\RequiredConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\TypeConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/TypeConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\UniqueItemsConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UniqueItemsConstraint.php', 'JsonSchema\\Constraints\\EnumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', 'JsonSchema\\Constraints\\Factory' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php', 'JsonSchema\\Constraints\\FormatConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php', @@ -1512,6 +1583,7 @@ 'OpenStack\\Compute\\v2\\Models\\Limit' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Limit.php', 'OpenStack\\Compute\\v2\\Models\\QuotaSet' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/QuotaSet.php', 'OpenStack\\Compute\\v2\\Models\\Server' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Server.php', + 'OpenStack\\Compute\\v2\\Models\\ServerGroup' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/ServerGroup.php', 'OpenStack\\Compute\\v2\\Params' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Params.php', 'OpenStack\\Compute\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Service.php', 'OpenStack\\Identity\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Api.php', diff --git a/composer/autoload_static.php b/composer/autoload_static.php index a75415bdf..e46bb9bbf 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -1791,6 +1791,77 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'JsonSchema\\Constraints\\Drafts\\Draft06\\RequiredConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/RequiredConstraint.php', 'JsonSchema\\Constraints\\Drafts\\Draft06\\TypeConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/TypeConstraint.php', 'JsonSchema\\Constraints\\Drafts\\Draft06\\UniqueItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/UniqueItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AdditionalItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AdditionalPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AllOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AllOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\AnyOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AnyOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ConstConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ConstConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ContainsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContainsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ContentConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContentConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\DependenciesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/DependenciesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\Draft07Constraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\EnumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/EnumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ExclusiveMaximumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ExclusiveMinimumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\Factory' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Factory.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\FormatConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\IfThenElseConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/IfThenElseConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\ItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxLengthConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaxPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MaximumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinLengthConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MinimumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\MultipleOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MultipleOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\NotConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/NotConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\OneOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/OneOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PatternConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PatternPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\PropertiesNamesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesNamesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\RefConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\RequiredConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\TypeConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/TypeConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft07\\UniqueItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/UniqueItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AdditionalItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AdditionalPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AllOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AllOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\AnyOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AnyOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ConstConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ConstConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ContainsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContainsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ContentConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContentConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\DependentRequiredConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentRequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\DependentSchemasConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentSchemasConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\Draft2019Constraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\EnumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/EnumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ExclusiveMaximumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ExclusiveMinimumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\Factory' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\FormatConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\IfThenElseConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/IfThenElseConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\ItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxLengthConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaxPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MaximumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaximumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinItemsConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinLengthConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinLengthConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MinimumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinimumConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\MultipleOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MultipleOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\NotConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/NotConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\OneOfConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/OneOfConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PatternConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PatternPropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PropertiesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\PropertiesNamesConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesNamesConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\RefConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\RequiredConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RequiredConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\TypeConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/TypeConstraint.php', + 'JsonSchema\\Constraints\\Drafts\\Draft2019\\UniqueItemsConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UniqueItemsConstraint.php', 'JsonSchema\\Constraints\\EnumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', 'JsonSchema\\Constraints\\Factory' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php', 'JsonSchema\\Constraints\\FormatConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php', @@ -2067,6 +2138,7 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'OpenStack\\Compute\\v2\\Models\\Limit' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Limit.php', 'OpenStack\\Compute\\v2\\Models\\QuotaSet' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/QuotaSet.php', 'OpenStack\\Compute\\v2\\Models\\Server' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Server.php', + 'OpenStack\\Compute\\v2\\Models\\ServerGroup' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/ServerGroup.php', 'OpenStack\\Compute\\v2\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Params.php', 'OpenStack\\Compute\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Service.php', 'OpenStack\\Identity\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Api.php', diff --git a/composer/installed.json b/composer/installed.json index 00439f648..480b20628 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -1149,22 +1149,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.2", - "version_normalized": "7.15.2.0", + "version": "7.15.3", + "version_normalized": "7.15.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "744101956d78b7c1384d0cbf379db13e859167bf" + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", - "reference": "744101956d78b7c1384d0cbf379db13e859167bf", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.1", + "guzzlehttp/promises": "^2.5.2", "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", @@ -1188,7 +1188,7 @@ "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, - "time": "2026-07-26T23:23:20+00:00", + "time": "2026-08-05T19:48:21+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -1260,7 +1260,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.2" + "source": "https://github.com/guzzle/guzzle/tree/7.15.3" }, "funding": [ { @@ -1280,17 +1280,17 @@ }, { "name": "guzzlehttp/promises", - "version": "2.5.1", - "version_normalized": "2.5.1.0", + "version": "2.5.2", + "version_normalized": "2.5.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29" + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29", - "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29", + "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", "shasum": "" }, "require": { @@ -1301,7 +1301,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "phpunit/phpunit": "^8.5.52 || ^9.6.34" }, - "time": "2026-07-08T15:48:39+00:00", + "time": "2026-08-05T19:30:54+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -1347,7 +1347,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.1" + "source": "https://github.com/guzzle/promises/tree/2.5.2" }, "funding": [ { @@ -1489,29 +1489,29 @@ }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.4", - "version_normalized": "1.0.4.0", + "version": "v1.0.10", + "version_normalized": "1.0.10.0", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", - "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/f6c24c21f42b990e9a58912b332d0874df6ba839", + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, - "time": "2025-02-03T10:55:03+00:00", + "time": "2026-07-17T13:53:03+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -1558,7 +1558,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.10" }, "funding": [ { @@ -1717,17 +1717,17 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.4", - "version_normalized": "6.6.4.0", + "version": "6.10.0", + "version_normalized": "6.10.0.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7" + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2eeb75d21cf73211335888e7f5e6fd7440723ec7", - "reference": "2eeb75d21cf73211335888e7f5e6fd7440723ec7", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", "shasum": "" }, "require": { @@ -1737,13 +1737,13 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "^23.2", + "json-schema/json-schema-test-suite": "dev-main", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", "phpunit/phpunit": "^8.5" }, - "time": "2025-12-19T15:01:32+00:00", + "time": "2026-06-16T20:50:26+00:00", "bin": [ "bin/validate-json" ], @@ -1789,7 +1789,7 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.4" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" }, "install-path": "../justinrainbow/json-schema" }, @@ -1977,17 +1977,17 @@ }, { "name": "marc-mabe/php-enum", - "version": "v4.7.1", - "version_normalized": "4.7.1.0", + "version": "v4.7.2", + "version_normalized": "4.7.2.0", "source": { "type": "git", "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", "shasum": "" }, "require": { @@ -2000,7 +2000,7 @@ "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", "vimeo/psalm": "^4.17.0 | ^5.26.1" }, - "time": "2024-11-28T04:54:44+00:00", + "time": "2025-09-14T11:18:39+00:00", "type": "library", "extra": { "branch-alias": { @@ -2047,7 +2047,7 @@ ], "support": { "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, "install-path": "../marc-mabe/php-enum" }, @@ -2980,22 +2980,22 @@ }, { "name": "php-opencloud/openstack", - "version": "v3.14.0", - "version_normalized": "3.14.0.0", + "version": "v3.17.0", + "version_normalized": "3.17.0.0", "source": { "type": "git", "url": "https://github.com/php-opencloud/openstack.git", - "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" + "reference": "8b0aa946dc89f942c69d2968caedaa4d58201aa7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", - "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/8b0aa946dc89f942c69d2968caedaa4d58201aa7", + "reference": "8b0aa946dc89f942c69d2968caedaa4d58201aa7", "shasum": "" }, "require": { - "guzzlehttp/guzzle": "^7.0", - "guzzlehttp/psr7": ">=1.7", + "guzzlehttp/guzzle": "^7.5.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", "guzzlehttp/uri-template": "^0.2 || ^1.0", "justinrainbow/json-schema": "^5.2 || ^6.0", "php": "^7.2.5 || ^8.0" @@ -3010,7 +3010,7 @@ "phpunit/phpunit": ">=8.5.23 <9.0", "psr/log": "^1.0" }, - "time": "2025-05-30T09:26:42+00:00", + "time": "2026-06-01T14:33:34+00:00", "type": "library", "extra": { "branch-alias": { @@ -3054,7 +3054,7 @@ ], "support": { "issues": "https://github.com/php-opencloud/openstack/issues", - "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" + "source": "https://github.com/php-opencloud/openstack/tree/v3.17.0" }, "install-path": "../php-opencloud/openstack" }, diff --git a/composer/installed.php b/composer/installed.php index c0b1a2807..9f5dc048d 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -155,18 +155,18 @@ 'dev_requirement' => false, ), 'guzzlehttp/guzzle' => array( - 'pretty_version' => '7.15.2', - 'version' => '7.15.2.0', - 'reference' => '744101956d78b7c1384d0cbf379db13e859167bf', + 'pretty_version' => '7.15.3', + 'version' => '7.15.3.0', + 'reference' => 'ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'aliases' => array(), 'dev_requirement' => false, ), 'guzzlehttp/promises' => array( - 'pretty_version' => '2.5.1', - 'version' => '2.5.1.0', - 'reference' => '9ad1e4fc607446a055b95870c7f668e93b5cff29', + 'pretty_version' => '2.5.2', + 'version' => '2.5.2.0', + 'reference' => '2823687acff28b2dbe67b2508a6b300e2c3fa4ce', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/promises', 'aliases' => array(), @@ -182,9 +182,9 @@ 'dev_requirement' => false, ), 'guzzlehttp/uri-template' => array( - 'pretty_version' => 'v1.0.4', - 'version' => '1.0.4.0', - 'reference' => '30e286560c137526eccd4ce21b2de477ab0676d2', + 'pretty_version' => 'v1.0.10', + 'version' => '1.0.10.0', + 'reference' => 'f6c24c21f42b990e9a58912b332d0874df6ba839', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/uri-template', 'aliases' => array(), @@ -218,9 +218,9 @@ 'dev_requirement' => false, ), 'justinrainbow/json-schema' => array( - 'pretty_version' => '6.6.4', - 'version' => '6.6.4.0', - 'reference' => '2eeb75d21cf73211335888e7f5e6fd7440723ec7', + 'pretty_version' => '6.10.0', + 'version' => '6.10.0.0', + 'reference' => '8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33', 'type' => 'library', 'install_path' => __DIR__ . '/../justinrainbow/json-schema', 'aliases' => array(), @@ -254,9 +254,9 @@ 'dev_requirement' => false, ), 'marc-mabe/php-enum' => array( - 'pretty_version' => 'v4.7.1', - 'version' => '4.7.1.0', - 'reference' => '7159809e5cfa041dca28e61f7f7ae58063aae8ed', + 'pretty_version' => 'v4.7.2', + 'version' => '4.7.2.0', + 'reference' => 'bb426fcdd65c60fb3638ef741e8782508fda7eef', 'type' => 'library', 'install_path' => __DIR__ . '/../marc-mabe/php-enum', 'aliases' => array(), @@ -425,9 +425,9 @@ 'dev_requirement' => false, ), 'php-opencloud/openstack' => array( - 'pretty_version' => 'v3.14.0', - 'version' => '3.14.0.0', - 'reference' => 'b92ea5581ca91779b88f08b1b44e6ca880b34fc3', + 'pretty_version' => 'v3.17.0', + 'version' => '3.17.0.0', + 'reference' => '8b0aa946dc89f942c69d2968caedaa4d58201aa7', 'type' => 'library', 'install_path' => __DIR__ . '/../php-opencloud/openstack', 'aliases' => array(), diff --git a/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php index 1b8a0f1ec..17a622f82 100644 --- a/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php +++ b/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -378,18 +378,38 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte $waitToken = new \stdClass(); + $promise = null; $promise = new Promise( - function () use ($id, $waitToken): void { - if ($this->multiExecDepth > 0) { - // Waiting cannot drive native cURL while a callback has - // the multi handle busy; fail the wait promptly instead - // of self-deadlocking. - $this->failNestedWait($id, $waitToken); - + function () use ($id, $waitToken, $easy, &$promise): void { + // Waiting cannot drive native cURL while a callback has the + // multi handle busy; fail the wait promptly instead of + // self-deadlocking. + $idReused = $this->multiExecDepth > 0 + ? $this->failNestedWait($id, $waitToken) + : $this->executeUntil($id, $waitToken); + + // Settling can be queued, and guzzlehttp/promises drains the + // queue before deciding a wait function achieved nothing. + P\Utils::queue()->run(); + + // Never null: assigned below before any wait can invoke this. + /** @var Promise $promise */ + if (!P\Is::pending($promise)) { return; } - $this->executeUntil($id, $waitToken); + // Neither path guarantees the transfer settled, and returning + // while pending makes guzzlehttp/promises reject with a bare + // string naming nothing. + $stalled = $idReused + ? 'its native cURL handle ID was reused by another request' + : 'its entry was removed without settling'; + + $message = \sprintf('Waiting on cURL multi handler transfer %d cannot make progress (%s).', $id, $stalled); + + // The entry is gone or belongs to another request, so + // attribute from this easy handle. + $promise->reject(new RequestException($message, $easy->request, $easy->response)); }, function () use ($id, $waitToken) { return $this->cancel($id, $waitToken); @@ -1086,8 +1106,11 @@ public function execute(): void * The native cURL handle ID can be reused by a request created from a * completion callback, so the wait token guards against waiting on an * unrelated transfer that inherited the ID. + * + * @return bool Whether another request had reused the native cURL handle + * ID by the time the loop stopped */ - private function executeUntil(int $id, object $waitToken): void + private function executeUntil(int $id, object $waitToken): bool { $queue = P\Utils::queue(); @@ -1100,9 +1123,15 @@ private function executeUntil(int $id, object $waitToken): void $this->tickFor($id, $waitToken); } + // Sample before the drain below, which can add or remove an entry + // under this ID and so rewrite the answer. + $idReused = isset($this->handles[$id]); + if (!$queue->isEmpty()) { $queue->run(); } + + return $idReused; } /** @@ -1124,6 +1153,17 @@ private function addRequest(array $entry): void $easy = $entry['easy']; $id = (int) $easy->handle; $entry['attached'] = false; + + $displaced = $this->handles[$id] ?? null; + if ($displaced !== null) { + // Never silently discard a tracked entry; settle it first. + unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]); + if (P\Is::pending($displaced['deferred'])) { + $message = \sprintf('cURL multi handler transfer %d was displaced by another request that reused its native cURL handle ID.', $id); + $displaced['deferred']->reject(new RequestException($message, $displaced['easy']->request, $displaced['easy']->response)); + } + } + $this->handles[$id] = $entry; if (!empty($easy->options['delay'])) { @@ -1161,11 +1201,15 @@ private function discardPendingRequest(int $id, array $entry, \Throwable $failur /** * Fails a synchronous wait attempted from inside a cURL callback, where * native execution cannot progress until the callback returns. + * + * @return bool Whether another request had reused the native cURL handle + * ID, which only matters when no transfer was left to fail */ - private function failNestedWait(int $id, object $token): void + private function failNestedWait(int $id, object $token): bool { if (!$this->hasRequest($id, $token)) { - return; + // Nothing left to fail, so report which way the entry went. + return isset($this->handles[$id]); } $entry = $this->handles[$id]; @@ -1180,6 +1224,8 @@ private function failNestedWait(int $id, object $token): void } $entry['deferred']->reject($failure); + + return false; } /** diff --git a/guzzlehttp/guzzle/src/Handler/StreamHandler.php b/guzzlehttp/guzzle/src/Handler/StreamHandler.php index d6ecb2b9e..cabf84c9b 100644 --- a/guzzlehttp/guzzle/src/Handler/StreamHandler.php +++ b/guzzlehttp/guzzle/src/Handler/StreamHandler.php @@ -530,6 +530,18 @@ private function resolveHost(RequestInterface $request, array $options): UriInte $uri = $request->getUri(); $host = $uri->getHost(); + + // Fold a numeric IPv4 spelling to the dotted quad libcurl connects + // to, rather than leaving it to the platform resolver: macOS reads + // the zero-padded 0177 as decimal 177 where glibc, musl and FreeBSD + // read octal 127. The Host header is serialized from the request and + // stays as written; the TLS peer name follows the same fold. + $canonicalHost = self::canonicalConnectionHost($host); + if ($canonicalHost !== $host) { + $uri = $uri->withHost($canonicalHost); + $host = $canonicalHost; + } + $hostForIpCheck = $host !== '' && $host[0] === '[' && \substr($host, -1) === ']' ? \substr($host, 1, -1) : $host; @@ -555,6 +567,84 @@ private function resolveHost(RequestInterface $request, array $options): UriInte return $uri; } + /** + * Returns a numeric IPv4 spelling folded to the dotted quad libcurl's + * ipv4_normalize() produces, and every other host unchanged. + */ + private static function canonicalConnectionHost(string $host): string + { + $binary = self::numericIpv4ToBinary($host); + if ($binary === null) { + return $host; + } + + return (string) \inet_ntop($binary); + } + + /** + * Returns the four-byte binary form of a host that a transport reads as a + * numeric IPv4 address, or null when it reads it as a name. + * + * The shape test is HostValidator::isNumericIpv4Host(); this method adds + * the range checks that predicate omits: every part but the last must fit + * one octet, and the last must fit the octets the earlier parts left. A + * trailing root dot is not swallowed, unlike libcurl 8.21.0 and later, + * because assertRequestHost() rejects that spelling first. + */ + private static function numericIpv4ToBinary(string $host): ?string + { + if (!HostValidator::isNumericIpv4Host($host)) { + return null; + } + + $values = []; + foreach (\explode('.', $host) as $part) { + $values[] = self::numericIpv4PartValue($part); + } + + // Every accepted value is a whole number no larger than 0xFFFFFFFF, + // which a float holds exactly, so the arithmetic below is correct on a + // 32-bit build too, where the widest part overflows an integer. + $address = (float) \array_pop($values); + + $packed = ''; + foreach ($values as $value) { + if ($value > 255.0) { + return null; + } + + $packed .= \chr((int) $value); + } + + $width = 4 - \count($values); + if ($address >= 256.0 ** $width) { + return null; + } + + for ($shift = $width - 1; $shift >= 0; --$shift) { + $packed .= \chr((int) \fmod(\floor($address / 256.0 ** $shift), 256.0)); + } + + return $packed; + } + + /** + * Returns the value of one accepted part as a float, so a part filling + * all four octets such as 2130706433 stays exact on every integer width. + */ + private static function numericIpv4PartValue(string $part): float + { + if ($part[0] === '0' && isset($part[1]) && ($part[1] === 'x' || $part[1] === 'X')) { + return (float) \hexdec((string) \substr($part, 2)); + } + + if ($part[0] === '0') { + return (float) \octdec($part); + } + + return (float) $part; + } + private function getDefaultContext(RequestInterface $request): array { $headers = ''; @@ -584,7 +674,7 @@ private function getDefaultContext(RequestInterface $request): array 'follow_location' => 0, ], 'ssl' => [ - 'peer_name' => $request->getUri()->getHost(), + 'peer_name' => self::canonicalConnectionHost($request->getUri()->getHost()), ], ]; diff --git a/guzzlehttp/promises/src/EachPromise.php b/guzzlehttp/promises/src/EachPromise.php index 4f8976224..5de9feb95 100644 --- a/guzzlehttp/promises/src/EachPromise.php +++ b/guzzlehttp/promises/src/EachPromise.php @@ -34,6 +34,9 @@ class EachPromise implements PromisorInterface /** @var bool|null */ private $mutex; + /** @var bool */ + private $stepWhileLocked = false; + /** * Configuration hash can include the following key value pairs: * @@ -123,16 +126,23 @@ private function createPromise(): void { $this->mutex = false; $this->aggregate = new Promise(function (): void { - if ($this->checkIfFinished()) { - return; - } - reset($this->pending); - // Consume a potentially fluctuating list of promises while - // ensuring that indexes are maintained (precluding array_shift). - while ($promise = current($this->pending)) { - next($this->pending); - $promise->wait(); - if (Is::settled($this->aggregate)) { + while (true) { + if ($this->checkIfFinished()) { + return; + } + reset($this->pending); + // Consume a potentially fluctuating list of promises while + // ensuring that indexes are maintained (precluding array_shift). + while ($promise = current($this->pending)) { + next($this->pending); + $promise->wait(); + if (Is::settled($this->aggregate)) { + return; + } + } + // Refill and re-sweep; give up only when nothing remains. + $this->refillPending(); + if (Is::settled($this->aggregate) || !$this->pending) { return; } } @@ -162,6 +172,10 @@ private function refillPending(): void $concurrency = is_callable($this->concurrency) ? ($this->concurrency)(count($this->pending)) : $this->concurrency; + // The callable can settle the aggregate; admit nothing more. + if (Is::settled($this->aggregate)) { + return; + } $concurrency = max($concurrency - count($this->pending), 0); // Concurrency may be set to 0 to disallow new promises. if (!$concurrency) { @@ -223,6 +237,8 @@ private function advanceIterator(): bool // Place a lock on the iterator so that we ensure to not recurse, // preventing fatal generator errors. if ($this->mutex) { + $this->stepWhileLocked = true; + return false; } @@ -231,14 +247,22 @@ private function advanceIterator(): bool try { $this->iterable->next(); $this->mutex = false; - - return true; } catch (\Throwable $e) { $this->aggregate->reject($e); $this->mutex = false; return false; } + + // Run the completion check that locked steps skipped. + if ($this->stepWhileLocked) { + $this->stepWhileLocked = false; + if (!Is::settled($this->aggregate)) { + $this->checkIfFinished(); + } + } + + return true; } private function step(int $idx): void @@ -259,6 +283,7 @@ private function step(int $idx): void } } + /** @phpstan-impure */ private function checkIfFinished(): bool { if (!$this->pending && !$this->iterable->valid()) { diff --git a/guzzlehttp/uri-template/src/UriTemplate.php b/guzzlehttp/uri-template/src/UriTemplate.php index e848cd3b6..482584e1b 100644 --- a/guzzlehttp/uri-template/src/UriTemplate.php +++ b/guzzlehttp/uri-template/src/UriTemplate.php @@ -25,54 +25,6 @@ final class UriTemplate '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true], ]; - /** - * @var string[] Delimiters - */ - private static $delims = [ - ':', - '/', - '?', - '#', - '[', - ']', - '@', - '!', - '$', - '&', - '\'', - '(', - ')', - '*', - '+', - ',', - ';', - '=', - ]; - - /** - * @var string[] Percent encoded delimiters - */ - private static $delimsPct = [ - '%3A', - '%2F', - '%3F', - '%23', - '%5B', - '%5D', - '%40', - '%21', - '%24', - '%26', - '%27', - '%28', - '%29', - '%2A', - '%2B', - '%2C', - '%3B', - '%3D', - ]; - /** * @param array $variables Variables to use in the template expansion * @@ -125,7 +77,8 @@ private static function expandMatch(array $matches, array $variables): string $prefix = self::$operatorHash[$parsed['operator']]['prefix']; $joiner = self::$operatorHash[$parsed['operator']]['joiner']; $useQuery = self::$operatorHash[$parsed['operator']]['query']; - $allUndefined = true; + $allowReserved = $parsed['operator'] === '+' || $parsed['operator'] === '#'; + $hasDefinedVariable = false; foreach ($parsed['values'] as $value) { if (!isset($variables[$value['value']])) { @@ -142,24 +95,25 @@ private static function expandMatch(array $matches, array $variables): string /** @var mixed $var */ foreach ($variable as $key => $var) { if ($isAssoc) { - $key = \rawurlencode((string) $key); + $rawKey = (string) $key; + $key = \rawurlencode($rawKey); $isNestedArray = \is_array($var); } else { $isNestedArray = false; } if (!$isNestedArray) { - $var = \rawurlencode((string) $var); - if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { - $var = self::decodeReserved($var); - } + $var = self::encodeValue(self::stringifyValue($var), $allowReserved); } if ($value['modifier'] === '*') { if ($isAssoc) { if ($isNestedArray) { // Nested arrays must allow for deeply nested structures. - $var = \http_build_query([$key => $var], '', '&', \PHP_QUERY_RFC3986); + $var = \http_build_query([$rawKey => self::stringifyNonFiniteFloats($var)], '', '&', \PHP_QUERY_RFC3986); + if ($var === '') { + continue; + } } else { $var = \sprintf('%s=%s', (string) $key, (string) $var); } @@ -172,8 +126,8 @@ private static function expandMatch(array $matches, array $variables): string $kvp[$key] = $var; } - if (0 === \count($variable)) { - $actuallyUseQuery = false; + if ($kvp === []) { + continue; } elseif ($value['modifier'] === '*') { $expanded = \implode($joiner, $kvp); if ($isAssoc) { @@ -194,14 +148,12 @@ private static function expandMatch(array $matches, array $variables): string $expanded = \implode(',', $kvp); } } else { - $allUndefined = false; + $variable = self::stringifyValue($variable); + if ($value['modifier'] === ':' && isset($value['position'])) { - $variable = \substr((string) $variable, 0, $value['position']); - } - $expanded = \rawurlencode((string) $variable); - if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { - $expanded = self::decodeReserved($expanded); + $variable = self::prefixValue($variable, $value['position']); } + $expanded = self::encodeValue($variable, $allowReserved); } if ($actuallyUseQuery) { @@ -212,20 +164,18 @@ private static function expandMatch(array $matches, array $variables): string } } + $hasDefinedVariable = true; + $replacements[] = $expanded; } $ret = \implode($joiner, $replacements); - if ('' === $ret) { - // Spec section 3.2.4 and 3.2.5 - if (false === $allUndefined && ('#' === $prefix || '.' === $prefix)) { - return $prefix; - } - } else { - if ('' !== $prefix) { - return \sprintf('%s%s', $prefix, $ret); - } + // Spec section 3.2.1 and appendix A: the operator's first string is + // appended once any variable in the expression is defined, even when + // every defined value expands to an empty string. + if ('' !== $prefix && $hasDefinedVariable) { + return \sprintf('%s%s', $prefix, $ret); } return $ret; @@ -252,7 +202,7 @@ private static function parseExpression(string $expression): array $result['values'] = []; foreach (\explode(',', $expression) as $value) { - $value = \trim($value); + $value = \trim($value, " \n\r\t\0\x0B"); $varspec = []; if ($colonPos = \strpos($value, ':')) { $varspec['value'] = (string) \substr($value, 0, $colonPos); @@ -285,11 +235,162 @@ private static function isAssoc(array $array): bool } /** - * Removes percent encoding on reserved characters (used with + and # - * modifiers). + * Cast a variable value to its expansion string. + * + * Non-finite floats are converted explicitly because coercing them to + * string triggers a warning on PHP 8.5. + * + * @param mixed $value + */ + private static function stringifyValue($value): string + { + if (\is_float($value) && !\is_finite($value)) { + return \is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF'); + } + + return (string) $value; + } + + /** + * Stringify non-finite float members of a nested array so that + * http_build_query does not trigger coercion warnings on PHP 8.5. + * + * @param array $value + * + * @return array + */ + private static function stringifyNonFiniteFloats(array $value): array + { + /** @var mixed $member */ + foreach ($value as $key => $member) { + if (\is_float($member) && !\is_finite($member)) { + $value[$key] = self::stringifyValue($member); + } elseif (\is_array($member)) { + $value[$key] = self::stringifyNonFiniteFloats($member); + } + } + + return $value; + } + + /** + * Select a prefix by Unicode code points and pct-encoded characters. + * + * Malformed bytes continue to count individually. */ - private static function decodeReserved(string $string): string + private static function prefixValue(string $value, int $length): string + { + if ($length < 1) { + return \substr($value, 0, $length); + } + + $valueLength = \strlen($value); + if ($valueLength <= $length) { + return $value; + } + + $offset = 0; + + for ($taken = 0; $taken < $length && $offset < $valueLength; ++$taken) { + $offset += self::prefixCharacterByteLength($value, $offset, $valueLength); + } + + return \substr($value, 0, $offset); + } + + private static function prefixCharacterByteLength(string $value, int $offset, int $valueLength): int + { + if ($value[$offset] === '%' && $offset + 2 < $valueLength && \strspn($value, '0123456789ABCDEFabcdef', $offset + 1, 2) === 2) { + $lead = (int) \hexdec(\substr($value, $offset + 1, 2)); + $octets = self::utf8SequenceByteLength($lead); + + if ($octets === 1) { + return 3; + } + + $candidate = \chr($lead); + + for ($index = 1; $index < $octets; ++$index) { + $tripletOffset = $offset + 3 * $index; + + if ($tripletOffset + 2 >= $valueLength || $value[$tripletOffset] !== '%' || \strspn($value, '0123456789ABCDEFabcdef', $tripletOffset + 1, 2) !== 2) { + return 3; + } + + $candidate .= \chr((int) \hexdec(\substr($value, $tripletOffset + 1, 2))); + } + + return self::isSingleUtf8CodePoint($candidate) ? 3 * $octets : 3; + } + + $octets = self::utf8SequenceByteLength(\ord($value[$offset])); + if ($octets === 1 || $offset + $octets > $valueLength) { + return 1; + } + + return self::isSingleUtf8CodePoint(\substr($value, $offset, $octets)) ? $octets : 1; + } + + private static function utf8SequenceByteLength(int $lead): int + { + if ($lead >= 0xC2 && $lead <= 0xDF) { + return 2; + } + + if ($lead >= 0xE0 && $lead <= 0xEF) { + return 3; + } + + return $lead >= 0xF0 && $lead <= 0xF4 ? 4 : 1; + } + + private static function isSingleUtf8CodePoint(string $candidate): bool + { + $result = \preg_match('/\A.\z/us', $candidate); + + if ($result !== false) { + return $result === 1; + } + + if (\preg_last_error() === \PREG_BAD_UTF8_ERROR) { + return false; + } + + throw new \RuntimeException(\sprintf('Unable to process template: %s', \preg_last_error_msg())); + } + + private static function encodeValue(string $value, bool $allowReserved): string { - return \str_replace(self::$delimsPct, self::$delims, $string); + if ($value === '') { + return ''; + } + + $matches = []; + if (\preg_match_all('/%[0-9A-Fa-f]{2}|./s', $value, $matches) === false) { + throw new \RuntimeException(\sprintf('Unable to encode URI template value: %s', \preg_last_error_msg())); + } + + $encoded = ''; + + foreach ($matches[0] as $token) { + if ($allowReserved && \preg_match('/\A%[0-9A-Fa-f]{2}\z/', $token) === 1) { + $encoded .= $token; + continue; + } + + if (\preg_match('/\A[A-Za-z0-9._~-]\z/', $token) === 1) { + $encoded .= $token; + continue; + } + + if ($allowReserved && \strlen($token) === 1 && \strpos(":/?#[]@!$&'()*+,;=", $token) !== false) { + $encoded .= $token; + continue; + } + + $encoded .= \rawurlencode($token); + } + + return $encoded; } } diff --git a/justinrainbow/json-schema/dist/schema/json-schema-draft-07.json b/justinrainbow/json-schema/dist/schema/json-schema-draft-07.json new file mode 100644 index 000000000..c173cb868 --- /dev/null +++ b/justinrainbow/json-schema/dist/schema/json-schema-draft-07.json @@ -0,0 +1,245 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#" + } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { + "$ref": "#/definitions/nonNegativeInteger" + }, + { + "default": 0 + } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true, + "default": [] + } + }, + "type": [ + "object", + "boolean" + ], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minLength": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "$ref": "#" + }, + "items": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/schemaArray" + } + ], + "default": true + }, + "maxItems": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minItems": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { + "$ref": "#" + }, + "maxProperties": { + "$ref": "#/definitions/nonNegativeInteger" + }, + "minProperties": { + "$ref": "#/definitions/nonNegativeIntegerDefault0" + }, + "required": { + "$ref": "#/definitions/stringArray" + }, + "additionalProperties": { + "$ref": "#" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#" + }, + "propertyNames": { + "format": "regex" + }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "$ref": "#" + }, + { + "$ref": "#/definitions/stringArray" + } + ] + } + }, + "propertyNames": { + "$ref": "#" + }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { + "$ref": "#/definitions/simpleTypes" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/simpleTypes" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { + "type": "string" + }, + "contentMediaType": { + "type": "string" + }, + "contentEncoding": { + "type": "string" + }, + "if": { + "$ref": "#" + }, + "then": { + "$ref": "#" + }, + "else": { + "$ref": "#" + }, + "allOf": { + "$ref": "#/definitions/schemaArray" + }, + "anyOf": { + "$ref": "#/definitions/schemaArray" + }, + "oneOf": { + "$ref": "#/definitions/schemaArray" + }, + "not": { + "$ref": "#" + } + }, + "default": true +} diff --git a/justinrainbow/json-schema/dist/schema/json-schema-draft-2019-09.json b/justinrainbow/json-schema/dist/schema/json-schema-draft-2019-09.json new file mode 100644 index 000000000..bc67d49b4 --- /dev/null +++ b/justinrainbow/json-schema/dist/schema/json-schema-draft-2019-09.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://json-schema.org/draft/2019-09/schema", + "title": "Core and Validation specifications meta-schema", + "type": ["object", "boolean"], + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/$defs/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": ["array", "boolean", "integer", "null", "number", "object", "string"] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "properties": { + "$id": { "type": "string", "format": "uri-reference" }, + "$schema": { "type": "string", "format": "uri" }, + "$anchor": { "type": "string", "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" }, + "$ref": { "type": "string", "format": "uri-reference" }, + "$recursiveRef": { "type": "string", "format": "uri-reference" }, + "$recursiveAnchor": { "type": "boolean", "default": false }, + "$vocabulary": { + "type": "object", + "propertyNames": { "type": "string", "format": "uri" }, + "additionalProperties": { "type": "boolean" } + }, + "$comment": { "type": "string" }, + "$defs": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "definitions": { + "$comment": "Retained for backwards compatibility, replaced by $defs.", + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "default": true, + "deprecated": { "type": "boolean", "default": false }, + "readOnly": { "type": "boolean", "default": false }, + "writeOnly": { "type": "boolean", "default": false }, + "examples": { "type": "array", "items": true }, + "multipleOf": { "type": "number", "exclusiveMinimum": 0 }, + "maximum": { "type": "number" }, + "exclusiveMaximum": { "type": "number" }, + "minimum": { "type": "number" }, + "exclusiveMinimum": { "type": "number" }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { "type": "string", "format": "regex" }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/$defs/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { "type": "boolean", "default": false }, + "contains": { "$ref": "#" }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "unevaluatedItems": { "$ref": "#" }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "unevaluatedProperties": { "$ref": "#" }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$ref": "#" } + }, + "dependentRequired": { + "type": "object", + "additionalProperties": { "$ref": "#/$defs/stringArray" } + }, + "dependencies": { + "$comment": "Retained for backwards compatibility, replaced by dependentSchemas/dependentRequired.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/$defs/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$ref": "#" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} \ No newline at end of file diff --git a/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php b/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php index ed6cf03e0..081e38ad7 100644 --- a/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php +++ b/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php @@ -39,7 +39,9 @@ class ConstraintError extends Enum public const INVALID_SCHEMA = 'invalidSchema'; public const LENGTH_MAX = 'maxLength'; public const LENGTH_MIN = 'minLength'; + public const MAX_CONTAINS = 'maxContains'; public const MAXIMUM = 'maximum'; + public const MIN_CONTAINS = 'minContains'; public const MIN_ITEMS = 'minItems'; public const MINIMUM = 'minimum'; public const MISSING_ERROR = 'missingError'; @@ -58,6 +60,8 @@ class ConstraintError extends Enum public const PROPERTY_NAMES = 'propertyNames'; public const TYPE = 'type'; public const UNIQUE_ITEMS = 'uniqueItems'; + public const CONTENT_MEDIA_TYPE = 'contentMediaType'; + public const CONTENT_ENCODING = 'contentEncoding'; /** * @return string @@ -97,8 +101,10 @@ public function getMessage() self::LENGTH_MAX => 'Must be at most %d characters long', self::INVALID_SCHEMA => 'Schema is not valid', self::LENGTH_MIN => 'Must be at least %d characters long', + self::MAX_CONTAINS => 'There must be a maximum of %d valid items in the array, %d found', self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found', self::MAXIMUM => 'Must have a maximum value less than or equal to %d', + self::MIN_CONTAINS => 'There must be a minimum of %d valid items in the array, %d found', self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found', self::MINIMUM => 'Must have a minimum value greater than or equal to %d', self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', @@ -115,7 +121,9 @@ public function getMessage() self::PROPERTIES_MAX => 'Must contain no more than %d properties', self::PROPERTY_NAMES => 'Property name %s is invalid', self::TYPE => '%s value found, but %s is required', - self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array' + self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array', + self::CONTENT_MEDIA_TYPE => 'Value is not valid with content media type', + self::CONTENT_ENCODING => 'Value is not valid with content encoding', ]; if (!isset($messages[$name])) { diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php index 3c7e824b1..7538ad673 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php @@ -11,6 +11,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface /** @var string */ protected $inlineSchemaProperty = '$schema'; + /** @deprecated CHECK_MODE_NONE is unused and will be removed in the next major release (7.0.0) */ public const CHECK_MODE_NONE = 0x00000000; public const CHECK_MODE_NORMAL = 0x00000001; public const CHECK_MODE_TYPE_CAST = 0x00000002; diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php index 5f2448961..5d6c55cb5 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/AdditionalPropertiesConstraint.php @@ -66,7 +66,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n } foreach ($additionalProperties as $key => $additionalPropertiesValue) { - $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $additionalPropertiesValue]); + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $key]); } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php index 578a27c37..ec61b73b6 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/FormatConstraint.php @@ -155,7 +155,7 @@ private function validateColor(string $color): bool return true; } - return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) !== false; + return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) === 1; } private function validateStyle(string $style): bool @@ -168,7 +168,7 @@ private function validateStyle(string $style): bool private function validatePhone(string $phone): bool { - return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) !== false; + return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) === 1; } private function validateHostname(string $host): bool diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesNamesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesNamesConstraint.php index d621ecf26..96312cef8 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesNamesConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft06/PropertiesNamesConstraint.php @@ -61,5 +61,20 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n } } } + + if (property_exists($schema->propertyNames, 'const')) { + foreach ($propertyNames as $propertyName => $_) { + if ($propertyName !== $schema->propertyNames->const) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'const', 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'enum')) { + $diff = array_diff(array_keys($propertyNames), $schema->propertyNames->enum); + foreach ($diff as $propertyName) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'enum', 'name' => $propertyName]); + } + } } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php new file mode 100644 index 000000000..8444d0535 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalItemsConstraint.php @@ -0,0 +1,61 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalItems')) { + return; + } + + if ($schema->additionalItems === true) { + return; + } + if ($schema->additionalItems === false && !property_exists($schema, 'items')) { + return; + } + + if (!is_array($value)) { + return; + } + if (!property_exists($schema, 'items')) { + return; + } + if (property_exists($schema, 'items') && is_object($schema->items)) { + return; + } + + $additionalItems = array_diff_key($value, property_exists($schema, 'items') ? $schema->items : []); + + foreach ($additionalItems as $propertyName => $propertyValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $schema->additionalItems, $path, $i); + + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $path, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php new file mode 100644 index 000000000..7162c11be --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AdditionalPropertiesConstraint.php @@ -0,0 +1,93 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalProperties')) { + return; + } + + if ($schema->additionalProperties === true) { + return; + } + + if (!is_object($value)) { + return; + } + + $additionalProperties = get_object_vars($value); + + if (isset($schema->properties)) { + $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); + } + + if (isset($schema->patternProperties)) { + $patterns = array_keys(get_object_vars($schema->patternProperties)); + + foreach ($additionalProperties as $key => $_) { + foreach ($patterns as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { + unset($additionalProperties[$key]); + break; + } + } + } + } + + if (is_object($schema->additionalProperties)) { + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $path, $i); // @todo increment path + if ($schemaConstraint->isValid()) { + unset($additionalProperties[$key]); + } + } + } + + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $key]); + } + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', +// '\d' => '[0-9]', + '\p{digit}' => '\p{Nd}', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AllOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AllOfConstraint.php new file mode 100644 index 000000000..6aa9fdf2e --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AllOfConstraint.php @@ -0,0 +1,42 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'allOf')) { + return; + } + + foreach ($schema->allOf as $allOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $allOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + continue; + } + $this->addError(ConstraintError::ALL_OF(), $path); + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AnyOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AnyOfConstraint.php new file mode 100644 index 000000000..85242962a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/AnyOfConstraint.php @@ -0,0 +1,51 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'anyOf')) { + return; + } + + foreach ($schema->anyOf as $anyOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + + try { + $schemaConstraint->check($value, $anyOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + $this->errorBag()->reset(); + + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } catch (ValidationException $e) { + } + } + + $this->addError(ConstraintError::ANY_OF(), $path); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ConstConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ConstConstraint.php new file mode 100644 index 000000000..e0ff337b1 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ConstConstraint.php @@ -0,0 +1,35 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'const')) { + return; + } + + if (DeepComparer::isEqual($value, $schema->const)) { + return; + } + + $this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContainsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContainsConstraint.php new file mode 100644 index 000000000..e2e48ffe2 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContainsConstraint.php @@ -0,0 +1,47 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'contains')) { + return; + } + + $properties = []; + if (!is_array($value)) { + return; + } + + foreach ($value as $propertyName => $propertyValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + + $schemaConstraint->check($propertyValue, $schema->contains, $path, $i); + if ($schemaConstraint->isValid()) { + return; + } + } + + $this->addError(ConstraintError::CONTAINS(), $path, ['contains' => $schema->contains]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContentConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContentConstraint.php new file mode 100644 index 000000000..4e5e3f010 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ContentConstraint.php @@ -0,0 +1,55 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'contentMediaType') && !property_exists($schema, 'contentEncoding')) { + return; + } + if (!is_string($value)) { + return; + } + + $decodedValue = $value; + + if (property_exists($schema, 'contentEncoding')) { + if ($schema->contentEncoding === 'base64') { + if (!preg_match('/^[A-Za-z0-9+\/=]+$/', $decodedValue)) { + $this->addError(ConstraintError::CONTENT_ENCODING(), $path, ['contentEncoding' => $schema->contentEncoding]); + + return; + } + $decodedValue = base64_decode($decodedValue); + } + } + + if (property_exists($schema, 'contentMediaType')) { + if ($schema->contentMediaType === 'application/json') { + json_decode($decodedValue, false); + if (json_last_error() === JSON_ERROR_NONE) { + return; + } + } + + $this->addError(ConstraintError::CONTENT_MEDIA_TYPE(), $path, ['contentMediaType' => $schema->contentMediaType]); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/DependenciesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/DependenciesConstraint.php new file mode 100644 index 000000000..f4b03ff8c --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/DependenciesConstraint.php @@ -0,0 +1,64 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'dependencies')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->dependencies as $dependant => $dependencies) { + if (!property_exists($value, $dependant)) { + continue; + } + if ($dependencies === true) { + continue; + } + if ($dependencies === false) { + $this->addError(ConstraintError::FALSE(), $path, ['dependant' => $dependant]); + continue; + } + + if (is_array($dependencies)) { + foreach ($dependencies as $dependency) { + if (property_exists($value, $dependant) && !property_exists($value, $dependency)) { + $this->addError(ConstraintError::DEPENDENCIES(), $path, ['dependant' => $dependant, 'dependency' => $dependency]); + } + } + } + + if (is_object($dependencies)) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $dependencies, $path, $i); + if (!$schemaConstraint->isValid()) { + $this->addErrors($schemaConstraint->getErrors()); + } + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php new file mode 100644 index 000000000..084ce96cc --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php @@ -0,0 +1,83 @@ +getSchemaStorage() : new SchemaStorage(), + $factory ? $factory->getUriRetriever() : new UriRetriever(), + $factory ? $factory->getConfig() : Constraint::CHECK_MODE_NORMAL + )); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (is_bool($schema)) { + if ($schema === false) { + $this->addError(ConstraintError::FALSE(), $path, []); + } + + return; + } + + // Apply defaults + $this->checkForKeyword('ref', $value, $schema, $path, $i); + $this->checkForKeyword('required', $value, $schema, $path, $i); + $this->checkForKeyword('contains', $value, $schema, $path, $i); + $this->checkForKeyword('properties', $value, $schema, $path, $i); + $this->checkForKeyword('propertyNames', $value, $schema, $path, $i); + $this->checkForKeyword('patternProperties', $value, $schema, $path, $i); + $this->checkForKeyword('type', $value, $schema, $path, $i); + $this->checkForKeyword('not', $value, $schema, $path, $i); + $this->checkForKeyword('dependencies', $value, $schema, $path, $i); + $this->checkForKeyword('allOf', $value, $schema, $path, $i); + $this->checkForKeyword('anyOf', $value, $schema, $path, $i); + $this->checkForKeyword('oneOf', $value, $schema, $path, $i); + $this->checkForKeyword('ifThenElse', $value, $schema, $path, $i); + + $this->checkForKeyword('additionalProperties', $value, $schema, $path, $i); + $this->checkForKeyword('items', $value, $schema, $path, $i); + $this->checkForKeyword('additionalItems', $value, $schema, $path, $i); + $this->checkForKeyword('uniqueItems', $value, $schema, $path, $i); + $this->checkForKeyword('minItems', $value, $schema, $path, $i); + $this->checkForKeyword('minProperties', $value, $schema, $path, $i); + $this->checkForKeyword('maxProperties', $value, $schema, $path, $i); + $this->checkForKeyword('minimum', $value, $schema, $path, $i); + $this->checkForKeyword('maximum', $value, $schema, $path, $i); + $this->checkForKeyword('minLength', $value, $schema, $path, $i); + $this->checkForKeyword('exclusiveMinimum', $value, $schema, $path, $i); + $this->checkForKeyword('maxItems', $value, $schema, $path, $i); + $this->checkForKeyword('maxLength', $value, $schema, $path, $i); + $this->checkForKeyword('exclusiveMaximum', $value, $schema, $path, $i); + $this->checkForKeyword('enum', $value, $schema, $path, $i); + $this->checkForKeyword('const', $value, $schema, $path, $i); + $this->checkForKeyword('multipleOf', $value, $schema, $path, $i); + $this->checkForKeyword('format', $value, $schema, $path, $i); + $this->checkForKeyword('pattern', $value, $schema, $path, $i); + $this->checkForKeyword('content', $value, $schema, $path, $i); + } + + /** + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor($keyword); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/EnumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/EnumConstraint.php new file mode 100644 index 000000000..6a81401d7 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/EnumConstraint.php @@ -0,0 +1,41 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'enum')) { + return; + } + + foreach ($schema->enum as $enumCase) { + if (DeepComparer::isEqual($value, $enumCase)) { + return; + } + + if (is_numeric($value) && is_numeric($enumCase) && DeepComparer::isEqual((float) $value, (float) $enumCase)) { + return; + } + } + + $this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMaximumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMaximumConstraint.php new file mode 100644 index 000000000..484768f8a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMaximumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'exclusiveMaximum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value < $schema->exclusiveMaximum) { + return; + } + + $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['exclusiveMaximum' => $schema->exclusiveMaximum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMinimumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMinimumConstraint.php new file mode 100644 index 000000000..c97c8c535 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ExclusiveMinimumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'exclusiveMinimum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value > $schema->exclusiveMinimum) { + return; + } + + $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['exclusiveMinimum' => $schema->exclusiveMinimum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Factory.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Factory.php new file mode 100644 index 000000000..cb5599525 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/Factory.php @@ -0,0 +1,48 @@ + + */ + protected $constraintMap = [ + 'schema' => Draft07Constraint::class, + 'additionalProperties' => AdditionalPropertiesConstraint::class, + 'additionalItems' => AdditionalItemsConstraint::class, + 'dependencies' => DependenciesConstraint::class, + 'type' => TypeConstraint::class, + 'const' => ConstConstraint::class, + 'enum' => EnumConstraint::class, + 'uniqueItems' => UniqueItemsConstraint::class, + 'minItems' => MinItemsConstraint::class, + 'minProperties' => MinPropertiesConstraint::class, + 'maxProperties' => MaxPropertiesConstraint::class, + 'minimum' => MinimumConstraint::class, + 'maximum' => MaximumConstraint::class, + 'exclusiveMinimum' => ExclusiveMinimumConstraint::class, + 'minLength' => MinLengthConstraint::class, + 'maxLength' => MaxLengthConstraint::class, + 'maxItems' => MaxItemsConstraint::class, + 'exclusiveMaximum' => ExclusiveMaximumConstraint::class, + 'multipleOf' => MultipleOfConstraint::class, + 'required' => RequiredConstraint::class, + 'format' => FormatConstraint::class, + 'anyOf' => AnyOfConstraint::class, + 'allOf' => AllOfConstraint::class, + 'oneOf' => OneOfConstraint::class, + 'not' => NotConstraint::class, + 'ifThenElse' => IfThenElseConstraint::class, + 'contains' => ContainsConstraint::class, + 'propertyNames' => PropertiesNamesConstraint::class, + 'patternProperties' => PatternPropertiesConstraint::class, + 'pattern' => PatternConstraint::class, + 'properties' => PropertiesConstraint::class, + 'items' => ItemsConstraint::class, + 'ref' => RefConstraint::class, + 'content' => ContentConstraint::class, + ]; +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php new file mode 100644 index 000000000..8ddc879dd --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php @@ -0,0 +1,354 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'format')) { + return; + } + + if (!is_string($value)) { + return; + } + + switch ($schema->format) { + case 'date': + if (!$this->validateDateTime($value, 'Y-m-d')) { + $this->addError(ConstraintError::FORMAT_DATE(), $path, ['date' => $value, 'format' => $schema->format]); + } + break; + case 'time': + if (!$this->validateDateTime($value, 'H:i:sP') + && !$this->validateDateTime($value, 'H:i:sp') + && !$this->validateDateTime($value, 'H:i:s.up') + && !$this->validateDateTime($value, 'H:i:s.uP') + ) { + $this->addError(ConstraintError::FORMAT_TIME(), $path, ['time' => $value, 'format' => $schema->format]); + } + break; + case 'date-time': + if (!$this->validateRfc3339DateTime($value)) { + $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, ['dateTime' => $value, 'format' => $schema->format]); + } + break; + case 'utc-millisec': + if (!$this->validateDateTime($value, 'U')) { + $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, ['value' => $value, 'format' => $schema->format]); + } + break; + case 'regex': + if (!$this->validateRegex($value)) { + $this->addError(ConstraintError::FORMAT_REGEX(), $path, ['value' => $value, 'format' => $schema->format]); + } + break; + case 'ip-address': + case 'ipv4': + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4) === null) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + case 'ipv6': + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6) === null) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + case 'color': + if (!$this->validateColor($value)) { + $this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]); + } + break; + case 'style': + if (!$this->validateStyle($value)) { + $this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]); + } + break; + case 'phone': + if (!$this->validatePhone($value)) { + $this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]); + } + break; + case 'uri': + if (!UriValidator::isValid($value)) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + + case 'uriref': + case 'uri-reference': + if (!(UriValidator::isValid($value) || RelativeReferenceValidator::isValid($value))) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + case 'uri-template': + if (!$this->validateUriTemplate($value)) { + $this->addError(ConstraintError::FORMAT_URI_TEMPLATE(), $path, ['format' => $schema->format]); + } + break; + + case 'email': + if (filter_var($value, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE) === null) { + $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]); + } + break; + case 'host-name': + case 'hostname': + if (!$this->validateHostname($value)) { + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); + } + break; + case 'idn-hostname': + if (!$this->validateInternationalizedHostname($value)) { + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); + } + break; + case 'json-pointer': + if (!$this->validateJsonPointer($value)) { + $this->addError(ConstraintError::FORMAT_JSON_POINTER(), $path, ['format' => $schema->format]); + } + break; + default: + break; + } + } + + private function validateDateTime(string $datetime, string $format): bool + { + $datetime = strtoupper($datetime); // Cleanup for lowercase z + $isPhpLt80WithZulu = PHP_VERSION_ID < 80000 && substr($datetime, -1) === 'Z'; + $isLeap = substr($datetime, 6, 2) === '60'; + $input = $datetime; + + // Correct for Zulu in PHP < 8.0 + if ($isPhpLt80WithZulu) { + $input = sprintf('%s+00:00', substr($input, 0, -1)); + } + // Correct for leap second + if ($isLeap) { + $input = sprintf('%s59%s', substr($datetime, 0, 6), substr($datetime, 8)); + } + + $dt = \DateTimeImmutable::createFromFormat($format, $input); + if (!$dt) { + return false; + } + + // Handle invalid timezone offsets + $timezoneOffset = $dt->getTimezone()->getOffset($dt); + if ($timezoneOffset >= 86400 || $timezoneOffset <= -86400) { + return false; + } + + $expected = $dt->format($format); + // Correct for trailing zeros on microseconds + if ($format === 'H:i:s.up' || $format === 'H:i:s.uP') { + $expected = sprintf( + '%s%s', + rtrim($dt->format('H:i:s.u'), '0'), + $dt->format(substr($format, -1)) + ); + } + // Correct back for leap seconds + if ($isLeap) { + // Only when 23:59:59 in UTC + $utcDT = $dt->setTimezone(new DateTimeZone('UTC')); + if ($utcDT->format('H:i:s') !== '23:59:59') { + return false; + } + + $expected = sprintf('%s60%s', substr($expected, 0, 6), substr($expected, 8)); + } + // Correct back for PHP > 8.0 and Zulu + if ($isPhpLt80WithZulu) { + $expected = sprintf('%sZ', substr($expected, 0, -6)); + } + + return $datetime === $expected; + } + + private function validateRegex(string $regex): bool + { + return preg_match(self::jsonPatternToPhpRegex($regex), '') !== false; + } + + /** + * Transform a JSON pattern into a PCRE regex + */ + private static function jsonPatternToPhpRegex(string $pattern): string + { + return '~' . str_replace('~', '\\~', $pattern) . '~u'; + } + + private function validateColor(string $color): bool + { + if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia', + 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', + 'red', 'silver', 'teal', 'white', 'yellow'])) { + return true; + } + + return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) === 1; + } + + private function validateStyle(string $style): bool + { + $properties = explode(';', rtrim($style, ';')); + $invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT); + + return empty($invalidEntries); + } + + private function validatePhone(string $phone): bool + { + return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) === 1; + } + + private function validateHostname(string $host): bool + { + $hostnameRegex = '/^(?!-)(?!.*?[^A-Za-z0-9\-\.])(?:(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)*(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/'; + + return preg_match($hostnameRegex, $host) === 1; + } + + private function validateInternationalizedHostname(string $host): bool + { + if ($host === '') { + return false; + } + $host = rtrim($host, '.'); + $labels = explode('.', $host); + $asciiLabels = []; + + if ($labels === false) { + return false; + } + + foreach ($labels as $label) { + if ($label === '') { + return false; + } + + // CONTEXTJ / CONTEXTO checks + if ( + // Greek KERAIA U+0375 + preg_match('/\x{0375}/u', $label) && + !preg_match('/\x{0375}[\x{0370}-\x{03FF}]/u', $label) + ) { + return false; + } + + // Hebrew GERESH / GERSHAYIM U+05F3 / U+05F4 + if (preg_match('/[\x{05F3}\x{05F4}]/u', $label) && + !preg_match('/[\x{0590}-\x{05FF}][\x{05F3}\x{05F4}]/u', $label) + ) { + return false; + } + + // Katakana middle dot U+30FB + if (str_contains($label, "\u{30FB}") && + !preg_match('/[\x{30A0}-\x{30FF}]/u', $label) + ) { + return false; + } + + // Arabic digit mixing + $hasArabicIndic = preg_match('/[\x{0660}-\x{0669}]/u', $label); + $hasExtArabicIndic = preg_match('/[\x{06F0}-\x{06F9}]/u', $label); + if ($hasArabicIndic && $hasExtArabicIndic) { + return false; + } + + // Devanagari danda U+0964 / U+0965 + if (preg_match('/[\x{0964}\x{0965}]/u', $label) && + !preg_match('/[\x{0900}-\x{097F}]/u', $label) + ) { + return false; + } + + // ZWNJ / ZWJ U+200C / U+200D + if (preg_match('/[\x{200C}\x{200D}]/u', $label)) { + return false; + } + + $ascii = idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46); + if ($ascii === false) { + return false; + } + // DNS label length + if (strlen($ascii) > 63) { + return false; + } + // LDH rule (after IDNA) + if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $ascii)) { + return false; + } + $asciiLabels[] = $ascii; + } + + // Total hostname length (ASCII) + $asciiHost = implode('.', $asciiLabels); + + return strlen($asciiHost) <= 253; + } + + private function validateJsonPointer(string $value): bool + { + // Must be empty or start with a forward slash + if ($value !== '' && $value[0] !== '/') { + return false; + } + + // Split into reference tokens and check for invalid escape sequences + $tokens = explode('/', $value); + array_shift($tokens); // remove leading empty part due to leading slash + + foreach ($tokens as $token) { + // "~" must only be followed by "0" or "1" + if (preg_match('/~(?![01])/', $token)) { + return false; + } + } + + return true; + } + + private function validateRfc3339DateTime(string $value): bool + { + $dateTime = Rfc3339::createFromString($value); + if (is_null($dateTime)) { + return false; + } + + // Compare value and date result to be equal + return true; + } + + private function validateUriTemplate(string $value): bool + { + return preg_match( + '/^(?:[^\{\}]*|\{[a-zA-Z0-9_:%\/\.~\-\+\*]+\})*$/', + $value + ) === 1; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/IfThenElseConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/IfThenElseConstraint.php new file mode 100644 index 000000000..d3fe82c7e --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/IfThenElseConstraint.php @@ -0,0 +1,67 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'if')) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $ifSchema = $schema->if; + + if (!is_bool($ifSchema)) { + $schemaConstraint->check($value, $ifSchema, $path, $i); + $meetsIfConditions = $schemaConstraint->isValid(); + $schemaConstraint->reset(); + } else { + $meetsIfConditions = $ifSchema; + } + + if ($meetsIfConditions) { + if (!property_exists($schema, 'then')) { + return; + } + + $schemaConstraint->check($value, $schema->then, $path, $i); + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + + return; + } + + if (!property_exists($schema, 'else')) { + return; + } + + $schemaConstraint->check($value, $schema->else, $path, $i); + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php new file mode 100644 index 000000000..16abae328 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/ItemsConstraint.php @@ -0,0 +1,52 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'items')) { + return; + } + + if (!is_array($value)) { + return; + } + + foreach ($value as $propertyName => $propertyValue) { + $itemSchema = $schema->items; + if (is_array($itemSchema)) { + if (!array_key_exists($propertyName, $itemSchema)) { + continue; + } + + $itemSchema = $itemSchema[$propertyName]; + } + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $itemSchema, $path, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxItemsConstraint.php new file mode 100644 index 000000000..101a0cf98 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxItemsConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxItems')) { + return; + } + + if (!is_array($value)) { + return; + } + + $count = count($value); + if ($count <= $schema->maxItems) { + return; + } + + $this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxLengthConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxLengthConstraint.php new file mode 100644 index 000000000..9043cf92d --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxLengthConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxLength')) { + return; + } + + if (!is_string($value)) { + return; + } + + $length = mb_strlen($value); + if ($length <= $schema->maxLength) { + return; + } + + $this->addError(ConstraintError::LENGTH_MAX(), $path, ['maxLength' => $schema->maxLength, 'found' => $length]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxPropertiesConstraint.php new file mode 100644 index 000000000..106e98b02 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaxPropertiesConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $count = count(get_object_vars($value)); + if ($count <= $schema->maxProperties) { + return; + } + + $this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $schema->maxProperties, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaximumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaximumConstraint.php new file mode 100644 index 000000000..dd9e1b982 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MaximumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maximum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value <= $schema->maximum) { + return; + } + + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinItemsConstraint.php new file mode 100644 index 000000000..026170392 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinItemsConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minItems')) { + return; + } + + if (!is_array($value)) { + return; + } + + $count = count($value); + if ($count >= $schema->minItems) { + return; + } + + $this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinLengthConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinLengthConstraint.php new file mode 100644 index 000000000..5c7243a82 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinLengthConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minLength')) { + return; + } + + if (!is_string($value)) { + return; + } + + $length = mb_strlen($value); + if ($length >= $schema->minLength) { + return; + } + + $this->addError(ConstraintError::LENGTH_MIN(), $path, ['minLength' => $schema->minLength, 'found' => $length]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinPropertiesConstraint.php new file mode 100644 index 000000000..07014fb6a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinPropertiesConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $count = count(get_object_vars($value)); + if ($count >= $schema->minProperties) { + return; + } + + $this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $schema->minProperties, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinimumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinimumConstraint.php new file mode 100644 index 000000000..5f1b480c7 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MinimumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minimum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value >= $schema->minimum) { + return; + } + + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MultipleOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MultipleOfConstraint.php new file mode 100644 index 000000000..134cfda2d --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/MultipleOfConstraint.php @@ -0,0 +1,54 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'multipleOf')) { + return; + } + + if (!is_int($schema->multipleOf) && !is_float($schema->multipleOf) && $schema->multipleOf <= 0.0) { + return; + } + + if (!is_int($value) && !is_float($value)) { + return; + } + + if ($this->isMultipleOf($value, $schema->multipleOf)) { + return; + } + + $this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf, 'found' => $value]); + } + + /** + * @param int|float $number1 + * @param int|float $number2 + */ + private function isMultipleOf($number1, $number2): bool + { + $modulus = ($number1 - round($number1 / $number2) * $number2); + $precision = 0.0000000001; + + return -$precision < $modulus && $modulus < $precision; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/NotConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/NotConstraint.php new file mode 100644 index 000000000..fdba81bfe --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/NotConstraint.php @@ -0,0 +1,40 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'not')) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $schema->not, $path, $i); + + if (!$schemaConstraint->isValid()) { + return; + } + + $this->addError(ConstraintError::NOT(), $path); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/OneOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/OneOfConstraint.php new file mode 100644 index 000000000..4523f73cb --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/OneOfConstraint.php @@ -0,0 +1,50 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'oneOf')) { + return; + } + + $matchedSchema = 0; + foreach ($schema->oneOf as $oneOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $oneOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + $matchedSchema++; + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + + if ($matchedSchema !== 1) { + $this->addError(ConstraintError::ONE_OF(), $path); + } else { + $this->errorBag()->reset(); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php new file mode 100644 index 000000000..062b61d3f --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternConstraint.php @@ -0,0 +1,63 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'pattern')) { + return; + } + + if (!is_string($value)) { + return; + } + + $matchPattern = $this->createPregMatchPattern($schema->pattern); + if (preg_match($matchPattern, $value) === 1) { + return; + } + + $this->addError(ConstraintError::PATTERN(), $path, ['found' => $value, 'pattern' => $schema->pattern]); + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ + '\D' => '[^0-9]', + '\d' => '[0-9]', + '\p{digit}' => '[0-9]', + '\w' => '[A-Za-z0-9_]', + '\W' => '[^A-Za-z0-9_]', + '\s' => '[\s\x{200B}]', // Explicitly include zero width white space + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php new file mode 100644 index 000000000..d821abf18 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PatternPropertiesConstraint.php @@ -0,0 +1,72 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'patternProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $properties = get_object_vars($value); + + foreach ($properties as $propertyName => $propertyValue) { + foreach ($schema->patternProperties as $patternPropertyRegex => $patternPropertySchema) { + $matchPattern = $this->createPregMatchPattern($patternPropertyRegex); + if (preg_match($matchPattern, (string) $propertyName)) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $patternPropertySchema, $path, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } + } + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', + '\d' => '[0-9]', + '\p{digit}' => '[0-9]', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php new file mode 100644 index 000000000..25c55e666 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesConstraint.php @@ -0,0 +1,48 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'properties')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->properties as $propertyName => $propertySchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + if (!property_exists($value, $propertyName)) { + continue; + } + + $schemaConstraint->check($value->{$propertyName}, $propertySchema, $path, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesNamesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesNamesConstraint.php new file mode 100644 index 000000000..e3838990e --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/PropertiesNamesConstraint.php @@ -0,0 +1,80 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'propertyNames')) { + return; + } + + if (!is_object($value)) { + return; + } + if ($schema->propertyNames === true) { + return; + } + + $propertyNames = get_object_vars($value); + + if ($schema->propertyNames === false) { + foreach ($propertyNames as $propertyName => $_) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'false', 'name' => $propertyName]); + } + + return; + } + + if (property_exists($schema->propertyNames, 'maxLength')) { + foreach ($propertyNames as $propertyName => $_) { + $length = mb_strlen($propertyName); + if ($length > $schema->propertyNames->maxLength) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'maxLength', 'length' => $length, 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'pattern')) { + foreach ($propertyNames as $propertyName => $_) { + if (!preg_match('/' . str_replace('/', '\/', $schema->propertyNames->pattern) . '/', $propertyName)) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'pattern', 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'const')) { + foreach ($propertyNames as $propertyName => $_) { + if ($propertyName !== $schema->propertyNames->const) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'const', 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'enum')) { + $diff = array_diff(array_keys($propertyNames), $schema->propertyNames->enum); + foreach ($diff as $propertyName) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'enum', 'name' => $propertyName]); + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php new file mode 100644 index 000000000..9ec6efdac --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php @@ -0,0 +1,45 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, '$ref')) { + return; + } + + try { + $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); + } catch (\Exception $e) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $refSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RequiredConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RequiredConstraint.php new file mode 100644 index 000000000..99a5b55af --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/RequiredConstraint.php @@ -0,0 +1,58 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'required')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->required as $required) { + if (property_exists($value, $required)) { + continue; + } + + $this->addError(ConstraintError::REQUIRED(), $this->incrementPath($path, $required), ['property' => $required]); + } + } + + /** + * @todo refactor as this was only copied from UndefinedConstraint + * Bubble down the path + * + * @param JsonPointer|null $path Current path + * @param mixed $i What to append to the path + */ + protected function incrementPath(?JsonPointer $path, $i): JsonPointer + { + $path = $path ?? new JsonPointer(''); + + if ($i === null || $i === '') { + return $path; + } + + return $path->withPropertyPaths(array_merge($path->getPropertyPaths(), [$i])); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/TypeConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/TypeConstraint.php new file mode 100644 index 000000000..9601954f1 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/TypeConstraint.php @@ -0,0 +1,50 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'type')) { + return; + } + + $schemaTypes = (array) $schema->type; + $valueType = strtolower(gettype($value)); + // All specific number types are a number + $valueIsNumber = $valueType === 'double' || $valueType === 'integer'; + // A float with zero fractional part is an integer + $isInteger = $valueIsNumber && fmod($value, 1.0) === 0.0; + + foreach ($schemaTypes as $type) { + if ($valueType === $type) { + return; + } + + if ($type === 'number' && $valueIsNumber) { + return; + } + if ($type === 'integer' && $isInteger) { + return; + } + } + + $this->addError(ConstraintError::TYPE(), $path, ['found' => $valueType, 'expected' => implode(', ', $schemaTypes)]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/UniqueItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/UniqueItemsConstraint.php new file mode 100644 index 000000000..5363b5c58 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft07/UniqueItemsConstraint.php @@ -0,0 +1,48 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'uniqueItems')) { + return; + } + if (!is_array($value)) { + return; + } + + if ($schema->uniqueItems !== true) { + // If unique items not is true duplicates are allowed. + return; + } + + $count = count($value); + for ($x = 0; $x < $count - 1; $x++) { + for ($y = $x + 1; $y < $count; $y++) { + if (DeepComparer::isEqual($value[$x], $value[$y])) { + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + + return; + } + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalItemsConstraint.php new file mode 100644 index 000000000..79079c379 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalItemsConstraint.php @@ -0,0 +1,64 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalItems')) { + return; + } + + if ($schema->additionalItems === true) { + return; + } + if ($schema->additionalItems === false && !property_exists($schema, 'items')) { + return; + } + + if (!is_array($value)) { + return; + } + if (!property_exists($schema, 'items')) { + return; + } + if (is_object($schema->items)) { + return; + } + + $additionalItems = array_diff_key($value, property_exists($schema, 'items') ? $schema->items : []); + $basePath = $path ?? new JsonPointer(''); + + foreach ($additionalItems as $propertyName => $propertyValue) { + $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $schema->additionalItems, $incrementedPath, $i); + + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addError(ConstraintError::ADDITIONAL_ITEMS(), $incrementedPath, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php new file mode 100644 index 000000000..9406c4770 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AdditionalPropertiesConstraint.php @@ -0,0 +1,93 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'additionalProperties')) { + return; + } + + if ($schema->additionalProperties === true) { + return; + } + + if (!is_object($value)) { + return; + } + + $additionalProperties = get_object_vars($value); + + if (isset($schema->properties)) { + $additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties); + } + + if (isset($schema->patternProperties)) { + $patterns = array_keys(get_object_vars($schema->patternProperties)); + + foreach ($additionalProperties as $key => $_) { + foreach ($patterns as $pattern) { + if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) { + unset($additionalProperties[$key]); + break; + } + } + } + } + + if (is_object($schema->additionalProperties)) { + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $path, $i); // @todo increment path + if ($schemaConstraint->isValid()) { + unset($additionalProperties[$key]); + } + } + } + + foreach ($additionalProperties as $key => $additionalPropertiesValue) { + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $key]); + } + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', +// '\d' => '[0-9]', + '\p{digit}' => '\p{Nd}', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space, + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AllOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AllOfConstraint.php new file mode 100644 index 000000000..42d29d395 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AllOfConstraint.php @@ -0,0 +1,42 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'allOf')) { + return; + } + + foreach ($schema->allOf as $allOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $allOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + continue; + } + $this->addError(ConstraintError::ALL_OF(), $path); + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AnyOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AnyOfConstraint.php new file mode 100644 index 000000000..f0b66049a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/AnyOfConstraint.php @@ -0,0 +1,51 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'anyOf')) { + return; + } + + foreach ($schema->anyOf as $anyOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + + try { + $schemaConstraint->check($value, $anyOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + $this->errorBag()->reset(); + + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } catch (ValidationException $e) { + } + } + + $this->addError(ConstraintError::ANY_OF(), $path); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ConstConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ConstConstraint.php new file mode 100644 index 000000000..9cfacf87a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ConstConstraint.php @@ -0,0 +1,35 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'const')) { + return; + } + + if (DeepComparer::isEqual($value, $schema->const)) { + return; + } + + $this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContainsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContainsConstraint.php new file mode 100644 index 000000000..3e0e5a1e6 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContainsConstraint.php @@ -0,0 +1,54 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'contains')) { + return; + } + + if (!is_array($value)) { + return; + } + + $validElementCount = 0; + foreach ($value as $propertyValue) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + + $schemaConstraint->check($propertyValue, $schema->contains, $path, $i); + if ($schemaConstraint->isValid()) { + $validElementCount++; + } + } + + if (property_exists($schema, 'maxContains') && $validElementCount > $schema->maxContains) { + $this->addError(ConstraintError::MAX_CONTAINS(), $path, ['maxContains' => $schema->maxContains, 'count' => $validElementCount]); + } + + $minContains = property_exists($schema, 'minContains') ? $schema->minContains : 1; + if ($validElementCount < $minContains) { + $this->addError(ConstraintError::MIN_CONTAINS(), $path, ['minContains' => $minContains, 'count' => $validElementCount]); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContentConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContentConstraint.php new file mode 100644 index 000000000..ed3c49039 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ContentConstraint.php @@ -0,0 +1,25 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // See https://json-schema.org/draft/2019-09/json-schema-validation#rfc.section.8 + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentRequiredConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentRequiredConstraint.php new file mode 100644 index 000000000..a1cf42ff9 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentRequiredConstraint.php @@ -0,0 +1,50 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'dependentRequired')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->dependentRequired as $dependant => $dependencies) { + if (!property_exists($value, $dependant)) { + continue; + } + + foreach ($dependencies as $dependency) { + if (!property_exists($value, $dependency)) { + $this->addError(ConstraintError::DEPENDENCIES(), $path, [ + 'dependant' => $dependant, + 'dependency' => $dependency, + ]); + } + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentSchemasConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentSchemasConstraint.php new file mode 100644 index 000000000..0b469c65b --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/DependentSchemasConstraint.php @@ -0,0 +1,58 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'dependentSchemas')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->dependentSchemas as $dependant => $dependentSchema) { + if (!property_exists($value, $dependant)) { + continue; + } + + if ($dependentSchema === true) { + continue; + } + + if ($dependentSchema === false) { + $this->addError(ConstraintError::FALSE(), $path, ['dependant' => $dependant]); + continue; + } + + if (is_object($dependentSchema)) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $dependentSchema, $path, $i); + if (!$schemaConstraint->isValid()) { + $this->addErrors($schemaConstraint->getErrors()); + } + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php new file mode 100644 index 000000000..0b003b546 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php @@ -0,0 +1,84 @@ +getSchemaStorage() : new SchemaStorage(), + $factory ? $factory->getUriRetriever() : new UriRetriever(), + $factory ? $factory->getConfig() : Constraint::CHECK_MODE_NORMAL + )); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (is_bool($schema)) { + if ($schema === false) { + $this->addError(ConstraintError::FALSE(), $path, []); + } + + return; + } + + // Apply defaults + $this->checkForKeyword('ref', $value, $schema, $path, $i); + $this->checkForKeyword('required', $value, $schema, $path, $i); + $this->checkForKeyword('contains', $value, $schema, $path, $i); + $this->checkForKeyword('properties', $value, $schema, $path, $i); + $this->checkForKeyword('propertyNames', $value, $schema, $path, $i); + $this->checkForKeyword('patternProperties', $value, $schema, $path, $i); + $this->checkForKeyword('type', $value, $schema, $path, $i); + $this->checkForKeyword('not', $value, $schema, $path, $i); + $this->checkForKeyword('dependentSchemas', $value, $schema, $path, $i); + $this->checkForKeyword('dependentRequired', $value, $schema, $path, $i); + $this->checkForKeyword('allOf', $value, $schema, $path, $i); + $this->checkForKeyword('anyOf', $value, $schema, $path, $i); + $this->checkForKeyword('oneOf', $value, $schema, $path, $i); + $this->checkForKeyword('ifThenElse', $value, $schema, $path, $i); + + $this->checkForKeyword('additionalProperties', $value, $schema, $path, $i); + $this->checkForKeyword('items', $value, $schema, $path, $i); + $this->checkForKeyword('additionalItems', $value, $schema, $path, $i); + $this->checkForKeyword('uniqueItems', $value, $schema, $path, $i); + $this->checkForKeyword('minItems', $value, $schema, $path, $i); + $this->checkForKeyword('minProperties', $value, $schema, $path, $i); + $this->checkForKeyword('maxProperties', $value, $schema, $path, $i); + $this->checkForKeyword('minimum', $value, $schema, $path, $i); + $this->checkForKeyword('maximum', $value, $schema, $path, $i); + $this->checkForKeyword('minLength', $value, $schema, $path, $i); + $this->checkForKeyword('exclusiveMinimum', $value, $schema, $path, $i); + $this->checkForKeyword('maxItems', $value, $schema, $path, $i); + $this->checkForKeyword('maxLength', $value, $schema, $path, $i); + $this->checkForKeyword('exclusiveMaximum', $value, $schema, $path, $i); + $this->checkForKeyword('enum', $value, $schema, $path, $i); + $this->checkForKeyword('const', $value, $schema, $path, $i); + $this->checkForKeyword('multipleOf', $value, $schema, $path, $i); + $this->checkForKeyword('format', $value, $schema, $path, $i); + $this->checkForKeyword('pattern', $value, $schema, $path, $i); + $this->checkForKeyword('content', $value, $schema, $path, $i); + } + + /** + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkForKeyword(string $keyword, $value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor($keyword); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/EnumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/EnumConstraint.php new file mode 100644 index 000000000..aba263711 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/EnumConstraint.php @@ -0,0 +1,41 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'enum')) { + return; + } + + foreach ($schema->enum as $enumCase) { + if (DeepComparer::isEqual($value, $enumCase)) { + return; + } + + if (is_numeric($value) && is_numeric($enumCase) && DeepComparer::isEqual((float) $value, (float) $enumCase)) { + return; + } + } + + $this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMaximumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMaximumConstraint.php new file mode 100644 index 000000000..0241197b1 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMaximumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'exclusiveMaximum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value < $schema->exclusiveMaximum) { + return; + } + + $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['exclusiveMaximum' => $schema->exclusiveMaximum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMinimumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMinimumConstraint.php new file mode 100644 index 000000000..9c9e7a323 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ExclusiveMinimumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'exclusiveMinimum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value > $schema->exclusiveMinimum) { + return; + } + + $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['exclusiveMinimum' => $schema->exclusiveMinimum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php new file mode 100644 index 000000000..cc490225e --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/Factory.php @@ -0,0 +1,49 @@ + + */ + protected $constraintMap = [ + 'schema' => Draft2019Constraint::class, + 'additionalProperties' => AdditionalPropertiesConstraint::class, + 'additionalItems' => AdditionalItemsConstraint::class, + 'dependentSchemas' => DependentSchemasConstraint::class, + 'dependentRequired' => DependentRequiredConstraint::class, + 'type' => TypeConstraint::class, + 'const' => ConstConstraint::class, + 'enum' => EnumConstraint::class, + 'uniqueItems' => UniqueItemsConstraint::class, + 'minItems' => MinItemsConstraint::class, + 'minProperties' => MinPropertiesConstraint::class, + 'maxProperties' => MaxPropertiesConstraint::class, + 'minimum' => MinimumConstraint::class, + 'maximum' => MaximumConstraint::class, + 'exclusiveMinimum' => ExclusiveMinimumConstraint::class, + 'minLength' => MinLengthConstraint::class, + 'maxLength' => MaxLengthConstraint::class, + 'maxItems' => MaxItemsConstraint::class, + 'exclusiveMaximum' => ExclusiveMaximumConstraint::class, + 'multipleOf' => MultipleOfConstraint::class, + 'required' => RequiredConstraint::class, + 'format' => FormatConstraint::class, + 'anyOf' => AnyOfConstraint::class, + 'allOf' => AllOfConstraint::class, + 'oneOf' => OneOfConstraint::class, + 'not' => NotConstraint::class, + 'ifThenElse' => IfThenElseConstraint::class, + 'contains' => ContainsConstraint::class, + 'propertyNames' => PropertiesNamesConstraint::class, + 'patternProperties' => PatternPropertiesConstraint::class, + 'pattern' => PatternConstraint::class, + 'properties' => PropertiesConstraint::class, + 'items' => ItemsConstraint::class, + 'ref' => RefConstraint::class, + 'content' => ContentConstraint::class, + ]; +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php new file mode 100644 index 000000000..5fb47c875 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/FormatConstraint.php @@ -0,0 +1,337 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'format')) { + return; + } + + if (!is_string($value)) { + return; + } + + switch ($schema->format) { + case 'date': + if (!$this->validateDateTime($value, 'Y-m-d')) { + $this->addError(ConstraintError::FORMAT_DATE(), $path, ['date' => $value, 'format' => $schema->format]); + } + break; + case 'time': + if (!$this->validateDateTime($value, 'H:i:sp') && !$this->validateDateTime($value, 'H:i:s.up')) { + $this->addError(ConstraintError::FORMAT_TIME(), $path, ['time' => $value, 'format' => $schema->format]); + } + break; + case 'date-time': + if (!$this->validateRfc3339DateTime($value)) { + $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, ['dateTime' => $value, 'format' => $schema->format]); + } + break; + case 'utc-millisec': + if (!$this->validateDateTime($value, 'U')) { + $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, ['value' => $value, 'format' => $schema->format]); + } + break; + case 'regex': + if (!$this->validateRegex($value)) { + $this->addError(ConstraintError::FORMAT_REGEX(), $path, ['value' => $value, 'format' => $schema->format]); + } + break; + case 'ip-address': + case 'ipv4': + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4) === null) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + case 'ipv6': + if (filter_var($value, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6) === null) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + case 'color': + if (!$this->validateColor($value)) { + $this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]); + } + break; + case 'style': + if (!$this->validateStyle($value)) { + $this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]); + } + break; + case 'phone': + if (!$this->validatePhone($value)) { + $this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]); + } + break; + case 'uri': + if (!UriValidator::isValid($value)) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + + case 'uriref': + case 'uri-reference': + if (!(UriValidator::isValid($value) || RelativeReferenceValidator::isValid($value))) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + case 'uri-template': + if (!$this->validateUriTemplate($value)) { + $this->addError(ConstraintError::FORMAT_URI_TEMPLATE(), $path, ['format' => $schema->format]); + } + break; + + case 'email': + if (filter_var($value, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE) === null) { + $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]); + } + break; + case 'host-name': + case 'hostname': + if (!$this->validateHostname($value)) { + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); + } + break; + case 'idn-hostname': + if (!$this->validateInternationalizedHostname($value)) { + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); + } + break; + case 'json-pointer': + if (!$this->validateJsonPointer($value)) { + $this->addError(ConstraintError::FORMAT_JSON_POINTER(), $path, ['format' => $schema->format]); + } + break; + default: + break; + } + } + + private function validateDateTime(string $datetime, string $format): bool + { + $datetime = strtoupper($datetime); // Cleanup for lowercase z + $isLeap = substr($datetime, 6, 2) === '60'; + $input = $datetime; + + // Correct for leap second + if ($isLeap) { + $input = sprintf('%s59%s', substr($datetime, 0, 6), substr($datetime, 8)); + } + + $dt = \DateTimeImmutable::createFromFormat($format, $input); + if (!$dt) { + return false; + } + + // Handle invalid timezone offsets + $timezoneOffset = $dt->getTimezone()->getOffset($dt); + if ($timezoneOffset >= 86400 || $timezoneOffset <= -86400) { + return false; + } + + $expected = $dt->format($format); + // Correct for trailing zeros on microseconds + if ($format === 'H:i:s.up') { + $expected = sprintf( + '%s%s', + rtrim($dt->format('H:i:s.u'), '0'), + $dt->format('p') + ); + } + // Correct back for leap seconds + if ($isLeap) { + // Only when 23:59:59 in UTC + $utcDT = $dt->setTimezone(new DateTimeZone('UTC')); + if ($utcDT->format('H:i:s') !== '23:59:59') { + return false; + } + + $expected = sprintf('%s60%s', substr($expected, 0, 6), substr($expected, 8)); + } + + return $datetime === $expected; + } + + private function validateRegex(string $regex): bool + { + return preg_match(self::jsonPatternToPhpRegex($regex), '') !== false; + } + + /** + * Transform a JSON pattern into a PCRE regex + */ + private static function jsonPatternToPhpRegex(string $pattern): string + { + return '~' . str_replace('~', '\\~', $pattern) . '~u'; + } + + private function validateColor(string $color): bool + { + if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia', + 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', + 'red', 'silver', 'teal', 'white', 'yellow'])) { + return true; + } + + return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) === 1; + } + + private function validateStyle(string $style): bool + { + $properties = explode(';', rtrim($style, ';')); + $invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT); + + return empty($invalidEntries); + } + + private function validatePhone(string $phone): bool + { + return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) === 1; + } + + private function validateHostname(string $host): bool + { + $hostnameRegex = '/^(?!-)(?!.*?[^A-Za-z0-9\-\.])(?:(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)*(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/'; + + return preg_match($hostnameRegex, $host) === 1; + } + + private function validateInternationalizedHostname(string $host): bool + { + if ($host === '') { + return false; + } + $host = rtrim($host, '.'); + $labels = explode('.', $host); + $asciiLabels = []; + + foreach ($labels as $label) { + if ($label === '') { + return false; + } + + // CONTEXTJ / CONTEXTO checks + if ( + // Greek KERAIA U+0375 + preg_match('/\x{0375}/u', $label) && + !preg_match('/\x{0375}[\x{0370}-\x{03FF}]/u', $label) + ) { + return false; + } + + // Hebrew GERESH / GERSHAYIM U+05F3 / U+05F4 + if (preg_match('/[\x{05F3}\x{05F4}]/u', $label) && + !preg_match('/[\x{0590}-\x{05FF}][\x{05F3}\x{05F4}]/u', $label) + ) { + return false; + } + + // Katakana middle dot U+30FB + if (str_contains($label, "\u{30FB}") && + !preg_match('/[\x{30A0}-\x{30FF}]/u', $label) + ) { + return false; + } + + // Arabic digit mixing + $hasArabicIndic = preg_match('/[\x{0660}-\x{0669}]/u', $label); + $hasExtArabicIndic = preg_match('/[\x{06F0}-\x{06F9}]/u', $label); + if ($hasArabicIndic && $hasExtArabicIndic) { + return false; + } + + // Devanagari danda U+0964 / U+0965 + if (preg_match('/[\x{0964}\x{0965}]/u', $label) && + !preg_match('/[\x{0900}-\x{097F}]/u', $label) + ) { + return false; + } + + // ZWNJ / ZWJ U+200C / U+200D + if (preg_match('/[\x{200C}\x{200D}]/u', $label)) { + return false; + } + + $ascii = idn_to_ascii($label); + if ($ascii === false) { + return false; + } + // DNS label length + if (strlen($ascii) > 63) { + return false; + } + // LDH rule (after IDNA) + if (!preg_match('/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i', $ascii)) { + return false; + } + $asciiLabels[] = $ascii; + } + + // Total hostname length (ASCII) + $asciiHost = implode('.', $asciiLabels); + + return strlen($asciiHost) <= 253; + } + + private function validateJsonPointer(string $value): bool + { + // Must be empty or start with a forward slash + if ($value !== '' && $value[0] !== '/') { + return false; + } + + // Split into reference tokens and check for invalid escape sequences + $tokens = explode('/', $value); + array_shift($tokens); // remove leading empty part due to leading slash + + foreach ($tokens as $token) { + // "~" must only be followed by "0" or "1" + if (preg_match('/~(?![01])/', $token)) { + return false; + } + } + + return true; + } + + private function validateRfc3339DateTime(string $value): bool + { + $dateTime = Rfc3339::createFromString($value); + if (is_null($dateTime)) { + return false; + } + + // Compare value and date result to be equal + return true; + } + + private function validateUriTemplate(string $value): bool + { + return preg_match( + '/^(?:[^\{\}]*|\{[a-zA-Z0-9_:%\/\.~\-\+\*]+\})*$/', + $value + ) === 1; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/IfThenElseConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/IfThenElseConstraint.php new file mode 100644 index 000000000..a94514208 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/IfThenElseConstraint.php @@ -0,0 +1,67 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'if')) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $ifSchema = $schema->if; + + if (!is_bool($ifSchema)) { + $schemaConstraint->check($value, $ifSchema, $path, $i); + $meetsIfConditions = $schemaConstraint->isValid(); + $schemaConstraint->reset(); + } else { + $meetsIfConditions = $ifSchema; + } + + if ($meetsIfConditions) { + if (!property_exists($schema, 'then')) { + return; + } + + $schemaConstraint->check($value, $schema->then, $path, $i); + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + + return; + } + + if (!property_exists($schema, 'else')) { + return; + } + + $schemaConstraint->check($value, $schema->else, $path, $i); + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ItemsConstraint.php new file mode 100644 index 000000000..38abb3df3 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/ItemsConstraint.php @@ -0,0 +1,54 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'items')) { + return; + } + + if (!is_array($value)) { + return; + } + + $basePath = $path ?? new JsonPointer(''); + foreach ($value as $propertyName => $propertyValue) { + $itemSchema = $schema->items; + if (is_array($itemSchema)) { + if (!array_key_exists($propertyName, $itemSchema)) { + continue; + } + + $itemSchema = $itemSchema[$propertyName]; + } + $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $itemSchema, $incrementedPath, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxItemsConstraint.php new file mode 100644 index 000000000..fd5c45be8 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxItemsConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxItems')) { + return; + } + + if (!is_array($value)) { + return; + } + + $count = count($value); + if ($count <= $schema->maxItems) { + return; + } + + $this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxLengthConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxLengthConstraint.php new file mode 100644 index 000000000..17eee392a --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxLengthConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxLength')) { + return; + } + + if (!is_string($value)) { + return; + } + + $length = mb_strlen($value); + if ($length <= $schema->maxLength) { + return; + } + + $this->addError(ConstraintError::LENGTH_MAX(), $path, ['maxLength' => $schema->maxLength, 'found' => $length]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxPropertiesConstraint.php new file mode 100644 index 000000000..8bae235e7 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaxPropertiesConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maxProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $count = count(get_object_vars($value)); + if ($count <= $schema->maxProperties) { + return; + } + + $this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $schema->maxProperties, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaximumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaximumConstraint.php new file mode 100644 index 000000000..3e10ee524 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MaximumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'maximum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value <= $schema->maximum) { + return; + } + + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinItemsConstraint.php new file mode 100644 index 000000000..f26082c06 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinItemsConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minItems')) { + return; + } + + if (!is_array($value)) { + return; + } + + $count = count($value); + if ($count >= $schema->minItems) { + return; + } + + $this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinLengthConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinLengthConstraint.php new file mode 100644 index 000000000..9784c39ff --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinLengthConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minLength')) { + return; + } + + if (!is_string($value)) { + return; + } + + $length = mb_strlen($value); + if ($length >= $schema->minLength) { + return; + } + + $this->addError(ConstraintError::LENGTH_MIN(), $path, ['minLength' => $schema->minLength, 'found' => $length]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinPropertiesConstraint.php new file mode 100644 index 000000000..ea4dd5d7f --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinPropertiesConstraint.php @@ -0,0 +1,39 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $count = count(get_object_vars($value)); + if ($count >= $schema->minProperties) { + return; + } + + $this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $schema->minProperties, 'found' => $count]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinimumConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinimumConstraint.php new file mode 100644 index 000000000..b0a8ab8e6 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MinimumConstraint.php @@ -0,0 +1,38 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'minimum')) { + return; + } + + if (!is_numeric($value)) { + return; + } + + if ($value >= $schema->minimum) { + return; + } + + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum, 'found' => $value]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MultipleOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MultipleOfConstraint.php new file mode 100644 index 000000000..0b33601dd --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/MultipleOfConstraint.php @@ -0,0 +1,54 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'multipleOf')) { + return; + } + + if ((!is_int($schema->multipleOf) && !is_float($schema->multipleOf)) || $schema->multipleOf <= 0.0) { + return; + } + + if (!is_int($value) && !is_float($value)) { + return; + } + + if ($this->isMultipleOf($value, $schema->multipleOf)) { + return; + } + + $this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf, 'found' => $value]); + } + + /** + * @param int|float $number1 + * @param int|float $number2 + */ + private function isMultipleOf($number1, $number2): bool + { + $modulus = ($number1 - round($number1 / $number2) * $number2); + $precision = 0.0000000001; + + return -$precision < $modulus && $modulus < $precision; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/NotConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/NotConstraint.php new file mode 100644 index 000000000..8b9b40bd2 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/NotConstraint.php @@ -0,0 +1,40 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'not')) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $schema->not, $path, $i); + + if (!$schemaConstraint->isValid()) { + return; + } + + $this->addError(ConstraintError::NOT(), $path); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/OneOfConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/OneOfConstraint.php new file mode 100644 index 000000000..fd93cefe4 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/OneOfConstraint.php @@ -0,0 +1,50 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'oneOf')) { + return; + } + + $matchedSchema = 0; + foreach ($schema->oneOf as $oneOfSchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $oneOfSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + $matchedSchema++; + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + + if ($matchedSchema !== 1) { + $this->addError(ConstraintError::ONE_OF(), $path); + } else { + $this->errorBag()->reset(); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php new file mode 100644 index 000000000..b36ec026b --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternConstraint.php @@ -0,0 +1,63 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'pattern')) { + return; + } + + if (!is_string($value)) { + return; + } + + $matchPattern = $this->createPregMatchPattern($schema->pattern); + if (preg_match($matchPattern, $value) === 1) { + return; + } + + $this->addError(ConstraintError::PATTERN(), $path, ['found' => $value, 'pattern' => $schema->pattern]); + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ + '\D' => '[^0-9]', + '\d' => '[0-9]', + '\p{digit}' => '[0-9]', + '\w' => '[A-Za-z0-9_]', + '\W' => '[^A-Za-z0-9_]', + '\s' => '[\s\x{200B}]', // Explicitly include zero width white space + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php new file mode 100644 index 000000000..441561540 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PatternPropertiesConstraint.php @@ -0,0 +1,75 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'patternProperties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $properties = get_object_vars($value); + + $basePath = $path ?? new JsonPointer(''); + foreach ($properties as $propertyName => $propertyValue) { + $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + + foreach ($schema->patternProperties as $patternPropertyRegex => $patternPropertySchema) { + $matchPattern = $this->createPregMatchPattern($patternPropertyRegex); + if (preg_match($matchPattern, (string) $propertyName)) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($propertyValue, $patternPropertySchema, $incrementedPath, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } + } + } + + private function createPregMatchPattern(string $pattern): string + { + $replacements = [ +// '\D' => '[^0-9]', + '\d' => '[0-9]', + '\p{digit}' => '[0-9]', +// '\w' => '[A-Za-z0-9_]', +// '\W' => '[^A-Za-z0-9_]', +// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space + '\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations + ]; + + $pattern = str_replace( + array_keys($replacements), + array_values($replacements), + $pattern + ); + + return '/' . str_replace('/', '\/', $pattern) . '/u'; + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesConstraint.php new file mode 100644 index 000000000..4052b1b9e --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesConstraint.php @@ -0,0 +1,51 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'properties')) { + return; + } + + if (!is_object($value)) { + return; + } + + $basePath = $path ?? new JsonPointer(''); + foreach ($schema->properties as $propertyName => $propertySchema) { + $schemaConstraint = $this->factory->createInstanceFor('schema'); + if (!property_exists($value, $propertyName)) { + continue; + } + + $incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName])); + + $schemaConstraint->check($value->{$propertyName}, $propertySchema, $incrementedPath, $i); + if ($schemaConstraint->isValid()) { + continue; + } + + $this->addErrors($schemaConstraint->getErrors()); + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesNamesConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesNamesConstraint.php new file mode 100644 index 000000000..1ce7f0c4f --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/PropertiesNamesConstraint.php @@ -0,0 +1,80 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'propertyNames')) { + return; + } + + if (!is_object($value)) { + return; + } + if ($schema->propertyNames === true) { + return; + } + + $propertyNames = get_object_vars($value); + + if ($schema->propertyNames === false) { + foreach ($propertyNames as $propertyName => $_) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'false', 'name' => $propertyName]); + } + + return; + } + + if (property_exists($schema->propertyNames, 'maxLength')) { + foreach ($propertyNames as $propertyName => $_) { + $length = mb_strlen($propertyName); + if ($length > $schema->propertyNames->maxLength) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'maxLength', 'length' => $length, 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'pattern')) { + foreach ($propertyNames as $propertyName => $_) { + if (!preg_match('/' . str_replace('/', '\/', $schema->propertyNames->pattern) . '/', $propertyName)) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'pattern', 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'const')) { + foreach ($propertyNames as $propertyName => $_) { + if ($propertyName !== $schema->propertyNames->const) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'const', 'name' => $propertyName]); + } + } + } + + if (property_exists($schema->propertyNames, 'enum')) { + $diff = array_diff(array_keys($propertyNames), $schema->propertyNames->enum); + foreach ($diff as $propertyName) { + $this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'enum', 'name' => $propertyName]); + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php new file mode 100644 index 000000000..989b1f039 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php @@ -0,0 +1,45 @@ +factory = $factory ?: new Factory(); + $this->initialiseErrorBag($this->factory); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, '$ref')) { + return; + } + + try { + $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); + } catch (\Exception $e) { + return; + } + + $schemaConstraint = $this->factory->createInstanceFor('schema'); + $schemaConstraint->check($value, $refSchema, $path, $i); + + if ($schemaConstraint->isValid()) { + return; + } + + $this->addErrors($schemaConstraint->getErrors()); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RequiredConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RequiredConstraint.php new file mode 100644 index 000000000..a5c4c93ad --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/RequiredConstraint.php @@ -0,0 +1,58 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'required')) { + return; + } + + if (!is_object($value)) { + return; + } + + foreach ($schema->required as $required) { + if (property_exists($value, $required)) { + continue; + } + + $this->addError(ConstraintError::REQUIRED(), $this->incrementPath($path, $required), ['property' => $required]); + } + } + + /** + * @todo refactor as this was only copied from UndefinedConstraint + * Bubble down the path + * + * @param JsonPointer|null $path Current path + * @param mixed $i What to append to the path + */ + protected function incrementPath(?JsonPointer $path, $i): JsonPointer + { + $path = $path ?? new JsonPointer(''); + + if ($i === null || $i === '') { + return $path; + } + + return $path->withPropertyPaths(array_merge($path->getPropertyPaths(), [$i])); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/TypeConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/TypeConstraint.php new file mode 100644 index 000000000..e9ba31ddb --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/TypeConstraint.php @@ -0,0 +1,50 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'type')) { + return; + } + + $schemaTypes = (array) $schema->type; + $valueType = strtolower(gettype($value)); + // All specific number types are a number + $valueIsNumber = $valueType === 'double' || $valueType === 'integer'; + // A float with zero fractional part is an integer + $isInteger = $valueIsNumber && fmod($value, 1.0) === 0.0; + + foreach ($schemaTypes as $type) { + if ($valueType === $type) { + return; + } + + if ($type === 'number' && $valueIsNumber) { + return; + } + if ($type === 'integer' && $isInteger) { + return; + } + } + + $this->addError(ConstraintError::TYPE(), $path, ['found' => $valueType, 'expected' => implode(', ', $schemaTypes)]); + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UniqueItemsConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UniqueItemsConstraint.php new file mode 100644 index 000000000..9406a2720 --- /dev/null +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Drafts/Draft2019/UniqueItemsConstraint.php @@ -0,0 +1,48 @@ +initialiseErrorBag($factory ?: new Factory()); + } + + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!property_exists($schema, 'uniqueItems')) { + return; + } + if (!is_array($value)) { + return; + } + + if ($schema->uniqueItems !== true) { + // If unique items not is true duplicates are allowed. + return; + } + + $count = count($value); + for ($x = 0; $x < $count - 1; $x++) { + for ($y = $x + 1; $y < $count; $y++) { + if (DeepComparer::isEqual($value[$x], $value[$y])) { + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + + return; + } + } + } + } +} diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php index 5d7e67d08..6cbefcef7 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php @@ -75,6 +75,8 @@ class Factory 'schema' => 'JsonSchema\Constraints\SchemaConstraint', 'validator' => 'JsonSchema\Validator', 'draft06' => Drafts\Draft06\Draft06Constraint::class, + 'draft07' => Drafts\Draft07\Draft07Constraint::class, + 'draft2019-09' => Drafts\Draft2019\Draft2019Constraint::class, ]; /** diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php index 9ed4df9dc..c38488dd1 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php @@ -210,7 +210,8 @@ protected function validateHostname($host) return true; } - $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i'; + // RFC 1035: labels are max 63 chars (1 start + 0-61 middle + 1 end) + $hostnameRegex = '/^(?!-)(?!.*?[^A-Za-z0-9\-\.])(?:(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.)*(?!-)[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/'; return preg_match($hostnameRegex, $host); } diff --git a/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php b/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php index c2c614a90..0e1252e30 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php @@ -61,7 +61,7 @@ public function validatePatternProperties($element, ?JsonPointer $path, $pattern continue; } foreach ($element as $i => $value) { - if (preg_match($fullRegex, $i)) { + if (preg_match($fullRegex, (string) $i)) { $matches[] = $i; $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); } diff --git a/justinrainbow/json-schema/src/JsonSchema/DraftIdentifiers.php b/justinrainbow/json-schema/src/JsonSchema/DraftIdentifiers.php index d0e05e787..e062b6a8a 100644 --- a/justinrainbow/json-schema/src/JsonSchema/DraftIdentifiers.php +++ b/justinrainbow/json-schema/src/JsonSchema/DraftIdentifiers.php @@ -21,24 +21,39 @@ class DraftIdentifiers extends Enum public const DRAFT_2019_09 = 'https://json-schema.org/draft/2019-09/schema'; public const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema'; + /** @var array */ + private const MAPPING = [ + self::DRAFT_3 => 'draft03', + self::DRAFT_4 => 'draft04', + self::DRAFT_6 => 'draft06', + self::DRAFT_7 => 'draft07', + self::DRAFT_2019_09 => 'draft2019-09', + self::DRAFT_2020_12 => 'draft2020-12', + ]; + + private const FALLBACK_MAPPING = [ + 'draft3' => self::DRAFT_3, + 'draft4' => self::DRAFT_4, + 'draft6' => self::DRAFT_6, + 'draft7' => self::DRAFT_7, + ]; + public function toConstraintName(): string { - switch ($this->getValue()) { - case self::DRAFT_3: - return 'draft03'; - case self::DRAFT_4: - return 'draft04'; - case self::DRAFT_6: - return 'draft06'; - case self::DRAFT_7: - return 'draft07'; - case self::DRAFT_2019_09: - return 'draft2019-09'; - case self::DRAFT_2020_12: - return 'draft2020-12'; - default: - throw new \Exception('Unsupported schema URI: ' . $this->getValue()); + return self::MAPPING[$this->getValue()]; + } + + public static function fromConstraintName(string $name): DraftIdentifiers + { + $reverseMap = array_flip(self::MAPPING); + if (!array_key_exists($name, $reverseMap)) { + if (array_key_exists($name, self::FALLBACK_MAPPING)) { + return DraftIdentifiers::byValue(self::FALLBACK_MAPPING[$name]); + } + throw new \InvalidArgumentException("$name is not a valid constraint name."); } + + return DraftIdentifiers::byValue($reverseMap[$name]); } public function withoutFragment(): string diff --git a/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php b/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php index e674fa6f8..e33866b68 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php +++ b/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php @@ -122,7 +122,7 @@ public function getPropertyPaths() public function withPropertyPaths(array $propertyPaths) { $new = clone $this; - $new->propertyPaths = array_map(function ($p): string { return (string) $p; }, $propertyPaths); + $new->propertyPaths = array_map(static function ($p): string { return (string) $p; }, $propertyPaths); return $new; } diff --git a/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php b/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php index 05397e7e2..2c5832bf9 100644 --- a/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php +++ b/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php @@ -203,6 +203,11 @@ private function scanForSubschemas($schema, string $parentId): void foreach ($schema as $propertyName => $potentialSubSchema) { if (!is_object($potentialSubSchema)) { + if (is_array($potentialSubSchema)) { + foreach ($potentialSubSchema as $potentialSubSchemaItem) { + $this->scanForSubschemas($potentialSubSchemaItem, $parentId); + } + } continue; } diff --git a/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php b/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php index fd95f7bfd..79fede0d8 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php +++ b/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php @@ -44,14 +44,6 @@ public static function isValid(string $ref): bool return false; // Spaces are not allowed in URIs } - if (preg_match('/^\?#|^#$/', $ref)) { - return false; // Missing path but having query and fragment - } - - if ($ref === '#' || $ref === '?') { - return false; // Missing path and having only fragment or query - } - return true; } } diff --git a/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php b/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php index 361512a9f..76f1c7723 100644 --- a/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php +++ b/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php @@ -11,6 +11,7 @@ namespace JsonSchema\Uri; +use JsonSchema\DraftIdentifiers; use JsonSchema\Exception\InvalidSchemaMediaTypeException; use JsonSchema\Exception\JsonDecodingException; use JsonSchema\Exception\ResourceNotFoundException; @@ -31,7 +32,8 @@ class UriRetriever implements BaseUriRetrieverInterface */ protected $translationMap = [ // use local copies of the spec schemas - '|^https?://json-schema.org/draft-(0[346])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' + '|^https?://json-schema.org/draft-(0[3467])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json', + '|^https://json-schema.org/draft/2019-09/schema#?|' => 'package://dist/schema/json-schema-draft-2019-09.json' ]; /** @@ -91,7 +93,7 @@ public function confirmMediaType($uriRetriever, $uri) } } - throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected', Validator::SCHEMA_MEDIA_TYPE)); + throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected, but %s given', Validator::SCHEMA_MEDIA_TYPE, $contentType)); } /** @@ -182,11 +184,17 @@ public function retrieve($uri, $baseUri = null, $translate = true) $jsonSchema = $this->loadSchema($fetchUri); + // Detect dialect from the root schema's $schema keyword before + // resolvePointer() may walk into a sub-schema. + $dialect = isset($jsonSchema->{'$schema'}) ? rtrim($jsonSchema->{'$schema'}, '#') : null; + $usesDollarId = !in_array($dialect, [DraftIdentifiers::DRAFT_3()->withoutFragment(), DraftIdentifiers::DRAFT_4()->withoutFragment()], true); + // Use the JSON pointer if specified $jsonSchema = $this->resolvePointer($jsonSchema, $resolvedUri); if ($jsonSchema instanceof \stdClass) { - $jsonSchema->id = $resolvedUri; + $idKeyword = $usesDollarId ? '$id' : 'id'; + $jsonSchema->{$idKeyword} = $resolvedUri; } return $jsonSchema; diff --git a/marc-mabe/php-enum/src/Enum.php b/marc-mabe/php-enum/src/Enum.php index d602d8bc6..e9ef6ff8e 100644 --- a/marc-mabe/php-enum/src/Enum.php +++ b/marc-mabe/php-enum/src/Enum.php @@ -96,6 +96,16 @@ final public function __sleep() throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); } + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @return array + */ + public function __serialize(): array + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + /** * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation * @@ -106,6 +116,16 @@ final public function __wakeup() throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); } + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @param array $data + */ + public function __unserialize(array $data): void + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + /** * Get the value of the enumerator * diff --git a/php-opencloud/openstack/AGENTS.md b/php-opencloud/openstack/AGENTS.md new file mode 100644 index 000000000..d4ab897a7 --- /dev/null +++ b/php-opencloud/openstack/AGENTS.md @@ -0,0 +1,130 @@ +# AGENTS.md + +## Project Scope + +This repository is the `php-opencloud/openstack` SDK: a PHP client for multiple OpenStack services. The public entry point is `src/OpenStack.php`, which creates versioned service objects through shared builder logic in `src/Common/`. + +The project follows semantic versioning and still supports PHP `^7.2.5 || ^8.0`. Keep changes narrowly scoped and avoid API breaks unless the task explicitly requires them. + +## Repository Layout + +- `src/` + - `Common/`: shared API, resource, service, auth, error, JSON-schema, and transport infrastructure. + - `//`: versioned SDK surface for each OpenStack service. Most service folders contain: + - `Api.php`: declarative operation definitions (`method`, `path`, `params`, optional `jsonKey`) + - `Params.php`: parameter schemas and wire-format metadata + - `Service.php`: top-level user-facing service methods + - `Models/`: resource classes with behavior and hydration rules +- `tests/unit/`: mocked unit tests, usually mirroring the `src/` namespace layout. +- `tests/sample/`: integration-style tests that execute files from `samples/`. +- `samples/`: runnable examples; these double as integration-test inputs. +- `doc/`: Sphinx/reStructuredText documentation. +- `.github/workflows/`: CI definitions for formatting, unit tests, and integration tests. + +## Architecture Conventions + +When adding or changing SDK functionality, preserve the existing layering: + +1. Define or update the REST operation in `Api.php`. +2. Add or reuse parameter definitions in `Params.php`. +3. Expose the behavior from `Service.php` if it is part of the top-level service API. +4. Implement resource behavior in `Models/*` when the operation belongs on a model instance. +5. Add unit tests and, for user-facing flows, a sample plus sample test when practical. + +Specific patterns used throughout the codebase: + +- `Service.php` methods are intentionally thin. They usually create a model, populate it, or delegate to `enumerate(...)`. +- Resource classes commonly extend `OpenStack\Common\Resource\OperatorResource`, set `$resourceKey`, `$resourcesKey`, and `$markerKey`, and use `execute(...)` plus `populateFromResponse(...)`. +- API field renames are handled with `$aliases` and `Alias` objects instead of ad hoc mapping code. +- Operation option arrays are documented with `{@see \Namespace\Api::methodName}` docblocks. Keep those references accurate when signatures change. +- Reuse shared abstractions in `src/Common/` before introducing service-specific helpers. + +## PHP Compatibility Rules + +The SDK is tested against PHP `7.2` through `8.4` in CI. Do not introduce syntax or standard-library dependencies that require newer PHP versions than `7.2.5`. + +In practice, avoid: + +- union types +- attributes +- constructor property promotion +- enums +- `match` +- `readonly` +- typed properties +- named arguments in code examples or tests + +Follow the surrounding file for `declare(strict_types=1);`. Many `src/` files use it, but not every file in the repository does. + +## Testing Expectations + +There are no Composer scripts in this repository; run tools directly from `vendor/bin`. + +Primary local checks: + +```bash +composer install +vendor/bin/parallel-lint --exclude vendor . +vendor/bin/phpunit --configuration phpunit.xml.dist +vendor/bin/php-cs-fixer fix --dry-run --diff +``` + +Additional checks when relevant: + +```bash +composer normalize +vendor/bin/phpunit --configuration phpunit.sample.xml.dist +``` + +Notes: + +- CI runs unit tests against both lowest and highest dependency sets, so avoid relying on the latest transitive behavior only. +- Integration tests require a live OpenStack environment, the environment variables from `env_test.sh.dist`, and an image named `cirros`. +- `php-cs-fixer` is configured for `src/`, `samples/`, and `.php-cs-fixer.dist.php`; tests are not auto-formatted by that config, so keep test edits manually consistent with surrounding code. + +## Unit Test Patterns + +Unit tests usually extend `tests/unit/TestCase.php`. + +Follow the existing test style: + +- set `rootFixturesDir` in `setUp()` when the test uses fixture responses +- use `mockRequest(...)` to assert HTTP method, path, body, headers, and returned response +- store larger or realistic HTTP responses as `.resp` files under a nearby `Fixtures/` directory +- mirror the production namespace and folder layout where possible + +Prefer adding focused tests around the exact operation being changed instead of broad cross-service rewrites. + +## Samples And Integration Coverage + +`samples/` are executable examples and are also exercised by `tests/sample/`. When you add a new user-facing capability, consider whether it should have: + +- a sample under the matching service/version folder in `samples/` +- a corresponding sample test under `tests/sample/` +- documentation in `doc/services/` if the feature is part of the supported public workflow + +All code snippets used in the docs must live in `samples/` rather than being maintained only inline in `.rst` files, and they must be covered by the sample test suite. + +Sample tests typically create a temporary PHP file from a template and `require_once` it, so keep samples self-contained and readable. + +When adding sample tests, prefer reusing resources created earlier in the same test file instead of provisioning duplicate ones. In practice, `testCreate` should return the created resource, dependent tests should consume it via `@depends`, and cleanup should happen in the final `testDelete`. + +## Documentation + +User docs live in `doc/` and use Sphinx plus reStructuredText. If a change affects public behavior, examples, or supported options, update docs as needed. + +Typical doc build: + +```bash +pip install -r doc/requirements.txt +make -C doc html +``` + +## Change Heuristics + +- Prefer small, service-scoped changes over broad refactors. +- Preserve public method names and option shapes unless the task explicitly calls for a breaking change. +- Keep docblocks accurate for public APIs and option arrays. +- Reuse existing fixtures, sample patterns, and helper methods before inventing new ones. +- If `composer.json` changes, run `composer normalize` because CI auto-normalizes that file. + diff --git a/php-opencloud/openstack/src/Compute/v2/Api.php b/php-opencloud/openstack/src/Compute/v2/Api.php index ee4adfa00..571543a73 100644 --- a/php-opencloud/openstack/src/Compute/v2/Api.php +++ b/php-opencloud/openstack/src/Compute/v2/Api.php @@ -18,6 +18,11 @@ public function __construct() $this->params = new Params(); } + private function serverParam(array $param): array + { + return array_merge($param, ['path' => 'server']); + } + public function getLimits(): array { return [ @@ -189,21 +194,21 @@ public function deleteImageMetadataKey(): array public function postServer(): array { return [ - 'path' => 'servers', - 'method' => 'POST', - 'jsonKey' => 'server', - 'params' => [ - 'imageId' => $this->notRequired($this->params->imageId()), - 'flavorId' => $this->params->flavorId(), - 'personality' => $this->params->personality(), - 'metadata' => $this->notRequired($this->params->metadata()), - 'name' => $this->isRequired($this->params->name('server')), - 'securityGroups' => $this->params->securityGroups(), - 'userData' => $this->params->userData(), - 'availabilityZone' => $this->params->availabilityZone(), - 'networks' => $this->params->networks(), - 'blockDeviceMapping' => $this->params->blockDeviceMapping(), - 'keyName' => $this->params->keyName(), + 'path' => 'servers', + 'method' => 'POST', + 'params' => [ + 'imageId' => $this->serverParam($this->notRequired($this->params->imageId())), + 'flavorId' => $this->serverParam($this->params->flavorId()), + 'personality' => $this->serverParam($this->params->personality()), + 'metadata' => $this->serverParam($this->notRequired($this->params->metadata())), + 'name' => $this->serverParam($this->isRequired($this->params->name('server'))), + 'securityGroups' => $this->serverParam($this->params->securityGroups()), + 'userData' => $this->serverParam($this->params->userData()), + 'availabilityZone' => $this->serverParam($this->params->availabilityZone()), + 'networks' => $this->serverParam($this->params->networks()), + 'blockDeviceMapping' => $this->serverParam($this->params->blockDeviceMapping()), + 'keyName' => $this->serverParam($this->params->keyName()), + 'schedulerHints' => $this->params->schedulerHints(), ], ]; } @@ -708,6 +713,68 @@ public function deleteKeypair(): array ]; } + public function getServerGroups(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-server-groups', + 'params' => [ + 'allProjects' => $this->params->allProjects(), + 'limit' => $this->params->limit(), + 'offset' => $this->params->offset(), + ], + ]; + } + + public function getServerGroup(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-server-groups/{id}', + 'params' => [ + 'id' => $this->params->urlId('server group'), + ], + ]; + } + + public function postServerGroup(): array + { + return [ + 'method' => 'POST', + 'path' => 'os-server-groups', + 'jsonKey' => 'server_group', + 'params' => [ + 'name' => $this->isRequired($this->params->name('server group')), + 'policies' => $this->isRequired($this->params->serverGroupPolicies()), + ], + ]; + } + + public function postServerGroupWithPolicy(): array + { + return [ + 'method' => 'POST', + 'path' => 'os-server-groups', + 'jsonKey' => 'server_group', + 'params' => [ + 'name' => $this->isRequired($this->params->name('server group')), + 'policy' => $this->isRequired($this->params->serverGroupPolicy()), + 'rules' => $this->notRequired($this->params->serverGroupRules()), + ], + ]; + } + + public function deleteServerGroup(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'os-server-groups/{id}', + 'params' => [ + 'id' => $this->params->urlId('server group'), + ], + ]; + } + public function postSecurityGroup(): array { return [ diff --git a/php-opencloud/openstack/src/Compute/v2/Models/ServerGroup.php b/php-opencloud/openstack/src/Compute/v2/Models/ServerGroup.php new file mode 100644 index 000000000..a36aba833 --- /dev/null +++ b/php-opencloud/openstack/src/Compute/v2/Models/ServerGroup.php @@ -0,0 +1,111 @@ + 'projectId', + 'user_id' => 'userId', + ]; + + protected $resourceKey = 'server_group'; + protected $resourcesKey = 'server_groups'; + + public function retrieve() + { + $response = $this->execute($this->api->getServerGroup(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function create(array $userOptions): Creatable + { + if (isset($userOptions['policy'], $userOptions['policies'])) { + throw new \RuntimeException('Provide either "policy" or "policies", not both.'); + } + + if (isset($userOptions['rules']) && !isset($userOptions['policy'])) { + throw new \RuntimeException('"rules" requires "policy".'); + } + + if (!isset($userOptions['policy']) && !isset($userOptions['policies'])) { + throw new \RuntimeException('"policy" or "policies" must be set.'); + } + + $definition = isset($userOptions['policy']) + ? $this->api->postServerGroupWithPolicy() + : $this->api->postServerGroup(); + + $response = $this->execute($definition, $userOptions); + + return $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteServerGroup(), ['id' => (string) $this->id]); + } + + public function populateFromArray(array $array): self + { + $array = Utils::flattenJson($array, $this->resourceKey); + + if (isset($array['policy']) && !isset($array['policies'])) { + $array['policies'] = [$array['policy']]; + } + + if (isset($array['policies']) && is_array($array['policies']) && !isset($array['policy']) && count($array['policies']) > 0) { + $array['policy'] = reset($array['policies']); + } + + $array += [ + 'members' => [], + 'metadata' => [], + 'policies' => [], + 'rules' => [], + ]; + + return parent::populateFromArray($array); + } +} diff --git a/php-opencloud/openstack/src/Compute/v2/Params.php b/php-opencloud/openstack/src/Compute/v2/Params.php index b8a8b73ee..1297b6b1e 100644 --- a/php-opencloud/openstack/src/Compute/v2/Params.php +++ b/php-opencloud/openstack/src/Compute/v2/Params.php @@ -366,6 +366,16 @@ public function blockDeviceMapping(): array ]; } + public function schedulerHints(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'os:scheduler_hints', + 'description' => 'Scheduler hints to pass alongside the server create request, for example ["group" => "{serverGroupId}"].', + ]; + } + public function filterHost(): array { return [ @@ -499,6 +509,55 @@ public function keypairType(): array ]; } + public function allProjects(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'all_projects', + 'description' => '(Admin only) Set this to true to list server groups from all projects.', + ]; + } + + public function offset(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::QUERY, + 'description' => 'The offset of the first server group to return.', + ]; + } + + public function serverGroupPolicies(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'description' => 'The list of policies for the server group. In the base Compute API, a single policy must be provided in this array.', + 'items' => [ + 'type' => self::STRING_TYPE, + ], + ]; + } + + public function serverGroupPolicy(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'The server group policy name. This is available with Compute microversion 2.64 or later.', + ]; + } + + public function serverGroupRules(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'description' => 'The server group rules object. This is available with Compute microversion 2.64 or later.', + ]; + } + public function flavorRam(): array { return [ diff --git a/php-opencloud/openstack/src/Compute/v2/Service.php b/php-opencloud/openstack/src/Compute/v2/Service.php index 968b22ebc..be701b5be 100644 --- a/php-opencloud/openstack/src/Compute/v2/Service.php +++ b/php-opencloud/openstack/src/Compute/v2/Service.php @@ -15,6 +15,7 @@ use OpenStack\Compute\v2\Models\Limit; use OpenStack\Compute\v2\Models\QuotaSet; use OpenStack\Compute\v2\Models\Server; +use OpenStack\Compute\v2\Models\ServerGroup; /** * Compute v2 service for OpenStack. @@ -172,6 +173,45 @@ public function getKeypair(array $options = []): Keypair return $keypair; } + /** + * List server groups. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getServerGroups} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listServerGroups(array $options = [], ?callable $mapFn = null): \Generator + { + return $this->model(ServerGroup::class)->enumerate($this->api->getServerGroups(), $options, $mapFn); + } + + /** + * Create server group. + * + * @param array $options {@see \OpenStack\Compute\v2\Models\ServerGroup::create} + */ + public function createServerGroup(array $options): ServerGroup + { + return $this->model(ServerGroup::class)->create($options); + } + + /** + * Retrieve a server group object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see ServerGroup::retrieve}. + * + * @param array $options An array of attributes that will be set on the {@see ServerGroup} object. The array keys need to + * correspond to the class public properties. + */ + public function getServerGroup(array $options = []): ServerGroup + { + $serverGroup = $this->model(ServerGroup::class); + $serverGroup->populateFromArray($options); + + return $serverGroup; + } + /** * Shows rate and absolute limits for the tenant. */ diff --git a/php-opencloud/openstack/src/Identity/v3/Api.php b/php-opencloud/openstack/src/Identity/v3/Api.php index 418684c91..09a49e171 100644 --- a/php-opencloud/openstack/src/Identity/v3/Api.php +++ b/php-opencloud/openstack/src/Identity/v3/Api.php @@ -692,9 +692,10 @@ public function headGroupUser(): array public function postCredentials(): array { return [ - 'method' => 'POST', - 'path' => 'credentials', - 'params' => [ + 'method' => 'POST', + 'path' => 'credentials', + 'jsonKey' => 'credential', + 'params' => [ 'blob' => $this->params->blob(), 'projectId' => $this->params->projectId(), 'type' => $this->params->type('credential'), @@ -724,9 +725,10 @@ public function getCredential(): array public function patchCredential(): array { return [ - 'method' => 'PATCH', - 'path' => 'credentials/{id}', - 'params' => ['id' => $this->params->idUrl('credential')] + $this->postCredentials()['params'], + 'method' => 'PATCH', + 'path' => 'credentials/{id}', + 'jsonKey' => 'credential', + 'params' => ['id' => $this->params->idUrl('credential')] + $this->postCredentials()['params'], ]; } diff --git a/php-opencloud/openstack/src/Identity/v3/Models/Credential.php b/php-opencloud/openstack/src/Identity/v3/Models/Credential.php index 294a3ec1f..325a66ce5 100644 --- a/php-opencloud/openstack/src/Identity/v3/Models/Credential.php +++ b/php-opencloud/openstack/src/Identity/v3/Models/Credential.php @@ -39,6 +39,9 @@ class Credential extends OperatorResource implements Creatable, Updateable, Retr 'user_id' => 'userId', ]; + protected $resourceKey = 'credential'; + protected $resourcesKey = 'credentials'; + public function create(array $userOptions): Creatable { $response = $this->execute($this->api->postCredentials(), $userOptions); diff --git a/php-opencloud/openstack/src/Images/v2/Models/Image.php b/php-opencloud/openstack/src/Images/v2/Models/Image.php index 9cff176b6..5ee322f60 100644 --- a/php-opencloud/openstack/src/Images/v2/Models/Image.php +++ b/php-opencloud/openstack/src/Images/v2/Models/Image.php @@ -8,6 +8,7 @@ use OpenStack\Common\Resource\Alias; use OpenStack\Common\Resource\Creatable; use OpenStack\Common\Resource\Deletable; +use OpenStack\Common\Resource\HasWaiterTrait; use OpenStack\Common\Resource\Listable; use OpenStack\Common\Resource\OperatorResource; use OpenStack\Common\Resource\Retrievable; @@ -20,6 +21,8 @@ */ class Image extends OperatorResource implements Creatable, Listable, Retrievable, Deletable { + use HasWaiterTrait; + /** @var string */ public $status;