From 07a0068eddc9d228981314c8ce44b5573d8b4f5d Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Tue, 19 Mar 2024 10:39:22 +0100 Subject: [PATCH] Opis JSON Schema validation for PHP 7.0 (#76) This PR adds a PHP 7.0-compatible a version of the [Opis](https://opis.io/json-schema/2.x/) JSON Schema validator. Opis seems to be the most complete and well-maintained JSON Schema validator for PHP. However, Blueprints aim for PHP 7.0+ support and Opis is not compatible with PHP 7.0. Therefore, it was transpiled to PHP 7.0 with [rector](https://github.com/rectorphp/rector) and provided here. See https://github.com/WordPress/blueprints-library/issues/75 for more context. cc @reimic --- .github/workflows/ci.yml | 11 +- README.md | 68 +- composer.json | 9 +- composer.lock | 1618 +++++------------ phpcs.xml | 9 + rector.php | 20 + src/opis/README.md | 8 + src/opis/json-schema/LICENSE | 176 ++ src/opis/json-schema/NOTICE | 9 + src/opis/json-schema/README.md | 65 + src/opis/json-schema/SECURITY.md | 12 + src/opis/json-schema/autoload.php | 23 + src/opis/json-schema/composer.json | 49 + .../json-schema/src/CompliantValidator.php | 51 + src/opis/json-schema/src/ContentEncoding.php | 28 + src/opis/json-schema/src/ContentMediaType.php | 28 + .../json-schema/src/Errors/CustomError.php | 37 + .../json-schema/src/Errors/ErrorContainer.php | 142 ++ .../json-schema/src/Errors/ErrorFormatter.php | 432 +++++ .../src/Errors/ValidationError.php | 111 ++ .../Exceptions/DuplicateSchemaIdException.php | 63 + .../Exceptions/InvalidKeywordException.php | 49 + .../src/Exceptions/InvalidPragmaException.php | 49 + .../src/Exceptions/ParseException.php | 48 + .../src/Exceptions/SchemaException.php | 25 + .../UnresolvedContentEncodingException.php | 47 + .../UnresolvedContentMediaTypeException.php | 47 + .../src/Exceptions/UnresolvedException.php | 63 + .../Exceptions/UnresolvedFilterException.php | 63 + .../UnresolvedReferenceException.php | 48 + src/opis/json-schema/src/Filter.php | 29 + .../json-schema/src/Filters/CommonFilters.php | 43 + .../src/Filters/DataExistsFilter.php | 44 + .../src/Filters/DateTimeFilters.php | 100 + .../src/Filters/FilterExistsFilter.php | 57 + .../src/Filters/FormatExistsFilter.php | 57 + .../src/Filters/GlobalVarExistsFilter.php | 50 + .../src/Filters/SchemaExistsFilter.php | 87 + .../src/Filters/SlotExistsFilter.php | 39 + src/opis/json-schema/src/Format.php | 27 + .../src/Formats/DateTimeFormats.php | 70 + .../json-schema/src/Formats/IriFormats.php | 135 ++ .../json-schema/src/Formats/MiscFormats.php | 59 + .../json-schema/src/Formats/UriFormats.php | 69 + src/opis/json-schema/src/Helper.php | 351 ++++ src/opis/json-schema/src/Info/DataInfo.php | 120 ++ src/opis/json-schema/src/Info/SchemaInfo.php | 129 ++ src/opis/json-schema/src/JsonPointer.php | 439 +++++ src/opis/json-schema/src/Keyword.php | 30 + src/opis/json-schema/src/KeywordValidator.php | 32 + .../AbstractKeywordValidator.php | 48 + .../CallbackKeywordValidator.php | 61 + .../PragmaKeywordValidator.php | 64 + .../src/Keywords/AbstractRefKeyword.php | 156 ++ .../src/Keywords/AdditionalItemsKeyword.php | 102 ++ .../Keywords/AdditionalPropertiesKeyword.php | 85 + .../json-schema/src/Keywords/AllOfKeyword.php | 80 + .../json-schema/src/Keywords/AnyOfKeyword.php | 99 + .../src/Keywords/ConstDataKeyword.php | 60 + .../json-schema/src/Keywords/ConstKeyword.php | 53 + .../src/Keywords/ContainsKeyword.php | 151 ++ .../src/Keywords/ContentEncodingKeyword.php | 85 + .../src/Keywords/ContentMediaTypeKeyword.php | 91 + .../src/Keywords/ContentSchemaKeyword.php | 66 + .../src/Keywords/DefaultKeyword.php | 58 + .../src/Keywords/DependenciesKeyword.php | 97 + .../src/Keywords/DependentRequiredKeyword.php | 68 + .../src/Keywords/DependentSchemasKeyword.php | 84 + .../src/Keywords/EnumDataKeyword.php | 60 + .../json-schema/src/Keywords/EnumKeyword.php | 84 + .../json-schema/src/Keywords/ErrorTrait.php | 55 + .../Keywords/ExclusiveMaximumDataKeyword.php | 60 + .../src/Keywords/ExclusiveMaximumKeyword.php | 55 + .../Keywords/ExclusiveMinimumDataKeyword.php | 60 + .../src/Keywords/ExclusiveMinimumKeyword.php | 55 + .../src/Keywords/FiltersKeyword.php | 95 + .../src/Keywords/FormatDataKeyword.php | 84 + .../src/Keywords/FormatKeyword.php | 87 + .../src/Keywords/IfThenElseKeyword.php | 106 ++ .../json-schema/src/Keywords/ItemsKeyword.php | 176 ++ .../Keywords/IterableDataValidationTrait.php | 110 ++ .../src/Keywords/MaxItemsDataKeyword.php | 60 + .../src/Keywords/MaxItemsKeyword.php | 63 + .../src/Keywords/MaxLengthDataKeyword.php | 60 + .../src/Keywords/MaxLengthKeyword.php | 67 + .../src/Keywords/MaxPropertiesDataKeyword.php | 60 + .../src/Keywords/MaxPropertiesKeywords.php | 63 + .../src/Keywords/MaximumDataKeyword.php | 60 + .../src/Keywords/MaximumKeyword.php | 59 + .../src/Keywords/MinItemsDataKeyword.php | 60 + .../src/Keywords/MinItemsKeyword.php | 63 + .../src/Keywords/MinLengthDataKeyword.php | 57 + .../src/Keywords/MinLengthKeyword.php | 66 + .../src/Keywords/MinPropertiesDataKeyword.php | 60 + .../src/Keywords/MinPropertiesKeyword.php | 63 + .../src/Keywords/MinimumDataKeyword.php | 60 + .../src/Keywords/MinimumKeyword.php | 59 + .../src/Keywords/MultipleOfDataKeyword.php | 60 + .../src/Keywords/MultipleOfKeyword.php | 60 + .../json-schema/src/Keywords/NotKeyword.php | 68 + src/opis/json-schema/src/Keywords/OfTrait.php | 52 + .../json-schema/src/Keywords/OneOfKeyword.php | 100 + .../src/Keywords/PatternDataKeyword.php | 61 + .../src/Keywords/PatternKeyword.php | 61 + .../src/Keywords/PatternPropertiesKeyword.php | 121 ++ .../src/Keywords/PointerRefKeyword.php | 61 + .../src/Keywords/PropertiesKeyword.php | 117 ++ .../src/Keywords/PropertyNamesKeyword.php | 76 + .../src/Keywords/RecursiveRefKeyword.php | 152 ++ .../src/Keywords/RequiredDataKeyword.php | 92 + .../src/Keywords/RequiredKeyword.php | 70 + .../json-schema/src/Keywords/SlotsKeyword.php | 153 ++ .../src/Keywords/TemplateRefKeyword.php | 132 ++ .../json-schema/src/Keywords/TypeKeyword.php | 60 + .../src/Keywords/URIRefKeyword.php | 62 + .../src/Keywords/UnevaluatedItemsKeyword.php | 77 + .../Keywords/UnevaluatedPropertiesKeyword.php | 79 + .../src/Keywords/UniqueItemsDataKeyword.php | 56 + .../src/Keywords/UniqueItemsKeyword.php | 59 + .../src/Parsers/DataKeywordTrait.php | 59 + .../src/Parsers/DefaultVocabulary.php | 56 + src/opis/json-schema/src/Parsers/Draft.php | 82 + .../src/Parsers/DraftOptionTrait.php | 48 + .../src/Parsers/Drafts/Draft06.php | 145 ++ .../src/Parsers/Drafts/Draft07.php | 43 + .../src/Parsers/Drafts/Draft201909.php | 159 ++ .../src/Parsers/Drafts/Draft202012.php | 163 ++ .../json-schema/src/Parsers/KeywordParser.php | 62 + .../src/Parsers/KeywordParserTrait.php | 76 + .../src/Parsers/KeywordValidatorParser.php | 34 + .../PragmaKeywordValidatorParser.php | 60 + .../Keywords/AdditionalItemsKeywordParser.php | 66 + .../AdditionalPropertiesKeywordParser.php | 57 + .../Parsers/Keywords/AllOfKeywordParser.php | 78 + .../Parsers/Keywords/AnyOfKeywordParser.php | 78 + .../Parsers/Keywords/ConstKeywordParser.php | 77 + .../Keywords/ContainsKeywordParser.php | 94 + .../Keywords/ContentEncodingKeywordParser.php | 65 + .../ContentMediaTypeKeywordParser.php | 65 + .../Keywords/ContentSchemaKeywordParser.php | 63 + .../Parsers/Keywords/DefaultKeywordParser.php | 91 + .../Keywords/DependenciesKeywordParser.php | 83 + .../DependentRequiredKeywordParser.php | 73 + .../DependentSchemasKeywordParser.php | 76 + .../Parsers/Keywords/EnumKeywordParser.php | 110 ++ .../ExclusiveMaximumKeywordParser.php | 73 + .../ExclusiveMinimumKeywordParser.php | 73 + .../Parsers/Keywords/FiltersKeywordParser.php | 164 ++ .../Parsers/Keywords/FormatKeywordParser.php | 74 + .../Keywords/IfThenElseKeywordParser.php | 118 ++ .../Parsers/Keywords/ItemsKeywordParser.php | 126 ++ .../Keywords/MaxItemsKeywordParser.php | 66 + .../Keywords/MaxLengthKeywordParser.php | 66 + .../Keywords/MaxPropertiesKeywordParser.php | 66 + .../Parsers/Keywords/MaximumKeywordParser.php | 97 + .../Keywords/MinItemsKeywordParser.php | 70 + .../Keywords/MinLengthKeywordParser.php | 70 + .../Keywords/MinPropertiesKeywordParser.php | 70 + .../Parsers/Keywords/MinimumKeywordParser.php | 97 + .../Keywords/MultipleOfKeywordParser.php | 70 + .../src/Parsers/Keywords/NotKeywordParser.php | 61 + .../Parsers/Keywords/OneOfKeywordParser.php | 82 + .../Parsers/Keywords/PatternKeywordParser.php | 70 + .../PatternPropertiesKeywordParser.php | 70 + .../Keywords/PropertiesKeywordParser.php | 67 + .../Keywords/PropertyNamesKeywordParser.php | 61 + .../src/Parsers/Keywords/RefKeywordParser.php | 234 +++ .../Keywords/RequiredKeywordParser.php | 110 ++ .../Parsers/Keywords/SlotsKeywordParser.php | 67 + .../Parsers/Keywords/TypeKeywordParser.php | 81 + .../UnevaluatedItemsKeywordParser.php | 79 + .../UnevaluatedPropertiesKeywordParser.php | 74 + .../Keywords/UniqueItemsKeywordParser.php | 66 + .../json-schema/src/Parsers/PragmaParser.php | 87 + .../src/Parsers/Pragmas/CastPragmaParser.php | 48 + .../Parsers/Pragmas/GlobalsPragmaParser.php | 51 + .../Parsers/Pragmas/MaxErrorsPragmaParser.php | 47 + .../src/Parsers/Pragmas/SlotsPragmaParser.php | 68 + .../json-schema/src/Parsers/ResolverTrait.php | 38 + .../json-schema/src/Parsers/SchemaParser.php | 644 +++++++ .../src/Parsers/VariablesTrait.php | 41 + .../json-schema/src/Parsers/Vocabulary.php | 126 ++ src/opis/json-schema/src/Pragma.php | 33 + .../json-schema/src/Pragmas/CastPragma.php | 203 +++ .../json-schema/src/Pragmas/GlobalsPragma.php | 66 + .../src/Pragmas/MaxErrorsPragma.php | 60 + .../json-schema/src/Pragmas/SlotsPragma.php | 57 + .../src/Resolvers/ContentEncodingResolver.php | 143 ++ .../Resolvers/ContentMediaTypeResolver.php | 153 ++ .../src/Resolvers/FilterResolver.php | 271 +++ .../src/Resolvers/FormatResolver.php | 143 ++ .../src/Resolvers/SchemaResolver.php | 342 ++++ src/opis/json-schema/src/Schema.php | 28 + src/opis/json-schema/src/SchemaLoader.php | 473 +++++ src/opis/json-schema/src/SchemaValidator.php | 29 + .../src/Schemas/AbstractSchema.php | 46 + .../json-schema/src/Schemas/BooleanSchema.php | 53 + .../json-schema/src/Schemas/EmptySchema.php | 57 + .../src/Schemas/ExceptionSchema.php | 52 + .../json-schema/src/Schemas/LazySchema.php | 67 + .../json-schema/src/Schemas/ObjectSchema.php | 124 ++ src/opis/json-schema/src/Uri.php | 94 + .../json-schema/src/ValidationContext.php | 743 ++++++++ src/opis/json-schema/src/ValidationResult.php | 56 + src/opis/json-schema/src/Validator.php | 289 +++ src/opis/json-schema/src/Variables.php | 28 + .../src/Variables/RefVariablesContainer.php | 112 ++ .../src/Variables/VariablesContainer.php | 169 ++ src/opis/string/.editorconfig | 8 + src/opis/string/CHANGELOG.md | 6 + src/opis/string/LICENSE | 176 ++ src/opis/string/NOTICE | 9 + src/opis/string/README.md | 50 + src/opis/string/composer.json | 40 + src/opis/string/res/ascii.php | 751 ++++++++ src/opis/string/res/fold.php | 1417 +++++++++++++++ src/opis/string/res/lower.php | 1396 ++++++++++++++ src/opis/string/res/upper.php | 1413 ++++++++++++++ .../Exception/InvalidCodePointException.php | 46 + .../src/Exception/InvalidStringException.php | 61 + .../string/src/Exception/UnicodeException.php | 25 + src/opis/string/src/UnicodeString.php | 1606 ++++++++++++++++ src/opis/uri/LICENSE | 176 ++ src/opis/uri/README.md | 40 + src/opis/uri/autoload.php | 42 + src/opis/uri/composer.json | 44 + src/opis/uri/src/Punycode.php | 282 +++ src/opis/uri/src/PunycodeException.php | 25 + src/opis/uri/src/Uri.php | 991 ++++++++++ src/opis/uri/src/UriTemplate.php | 523 ++++++ 230 files changed, 28265 insertions(+), 1167 deletions(-) create mode 100644 phpcs.xml create mode 100644 rector.php create mode 100644 src/opis/README.md create mode 100644 src/opis/json-schema/LICENSE create mode 100644 src/opis/json-schema/NOTICE create mode 100644 src/opis/json-schema/README.md create mode 100644 src/opis/json-schema/SECURITY.md create mode 100644 src/opis/json-schema/autoload.php create mode 100644 src/opis/json-schema/composer.json create mode 100644 src/opis/json-schema/src/CompliantValidator.php create mode 100644 src/opis/json-schema/src/ContentEncoding.php create mode 100644 src/opis/json-schema/src/ContentMediaType.php create mode 100644 src/opis/json-schema/src/Errors/CustomError.php create mode 100644 src/opis/json-schema/src/Errors/ErrorContainer.php create mode 100644 src/opis/json-schema/src/Errors/ErrorFormatter.php create mode 100644 src/opis/json-schema/src/Errors/ValidationError.php create mode 100644 src/opis/json-schema/src/Exceptions/DuplicateSchemaIdException.php create mode 100644 src/opis/json-schema/src/Exceptions/InvalidKeywordException.php create mode 100644 src/opis/json-schema/src/Exceptions/InvalidPragmaException.php create mode 100644 src/opis/json-schema/src/Exceptions/ParseException.php create mode 100644 src/opis/json-schema/src/Exceptions/SchemaException.php create mode 100644 src/opis/json-schema/src/Exceptions/UnresolvedContentEncodingException.php create mode 100644 src/opis/json-schema/src/Exceptions/UnresolvedContentMediaTypeException.php create mode 100644 src/opis/json-schema/src/Exceptions/UnresolvedException.php create mode 100644 src/opis/json-schema/src/Exceptions/UnresolvedFilterException.php create mode 100644 src/opis/json-schema/src/Exceptions/UnresolvedReferenceException.php create mode 100644 src/opis/json-schema/src/Filter.php create mode 100644 src/opis/json-schema/src/Filters/CommonFilters.php create mode 100644 src/opis/json-schema/src/Filters/DataExistsFilter.php create mode 100644 src/opis/json-schema/src/Filters/DateTimeFilters.php create mode 100644 src/opis/json-schema/src/Filters/FilterExistsFilter.php create mode 100644 src/opis/json-schema/src/Filters/FormatExistsFilter.php create mode 100644 src/opis/json-schema/src/Filters/GlobalVarExistsFilter.php create mode 100644 src/opis/json-schema/src/Filters/SchemaExistsFilter.php create mode 100644 src/opis/json-schema/src/Filters/SlotExistsFilter.php create mode 100644 src/opis/json-schema/src/Format.php create mode 100644 src/opis/json-schema/src/Formats/DateTimeFormats.php create mode 100644 src/opis/json-schema/src/Formats/IriFormats.php create mode 100644 src/opis/json-schema/src/Formats/MiscFormats.php create mode 100644 src/opis/json-schema/src/Formats/UriFormats.php create mode 100644 src/opis/json-schema/src/Helper.php create mode 100644 src/opis/json-schema/src/Info/DataInfo.php create mode 100644 src/opis/json-schema/src/Info/SchemaInfo.php create mode 100644 src/opis/json-schema/src/JsonPointer.php create mode 100644 src/opis/json-schema/src/Keyword.php create mode 100644 src/opis/json-schema/src/KeywordValidator.php create mode 100644 src/opis/json-schema/src/KeywordValidators/AbstractKeywordValidator.php create mode 100644 src/opis/json-schema/src/KeywordValidators/CallbackKeywordValidator.php create mode 100644 src/opis/json-schema/src/KeywordValidators/PragmaKeywordValidator.php create mode 100644 src/opis/json-schema/src/Keywords/AbstractRefKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/AdditionalItemsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/AdditionalPropertiesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/AllOfKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/AnyOfKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ConstDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ConstKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ContainsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ContentEncodingKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ContentMediaTypeKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ContentSchemaKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/DefaultKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/DependenciesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/DependentRequiredKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/DependentSchemasKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/EnumDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/EnumKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ErrorTrait.php create mode 100644 src/opis/json-schema/src/Keywords/ExclusiveMaximumDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ExclusiveMaximumKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ExclusiveMinimumDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ExclusiveMinimumKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/FiltersKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/FormatDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/FormatKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/IfThenElseKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/ItemsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/IterableDataValidationTrait.php create mode 100644 src/opis/json-schema/src/Keywords/MaxItemsDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaxItemsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaxLengthDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaxLengthKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaxPropertiesDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaxPropertiesKeywords.php create mode 100644 src/opis/json-schema/src/Keywords/MaximumDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MaximumKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinItemsDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinItemsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinLengthDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinLengthKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinPropertiesDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinPropertiesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinimumDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MinimumKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MultipleOfDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/MultipleOfKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/NotKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/OfTrait.php create mode 100644 src/opis/json-schema/src/Keywords/OneOfKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PatternDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PatternKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PatternPropertiesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PointerRefKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PropertiesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/PropertyNamesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/RecursiveRefKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/RequiredDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/RequiredKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/SlotsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/TemplateRefKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/TypeKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/URIRefKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/UnevaluatedItemsKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/UnevaluatedPropertiesKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/UniqueItemsDataKeyword.php create mode 100644 src/opis/json-schema/src/Keywords/UniqueItemsKeyword.php create mode 100644 src/opis/json-schema/src/Parsers/DataKeywordTrait.php create mode 100644 src/opis/json-schema/src/Parsers/DefaultVocabulary.php create mode 100644 src/opis/json-schema/src/Parsers/Draft.php create mode 100644 src/opis/json-schema/src/Parsers/DraftOptionTrait.php create mode 100644 src/opis/json-schema/src/Parsers/Drafts/Draft06.php create mode 100644 src/opis/json-schema/src/Parsers/Drafts/Draft07.php create mode 100644 src/opis/json-schema/src/Parsers/Drafts/Draft201909.php create mode 100644 src/opis/json-schema/src/Parsers/Drafts/Draft202012.php create mode 100644 src/opis/json-schema/src/Parsers/KeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/KeywordParserTrait.php create mode 100644 src/opis/json-schema/src/Parsers/KeywordValidatorParser.php create mode 100644 src/opis/json-schema/src/Parsers/KeywordValidators/PragmaKeywordValidatorParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/AdditionalItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/AdditionalPropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/AllOfKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/AnyOfKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ConstKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ContainsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ContentEncodingKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ContentMediaTypeKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ContentSchemaKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/DefaultKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/DependenciesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/DependentRequiredKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/DependentSchemasKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/EnumKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ExclusiveMaximumKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ExclusiveMinimumKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/FiltersKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/FormatKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/IfThenElseKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/ItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MaxItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MaxLengthKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MaxPropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MaximumKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MinItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MinLengthKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MinPropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MinimumKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/MultipleOfKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/NotKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/OneOfKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/PatternKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/PatternPropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/PropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/PropertyNamesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/RefKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/RequiredKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/SlotsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/TypeKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/UnevaluatedItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/UnevaluatedPropertiesKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/Keywords/UniqueItemsKeywordParser.php create mode 100644 src/opis/json-schema/src/Parsers/PragmaParser.php create mode 100644 src/opis/json-schema/src/Parsers/Pragmas/CastPragmaParser.php create mode 100644 src/opis/json-schema/src/Parsers/Pragmas/GlobalsPragmaParser.php create mode 100644 src/opis/json-schema/src/Parsers/Pragmas/MaxErrorsPragmaParser.php create mode 100644 src/opis/json-schema/src/Parsers/Pragmas/SlotsPragmaParser.php create mode 100644 src/opis/json-schema/src/Parsers/ResolverTrait.php create mode 100644 src/opis/json-schema/src/Parsers/SchemaParser.php create mode 100644 src/opis/json-schema/src/Parsers/VariablesTrait.php create mode 100644 src/opis/json-schema/src/Parsers/Vocabulary.php create mode 100644 src/opis/json-schema/src/Pragma.php create mode 100644 src/opis/json-schema/src/Pragmas/CastPragma.php create mode 100644 src/opis/json-schema/src/Pragmas/GlobalsPragma.php create mode 100644 src/opis/json-schema/src/Pragmas/MaxErrorsPragma.php create mode 100644 src/opis/json-schema/src/Pragmas/SlotsPragma.php create mode 100644 src/opis/json-schema/src/Resolvers/ContentEncodingResolver.php create mode 100644 src/opis/json-schema/src/Resolvers/ContentMediaTypeResolver.php create mode 100644 src/opis/json-schema/src/Resolvers/FilterResolver.php create mode 100644 src/opis/json-schema/src/Resolvers/FormatResolver.php create mode 100644 src/opis/json-schema/src/Resolvers/SchemaResolver.php create mode 100644 src/opis/json-schema/src/Schema.php create mode 100644 src/opis/json-schema/src/SchemaLoader.php create mode 100644 src/opis/json-schema/src/SchemaValidator.php create mode 100644 src/opis/json-schema/src/Schemas/AbstractSchema.php create mode 100644 src/opis/json-schema/src/Schemas/BooleanSchema.php create mode 100644 src/opis/json-schema/src/Schemas/EmptySchema.php create mode 100644 src/opis/json-schema/src/Schemas/ExceptionSchema.php create mode 100644 src/opis/json-schema/src/Schemas/LazySchema.php create mode 100644 src/opis/json-schema/src/Schemas/ObjectSchema.php create mode 100644 src/opis/json-schema/src/Uri.php create mode 100644 src/opis/json-schema/src/ValidationContext.php create mode 100644 src/opis/json-schema/src/ValidationResult.php create mode 100644 src/opis/json-schema/src/Validator.php create mode 100644 src/opis/json-schema/src/Variables.php create mode 100644 src/opis/json-schema/src/Variables/RefVariablesContainer.php create mode 100644 src/opis/json-schema/src/Variables/VariablesContainer.php create mode 100644 src/opis/string/.editorconfig create mode 100644 src/opis/string/CHANGELOG.md create mode 100644 src/opis/string/LICENSE create mode 100644 src/opis/string/NOTICE create mode 100644 src/opis/string/README.md create mode 100644 src/opis/string/composer.json create mode 100644 src/opis/string/res/ascii.php create mode 100644 src/opis/string/res/fold.php create mode 100644 src/opis/string/res/lower.php create mode 100644 src/opis/string/res/upper.php create mode 100644 src/opis/string/src/Exception/InvalidCodePointException.php create mode 100644 src/opis/string/src/Exception/InvalidStringException.php create mode 100644 src/opis/string/src/Exception/UnicodeException.php create mode 100644 src/opis/string/src/UnicodeString.php create mode 100644 src/opis/uri/LICENSE create mode 100644 src/opis/uri/README.md create mode 100644 src/opis/uri/autoload.php create mode 100644 src/opis/uri/composer.json create mode 100644 src/opis/uri/src/Punycode.php create mode 100644 src/opis/uri/src/PunycodeException.php create mode 100644 src/opis/uri/src/Uri.php create mode 100644 src/opis/uri/src/UriTemplate.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fdb442d..76daa54f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - trunk + - trunk pull_request: jobs: @@ -25,15 +25,10 @@ jobs: - name: Install Dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - - name: Check PHP version compatibility - uses: pantheon-systems/phpcompatibility-action@v1 - with: - skip-php-setup: true - test-versions: 7.4- - paths: ${{ github.workspace }}/src + - name: PHPCS checks + run: ./vendor/bin/phpcs ${{ github.workspace }}/src test-unit: - needs: [php-cs-check] runs-on: ${{ matrix.os }} strategy: fail-fast: true diff --git a/README.md b/README.md index 92fb07c1..f121743c 100644 --- a/README.md +++ b/README.md @@ -91,15 +91,30 @@ vendor/bin/phpunit --testdox ### Regenerate models files from JSON schema with ```shell - php src/WordPress/Blueprints/bin/autogenerate_models.php +composer global require jane-php/json-schema +php src/WordPress/Blueprints/bin/autogenerate_models.php ``` ## Building to .phar -The Blueprints library is distributed as a .phar library. To build the .phar file, run: +The Blueprints library is distributed as a .phar library. To build the .phar file, install box: ```shell -vendor/bin/box compile +composer global require humbug/box +``` + +And then run: + +```shell +rm composer.lock +rm -rf vendor +COMPOSER=composer-web.json composer install --no-dev +rm -rf vendor/pimple/pimple/ext/ +rm -rf vendor/symfony/*/*.md +rm -rf vendor/symfony/*/composer.json +rm -rf vendor/symfony/*/*.dist +rm -rf vendor/*/*/LICENSE +box compile ``` Note that in box.json, the `"check-requirements"` option is set to `false`. Somehow, keeping it as `true` results in a @@ -112,6 +127,53 @@ To try the built .phar file, run: rm -rf new-wp/* && USE_PHAR=1 php blueprint_compiling.php ``` +## PHP 7.0 Compatibility + +This project is compatible with PHP >= 7.0. + +Part of the process is automated with [rector](https://github.com/rectorphp/rector), +which transpiles the features added in PHP 7.2 and later to PHP 7.1. Unfortunately, +that's as far as Rector goes. + +From there, manual transformations are required to bring the compatibility further down to PHP 7.0. + +### Automated part + +To transpile the code to PHP 7.1, run: + +```bash +# Install rector: +composer require rector/rector --ignore-platform-req=php + +# Transpile: +php vendor/bin/rector process src +``` + +Unfortunately, Rector does not support downgrading to PHP 7.0 yet, so we need to do the +last stretch manually. + +### Manual part + +Rector will downgrade PHP code to PHP 7.1 but not further. We need PHP 7.0 compat +so here's a few additional regexps to run. Regexps are not, of course, reliable in +the general case, but they seem to do the trick here. + +List of manual replacements + +* `: \?[a-zA-Z_0-9]+` -> (empty string) to remove the unsupported return type + from `function(): ?SchemaResolver {}` -> `function() {}`. +* `: iterable` to fix `Fatal error: Generators may only declare a return type of Generator, Iterator or Traversable`. +* `\?[a-zA-Z_0-9]+ \$` -> `$` to remove the unsupported nullable type from function signatures, + e.g. `function(?Schema $schema){}` -> `function($schema){}`. +* `(protected|public|private) const` -> `const` as const visibility is not supported in PHP 7.0. +* `: void` -> `` as `void` return type is unsupported in PHP 7.0. + +@TODO: + +* `[$ns, $name] = $this->parseName($name);` -> `list($ns, $name) = $this->parseName($name);` +* `foreach ($data as [$cp, $chars]) {` -> `foreach ($data as list($cp, $chars)) {` +* Find or write Rector rules for downgrading to PHP 7.0 + ## License WordPress Blueprints are open-source software licensed under the GPL. diff --git a/composer.json b/composer.json index 2421ffe6..9eeb687e 100644 --- a/composer.json +++ b/composer.json @@ -6,9 +6,7 @@ "symfony/process": "*", "pimple/pimple": "*", "psr/simple-cache": "*", - "opis/json-schema": "*", - "ext-json": "*", - "nikic/php-parser": "v4.18.0" + "ext-json": "*" }, "require-dev": { "phpunit/phpunit": "*", @@ -36,7 +34,10 @@ ], "psr-4": { "WordPress\\": "src/WordPress", - "Symfony\\Component\\Process\\": "vendor/symfony/process" + "Symfony\\Component\\Process\\": "vendor/symfony/process", + "Opis\\JsonSchema\\": "src/opis/json-schema/src", + "Opis\\String\\": "src/opis/string/src", + "Opis\\Uri\\": "src/opis/uri/src" }, "files": [ "src/WordPress/Blueprints/functions.php", diff --git a/composer.lock b/composer.lock index d02b4b9d..46abc4a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,254 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bfa73126bb8c7534096d54d79774d82b", + "content-hash": "71709bd85d8f5389e7b741c8f505994c", "packages": [ - { - "name": "nikic/php-parser", - "version": "v4.18.0", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "reference": "1bcbb2179f97633e98bbbc87044ee2611c7d7999", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.18.0" - }, - "time": "2023-12-10T21:03:43+00:00" - }, - { - "name": "opis/json-schema", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/opis/json-schema.git", - "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/json-schema/zipball/c48df6d7089a45f01e1c82432348f2d5976f9bfb", - "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb", - "shasum": "" - }, - "require": { - "ext-json": "*", - "opis/string": "^2.0", - "opis/uri": "^1.0", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "ext-bcmath": "*", - "ext-intl": "*", - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\JsonSchema\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - }, - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - } - ], - "description": "Json Schema Validator for PHP", - "homepage": "https://opis.io/json-schema", - "keywords": [ - "json", - "json-schema", - "schema", - "validation", - "validator" - ], - "support": { - "issues": "https://github.com/opis/json-schema/issues", - "source": "https://github.com/opis/json-schema/tree/2.3.0" - }, - "time": "2022-01-08T20:38:03+00:00" - }, - { - "name": "opis/string", - "version": "2.0.1", - "source": { - "type": "git", - "url": "https://github.com/opis/string.git", - "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/string/zipball/9ebf1a1f873f502f6859d11210b25a4bf5d141e7", - "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7", - "shasum": "" - }, - "require": { - "ext-iconv": "*", - "ext-json": "*", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\String\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "Multibyte strings as objects", - "homepage": "https://opis.io/string", - "keywords": [ - "multi-byte", - "opis", - "string", - "string manipulation", - "utf-8" - ], - "support": { - "issues": "https://github.com/opis/string/issues", - "source": "https://github.com/opis/string/tree/2.0.1" - }, - "time": "2022-01-14T15:42:23+00:00" - }, - { - "name": "opis/uri", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/opis/uri.git", - "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", - "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", - "shasum": "" - }, - "require": { - "opis/string": "^2.0", - "php": "^7.4 || ^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Opis\\Uri\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Marius Sarca", - "email": "marius.sarca@gmail.com" - }, - { - "name": "Sorin Sarca", - "email": "sarca_sorin@hotmail.com" - } - ], - "description": "Build, parse and validate URIs and URI-templates", - "homepage": "https://opis.io", - "keywords": [ - "URI Template", - "parse url", - "punycode", - "uri", - "uri components", - "url", - "validate uri" - ], - "support": { - "issues": "https://github.com/opis/uri/issues", - "source": "https://github.com/opis/uri/tree/1.1.0" - }, - "time": "2021-05-22T15:57:08+00:00" - }, { "name": "pimple/pimple", "version": "v3.5.0", @@ -307,22 +61,27 @@ }, { "name": "psr/container", - "version": "1.1.2", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { "php": ">=7.4.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -349,9 +108,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "time": "2021-11-05T16:50:12+00:00" + "time": "2021-11-05T16:47:00+00:00" }, { "name": "psr/event-dispatcher", @@ -403,56 +162,6 @@ }, "time": "2019-01-08T18:20:26+00:00" }, - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, { "name": "psr/simple-cache", "version": "1.0.1", @@ -572,36 +281,50 @@ "time": "2022-01-02T09:53:40+00:00" }, { - "name": "symfony/error-handler", - "version": "v5.4.36", + "name": "symfony/event-dispatcher", + "version": "v5.4.35", "source": { "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "90b1d7799bfc1b3ed5f902e8b334eeb7dba537a1" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "7a69a85c7ea5bdd1e875806a99c51a87d3a74b38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/90b1d7799bfc1b3ed5f902e8b334eeb7dba537a1", - "reference": "90b1d7799bfc1b3ed5f902e8b334eeb7dba537a1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7a69a85c7ea5bdd1e875806a99c51a87d3a74b38", + "reference": "7a69a85c7ea5bdd1e875806a99c51a87d3a74b38", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4.4|^5.0|^6.0" + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher-contracts": "^2|^3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/dependency-injection": "<4.4" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0" }, "require-dev": { - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" + "psr/log": "^1|^2|^3", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/error-handler": "^4.4|^5.0|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-foundation": "^4.4|^5.0|^6.0", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/stopwatch": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/dependency-injection": "", + "symfony/http-kernel": "" }, - "bin": [ - "Resources/bin/patch-type-declarations" - ], "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" + "Symfony\\Component\\EventDispatcher\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -621,10 +344,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to manage errors and ease debugging PHP code", + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.36" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.35" }, "funding": [ { @@ -640,100 +363,15 @@ "type": "tidelift" } ], - "time": "2024-02-22T11:40:53+00:00" + "time": "2024-01-23T13:51:25+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v5.4.35", + "name": "symfony/event-dispatcher-contracts", + "version": "v2.5.2", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "7a69a85c7ea5bdd1e875806a99c51a87d3a74b38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/7a69a85c7ea5bdd1e875806a99c51a87d3a74b38", - "reference": "7a69a85c7ea5bdd1e875806a99c51a87d3a74b38", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.35" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T13:51:25+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" }, "dist": { "type": "zip", @@ -871,614 +509,28 @@ "time": "2024-01-23T13:51:25+00:00" }, { - "name": "symfony/http-client", - "version": "v5.4.37", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "63d93fd99523b9608929a38172da3365a6c0821c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/63d93fd99523b9608929a38172da3365a6c0821c", - "reference": "63d93fd99523b9608929a38172da3365a6c0821c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-client-contracts": "^2.4", - "symfony/polyfill-php73": "^1.11", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.0|^2|^3" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "2.4" - }, - "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "php-http/message-factory": "^1.0", - "psr/http-client": "^1.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4.13|^5.1.5|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v5.4.37" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-02-28T15:18:15+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70", - "reference": "ba6a9f0e8f3edd190520ee3b9a958596b6ca2e70", - "shasum": "" - }, - "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v2.5.2" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2022-04-12T15:48:08+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v5.4.35", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "f2ab692a22aef1cd54beb893aa0068bdfb093928" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f2ab692a22aef1cd54beb893aa0068bdfb093928", - "reference": "f2ab692a22aef1cd54beb893aa0068bdfb093928", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "predis/predis": "~1.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Defines an object-oriented layer for the HTTP specification", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.35" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T13:51:25+00:00" - }, - { - "name": "symfony/http-kernel", - "version": "v5.4.37", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "4ef7ed872564852b3c6c15fecf492975a52cbff3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4ef7ed872564852b3c6c15fecf492975a52cbff3", - "reference": "4ef7ed872564852b3c6c15fecf492975a52cbff3", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^5.0|^6.0", - "symfony/http-foundation": "^5.4.21|^6.2.7", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.0", - "symfony/config": "<5.0", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<5.3", - "symfony/doctrine-bridge": "<5.0", - "symfony/form": "<5.0", - "symfony/http-client": "<5.0", - "symfony/mailer": "<5.0", - "symfony/messenger": "<5.0", - "symfony/translation": "<5.0", - "symfony/twig-bridge": "<5.0", - "symfony/validator": "<5.0", - "twig/twig": "<2.13" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2|^3", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a structured process for converting a Request into a Response", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.37" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-03-04T20:55:44+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-29T20:11:03+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.29.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, "type": "library", "extra": { "thanks": { @@ -1491,11 +543,8 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1503,28 +552,24 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "ctype", "polyfill", - "portable", - "shim" + "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" }, "funding": [ { @@ -1543,31 +588,42 @@ "time": "2024-01-29T20:11:03+00:00" }, { - "name": "symfony/process", - "version": "v5.4.36", + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1575,18 +631,25 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Executes commands in sub-processes", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/process/tree/v5.4.36" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" }, "funding": [ { @@ -1602,53 +665,52 @@ "type": "tidelift" } ], - "time": "2024-02-12T15:49:53+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.5.2", + "name": "symfony/polyfill-php80", + "version": "v1.29.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" + "php": ">=7.1" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -1658,18 +720,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" }, "funding": [ { @@ -1685,53 +745,30 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2024-01-29T20:11:03+00:00" }, { - "name": "symfony/var-dumper", + "name": "symfony/process", "version": "v5.4.36", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90" + "url": "https://github.com/symfony/process.git", + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90", - "reference": "2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90", + "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", + "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", "shasum": "" }, "require": { "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16" }, - "conflict": { - "symfony/console": "<4.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" - }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", "autoload": { - "files": [ - "Resources/functions/dump.php" - ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Component\\Process\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -1743,22 +780,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.36" + "source": "https://github.com/symfony/process/tree/v5.4.36" }, "funding": [ { @@ -1774,7 +807,7 @@ "type": "tidelift" } ], - "time": "2024-02-15T11:19:14+00:00" + "time": "2024-02-12T15:49:53+00:00" } ], "packages-dev": [ @@ -2557,43 +1590,99 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause", - "GPL-2.0-only", - "GPL-3.0-only" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v3.2.10" + }, + "time": "2023-07-30T15:38:18+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.19.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], "authors": [ { - "name": "David Grudl", - "homepage": "https://davidgrudl.com" - }, - { - "name": "Nette Community", - "homepage": "https://nette.org/contributors" + "name": "Nikita Popov" } ], - "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", - "homepage": "https://nette.org", + "description": "A PHP parser written in PHP", "keywords": [ - "array", - "core", - "datetime", - "images", - "json", - "nette", - "paginator", - "password", - "slugify", - "string", - "unicode", - "utf-8", - "utility", - "validation" + "parser", + "php" ], "support": { - "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v3.2.10" + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" }, - "time": "2023-07-30T15:38:18+00:00" + "time": "2024-03-17T08:10:35+00:00" }, { "name": "phar-io/manifest", @@ -2849,22 +1938,22 @@ }, { "name": "phpcsstandards/phpcsutils", - "version": "1.0.9", + "version": "1.0.10", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70" + "reference": "51609a5b89f928e0c463d6df80eb38eff1eaf544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/908247bc65010c7b7541a9551e002db12e9dae70", - "reference": "908247bc65010c7b7541a9551e002db12e9dae70", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/51609a5b89f928e0c463d6df80eb38eff1eaf544", + "reference": "51609a5b89f928e0c463d6df80eb38eff1eaf544", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.8.0 || 4.0.x-dev@dev" + "squizlabs/php_codesniffer": "^3.9.0 || 4.0.x-dev@dev" }, "require-dev": { "ext-filter": "*", @@ -2933,7 +2022,7 @@ "type": "open_collective" } ], - "time": "2023-12-08T14:50:00+00:00" + "time": "2024-03-17T23:44:50+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4212,16 +3301,16 @@ }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", "shasum": "" }, "require": { @@ -4233,7 +3322,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -4254,8 +3343,7 @@ "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" }, "funding": [ { @@ -4263,7 +3351,7 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2024-03-14T16:00:52+00:00" }, { "name": "sebastian/type", @@ -4781,6 +3869,82 @@ ], "time": "2024-01-29T20:11:03+00:00" }, + { + "name": "symfony/polyfill-php73", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", + "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, { "name": "symfony/polyfill-php81", "version": "v1.29.0", @@ -4960,6 +4124,67 @@ ], "time": "2024-02-22T18:40:43+00:00" }, + { + "name": "symfony/service-contracts", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/191afdcb5804db960d26d8566b7e9a2843cab3a0", + "reference": "191afdcb5804db960d26d8566b7e9a2843cab3a0", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "suggest": { + "psr/container": "", + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v1.1.2" + }, + "time": "2019-05-28T07:50:59+00:00" + }, { "name": "symfony/string", "version": "v5.4.36", @@ -5236,6 +4461,95 @@ ], "time": "2024-02-21T11:39:05+00:00" }, + { + "name": "symfony/var-dumper", + "version": "v5.4.36", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90", + "reference": "2e9c2b11267119d9c90d6b3fdce5e4e9f15e2e90", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "symfony/console": "<4.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/uid": "^5.1|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "suggest": { + "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", + "ext-intl": "To show region name in time zone dump", + "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v5.4.36" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-15T11:19:14+00:00" + }, { "name": "symfony/yaml", "version": "v5.4.35", @@ -5434,5 +4748,5 @@ "platform-overrides": { "php": "7.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..5645e28d --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,9 @@ + + PHP 7.0 compatibility. + + + + + + + diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..f3e3ad58 --- /dev/null +++ b/rector.php @@ -0,0 +1,20 @@ +withPhpVersion(PhpVersion::PHP_70) + ->withPaths([ + __DIR__ . '/src', + ]) + ->withSets([ + DowngradeLevelSetList::DOWN_TO_PHP_71 + ]) + ->withRules([ + AddVoidReturnTypeWhereNoReturnRector::class, + ]); diff --git a/src/opis/README.md b/src/opis/README.md new file mode 100644 index 00000000..dc255bf5 --- /dev/null +++ b/src/opis/README.md @@ -0,0 +1,8 @@ +## Opis JSON Schema validation for PHP 7.0 + +This is a version of the [Opis](https://opis.io/json-schema/2.x/) JSON Schema validator that is compatible with PHP 7.0. + +Opis seems to be the most complete and well-maintained JSON Schema validator for PHP. However, +Blueprints aim for PHP 7.0+ support and Opis is not compatible with PHP 7.0. Therefore, it was +transpiled to PHP 7.0 with [rector](https://github.com/rectorphp/rector) and provided with this +repo. diff --git a/src/opis/json-schema/LICENSE b/src/opis/json-schema/LICENSE new file mode 100644 index 00000000..2bb9ad24 --- /dev/null +++ b/src/opis/json-schema/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/src/opis/json-schema/NOTICE b/src/opis/json-schema/NOTICE new file mode 100644 index 00000000..6ebfe377 --- /dev/null +++ b/src/opis/json-schema/NOTICE @@ -0,0 +1,9 @@ +Opis Json Schema +Copyright 2018-2021 Zindex Software + +This product includes software developed at +Zindex Software (http://zindex.software). + +This software was originally developed by Marius Sarca and Sorin Sarca +(Copyright 2017-2018). The copyright info was changed with the permission +of the original authors. diff --git a/src/opis/json-schema/README.md b/src/opis/json-schema/README.md new file mode 100644 index 00000000..1ea86f4a --- /dev/null +++ b/src/opis/json-schema/README.md @@ -0,0 +1,65 @@ +Opis JSON Schema +==================== +[![Tests](https://github.com/opis/json-schema/workflows/Tests/badge.svg)](https://github.com/opis/json-schema/actions) +[![Packagist Version](https://img.shields.io/packagist/v/opis/json-schema?label=Version)](https://packagist.org/packages/opis/json-schema) +[![Packagist Downloads](https://img.shields.io/packagist/dt/opis/json-schema?label=Downloads)](https://packagist.org/packages/opis/json-schema) +[![Packagist License](https://img.shields.io/packagist/l/opis/json-schema?color=teal&label=License)](https://packagist.org/packages/opis/json-schema) + +Validate JSON documents +----------- + +**Opis JSON Schema** is a PHP implementation for the [JSON Schema] standard (draft-2020-12, draft-2019-09, draft-07 and draft-06), that +will help you validate all sorts of JSON documents, whether they are configuration files or a set +of data sent to a RESTful API endpoint. + + +**The library's key features:** + +- Supports all keywords from all drafts (draft-2020-12 down to draft-06) +- Support for custom PHP filters using [`$filters` keyword](https://docs.opis.io/json-schema/2.x/filters.html) +- Advanced schema reuse using [`$map` keyword](https://docs.opis.io/json-schema/2.x/mappers.html) +- Intuitive schema composition using [slots](https://docs.opis.io/json-schema/2.x/slots.html) +- Support for absolute & relative [json pointers](https://docs.opis.io/json-schema/2.x/pointers.html) +- Support for [URI templates](https://docs.opis.io/json-schema/2.x/uri-template.html) +- Support for [`$data` keyword](https://docs.opis.io/json-schema/2.x/data-keyword.html) +- Support for [casting](https://docs.opis.io/json-schema/2.x/pragma.html#cast) +- Support for custom [formats](https://docs.opis.io/json-schema/2.x/php-format.html) and [media types](https://docs.opis.io/json-schema/2.x/php-media-type.html) + +### Documentation + +The full documentation for this library can be found [here][documentation]. +We provide documentation for both [JSON Schema] standard itself as well as for +the library's own API. + +### License + +**Opis JSON Schema** is licensed under the [Apache License, Version 2.0][apache_license]. + +### Requirements + +* PHP ^7.4 || ^8.0 + +## Installation + +**Opis JSON Schema** is available on [Packagist] and it can be installed from a +command line interface by using [Composer]. + +```bash +composer require opis/json-schema +``` + +Or you could directly reference it into your `composer.json` file as a dependency + +```json +{ + "require": { + "opis/json-schema": "^2.2" + } +} +``` + +[documentation]: https://opis.io/json-schema +[apache_license]: https://www.apache.org/licenses/LICENSE-2.0 "Apache License" +[Packagist]: https://packagist.org/packages/opis/json-schema "Packagist" +[Composer]: https://getcomposer.org "Composer" +[JSON Schema]: http://json-schema.org/ "JSON Schema" diff --git a/src/opis/json-schema/SECURITY.md b/src/opis/json-schema/SECURITY.md new file mode 100644 index 00000000..90e66c1b --- /dev/null +++ b/src/opis/json-schema/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 2.x | :white_check_mark: | +| 1.x | :white_check_mark: | + +## Reporting a Vulnerability + +Please send an e-mail to authors specified in the [composer.json](https://github.com/opis/json-schema/blob/master/composer.json) file. diff --git a/src/opis/json-schema/autoload.php b/src/opis/json-schema/autoload.php new file mode 100644 index 00000000..649fab8c --- /dev/null +++ b/src/opis/json-schema/autoload.php @@ -0,0 +1,23 @@ + false, + 'allowFormats' => true, + 'allowMappers' => false, + 'allowTemplates' => false, + 'allowGlobals' => false, + 'allowDefaults' => false, + 'allowSlots' => false, + 'allowKeywordValidators' => false, + 'allowPragmas' => false, + 'allowDataKeyword' => false, + 'allowKeywordsAlongsideRef' => false, + 'allowUnevaluated' => true, + 'allowRelativeJsonPointerInRef' => false, + 'allowExclusiveMinMaxAsBool' => false, + 'keepDependenciesKeyword' => false, + 'keepAdditionalItemsKeyword' => false, + ]; + + public function __construct($loader = null, int $max_errors = 1) + { + parent::__construct($loader, $max_errors); + + // Set parser options + $parser = $this->parser(); + foreach (static::COMPLIANT_OPTIONS as $name => $value) { + $parser->setOption($name, $value); + } + } +} diff --git a/src/opis/json-schema/src/ContentEncoding.php b/src/opis/json-schema/src/ContentEncoding.php new file mode 100644 index 00000000..9c8255cf --- /dev/null +++ b/src/opis/json-schema/src/ContentEncoding.php @@ -0,0 +1,28 @@ +args = $args; + } + + public function getArgs(): array { + return $this->args; + } +} diff --git a/src/opis/json-schema/src/Errors/ErrorContainer.php b/src/opis/json-schema/src/Errors/ErrorContainer.php new file mode 100644 index 00000000..48e4b4d9 --- /dev/null +++ b/src/opis/json-schema/src/Errors/ErrorContainer.php @@ -0,0 +1,142 @@ +maxErrors = $max_errors; + } + + /** + * @return int + */ + public function maxErrors(): int { + return $this->maxErrors; + } + + /** + * @param ValidationError $error + * + * @return ErrorContainer + */ + public function add( $error ): self { + $this->errors[] = $error; + + return $this; + } + + /** + * @return ValidationError[] + */ + public function all(): array { + return $this->errors; + } + + /** + * @return ValidationError|null + */ + public function first() { + if ( ! $this->errors ) { + return null; + } + + return reset( $this->errors ); + } + + /** + * @return bool + */ + public function isFull(): bool { + return count( $this->errors ) >= $this->maxErrors; + } + + /** + * @return bool + */ + public function isEmpty(): bool { + return ! $this->errors; + } + + /** + * @inheritDoc + */ + public function count(): int { + return count( $this->errors ); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function current() { + return current( $this->errors ) ?: null; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function next() { + return next( $this->errors ) ?: null; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function key() { + return key( $this->errors ); + } + + /** + * @inheritDoc + */ + public function valid(): bool { + return key( $this->errors ) !== null; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function rewind() { + return reset( $this->errors ) ?: null; + } +} diff --git a/src/opis/json-schema/src/Errors/ErrorFormatter.php b/src/opis/json-schema/src/Errors/ErrorFormatter.php new file mode 100644 index 00000000..f14828b9 --- /dev/null +++ b/src/opis/json-schema/src/Errors/ErrorFormatter.php @@ -0,0 +1,432 @@ +getErrors($error) as $error => $message) { + $key = $key_formatter($error); + + if ($multiple) { + if (!isset($list[$key])) { + $list[$key] = []; + } + $list[$key][] = $formatter($error, $message); + } else { + if (!isset($list[$key])) { + $list[$key] = $formatter($error, $message); + } + } + } + + return $list; + } + + /** + * @param ValidationError|null $error + * @param string $mode One of: flag, basic, detailed or verbose + * @return array + */ + public function formatOutput($error, $mode = "flag"): array + { + if ($error === null) { + return ['valid' => true]; + } + + if ($mode === 'flag') { + return ['valid' => false]; + } + + if ($mode === 'basic') { + return [ + 'valid' => false, + 'errors' => $this->formatFlat($error, [$this, 'formatOutputError']), + ]; + } + + if ($mode === 'detailed' || $mode === 'verbose') { + $isVerbose = $mode === 'verbose'; + + return $this->getNestedErrors($error, function (ValidationError $error, $subErrors = null) use ($isVerbose) { + $info = $this->formatOutputError($error); + + $info['valid'] = false; + + if ($isVerbose) { + $id = $error->schema()->info(); + $id = $id->root() ?? $id->id(); + if ($id) { + $id = rtrim($id, '#'); + } + $info['absoluteKeywordLocation'] = $id . $info['keywordLocation']; + } + + if ($subErrors) { + $info['errors'] = $subErrors; + if (!$isVerbose) { + unset($info['error']); + } + } + + return $info; + } + ); + } + + return ['valid' => false]; + } + + /** + * @param ValidationError $error + * @param ?callable(ValidationError,?array):mixed $formatter + * @return mixed + */ + public function formatNested($error, $formatter = null) + { + if (!$formatter) { + $formatter = function (ValidationError $error, $subErrors = null): array { + $ret = [ + 'message' => $this->formatErrorMessage($error), + 'keyword' => $error->keyword(), + 'path' => $this->formatErrorKey($error), + ]; + + if ($subErrors) { + $ret['errors'] = $subErrors; + } + + return $ret; + }; + } + + return $this->getNestedErrors($error, $formatter); + } + + /** + * @param ValidationError $error + * @param ?callable(ValidationError):mixed $formatter + * @return array + */ + public function formatFlat($error, $formatter = null): array + { + if (!$formatter) { + $formatter = [$this, 'formatErrorMessage']; + } + + $list = []; + + foreach ($this->getFlatErrors($error) as $error) { + $list[] = $formatter($error); + } + + return $list; + } + + /** + * @param ValidationError $error + * @param ?callable(ValidationError):mixed $formatter + * @param ?callable(ValidationError):string $key_formatter + * @return array + */ + public function formatKeyed( + $error, + $formatter = null, + $key_formatter = null + ): array { + if (!$formatter) { + $formatter = [$this, 'formatErrorMessage']; + } + + if (!$key_formatter) { + $key_formatter = [$this, 'formatErrorKey']; + } + + $list = []; + + foreach ($this->getLeafErrors($error) as $error) { + $key = $key_formatter($error); + + if (!isset($list[$key])) { + $list[$key] = []; + } + + $list[$key][] = $formatter($error); + } + + return $list; + } + + /** + * @param ValidationError $error + * @param string|null $message The message to use, if null $error->message() is used + * @return string + */ + public function formatErrorMessage($error, $message = null): string + { + $message = $message ?? $error->message(); + $args = $this->getDefaultArgs($error) + $error->args(); + + if (!$args) { + return $message; + } + + return preg_replace_callback( + '~{([^}]+)}~imu', + static function (array $m) use ($args) { + if (!isset($args[$m[1]])) { + return $m[0]; + } + + $value = $args[$m[1]]; + + if (is_array($value)) { + return implode(', ', $value); + } + + return (string) $value; + }, + $message + ); + } + + /** + * @param \Opis\JsonSchema\Errors\ValidationError $error + */ + public function formatErrorKey($error): string + { + return JsonPointer::pathToString($error->data()->fullPath()); + } + + /** + * @param \Opis\JsonSchema\Errors\ValidationError $error + */ + protected function getDefaultArgs($error): array + { + $data = $error->data(); + $info = $error->schema()->info(); + + $path = $info->path(); + $path[] = $error->keyword(); + + return [ + 'data:type' => $data->type(), + 'data:value' => $data->value(), + 'data:path' => JsonPointer::pathToString($data->fullPath()), + + 'schema:id' => $info->id(), + 'schema:root' => $info->root(), + 'schema:base' => $info->base(), + 'schema:draft' => $info->draft(), + 'schema:keyword' => $error->keyword(), + 'schema:path' => JsonPointer::pathToString($path), + ]; + } + + /** + * @param \Opis\JsonSchema\Errors\ValidationError $error + */ + protected function formatOutputError($error): array + { + $path = $error->schema()->info()->path(); + + $path[] = $error->keyword(); + + return [ + 'keywordLocation' => JsonPointer::pathToFragment($path), + 'instanceLocation' => JsonPointer::pathToFragment($error->data()->fullPath()), + 'error' => $this->formatErrorMessage($error), + ]; + } + + /** + * @param ValidationError $error + * @param callable(ValidationError,?array):mixed $formatter + * @return mixed + */ + protected function getNestedErrors($error, $formatter) + { + if ($subErrors = $error->subErrors()) { + foreach ($subErrors as &$subError) { + $subError = $this->getNestedErrors($subError, $formatter); + unset($subError); + } + } + + return $formatter($error, $subErrors); + } + + /** + * @param ValidationError $error + * @return iterable|ValidationError[] + */ + protected function getFlatErrors($error) + { + yield $error; + + foreach ($error->subErrors() as $subError) { + yield from $this->getFlatErrors($subError); + } + } + + /** + * @param ValidationError $error + * @return iterable|ValidationError[] + */ + protected function getLeafErrors($error) + { + if ($subErrors = $error->subErrors()) { + foreach ($subErrors as $subError) { + yield from $this->getLeafErrors($subError); + } + } else { + yield $error; + } + } + + /** + * @param ValidationError $error + * @return iterable + */ + protected function getErrors($error) + { + $data = $error->schema()->info()->data(); + + $map = null; + $pMap = null; + + if (is_object($data)) { + switch ($error->keyword()) { + case 'required': + if (isset($data->{'$error'}->required) && is_object($data->{'$error'}->required)) { + $e = $data->{'$error'}->required; + $found = false; + foreach ($error->args()['missing'] as $prop) { + if (isset($e->{$prop})) { + yield $error => $e->{$prop}; + $found = true; + } + } + if ($found) { + return; + } + if (isset($e->{'*'})) { + yield $error => $e->{'*'}; + return; + } + unset($e, $found, $prop); + } + break; + case '$filters': + if (($args = $error->args()) && isset($args['args']['$error'])) { + yield $error => $args['args']['$error']; + return; + } + unset($args); + break; + } + + if (isset($data->{'$error'})) { + $map = $data->{'$error'}; + + if (is_string($map)) { + // We have an global error + yield $error => $map; + return; + } + + if (is_object($map)) { + if (isset($map->{$error->keyword()})) { + $pMap = $map->{'*'} ?? null; + $map = $map->{$error->keyword()}; + if (is_string($map)) { + yield $error => $map; + return; + } + } elseif (isset($map->{'*'})) { + yield $error => $map->{'*'}; + return; + } + } + } + } + + if (!is_object($map)) { + $map = null; + } + + $subErrors = $error->subErrors(); + + if (!$subErrors) { + yield $error => $pMap ?? $error->message(); + return; + } + + if (!$map) { + foreach ($subErrors as $subError) { + yield from $this->getErrors($subError); + } + return; + } + + foreach ($subErrors as $subError) { + $path = $subError->data()->path(); + if (count($path) !== 1) { + yield from $this->getErrors($subError); + } else { + $path = $path[0]; + if (isset($map->{$path})) { + yield $subError => $map->{$path}; + } elseif (isset($map->{'*'})) { + yield $subError => $map->{'*'}; + } else { + yield from $this->getErrors($subError); + } + } + } + } +} diff --git a/src/opis/json-schema/src/Errors/ValidationError.php b/src/opis/json-schema/src/Errors/ValidationError.php new file mode 100644 index 00000000..84fd5f7f --- /dev/null +++ b/src/opis/json-schema/src/Errors/ValidationError.php @@ -0,0 +1,111 @@ +keyword = $keyword; + $this->schema = $schema; + $this->data = $data; + $this->message = $message; + $this->args = $args; + $this->subErrors = $subErrors; + } + + public function keyword(): string + { + return $this->keyword; + } + + public function schema(): Schema + { + return $this->schema; + } + + public function data(): DataInfo + { + return $this->data; + } + + public function args(): array + { + return $this->args; + } + + public function message(): string + { + return $this->message; + } + + public function subErrors(): array + { + return $this->subErrors; + } + + public function __toString(): string + { + return $this->message; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/DuplicateSchemaIdException.php b/src/opis/json-schema/src/Exceptions/DuplicateSchemaIdException.php new file mode 100644 index 00000000..a6cc228c --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/DuplicateSchemaIdException.php @@ -0,0 +1,63 @@ +id = $id; + $this->data = $data; + } + + /** + * @return null|object + */ + public function getData() + { + return $this->data; + } + + /** + * @return Uri + */ + public function getId(): Uri + { + return $this->id; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/InvalidKeywordException.php b/src/opis/json-schema/src/Exceptions/InvalidKeywordException.php new file mode 100644 index 00000000..f17bd002 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/InvalidKeywordException.php @@ -0,0 +1,49 @@ +keyword = $keyword; + } + + /** + * @return string + */ + public function keyword(): string + { + return $this->keyword; + } +} diff --git a/src/opis/json-schema/src/Exceptions/InvalidPragmaException.php b/src/opis/json-schema/src/Exceptions/InvalidPragmaException.php new file mode 100644 index 00000000..87f89895 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/InvalidPragmaException.php @@ -0,0 +1,49 @@ +pragma = $pragma; + } + + /** + * @return string + */ + public function pragma(): string + { + return $this->pragma; + } +} diff --git a/src/opis/json-schema/src/Exceptions/ParseException.php b/src/opis/json-schema/src/Exceptions/ParseException.php new file mode 100644 index 00000000..c77c4c71 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/ParseException.php @@ -0,0 +1,48 @@ +info = $info; + } + + /** + * @return SchemaInfo|null + */ + public function schemaInfo() + { + return $this->info; + } +} diff --git a/src/opis/json-schema/src/Exceptions/SchemaException.php b/src/opis/json-schema/src/Exceptions/SchemaException.php new file mode 100644 index 00000000..feedbcd4 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/SchemaException.php @@ -0,0 +1,25 @@ +encoding = $encoding; + } + + /** + * @return string + */ + public function getContentEncoding(): string + { + return $this->encoding; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/UnresolvedContentMediaTypeException.php b/src/opis/json-schema/src/Exceptions/UnresolvedContentMediaTypeException.php new file mode 100644 index 00000000..1f23677c --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/UnresolvedContentMediaTypeException.php @@ -0,0 +1,47 @@ +media = $media; + } + + /** + * @return string + */ + public function getContentMediaType(): string + { + return $this->media; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/UnresolvedException.php b/src/opis/json-schema/src/Exceptions/UnresolvedException.php new file mode 100644 index 00000000..e122cae2 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/UnresolvedException.php @@ -0,0 +1,63 @@ +schema = $schema; + $this->context = $context; + } + + /** + * @return Schema + */ + public function getSchema(): Schema + { + return $this->schema; + } + + /** + * @return ValidationContext + */ + public function getContext(): ValidationContext + { + return $this->context; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/UnresolvedFilterException.php b/src/opis/json-schema/src/Exceptions/UnresolvedFilterException.php new file mode 100644 index 00000000..3acddb7b --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/UnresolvedFilterException.php @@ -0,0 +1,63 @@ +filter = $filter; + $this->type = $type; + } + + /** + * @return string + */ + public function getFilter(): string + { + return $this->filter; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Exceptions/UnresolvedReferenceException.php b/src/opis/json-schema/src/Exceptions/UnresolvedReferenceException.php new file mode 100644 index 00000000..7b9c2f92 --- /dev/null +++ b/src/opis/json-schema/src/Exceptions/UnresolvedReferenceException.php @@ -0,0 +1,48 @@ +ref = $ref; + } + + /** + * @return string + */ + public function getRef(): string + { + return $this->ref; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filter.php b/src/opis/json-schema/src/Filter.php new file mode 100644 index 00000000..90583080 --- /dev/null +++ b/src/opis/json-schema/src/Filter.php @@ -0,0 +1,29 @@ +currentData(); + if (!is_string($ref)) { + return false; + } + + $ref = JsonPointer::parse($ref); + if ($ref === null) { + return false; + } + + return $ref->data($context->rootData(), $context->currentDataPath(), $this) !== $this; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filters/DateTimeFilters.php b/src/opis/json-schema/src/Filters/DateTimeFilters.php new file mode 100644 index 00000000..1df0fe2e --- /dev/null +++ b/src/opis/json-schema/src/Filters/DateTimeFilters.php @@ -0,0 +1,100 @@ += self::CreateDate($min, $tz, false); + } + + public static function MaxDate(string $date, array $args): bool + { + $max = $args['value']; + $tz = $args['timezone'] ?? null; + + return self::CreateDate($date, $tz, false) <= self::CreateDate($max, $tz, false); + } + + public static function NotDate(string $date, array $args): bool + { + $not = $args['value']; + $tz = $args['timezone'] ?? null; + + if (!is_array($not)) { + $not = [$not]; + } + + $date = self::CreateDate($date, $tz, false); + + foreach ($not as $d) { + if ($date == self::CreateDate($d, $tz, false)) { + return false; + } + } + + return true; + } + + public static function MinDateTime(string $date, array $args): bool + { + $min = $args['value']; + $tz = $args['timezone'] ?? null; + + return self::CreateDate($date, $tz) >= self::CreateDate($min, $tz); + } + + public static function MaxDateTime(string $date, array $args): bool + { + $max = $args['value']; + $tz = $args['timezone'] ?? null; + + return self::CreateDate($date, $tz) <= self::CreateDate($max, $tz); + } + + public static function MinTime(string $time, array $args): bool + { + $min = $args['value']; + $prefix = '1970-01-01 '; + + return self::CreateDate($prefix . $time) >= self::CreateDate($prefix . $min); + } + + public static function MaxTime(string $time, array $args): bool + { + $max = $args['value']; + $prefix = '1970-01-01 '; + + return self::CreateDate($prefix . $time) <= self::CreateDate($prefix . $max); + } + + private static function CreateDate(string $value, $timezone = null, bool $time = true): DateTime + { + $date = new DateTime($value, $timezone); + if (!$time) { + return $date->setTime(0, 0, 0, 0); + } + return $date; + } +} diff --git a/src/opis/json-schema/src/Filters/FilterExistsFilter.php b/src/opis/json-schema/src/Filters/FilterExistsFilter.php new file mode 100644 index 00000000..3ce635d3 --- /dev/null +++ b/src/opis/json-schema/src/Filters/FilterExistsFilter.php @@ -0,0 +1,57 @@ +currentData(); + if (!is_string($filter)) { + return false; + } + + $type = null; + if (isset($args['type'])) { + if (!is_string($args['type'])) { + return false; + } + $type = $args['type']; + } + + $resolver = $context->loader()->parser()->getFilterResolver(); + + if (!$resolver) { + return false; + } + + if ($type === null) { + return (bool)$resolver->resolveAll($filter); + } + + return (bool)$resolver->resolve($filter, $type); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filters/FormatExistsFilter.php b/src/opis/json-schema/src/Filters/FormatExistsFilter.php new file mode 100644 index 00000000..7dc7f4e2 --- /dev/null +++ b/src/opis/json-schema/src/Filters/FormatExistsFilter.php @@ -0,0 +1,57 @@ +currentData(); + if (!is_string($format)) { + return false; + } + + $type = null; + if (isset($args['type'])) { + if (!is_string($args['type'])) { + return false; + } + $type = $args['type']; + } + + $resolver = $context->loader()->parser()->getFormatResolver(); + + if (!$resolver) { + return false; + } + + if ($type === null) { + return (bool)$resolver->resolveAll($format); + } + + return (bool)$resolver->resolve($format, $type); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filters/GlobalVarExistsFilter.php b/src/opis/json-schema/src/Filters/GlobalVarExistsFilter.php new file mode 100644 index 00000000..e8393d40 --- /dev/null +++ b/src/opis/json-schema/src/Filters/GlobalVarExistsFilter.php @@ -0,0 +1,50 @@ +currentData(); + + if (!is_string($var)) { + return false; + } + + $globals = $context->globals(); + + if (!array_key_exists($var, $globals)) { + return false; + } + + if (array_key_exists('value', $args)) { + return $globals[$var] == $args['value']; + } + + return true; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filters/SchemaExistsFilter.php b/src/opis/json-schema/src/Filters/SchemaExistsFilter.php new file mode 100644 index 00000000..071d7448 --- /dev/null +++ b/src/opis/json-schema/src/Filters/SchemaExistsFilter.php @@ -0,0 +1,87 @@ +currentData(); + if (!is_string($ref)) { + return false; + } + + if (UriTemplate::isTemplate($ref)) { + if (isset($args['vars']) && is_object($args['vars'])) { + $vars = new VariablesContainer($args['vars'], false); + $vars = $vars->resolve($context->rootData(), $context->currentDataPath()); + if (!is_array($vars)) { + $vars = (array)$vars; + } + $vars += $context->globals(); + } else { + $vars = $context->globals(); + } + + $ref = (new UriTemplate($ref))->resolve($vars); + + unset($vars); + } + + unset($args); + + return $this->refExists($ref, $context, $schema); + } + + /** + * @param string $ref + * @param ValidationContext $context + * @param Schema $schema + * @return bool + */ + protected function refExists($ref, $context, $schema): bool + { + if ($ref === '') { + return false; + } + + if ($ref === '#') { + return true; + } + + $info = $schema->info(); + + $id = Uri::merge($ref, $info->idBaseRoot(), true); + + if ($id === null) { + return false; + } + + return $context->loader()->loadSchemaById($id) !== null; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Filters/SlotExistsFilter.php b/src/opis/json-schema/src/Filters/SlotExistsFilter.php new file mode 100644 index 00000000..af4c8cb6 --- /dev/null +++ b/src/opis/json-schema/src/Filters/SlotExistsFilter.php @@ -0,0 +1,39 @@ +currentData(); + if (!is_string($slot)) { + return false; + } + + return $context->slot($slot) !== null; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Format.php b/src/opis/json-schema/src/Format.php new file mode 100644 index 00000000..577a5349 --- /dev/null +++ b/src/opis/json-schema/src/Format.php @@ -0,0 +1,27 @@ +.+)@(?.+)$/u', $value, $m)) { + return false; + } + + $m['name'] = $idn($m['name']); + if ($m['name'] === null) { + return false; + } + + $m['domain'] = $idn($m['domain']); + if ($m['domain'] === null) { + return false; + } + + $value = $m['name'] . '@' . $m['domain']; + } + + return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * @return callable|null + */ + public static function idn() + { + if (static::$idn === false) { + if (function_exists('idn_to_ascii')) { + static::$idn = static function (string $value) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $value = idn_to_ascii($value, 0, INTL_IDNA_VARIANT_UTS46); + + return is_string($value) ? $value : null; + }; + } else { + static::$idn = null; + } + } + + return static::$idn; + } +} diff --git a/src/opis/json-schema/src/Formats/MiscFormats.php b/src/opis/json-schema/src/Formats/MiscFormats.php new file mode 100644 index 00000000..a5098907 --- /dev/null +++ b/src/opis/json-schema/src/Formats/MiscFormats.php @@ -0,0 +1,59 @@ +isAbsolute(); + } + + /** + * @param string $value + * @return bool + */ + public static function uriReference($value): bool + { + if ($value === '') { + return true; + } + + return Uri::parse($value) !== null; + } + + /** + * @param string $value + * @return bool + */ + public static function uriTemplate($value): bool + { + if ($value === '') { + return true; + } + + if (UriTemplate::isTemplate($value)) { + return true; + } + + return Uri::parse($value) !== null; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Helper.php b/src/opis/json-schema/src/Helper.php new file mode 100644 index 00000000..7ba5578b --- /dev/null +++ b/src/opis/json-schema/src/Helper.php @@ -0,0 +1,351 @@ + 'number']; + + /** @var string[] */ + const PHP_TYPE_MAP = [ + 'NULL' => 'null', + 'integer' => 'integer', + 'double' => 'number', + 'boolean' => 'boolean', + 'array' => 'array', + 'object' => 'object', + 'string' => 'string', + ]; + + /** + * @param string $type + * @return bool + */ + public static function isValidJsonType(string $type): bool + { + if (isset(self::JSON_SUBTYPES[$type])) { + return true; + } + + return in_array($type, self::JSON_TYPES, true); + } + + /** + * @param string $type + * @return null|string + */ + public static function getJsonSuperType(string $type) + { + return self::JSON_SUBTYPES[$type] ?? null; + } + + /** + * @param mixed $value + * @param bool $use_subtypes + * @return null|string + */ + public static function getJsonType($value, bool $use_subtypes = true) + { + $type = self::PHP_TYPE_MAP[gettype($value)] ?? null; + if ($type === null) { + return null; + } elseif ($type === 'array') { + return self::isIndexedArray($value) ? 'array' : null; + } + + if ($use_subtypes) { + if ($type === 'number' && self::isMultipleOf($value, 1)) { + return 'integer'; + } + } elseif ($type === 'integer') { + return 'number'; + } + + return $type; + } + + /** + * @param string $type + * @param string|string[] $allowed + * @return bool + */ + public static function jsonTypeMatches(string $type, $allowed): bool + { + if (!$allowed) { + return false; + } + + if (is_string($allowed)) { + if ($type === $allowed) { + return true; + } + + return $allowed === self::getJsonSuperType($type); + } + + if (is_array($allowed)) { + if (in_array($type, $allowed, true)) { + return true; + } + + if ($type = self::getJsonSuperType($type)) { + return in_array($type, $allowed, true); + } + } + + return false; + } + + /** + * @param mixed $value + * @param string|string[] $type + * @return bool + */ + public static function valueIsOfJsonType($value, $type): bool + { + $t = self::getJsonType($value); + if ($t === null) { + return false; + } + + return self::jsonTypeMatches($t, $type); + } + + /** + * @param array $array + * @return bool + */ + public static function isIndexedArray(array $array): bool + { + for ($i = 0, $max = count($array); $i < $max; $i++) { + if (!array_key_exists($i, $array)) { + return false; + } + } + + return true; + } + + /** + * Converts assoc-arrays to objects (recursive) + * @param scalar|object|array|null $schema + * @return scalar|object|array|null + */ + public static function convertAssocArrayToObject($schema) + { + if (is_null($schema) || is_scalar($schema)) { + return $schema; + } + + $keepArray = is_array($schema) && self::isIndexedArray($schema); + + $data = []; + + foreach ($schema as $key => $value) { + $data[$key] = is_array($value) || is_object($value) ? self::convertAssocArrayToObject($value) : $value; + } + + return $keepArray ? $data : (object) $data; + } + + /** + * @param mixed $a + * @param mixed $b + * @return bool + */ + public static function equals($a, $b): bool + { + if ($a === $b) { + return true; + } + + $type = self::getJsonType($a, false); + if ($type === null || $type !== self::getJsonType($b, false)) { + return false; + } + + if ($type === 'number') { + return $a == $b; + } + + if ($type === "array") { + $count = count($a); + if ($count !== count($b)) { + return false; + } + + for ($i = 0; $i < $count; $i++) { + if (!array_key_exists($i, $a) || !array_key_exists($i, $b)) { + return false; + } + if (!self::equals($a[$i], $b[$i])) { + return false; + } + } + + return true; + } + + if ($type === "object") { + $a = get_object_vars($a); + if ($a === null) { + return false; + } + + $b = get_object_vars($b); + if ($b === null) { + return false; + } + + if (count($a) !== count($b)) { + return false; + } + + foreach ($a as $prop => $value) { + if (!array_key_exists($prop, $b)) { + return false; + } + if (!self::equals($value, $b[$prop])) { + return false; + } + } + + return true; + } + + return false; + } + + /** + * @param $number + * @param $divisor + * @param int $scale + * @return bool + */ + public static function isMultipleOf($number, $divisor, int $scale = 14): bool + { + static $bcMath = null; + if ($bcMath === null) { + $bcMath = extension_loaded('bcmath'); + } + if ($divisor == 0) { + return $number == 0; + } + + if ($bcMath) { + $number = number_format($number, $scale, '.', ''); + $divisor = number_format($divisor, $scale, '.', ''); + + /** @noinspection PhpComposerExtensionStubsInspection */ + $x = bcdiv($number, $divisor, 0); + /** @noinspection PhpComposerExtensionStubsInspection */ + $x = bcmul($divisor, $x, $scale); + /** @noinspection PhpComposerExtensionStubsInspection */ + $x = bcsub($number, $x, $scale); + + /** @noinspection PhpComposerExtensionStubsInspection */ + return 0 === bccomp($x, 0, $scale); + } + + $div = $number / $divisor; + + return $div == (int)$div; + } + + /** + * @param $value + * @return mixed + */ + public static function cloneValue($value) + { + if ($value === null || is_scalar($value)) { + return $value; + } + + if (is_array($value)) { + return array_map(self::class . '::cloneValue', $value); + } + + if (is_object($value)) { + return (object)array_map(self::class . '::cloneValue', get_object_vars($value)); + } + + return null; + } + + /** + * @param string $pattern + * @return bool + */ + public static function isValidPattern(string $pattern): bool + { + if (strpos($pattern, '\Z') !== false) { + return false; + } + + return @preg_match("\x07{$pattern}\x07u", '') !== false; + } + + /** + * @param string $pattern + * @return string + */ + public static function patternToRegex(string $pattern): string + { + return "\x07{$pattern}\x07uD"; + } + + /** + * @param mixed $data + * @return mixed + */ + public static function toJSON($data) + { + if ($data === null || is_scalar($data)) { + return $data; + } + + $map = []; + + $isArray = true; + $index = 0; + foreach ($data as $key => $value) { + $map[$key] = self::toJSON($value); + if ($isArray) { + if ($index !== $key) { + $isArray = false; + } else { + $index++; + } + } + } + + if ($isArray) { + if (!$map && is_object($data)) { + return (object) $map; + } + return $map; + } + + return (object) $map; + } +} diff --git a/src/opis/json-schema/src/Info/DataInfo.php b/src/opis/json-schema/src/Info/DataInfo.php new file mode 100644 index 00000000..d2560ab0 --- /dev/null +++ b/src/opis/json-schema/src/Info/DataInfo.php @@ -0,0 +1,120 @@ +value = $value; + $this->type = $type; + $this->root = $root; + $this->path = $path; + $this->parent = $parent; + } + + public function value() + { + return $this->value; + } + + public function type() + { + return $this->type; + } + + public function root() + { + return $this->root; + } + + /** + * @return int[]|string[] + */ + public function path(): array + { + return $this->path; + } + + public function parent() + { + return $this->parent; + } + + /** + * @return int[]|string[] + */ + public function fullPath(): array + { + if ($this->parent === null) { + return $this->path; + } + + if ($this->fullPath === null) { + $this->fullPath = array_merge($this->parent->fullPath(), $this->path); + } + + return $this->fullPath; + } + + /** + * @param ValidationContext $context + * @return static + */ + public static function fromContext($context): self + { + if ($parent = $context->parent()) { + $parent = self::fromContext($parent); + } + + return new self($context->currentData(), $context->currentDataType(), $context->rootData(), + $context->currentDataPath(), $parent); + } +} diff --git a/src/opis/json-schema/src/Info/SchemaInfo.php b/src/opis/json-schema/src/Info/SchemaInfo.php new file mode 100644 index 00000000..fb381403 --- /dev/null +++ b/src/opis/json-schema/src/Info/SchemaInfo.php @@ -0,0 +1,129 @@ +data = $data; + $this->id = $id; + $this->root = $root; + $this->base = $base; + $this->path = $path; + $this->draft = $draft; + } + + public function id() + { + return $this->id; + } + + public function root() + { + return $this->root; + } + + public function base() + { + return $this->base; + } + + public function draft() + { + return $this->draft; + } + + public function data() + { + return $this->data; + } + + public function path(): array + { + return $this->path; + } + + /** + * Returns first non-null property: id, base or root + * @return Uri|null + */ + public function idBaseRoot() + { + return $this->id ?? $this->base ?? $this->root; + } + + public function isBoolean(): bool + { + return is_bool($this->data); + } + + public function isObject(): bool + { + return is_object($this->data); + } + + public function isDocumentRoot(): bool + { + return $this->id && !$this->root && !$this->base; + } +} diff --git a/src/opis/json-schema/src/JsonPointer.php b/src/opis/json-schema/src/JsonPointer.php new file mode 100644 index 00000000..f9baa545 --- /dev/null +++ b/src/opis/json-schema/src/JsonPointer.php @@ -0,0 +1,439 @@ +0|[1-9][0-9]*)(?(?:\+|-)(?:0|[1-9][0-9]*))?)?(?(?:/[^/#]*)*)(?#)?$~'; + + /** @var string */ + const UNESCAPED = '/~([^01]|$)/'; + + /** + * @var int + */ + protected $level = -1; + + /** + * @var int + */ + protected $shift = 0; + + /** + * @var bool + */ + protected $fragment = false; + + /** @var string[]|int[] */ + protected $path; + + /** + * @var string|null + */ + protected $str; + + final protected function __construct(array $path, int $level = -1, int $shift = 0, bool $fragment = false) + { + $this->path = $path; + $this->level = $level < 0 ? -1 : $level; + $this->shift = $shift; + $this->fragment = $level >= 0 && $fragment; + } + + public function isRelative(): bool + { + return $this->level >= 0; + } + + public function isAbsolute(): bool + { + return $this->level < 0; + } + + public function level(): int + { + return $this->level; + } + + public function shift(): int + { + return $this->shift; + } + + /** + * @return string[] + */ + public function path(): array + { + return $this->path; + } + + /** + * @return bool + */ + public function hasFragment(): bool + { + return $this->fragment; + } + + /** + * @return string + */ + public function __toString(): string + { + if ($this->str === null) { + if ($this->level >= 0) { + $this->str = (string)$this->level; + + if ($this->shift !== 0) { + if ($this->shift > 0) { + $this->str .= '+'; + } + $this->str .= $this->shift; + } + + if ($this->path) { + $this->str .= '/'; + $this->str .= implode('/', self::encodePath($this->path)); + } + + if ($this->fragment) { + $this->str .= '#'; + } + } else { + $this->str = '/'; + $this->str .= implode('/', self::encodePath($this->path)); + } + } + + return $this->str; + } + + /** + * @param $data + * @param array|null $path + * @param null $default + * @return mixed + */ + public function data($data, $path = null, $default = null) + { + if ($this->level < 0) { + return self::getData($data, $this->path, false, $default); + } + + if ($path !== null) { + $path = $this->absolutePath($path); + } + + if ($path === null) { + return $default; + } + + return self::getData($data, $path, $this->fragment, $default); + } + + /** + * @param array $path + * @return array|null + */ + public function absolutePath($path = []) + { + if ($this->level < 0) { + // Absolute pointer + return $this->path; + } + + if ($this->level === 0) { + if ($this->shift && !$this->handleShift($path)) { + return null; + } + return $this->path ? array_merge($path, $this->path) : $path; + } + + $count = count($path); + if ($count === $this->level) { + if ($this->shift) { + return null; + } + return $this->path; + } + + if ($count > $this->level) { + $count -= $this->level; + + /** @var array $path */ + $path = array_slice($path, 0, $count); + + if ($this->shift && !$this->handleShift($path, $count)) { + return null; + } + + return $this->path ? array_merge($path, $this->path) : $path; + } + + return null; + } + + /** + * @param mixed[] $path + * @param int|null $count + */ + protected function handleShift(&$path, $count = null): bool + { + if (!$path) { + return false; + } + + $count = $count ?? count($path); + + $last = $path[$count - 1]; + + if (is_string($last) && preg_match('/^[1-9]\d*$/', $last)) { + $last = (int) $last; + } + + if (!is_int($last)) { + return false; + } + + $path[$count - 1] = $last + $this->shift; + + return true; + } + + /** + * @param string $pointer + * @param bool $decode + */ + public static function parse($pointer, $decode = true) + { + if ($pointer === '' || !preg_match(self::PATTERN, $pointer, $m)) { + // Not a pointer + return null; + } + + $pointer = $m['pointer'] ?? null; + + // Check if the pointer is escaped + if ($decode && $pointer && preg_match(self::UNESCAPED, $pointer)) { + // Invalid pointer + return null; + } + + $level = isset($m['level']) && $m['level'] !== '' + ? (int)$m['level'] + : -1; + + $shift = 0; + if ($level >= 0 && isset($m['shift']) && $m['shift'] !== '') { + $shift = (int) $m['shift']; + } + + $fragment = isset($m['fragment']) && $m['fragment'] === '#'; + unset($m); + + if ($fragment && $level < 0) { + return null; + } + + if ($pointer === '') { + $pointer = null; + } elseif ($pointer !== null) { + // Remove leading slash + $pointer = substr($pointer, 1); + + if ($pointer !== '') { + $pointer = self::decodePath(explode('/', $pointer)); + } else { + $pointer = null; + } + } + + return new self($pointer ?? [], $level, $shift, $fragment); + } + + /** + * @param $data + * @param array|null $path + * @param bool $fragment + * @param null $default + * @return mixed + */ + public static function getData($data, $path = null, $fragment = false, $default = null) + { + if ($path === null) { + return $default; + } + + if (!$path) { + return $fragment ? $default : $data; + } + + if ($fragment) { + return end($path); + } + + foreach ($path as $key) { + if (is_array($data)) { + if (!array_key_exists($key, $data)) { + return $default; + } + $data = $data[$key]; + } elseif (is_object($data)) { + if (!property_exists($data, $key)) { + return $default; + } + $data = $data->{$key}; + } else { + return $default; + } + } + + return $data; + } + + /** + * @param string|string[] $path + * @return string|string[] + */ + public static function encodePath($path) + { + $path = str_replace('~', '~0', $path); + $path = str_replace('/', '~1', $path); + + if (is_array($path)) { + return array_map('rawurlencode', $path); + } + + return rawurlencode($path); + } + + /** + * @param string|string[] $path + * @return string|string[] + */ + public static function decodePath($path) + { + if (is_array($path)) { + $path = array_map('rawurldecode', $path); + } else { + $path = rawurldecode($path); + } + + $path = str_replace('~1', '/', $path); + $path = str_replace('~0', '~', $path); + + return $path; + } + + /** + * @param array $path + * @return string + */ + public static function pathToString($path): string + { + if (!$path) { + return '/'; + } + + return '/' . implode('/', self::encodePath($path)); + } + + /** + * @param array $path + * @return string + */ + public static function pathToFragment($path): string + { + if (!$path) { + return '#'; + } + + return '#/' . implode('/', self::encodePath($path)); + } + + /** + * @param string $pointer + * @return bool + */ + public static function isAbsolutePointer($pointer): bool + { + if ($pointer === '/') { + return true; + } + + if (!preg_match(self::PATTERN, $pointer, $m)) { + return false; + } + + if (isset($m['fragment']) || isset($m['level']) && $m['level'] !== '') { + return false; + } + + if (!isset($m['pointer']) || $m['pointer'] === '') { + return true; + } + + return !preg_match(self::UNESCAPED, $m['pointer']); + } + + /** + * @param string $pointer + * @return bool + */ + public static function isRelativePointer($pointer): bool + { + if ($pointer === '') { + return false; + } + + if (!preg_match(self::PATTERN, $pointer, $m)) { + return false; + } + + if (!isset($m['level']) || $m['level'] === '' || (int)$m['level'] < 0) { + return false; + } + + if (!isset($m['pointer']) || $m['pointer'] === '') { + return true; + } + + return !preg_match(self::UNESCAPED, $m['pointer']); + } + + /** + * @param mixed[] $path + */ + public static function createAbsolute($path): self + { + return new self($path, -1, 0, false); + } + + /** + * @param int $level + * @param mixed[] $path + * @param int $shift + * @param bool $fragment + */ + public static function createRelative($level, $path = [], $shift = 0, $fragment = false): self + { + return new self($path, $level, $shift, $fragment); + } +} diff --git a/src/opis/json-schema/src/Keyword.php b/src/opis/json-schema/src/Keyword.php new file mode 100644 index 00000000..3fd9acb0 --- /dev/null +++ b/src/opis/json-schema/src/Keyword.php @@ -0,0 +1,30 @@ +next; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\KeywordValidator|null $next + */ + public function setNext($next): KeywordValidator + { + $this->next = $next; + + return $this; + } +} diff --git a/src/opis/json-schema/src/KeywordValidators/CallbackKeywordValidator.php b/src/opis/json-schema/src/KeywordValidators/CallbackKeywordValidator.php new file mode 100644 index 00000000..4d20d6ea --- /dev/null +++ b/src/opis/json-schema/src/KeywordValidators/CallbackKeywordValidator.php @@ -0,0 +1,61 @@ +callback = $callback; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + return ($this->callback)($context); + } + + /** + * @inheritDoc + */ + public function next() + { + return null; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\KeywordValidator|null $next + */ + public function setNext($next): KeywordValidator + { + return $this; + } +} diff --git a/src/opis/json-schema/src/KeywordValidators/PragmaKeywordValidator.php b/src/opis/json-schema/src/KeywordValidators/PragmaKeywordValidator.php new file mode 100644 index 00000000..5613c92a --- /dev/null +++ b/src/opis/json-schema/src/KeywordValidators/PragmaKeywordValidator.php @@ -0,0 +1,64 @@ +pragmas = $pragmas; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + if (!$this->next) { + return null; + } + + if (!$this->pragmas) { + return $this->next->validate($context); + } + + $data = []; + + foreach ($this->pragmas as $key => $handler) { + $data[$key] = $handler->enter($context); + } + + $error = $this->next->validate($context); + + foreach (array_reverse($this->pragmas, true) as $key => $handler) { + $handler->leave($context, $data[$key] ?? null); + } + + return $error; + } +} diff --git a/src/opis/json-schema/src/Keywords/AbstractRefKeyword.php b/src/opis/json-schema/src/Keywords/AbstractRefKeyword.php new file mode 100644 index 00000000..29ea1674 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/AbstractRefKeyword.php @@ -0,0 +1,156 @@ +mapper = $mapper; + $this->globals = $globals; + $this->slots = $slots; + $this->keyword = $keyword; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($error = $this->doValidate($context, $schema)) { + $uri = $this->lastRefUri; + $this->lastRefUri = null; + + return $this->error($schema, $context, $this->keyword, 'The data must match {keyword}', [ + 'keyword' => $this->keyword, + 'uri' => (string) $uri, + ], $error); + } + + $this->lastRefUri = null; + + return null; + } + + + /** + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + abstract protected function doValidate($context, $schema); + + /** + * @param \Opis\JsonSchema\Uri|null $uri + */ + protected function setLastRefUri($uri) + { + $this->lastRefUri = $uri; + } + + /** + * @param \Opis\JsonSchema\Schema $schema + */ + protected function setLastRefSchema($schema) + { + $info = $schema->info(); + + if ($info->id()) { + $this->lastRefUri = $info->id(); + } else { + $this->lastRefUri = Uri::merge(JsonPointer::pathToFragment($info->path()), $info->idBaseRoot()); + } + } + + /** + * @param ValidationContext $context + * @param Schema $schema + * @return ValidationContext + */ + protected function createContext($context, $schema): ValidationContext + { + return $context->create($schema, $this->mapper, $this->globals, $this->slots); + } + + /** + * @param SchemaLoader $repo + * @param JsonPointer $pointer + * @param Uri $base + * @param array|null $path + * @return null|Schema + */ + protected function resolvePointer($repo, $pointer, + $base, $path = null) + { + if ($pointer->isAbsolute()) { + $path = (string)$pointer; + } else { + if ($pointer->hasFragment()) { + return null; + } + + $path = $path ? $pointer->absolutePath($path) : $pointer->path(); + if ($path === null) { + return null; + } + + $path = JsonPointer::pathToString($path); + } + + return $repo->loadSchemaById(Uri::merge('#' . $path, $base)); + } +} diff --git a/src/opis/json-schema/src/Keywords/AdditionalItemsKeyword.php b/src/opis/json-schema/src/Keywords/AdditionalItemsKeyword.php new file mode 100644 index 00000000..a3422b29 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/AdditionalItemsKeyword.php @@ -0,0 +1,102 @@ +value = $value; + $this->index = $startIndex; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->value === true) { + $context->markAllAsEvaluatedItems(); + return null; + } + + $data = $context->currentData(); + $count = count($data); + + if ($this->index >= $count) { + return null; + } + + if ($this->value === false) { + return $this->error($schema, $context, 'additionalItems', 'Array should not have additional items', [ + 'index' => $this->index, + ]); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $object = $this->createArrayObject($context); + + $error = $this->validateIterableData($schema, $this->value, $context, $this->indexes($this->index, $count), + 'additionalItems', 'All additional array items must match schema', [], $object); + + if ($object && $object->count()) { + $context->addEvaluatedItems($object->getArrayCopy()); + } + + return $error; + } + + /** + * @param int $start + * @param int $max + * @return iterable|int[] + */ + protected function indexes($start, $max) + { + for ($i = $start; $i < $max; $i++) { + yield $i; + } + } +} diff --git a/src/opis/json-schema/src/Keywords/AdditionalPropertiesKeyword.php b/src/opis/json-schema/src/Keywords/AdditionalPropertiesKeyword.php new file mode 100644 index 00000000..71b7db6f --- /dev/null +++ b/src/opis/json-schema/src/Keywords/AdditionalPropertiesKeyword.php @@ -0,0 +1,85 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->value === true) { + $context->markAllAsEvaluatedProperties(); + return null; + } + + $props = $context->getUncheckedProperties(); + + if (!$props) { + return null; + } + + if ($this->value === false) { + return $this->error($schema, $context, + 'additionalProperties', 'Additional object properties are not allowed: {properties}', [ + 'properties' => $props + ]); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $object = $this->createArrayObject($context); + + $error = $this->validateIterableData($schema, $this->value, $context, $props, + 'additionalProperties', 'All additional object properties must match schema: {properties}', [ + 'properties' => $props + ], $object); + + if ($object && $object->count()) { + $context->addEvaluatedProperties($object->getArrayCopy()); + } + + return $error; + } +} diff --git a/src/opis/json-schema/src/Keywords/AllOfKeyword.php b/src/opis/json-schema/src/Keywords/AllOfKeyword.php new file mode 100644 index 00000000..298d6405 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/AllOfKeyword.php @@ -0,0 +1,80 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $object = $this->createArrayObject($context); + + foreach ($this->value as $index => $value) { + if ($value === true) { + continue; + } + + if ($value === false) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'allOf', 'The data should match all schemas', [ + 'index' => $index, + ]); + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$index] = $context->loader()->loadObjectSchema($value); + } + + if ($error = $context->validateSchemaWithoutEvaluated($value, null, false, $object)) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'allOf', 'The data should match all schemas', [ + 'index' => $index, + ], $error); + } + } + + $this->addEvaluatedFromArrayObject($object, $context); + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/AnyOfKeyword.php b/src/opis/json-schema/src/Keywords/AnyOfKeyword.php new file mode 100644 index 00000000..855594e6 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/AnyOfKeyword.php @@ -0,0 +1,99 @@ +value = $value; + $this->alwaysValid = $alwaysValid; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $object = $this->createArrayObject($context); + if ($this->alwaysValid && !$object) { + return null; + } + + $errors = []; + $ok = false; + + foreach ($this->value as $index => $value) { + if ($value === true) { + $ok = true; + if ($object) { + continue; + } + return null; + } + + if ($value === false) { + continue; + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$index] = $context->loader()->loadObjectSchema($value); + } + + if ($error = $context->validateSchemaWithoutEvaluated($value, null, false, $object)) { + $errors[] = $error; + continue; + } + + if (!$object) { + return null; + } + $ok = true; + } + + $this->addEvaluatedFromArrayObject($object, $context); + + if ($ok) { + return null; + } + + return $this->error($schema, $context, 'anyOf', 'The data should match at least one schema', [], $errors); + } +} diff --git a/src/opis/json-schema/src/Keywords/ConstDataKeyword.php b/src/opis/json-schema/src/Keywords/ConstDataKeyword.php new file mode 100644 index 00000000..3e6b8f11 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ConstDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(null); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $value = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + if ($value === $this) { + return $this->error($schema, $context, 'const', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->const = $value; + $ret = parent::validate($context, $schema); + $this->const = null; + + return $ret; + } +} diff --git a/src/opis/json-schema/src/Keywords/ConstKeyword.php b/src/opis/json-schema/src/Keywords/ConstKeyword.php new file mode 100644 index 00000000..7cf7ff18 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ConstKeyword.php @@ -0,0 +1,53 @@ +const = $const; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (Helper::equals($this->const, $context->currentData())) { + return null; + } + + return $this->error($schema, $context, 'const', 'The data must must match the const value', [ + 'const' => $this->const + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/ContainsKeyword.php b/src/opis/json-schema/src/Keywords/ContainsKeyword.php new file mode 100644 index 00000000..76223426 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ContainsKeyword.php @@ -0,0 +1,151 @@ +value = $value; + $this->min = $min; + $this->max = $max; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + $count = count($data); + + $context->markAllAsEvaluatedItems(); + + if ($this->min > $count) { + return $this->error($schema, $context, 'minContains', 'Array must have at least {min} items', [ + 'min' => $this->min, + 'count' => $count, + ]); + } + + $isMaxNull = $this->max === null; + + if ($this->value === true) { + if ($count) { + if (!$isMaxNull && $count > $this->max) { + return $this->error($schema, $context, 'maxContains', 'Array must have at most {max} items', [ + 'max' => $this->max, + 'count' => $count, + ]); + } + return null; + } + + return $this->error($schema, $context, 'contains', 'Array must not be empty'); + } + + if ($this->value === false) { + return $this->error($schema, $context, 'contains', 'Any array is invalid'); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $errors = []; + $valid = 0; + + $isMinNull = $this->min === null; + + if ($isMaxNull && $isMinNull) { + foreach ($data as $key => $item) { + $context->pushDataPath($key); + $error = $this->value->validate($context); + $context->popDataPath(); + if ($error) { + $errors[] = $error; + } else { + return null; + } + } + + return $this->error($schema, $context, 'contains', 'At least one array item must match schema', [], + $errors); + } + + foreach ($data as $key => $item) { + $context->pushDataPath($key); + $error = $this->value->validate($context); + $context->popDataPath(); + + if ($error) { + $errors[] = $error; + } else { + $valid++; + } + } + + if (!$isMinNull && $valid < $this->min) { + return $this->error($schema, $context, 'minContains', 'At least {min} array items must match schema', [ + 'min' => $this->min, + 'count' => $valid, + ]); + } + + if (!$isMaxNull && $valid > $this->max) { + return $this->error($schema, $context, 'maxContains', 'At most {max} array items must match schema', [ + 'max' => $this->max, + 'count' => $valid, + ]); + } + + if ($valid) { + return null; + } + + return $this->error($schema, $context, 'contains', 'At least one array item must match schema', [], + $errors); + } +} diff --git a/src/opis/json-schema/src/Keywords/ContentEncodingKeyword.php b/src/opis/json-schema/src/Keywords/ContentEncodingKeyword.php new file mode 100644 index 00000000..ca74a628 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ContentEncodingKeyword.php @@ -0,0 +1,85 @@ +name = $name; + $this->resolver = $resolver; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (!$this->resolver) { + return null; + } + + if ($this->encoding === false) { + $this->encoding = $this->resolver->resolve($this->name); + } + + if ($this->encoding === null) { + throw new UnresolvedContentEncodingException($this->name, $schema, $context); + } + + $result = $this->encoding instanceof ContentEncoding + ? $this->encoding->decode($context->currentData(), $this->name) + : ($this->encoding)($context->currentData(), $this->name); + + if ($result === null) { + return $this->error($schema, $context, 'contentEncoding', "The value must be encoded as '{encoding}'", [ + 'encoding' => $this->name, + ]); + } + + $context->setDecodedContent($result); + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/ContentMediaTypeKeyword.php b/src/opis/json-schema/src/Keywords/ContentMediaTypeKeyword.php new file mode 100644 index 00000000..411e75b7 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ContentMediaTypeKeyword.php @@ -0,0 +1,91 @@ +name = $name; + $this->resolver = $resolver; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (!$this->resolver) { + return null; + } + + if ($this->media === false) { + $this->media = $this->resolver->resolve($this->name); + } + + if ($this->media === null) { + throw new UnresolvedContentMediaTypeException($this->name, $schema, $context); + } + + $data = $context->getDecodedContent(); + + $ok = $this->media instanceof ContentMediaType + ? $this->media->validate($data, $this->name) + : ($this->media)($data, $this->name); + if ($ok) { + return null; + } + + unset($data); + + return $this->error($schema, $context, 'contentMediaType', "The media type of the data must be '{media}'", [ + 'media' => $this->name, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/ContentSchemaKeyword.php b/src/opis/json-schema/src/Keywords/ContentSchemaKeyword.php new file mode 100644 index 00000000..2ee3abea --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ContentSchemaKeyword.php @@ -0,0 +1,66 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = json_decode($context->getDecodedContent(), false); + + if ($error = json_last_error() !== JSON_ERROR_NONE) { + $message = json_last_error_msg(); + + return $this->error($schema, $context, 'contentSchema', "Invalid JSON content: {message}", [ + 'error' => $error, + 'message' => $message, + ]); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + if ($error = $this->value->validate($context->newInstance($data, $schema))) { + return $this->error($schema, $context, 'contentSchema', "The JSON content must match schema", [], $error); + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/DefaultKeyword.php b/src/opis/json-schema/src/Keywords/DefaultKeyword.php new file mode 100644 index 00000000..85cf6397 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/DefaultKeyword.php @@ -0,0 +1,58 @@ +defaults = $defaults; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + + if (is_object($data)) { + foreach ($this->defaults as $name => $value) { + if (!property_exists($data, $name)) { + $data->{$name} = Helper::cloneValue($value); + } + } + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/DependenciesKeyword.php b/src/opis/json-schema/src/Keywords/DependenciesKeyword.php new file mode 100644 index 00000000..82bc778b --- /dev/null +++ b/src/opis/json-schema/src/Keywords/DependenciesKeyword.php @@ -0,0 +1,97 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + $object = $this->createArrayObject($context); + + foreach ($this->value as $name => $value) { + if ($value === true || !property_exists($data, $name)) { + continue; + } + + if ($value === false) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'dependencies', "Property '{property}' is not allowed", [ + 'property' => $name, + ]); + } + + if (is_array($value)) { + foreach ($value as $prop) { + if (!property_exists($data, $prop)) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'dependencies', + "Property '{missing}' property is required by property '{property}'", [ + 'property' => $name, + 'missing' => $prop, + ]); + } + } + + continue; + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$name] = $context->loader()->loadObjectSchema($value); + } + + if ($error = $context->validateSchemaWithoutEvaluated($value, null, false, $object)) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'dependencies', + "The object must match dependency schema defined on property '{property}'", [ + 'property' => $name, + ], $error); + } + } + + $this->addEvaluatedFromArrayObject($object, $context); + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/DependentRequiredKeyword.php b/src/opis/json-schema/src/Keywords/DependentRequiredKeyword.php new file mode 100644 index 00000000..0b64822b --- /dev/null +++ b/src/opis/json-schema/src/Keywords/DependentRequiredKeyword.php @@ -0,0 +1,68 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + + foreach ($this->value as $name => $value) { + if (!property_exists($data, $name)) { + continue; + } + foreach ($value as $prop) { + if (!property_exists($data, $prop)) { + return $this->error($schema, $context, 'dependentRequired', + "'{$prop}' property is required by '{$name}' property", [ + 'property' => $name, + 'missing' => $prop, + ]); + } + } + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/DependentSchemasKeyword.php b/src/opis/json-schema/src/Keywords/DependentSchemasKeyword.php new file mode 100644 index 00000000..4d44b23b --- /dev/null +++ b/src/opis/json-schema/src/Keywords/DependentSchemasKeyword.php @@ -0,0 +1,84 @@ +value = (array)$value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + $object = $this->createArrayObject($context); + + foreach ($this->value as $name => $value) { + if ($value === true || !property_exists($data, $name)) { + continue; + } + + if ($value === false) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'dependentSchemas', "'{$name}' property is not allowed", [ + 'property' => $name, + ]); + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$name] = $context->loader()->loadObjectSchema($value); + } + + if ($error = $context->validateSchemaWithoutEvaluated($value, null, false, $object)) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'dependentSchemas', + "The object must match dependency schema defined on property '{$name}'", [ + 'property' => $name, + ], $error); + } + } + + $this->addEvaluatedFromArrayObject($object, $context); + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/EnumDataKeyword.php b/src/opis/json-schema/src/Keywords/EnumDataKeyword.php new file mode 100644 index 00000000..e5ec2e89 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/EnumDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct([]); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $value = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + if ($value === $this || !is_array($value) || empty($value)) { + return $this->error($schema, $context, 'enum', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->enum = $this->listByType($value); + $ret = parent::validate($context, $schema); + $this->enum = null; + + return $ret; + } +} diff --git a/src/opis/json-schema/src/Keywords/EnumKeyword.php b/src/opis/json-schema/src/Keywords/EnumKeyword.php new file mode 100644 index 00000000..c182cdce --- /dev/null +++ b/src/opis/json-schema/src/Keywords/EnumKeyword.php @@ -0,0 +1,84 @@ +enum = $this->listByType($enum); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $type = $context->currentDataType(); + $data = $context->currentData(); + + if (isset($this->enum[$type])) { + foreach ($this->enum[$type] as $value) { + if (Helper::equals($value, $data)) { + return null; + } + } + } + + return $this->error($schema, $context, 'enum', 'The data should match one item from enum'); + } + + /** + * @param array $values + * @return array + */ + protected function listByType($values): array + { + $list = []; + + foreach ($values as $value) { + $type = Helper::getJsonType($value); + if (!isset($list[$type])) { + $list[$type] = []; + } + $list[$type][] = $value; + } + + return $list; + } +} diff --git a/src/opis/json-schema/src/Keywords/ErrorTrait.php b/src/opis/json-schema/src/Keywords/ErrorTrait.php new file mode 100644 index 00000000..d7ba5a91 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ErrorTrait.php @@ -0,0 +1,55 @@ +all(); + } + } + + return new ValidationError($keyword, $schema, DataInfo::fromContext($context), $message, $args, + is_array($errors) ? $errors : []); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Keywords/ExclusiveMaximumDataKeyword.php b/src/opis/json-schema/src/Keywords/ExclusiveMaximumDataKeyword.php new file mode 100644 index 00000000..7912ec94 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ExclusiveMaximumDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var float|int $number */ + $number = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($number === $this || !(is_float($number) || is_int($number))) { + return $this->error($schema, $context, 'exclusiveMaximum', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->number = $number; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/ExclusiveMaximumKeyword.php b/src/opis/json-schema/src/Keywords/ExclusiveMaximumKeyword.php new file mode 100644 index 00000000..442dd407 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ExclusiveMaximumKeyword.php @@ -0,0 +1,55 @@ +number = $number; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($context->currentData() < $this->number) { + return null; + } + + return $this->error($schema, $context, 'exclusiveMaximum', "Number must be lower than {max}", [ + 'max' => $this->number, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/ExclusiveMinimumDataKeyword.php b/src/opis/json-schema/src/Keywords/ExclusiveMinimumDataKeyword.php new file mode 100644 index 00000000..a27b853e --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ExclusiveMinimumDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var float|int $number */ + $number = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($number === $this || !(is_float($number) || is_int($number))) { + return $this->error($schema, $context, 'exclusiveMinimum', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->number = $number; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/ExclusiveMinimumKeyword.php b/src/opis/json-schema/src/Keywords/ExclusiveMinimumKeyword.php new file mode 100644 index 00000000..2c123b05 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ExclusiveMinimumKeyword.php @@ -0,0 +1,55 @@ +number = $number; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($context->currentData() > $this->number) { + return null; + } + + return $this->error($schema, $context, 'exclusiveMinimum', "Number must be greater than {min}", [ + 'min' => $this->number, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/FiltersKeyword.php b/src/opis/json-schema/src/Keywords/FiltersKeyword.php new file mode 100644 index 00000000..8a39f5c9 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/FiltersKeyword.php @@ -0,0 +1,95 @@ +filters = $filters; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $type = $context->currentDataType(); + + foreach ($this->filters as $filter) { + if (!isset($filter->types[$type])) { + throw new UnresolvedFilterException($filter->name, $type, $schema, $context); + } + + $func = $filter->types[$type]; + + if ($filter->args) { + $args = (array)$filter->args->resolve($context->rootData(), $context->currentDataPath()); + $args += $context->globals(); + } else { + $args = $context->globals(); + } + + try { + if ($func instanceof Filter) { + $ok = $func->validate($context, $schema, $args); + } else { + $ok = $func($context->currentData(), $args); + } + } catch (CustomError $error) { + return $this->error($schema, $context, '$filters', $error->getMessage(), $error->getArgs() + [ + 'filter' => $filter->name, + 'type' => $type, + 'args' => $args, + ]); + } + + if ($ok) { + unset($func, $args, $ok); + continue; + } + + return $this->error($schema, $context, '$filters', "Filter '{filter}' ({type}) was not passed", [ + 'filter' => $filter->name, + 'type' => $type, + 'args' => $args, + ]); + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/FormatDataKeyword.php b/src/opis/json-schema/src/Keywords/FormatDataKeyword.php new file mode 100644 index 00000000..8adf8922 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/FormatDataKeyword.php @@ -0,0 +1,84 @@ +value = $value; + $this->resolver = $resolver; + parent::__construct('', []); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $value = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + if ($value === $this || !is_string($value)) { + return $this->error($schema, $context, 'format', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + /** @var string $value */ + + $type = $context->currentDataType(); + + $types = [ + $type => $this->resolver->resolve($value, $type), + ]; + + if (!$types[$type] && ($super = Helper::getJsonSuperType($type))) { + $types[$super] = $this->resolver->resolve($value, $super); + unset($super); + } + + unset($type); + + $this->name = $value; + $this->types = $types; + $ret = parent::validate($context, $schema); + $this->name = $this->types = null; + + return $ret; + } +} diff --git a/src/opis/json-schema/src/Keywords/FormatKeyword.php b/src/opis/json-schema/src/Keywords/FormatKeyword.php new file mode 100644 index 00000000..93f8c168 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/FormatKeyword.php @@ -0,0 +1,87 @@ +name = $name; + $this->types = $types; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $type = $context->currentDataType(); + + if (!isset($this->types[$type])) { + return null; + } + + $format = $this->types[$type]; + + try { + if ($format instanceof Format) { + $ok = $format->validate($context->currentData()); + } else { + $ok = $format($context->currentData()); + } + } catch (CustomError $error) { + return $this->error($schema, $context, 'format', $error->getMessage(), $error->getArgs() + [ + 'format' => $this->name, + 'type' => $type, + ]); + } + + if ($ok) { + return null; + } + + return $this->error($schema, $context, 'format', "The data must match the '{format}' format", [ + 'format' => $this->name, + 'type' => $type, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/IfThenElseKeyword.php b/src/opis/json-schema/src/Keywords/IfThenElseKeyword.php new file mode 100644 index 00000000..ef3650fa --- /dev/null +++ b/src/opis/json-schema/src/Keywords/IfThenElseKeyword.php @@ -0,0 +1,106 @@ +if = $if; + $this->then = $then; + $this->else = $else; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->if === true) { + return $this->validateBranch('then', $context, $schema); + } elseif ($this->if === false) { + return $this->validateBranch('else', $context, $schema); + } + + if (is_object($this->if) && !($this->if instanceof Schema)) { + $this->if = $context->loader()->loadObjectSchema($this->if); + } + + if ($context->validateSchemaWithoutEvaluated($this->if, null, true)) { + return $this->validateBranch('else', $context, $schema); + } + + return $this->validateBranch('then', $context, $schema); + } + + /** + * @param string $branch + * @param ValidationContext $context + * @param Schema $schema + * @return ValidationError|null + */ + protected function validateBranch($branch, $context, $schema) + { + $value = $this->{$branch}; + + if ($value === true) { + return null; + } elseif ($value === false) { + return $this->error($schema, $context, $branch, "The data is never valid on '{branch}' branch", [ + 'branch' => $branch, + ]); + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->{$branch} = $context->loader()->loadObjectSchema($value); + } + + if ($error = $value->validate($context)) { + return $this->error($schema, $context, $branch, "The data is not valid on '{branch}' branch", [ + 'branch' => $branch, + ], $error); + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/ItemsKeyword.php b/src/opis/json-schema/src/Keywords/ItemsKeyword.php new file mode 100644 index 00000000..ce4bc32e --- /dev/null +++ b/src/opis/json-schema/src/Keywords/ItemsKeyword.php @@ -0,0 +1,176 @@ +value = $value; + $this->alwaysValid = $alwaysValid; + + if (is_array($value)) { + $this->count = count($value); + } + + $this->keyword = $keyword; + $this->startIndex = $startIndex; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->alwaysValid || $this->value === true) { + if ($this->count === -1) { + $context->markAllAsEvaluatedItems(); + } else { + $context->markCountAsEvaluatedItems($this->count); + } + return null; + } + + $count = count($context->currentData()); + + if ($this->startIndex >= $count) { + // Already validated by other keyword + return null; + } + + if ($this->value === false) { + if ($count === 0) { + return null; + } + + return $this->error($schema, $context, $this->keyword, 'Array must be empty'); + } + + if ($this->count >= 0) { + + $errors = $this->errorContainer($context->maxErrors()); + $max = min($count, $this->count); + $evaluated = []; + + for ($i = $this->startIndex; $i < $max; $i++) { + if ($this->value[$i] === true) { + $evaluated[] = $i; + continue; + } + + if ($this->value[$i] === false) { + $context->addEvaluatedItems($evaluated); + return $this->error($schema, $context, $this->keyword, "Array item at index {index} is not allowed", [ + 'index' => $i, + ]); + } + + if (is_object($this->value[$i]) && !($this->value[$i] instanceof Schema)) { + $this->value[$i] = $context->loader()->loadObjectSchema($this->value[$i]); + } + + $context->pushDataPath($i); + $error = $this->value[$i]->validate($context); + $context->popDataPath(); + + if ($error) { + $errors->add($error); + if ($errors->isFull()) { + break; + } + } else { + $evaluated[] = $i; + } + } + + $context->addEvaluatedItems($evaluated); + + if ($errors->isEmpty()) { + return null; + } + + return $this->error($schema, $context, $this->keyword, 'Array items must match corresponding schemas', [], + $errors); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $object = $this->createArrayObject($context); + + $error = $this->validateIterableData($schema, $this->value, $context, $this->indexes($this->startIndex, $count), + $this->keyword, 'All array items must match schema', [], $object); + + if ($object && $object->count()) { + $context->addEvaluatedItems($object->getArrayCopy()); + } + + return $error; + } + + /** + * @param int $start + * @param int $max + * @return iterable|int[] + */ + protected function indexes($start, $max) + { + for ($i = $start; $i < $max; $i++) { + yield $i; + } + } +} diff --git a/src/opis/json-schema/src/Keywords/IterableDataValidationTrait.php b/src/opis/json-schema/src/Keywords/IterableDataValidationTrait.php new file mode 100644 index 00000000..d9f4780e --- /dev/null +++ b/src/opis/json-schema/src/Keywords/IterableDataValidationTrait.php @@ -0,0 +1,110 @@ +errorContainer($context->maxErrors()); + + if ($keys) { + foreach ($iterator as $key) { + $context->pushDataPath($key); + $error = $schema->validate($context); + $context->popDataPath(); + + if ($error) { + if (!$container->isFull()) { + $container->add($error); + } + } else { + $keys[] = $key; + } + } + } else { + foreach ($iterator as $key) { + $context->pushDataPath($key); + $error = $schema->validate($context); + $context->popDataPath(); + + if ($error && $container->add($error)->isFull()) { + break; + } + } + } + + return $container; + } + + /** + * @param Schema $parentSchema + * @param Schema $schema + * @param ValidationContext $context + * @param iterable $iterator + * @param string $keyword + * @param string $message + * @param array $args + * @param ArrayObject|null $visited_keys + * @return ValidationError|null + */ + protected function validateIterableData( + $parentSchema, + $schema, + $context, + $iterator, + $keyword, + $message, + $args = [], + $visited_keys = null + ) { + $errors = $this->iterateAndValidate($schema, $context, $iterator, $visited_keys); + + if ($errors->isEmpty()) { + return null; + } + + return $this->error($parentSchema, $context, $keyword, $message, $args, $errors); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxItemsDataKeyword.php b/src/opis/json-schema/src/Keywords/MaxItemsDataKeyword.php new file mode 100644 index 00000000..f846fdba --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxItemsDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $count */ + $count = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($count === $this || !is_int($count) || $count < 0) { + return $this->error($schema, $context, 'maxItems', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->count = $count; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxItemsKeyword.php b/src/opis/json-schema/src/Keywords/MaxItemsKeyword.php new file mode 100644 index 00000000..feb3fce0 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxItemsKeyword.php @@ -0,0 +1,63 @@ +count = $count; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $count = count($context->currentData()); + + if ($count <= $this->count) { + return null; + } + + return $this->error($schema, $context, "maxItems", + "Array should have at most {max} items, {count} found", [ + 'max' => $this->count, + 'count' => $count, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxLengthDataKeyword.php b/src/opis/json-schema/src/Keywords/MaxLengthDataKeyword.php new file mode 100644 index 00000000..acf42128 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxLengthDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $length */ + $length = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($length === $this || !is_int($length) || $length < 0) { + return $this->error($schema, $context, 'maxLength', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->length = $length; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxLengthKeyword.php b/src/opis/json-schema/src/Keywords/MaxLengthKeyword.php new file mode 100644 index 00000000..b1978559 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxLengthKeyword.php @@ -0,0 +1,67 @@ +length = $length; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->length === 0) { + return null; + } + + $length = $context->getStringLength(); + + if ($length <= $this->length) { + return null; + } + + return $this->error($schema, $context, 'maxLength', "Maximum string length is {max}, found {length}", + [ + 'max' => $this->length, + 'length' => $length, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxPropertiesDataKeyword.php b/src/opis/json-schema/src/Keywords/MaxPropertiesDataKeyword.php new file mode 100644 index 00000000..14ea37dd --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxPropertiesDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $count */ + $count = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($count === $this || !is_int($count) || $count < 0) { + return $this->error($schema, $context, 'maxProperties', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->count = $count; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaxPropertiesKeywords.php b/src/opis/json-schema/src/Keywords/MaxPropertiesKeywords.php new file mode 100644 index 00000000..e9f948a3 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaxPropertiesKeywords.php @@ -0,0 +1,63 @@ +count = $count; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $count = count($context->getObjectProperties()); + + if ($count <= $this->count) { + return null; + } + + return $this->error($schema, $context, 'maxProperties', + "Object must have at most {max} properties, {count} found", [ + 'max' => $this->count, + 'count' => $count, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaximumDataKeyword.php b/src/opis/json-schema/src/Keywords/MaximumDataKeyword.php new file mode 100644 index 00000000..6cb7dd16 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaximumDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var float|int $number */ + $number = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($number === $this || !(is_float($number) || is_int($number))) { + return $this->error($schema, $context, 'maximum', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->number = $number; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MaximumKeyword.php b/src/opis/json-schema/src/Keywords/MaximumKeyword.php new file mode 100644 index 00000000..a0ab6c4f --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MaximumKeyword.php @@ -0,0 +1,59 @@ +number = $number; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($context->currentData() <= $this->number) { + return null; + } + + return $this->error($schema, $context, 'maximum', "Number must be lower than or equal to {max}", [ + 'max' => $this->number, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinItemsDataKeyword.php b/src/opis/json-schema/src/Keywords/MinItemsDataKeyword.php new file mode 100644 index 00000000..c175c9c1 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinItemsDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $count */ + $count = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($count === $this || !is_int($count) || $count < 0) { + return $this->error($schema, $context, 'minItems', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->count = $count; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinItemsKeyword.php b/src/opis/json-schema/src/Keywords/MinItemsKeyword.php new file mode 100644 index 00000000..60a86e6f --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinItemsKeyword.php @@ -0,0 +1,63 @@ +count = $count; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $count = count($context->currentData()); + + if ($count >= $this->count) { + return null; + } + + return $this->error($schema, $context, "minItems", + "Array should have at least {min} items, {count} found", [ + 'min' => $this->count, + 'count' => $count, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinLengthDataKeyword.php b/src/opis/json-schema/src/Keywords/MinLengthDataKeyword.php new file mode 100644 index 00000000..c579ba07 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinLengthDataKeyword.php @@ -0,0 +1,57 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $length */ + $length = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($length === $this || !is_int($length) || $length < 0) { + return $this->error($schema, $context, 'minLength', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->length = $length; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinLengthKeyword.php b/src/opis/json-schema/src/Keywords/MinLengthKeyword.php new file mode 100644 index 00000000..6e0b1596 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinLengthKeyword.php @@ -0,0 +1,66 @@ +length = $length; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->length === 0) { + return null; + } + + $length = $context->getStringLength(); + + if ($length >= $this->length) { + return null; + } + + return $this->error($schema, $context, 'minLength', "Minimum string length is {min}, found {length}", [ + 'min' => $this->length, + 'length' => $length, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinPropertiesDataKeyword.php b/src/opis/json-schema/src/Keywords/MinPropertiesDataKeyword.php new file mode 100644 index 00000000..b1f06cfe --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinPropertiesDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var int $count */ + $count = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($count === $this || !is_int($count) || $count < 0) { + return $this->error($schema, $context, 'minProperties', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->count = $count; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinPropertiesKeyword.php b/src/opis/json-schema/src/Keywords/MinPropertiesKeyword.php new file mode 100644 index 00000000..0a9eb1e1 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinPropertiesKeyword.php @@ -0,0 +1,63 @@ +count = $count; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $count = count($context->getObjectProperties()); + + if ($this->count <= $count) { + return null; + } + + return $this->error($schema, $context, 'minProperties', + "Object must have at least {min} properties, {count} found", [ + 'min' => $this->count, + 'count' => $count, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinimumDataKeyword.php b/src/opis/json-schema/src/Keywords/MinimumDataKeyword.php new file mode 100644 index 00000000..77335dd1 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinimumDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var float|int $number */ + $number = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($number === $this || !(is_float($number) || is_int($number))) { + return $this->error($schema, $context, 'minimum', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->number = $number; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MinimumKeyword.php b/src/opis/json-schema/src/Keywords/MinimumKeyword.php new file mode 100644 index 00000000..99c67ffb --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MinimumKeyword.php @@ -0,0 +1,59 @@ +number = $number; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($context->currentData() >= $this->number) { + return null; + } + + return $this->error($schema, $context, 'minimum', "Number must be greater than or equal to {min}", [ + 'min' => $this->number, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/MultipleOfDataKeyword.php b/src/opis/json-schema/src/Keywords/MultipleOfDataKeyword.php new file mode 100644 index 00000000..a32df2b0 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MultipleOfDataKeyword.php @@ -0,0 +1,60 @@ +value = $value; + parent::__construct(0); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + /** @var float|int $number */ + $number = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($number === $this || !(is_float($number) || is_int($number)) || $number <= 0) { + return $this->error($schema, $context, 'multipleOf', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->number = $number; + + return parent::validate($context, $schema); + } +} diff --git a/src/opis/json-schema/src/Keywords/MultipleOfKeyword.php b/src/opis/json-schema/src/Keywords/MultipleOfKeyword.php new file mode 100644 index 00000000..be324434 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/MultipleOfKeyword.php @@ -0,0 +1,60 @@ +number = $number; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (Helper::isMultipleOf($context->currentData(), $this->number)) { + return null; + } + + return $this->error($schema, $context, 'multipleOf', "Number must be a multiple of {divisor}", [ + 'divisor' => $this->number, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/NotKeyword.php b/src/opis/json-schema/src/Keywords/NotKeyword.php new file mode 100644 index 00000000..39dc7751 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/NotKeyword.php @@ -0,0 +1,68 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->value === false) { + return null; + } + if ($this->value === true) { + return $this->error($schema, $context, 'not', "The data is never valid"); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $error = $context->validateSchemaWithoutEvaluated($this->value, 1); + + if ($error) { + return null; + } + + return $this->error($schema, $context, 'not', 'The data must not match schema'); + } +} diff --git a/src/opis/json-schema/src/Keywords/OfTrait.php b/src/opis/json-schema/src/Keywords/OfTrait.php new file mode 100644 index 00000000..2cef55b1 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/OfTrait.php @@ -0,0 +1,52 @@ +trackUnevaluated() ? new ArrayObject() : null; + } + + /** + * @param \ArrayObject|null $object + * @param \Opis\JsonSchema\ValidationContext $context + */ + protected function addEvaluatedFromArrayObject($object, $context) + { + if (!$object || !$object->count()) { + return; + } + + foreach ($object as $value) { + if (isset($value['properties'])) { + $context->addEvaluatedProperties($value['properties']); + } + if (isset($value['items'])) { + $context->addEvaluatedItems($value['items']); + } + } + } +} diff --git a/src/opis/json-schema/src/Keywords/OneOfKeyword.php b/src/opis/json-schema/src/Keywords/OneOfKeyword.php new file mode 100644 index 00000000..fdbed540 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/OneOfKeyword.php @@ -0,0 +1,100 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $count = 0; + $matchedIndex = -1; + $object = $this->createArrayObject($context); + $errors = []; + + foreach ($this->value as $index => $value) { + if ($value === false) { + continue; + } + + if ($value === true) { + if (++$count > 1) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'oneOf', 'The data should match exactly one schema', [ + 'matched' => [$matchedIndex, $index], + ]); + } + + $matchedIndex = $index; + continue; + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$index] = $context->loader()->loadObjectSchema($value); + } + + $error = $context->validateSchemaWithoutEvaluated($value, null, false, $object); + if ($error) { + $errors[] = $error; + } else { + if (++$count > 1) { + $this->addEvaluatedFromArrayObject($object, $context); + return $this->error($schema, $context, 'oneOf', 'The data should match exactly one schema', [ + 'matched' => [$matchedIndex, $index], + ]); + } + $matchedIndex = $index; + } + } + + $this->addEvaluatedFromArrayObject($object, $context); + + if ($count === 1) { + return null; + } + + return $this->error($schema, $context, 'oneOf', 'The data should match exactly one schema', [ + 'matched' => [], + ], $errors); + } +} diff --git a/src/opis/json-schema/src/Keywords/PatternDataKeyword.php b/src/opis/json-schema/src/Keywords/PatternDataKeyword.php new file mode 100644 index 00000000..a64592d0 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PatternDataKeyword.php @@ -0,0 +1,61 @@ +value = $value; + parent::__construct(''); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $pattern = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + if ($pattern === $this || !is_string($pattern) || !Helper::isValidPattern($pattern)) { + return $this->error($schema, $context, 'pattern', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $this->pattern = $pattern; + $this->regex = Helper::patternToRegex($pattern); + $ret = parent::validate($context, $schema); + $this->pattern = $this->regex = null; + + return $ret; + } +} diff --git a/src/opis/json-schema/src/Keywords/PatternKeyword.php b/src/opis/json-schema/src/Keywords/PatternKeyword.php new file mode 100644 index 00000000..cb50a622 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PatternKeyword.php @@ -0,0 +1,61 @@ +pattern = $pattern; + $this->regex = Helper::patternToRegex($pattern); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (preg_match($this->regex, $context->currentData())) { + return null; + } + + return $this->error($schema, $context, 'pattern', "The string should match pattern: {pattern}", [ + 'pattern' => $this->pattern, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/PatternPropertiesKeyword.php b/src/opis/json-schema/src/Keywords/PatternPropertiesKeyword.php new file mode 100644 index 00000000..d1f90580 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PatternPropertiesKeyword.php @@ -0,0 +1,121 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $props = $context->getObjectProperties(); + + if (!$props) { + return null; + } + + $checked = []; + + foreach ($this->value as $pattern => $value) { + if ($value === true) { + iterator_to_array($this->matchedProperties($pattern, $props, $checked)); + continue; + } + + if ($value === false) { + $list = iterator_to_array($this->matchedProperties($pattern, $props, $checked)); + + if ($list) { + if ($context->trackUnevaluated()) { + $context->addEvaluatedProperties(array_diff(array_keys($checked), $list)); + } + return $this->error($schema, $context, 'patternProperties', "Object properties that match pattern '{pattern}' are not allowed", [ + 'pattern' => $pattern, + 'forbidden' => $list, + ]); + } + + unset($list); + continue; + } + + if (is_object($value) && !($value instanceof Schema)) { + $value = $this->value[$pattern] = $context->loader()->loadObjectSchema($value); + } + + $subErrors = $this->iterateAndValidate($value, $context, $this->matchedProperties($pattern, $props, $checked)); + + if (!$subErrors->isEmpty()) { + if ($context->trackUnevaluated()) { + $context->addEvaluatedProperties(array_keys($checked)); + } + return $this->error($schema, $context, 'patternProperties', "Object properties that match pattern '{pattern}' must also match pattern's schema", [ + 'pattern' => $pattern, + ], $subErrors); + } + + unset($subErrors); + } + + if ($checked) { + $checked = array_keys($checked); + $context->addCheckedProperties($checked); + $context->addEvaluatedProperties($checked); + } + + return null; + } + + /** + * @param string $pattern + * @param array $props + * @param array $checked + * @return Traversable|string[] + */ + protected function matchedProperties($pattern, $props, &$checked): Traversable + { + $pattern = Helper::patternToRegex($pattern); + + foreach ($props as $prop) { + if (preg_match($pattern, (string)$prop)) { + $checked[$prop] = true; + yield $prop; + } + } + } +} diff --git a/src/opis/json-schema/src/Keywords/PointerRefKeyword.php b/src/opis/json-schema/src/Keywords/PointerRefKeyword.php new file mode 100644 index 00000000..c3396366 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PointerRefKeyword.php @@ -0,0 +1,61 @@ +pointer = $pointer; + } + + /** + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + protected function doValidate($context, $schema) + { + if ($this->resolved === false) { + $info = $schema->info(); + $this->resolved = $this->resolvePointer($context->loader(), $this->pointer, $info->idBaseRoot(), $info->path()); + } + + if ($this->resolved === null) { + throw new UnresolvedReferenceException((string)$this->pointer, $schema, $context); + } + + return $this->resolved->validate($this->createContext($context, $schema)); + } +} diff --git a/src/opis/json-schema/src/Keywords/PropertiesKeyword.php b/src/opis/json-schema/src/Keywords/PropertiesKeyword.php new file mode 100644 index 00000000..28ad31e8 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PropertiesKeyword.php @@ -0,0 +1,117 @@ +properties = $properties; + $this->propertyKeys = array_keys($properties); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if (!$this->properties) { + return null; + } + + $checked = []; + $evaluated = []; + + $data = $context->currentData(); + + $errors = $this->errorContainer($context->maxErrors()); + + foreach ($this->properties as $name => $prop) { + if (!property_exists($data, $name)) { + continue; + } + + $checked[] = $name; + + if ($prop === true) { + $evaluated[] = $name; + continue; + } + + if ($prop === false) { + $context->addEvaluatedProperties($evaluated); + return $this->error($schema, $context, 'properties', "Property '{property}' is not allowed", [ + 'property' => $name, + ]); + } + + if (is_object($prop) && !($prop instanceof Schema)) { + $prop = $this->properties[$name] = $context->loader()->loadObjectSchema($prop); + } + + $context->pushDataPath($name); + $error = $prop->validate($context); + $context->popDataPath(); + + if ($error) { + $errors->add($error); + if ($errors->isFull()) { + break; + } + } else { + $evaluated[] = $name; + } + } + + $context->addEvaluatedProperties($evaluated); + + if (!$errors->isEmpty()) { + return $this->error($schema, $context, 'properties', "The properties must match schema: {properties}", [ + 'properties' => array_values(array_diff($checked, $evaluated)) + ], $errors); + } + unset($errors); + + $context->addCheckedProperties($checked); + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/PropertyNamesKeyword.php b/src/opis/json-schema/src/Keywords/PropertyNamesKeyword.php new file mode 100644 index 00000000..6ba547fe --- /dev/null +++ b/src/opis/json-schema/src/Keywords/PropertyNamesKeyword.php @@ -0,0 +1,76 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + if ($this->value === true) { + return null; + } + + $props = $context->getObjectProperties(); + if (!$props) { + return null; + } + + if ($this->value === false) { + return $this->error($schema, $context, 'propertyNames', "No properties are allowed"); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + foreach ($props as $prop) { + if ($error = $this->value->validate($context->newInstance($prop, $schema))) { + return $this->error($schema, $context, 'propertyNames', "Property '{property}' must match schema", [ + 'property' => $prop, + ], $error); + } + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Keywords/RecursiveRefKeyword.php b/src/opis/json-schema/src/Keywords/RecursiveRefKeyword.php new file mode 100644 index 00000000..25a6610d --- /dev/null +++ b/src/opis/json-schema/src/Keywords/RecursiveRefKeyword.php @@ -0,0 +1,152 @@ +uri = $uri; + $this->anchor = $anchor; + $this->anchorValue = $anchorValue; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function doValidate($context, $schema) + { + if ($this->resolved === false) { + $this->resolved = $context->loader()->loadSchemaById($this->uri); + } + + if ($this->resolved === null) { + throw new UnresolvedReferenceException((string)$this->uri, $schema, $context); + } + + $new_context = $this->createContext($context, $schema); + + if (!$this->hasRecursiveAnchor($this->resolved)) { + $this->setLastRefSchema($this->resolved); + return $this->resolved->validate($new_context); + } + + $ok_sender = $this->resolveSchema($context); + + if (!$ok_sender) { + $this->setLastRefSchema($this->resolved); + return $this->resolved->validate($new_context); + } + + $this->setLastRefSchema($ok_sender); + + return $ok_sender->validate($new_context); + } + + /** + * @param \Opis\JsonSchema\ValidationContext $context + */ + protected function resolveSchema($context) + { + $ok = null; + $loader = $context->loader(); + + while ($context) { + $sender = $context->sender(); + + if (!$sender) { + break; + } + + if (!$this->hasRecursiveAnchor($sender)) { + if ($sender->info()->id()) { + // id without recursiveAnchor + break; + } + + $sender = $loader->loadSchemaById($sender->info()->root()); + if (!$sender || !$this->hasRecursiveAnchor($sender)) { + // root without recursiveAnchor + break; + } + } + + if ($sender->info()->id()) { + // id with recursiveAnchor + $ok = $sender; + } else { + // root with recursiveAnchor + $ok = $loader->loadSchemaById($sender->info()->root()); + } + + $context = $context->parent(); + } + + return $ok; + } + + /** + * @param \Opis\JsonSchema\Schema|null $schema + */ + protected function hasRecursiveAnchor($schema): bool + { + if (!$schema) { + return false; + } + + $info = $schema->info(); + + if (!$info->isObject()) { + return false; + } + + $data = $info->data(); + + if (!property_exists($data, $this->anchor)) { + return false; + } + + return $data->{$this->anchor} === $this->anchorValue; + } +} diff --git a/src/opis/json-schema/src/Keywords/RequiredDataKeyword.php b/src/opis/json-schema/src/Keywords/RequiredDataKeyword.php new file mode 100644 index 00000000..672be71d --- /dev/null +++ b/src/opis/json-schema/src/Keywords/RequiredDataKeyword.php @@ -0,0 +1,92 @@ +value = $value; + $this->filter = $filter; + parent::__construct([]); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $required = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + if ($required === $this || !is_array($required) || !$this->requiredPropsAreValid($required)) { + return $this->error($schema, $context, 'required', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + $required = array_unique($required); + + if ($this->filter) { + $required = array_filter($required, $this->filter === null ? function ($value, $key) : bool { + return !empty($value); + } : $this->filter, $this->filter === null ? ARRAY_FILTER_USE_BOTH : 0); + } + + if (!$required) { + return null; + } + + $this->required = $required; + $ret = parent::validate($context, $schema); + $this->required = null; + + return $ret; + } + + /** + * @param array $props + * @return bool + */ + protected function requiredPropsAreValid($props): bool + { + foreach ($props as $prop) { + if (!is_string($prop)) { + return false; + } + } + + return true; + } +} diff --git a/src/opis/json-schema/src/Keywords/RequiredKeyword.php b/src/opis/json-schema/src/Keywords/RequiredKeyword.php new file mode 100644 index 00000000..ed10c5a7 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/RequiredKeyword.php @@ -0,0 +1,70 @@ +required = $required; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $data = $context->currentData(); + $max = $context->maxErrors(); + $list = []; + + foreach ($this->required as $name) { + if (!property_exists($data, $name)) { + $list[] = $name; + if (--$max <= 0) { + break; + } + } + } + + if (!$list) { + return null; + } + + return $this->error($schema, $context, 'required', 'The required properties ({missing}) are missing', [ + 'missing' => $list, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/SlotsKeyword.php b/src/opis/json-schema/src/Keywords/SlotsKeyword.php new file mode 100644 index 00000000..2b27fef8 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/SlotsKeyword.php @@ -0,0 +1,153 @@ +slots = $slots; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $newContext = $context->newInstance($context->currentData(), $schema); + + foreach ($this->slots as $name => $fallback) { + $slot = $this->resolveSlotSchema($name, $context); + + if ($slot === null) { + $save = true; + if (is_string($fallback)) { + $save = false; + $fallback = $this->resolveSlot($fallback, $context); + } + + if ($fallback === true) { + continue; + } + + if ($fallback === false) { + return $this->error($schema, $context, '$slots', "Required slot '{slot}' is missing", [ + 'slot' => $name, + ]); + } + + if (is_object($fallback) && !($fallback instanceof Schema)) { + $fallback = $context->loader()->loadObjectSchema($fallback); + if ($save) { + $this->slots[$name] = $fallback; + } + } + + $slot = $fallback; + } + + if ($error = $slot->validate($newContext)) { + return $this->error($schema, $context,'$slots', "Schema for slot '{slot}' was not matched", [ + 'slot' => $name, + ], $error); + } + } + + return null; + } + + /** + * @param string $name + * @param ValidationContext $context + * @return Schema|null + */ + protected function resolveSlotSchema($name, $context) + { + do { + $slot = $context->slot($name); + } while ($slot === null && $context = $context->parent()); + + return $slot; + } + + /** + * @param string $name + * @param ValidationContext $context + * @return bool|Schema + */ + protected function resolveSlot($name, $context) + { + $slot = $this->resolveSlotSchema($name, $context); + + if ($slot !== null) { + return $slot; + } + + if (!isset($this->slots[$name])) { + return false; + } + + $slot = $this->slots[$name]; + + if (is_bool($slot)) { + return $slot; + } + + if (is_object($slot)) { + if ($slot instanceof Schema) { + return $slot; + } + + $slot = $context->loader()->loadObjectSchema($slot); + $this->slots[$name] = $slot; + return $slot; + } + + if (!is_string($slot)) { + // Looks like the slot is missing + return false; + } + + if (in_array($slot, $this->stack)) { + // Recursive + return false; + } + + $this->stack[] = $slot; + $slot = $this->resolveSlot($slot, $context); + array_pop($this->stack); + + return $slot; + } +} diff --git a/src/opis/json-schema/src/Keywords/TemplateRefKeyword.php b/src/opis/json-schema/src/Keywords/TemplateRefKeyword.php new file mode 100644 index 00000000..3ba07048 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/TemplateRefKeyword.php @@ -0,0 +1,132 @@ +template = $template; + $this->vars = $vars; + $this->allowRelativeJsonPointerInRef = $allowRelativeJsonPointerInRef; + } + + /** + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + protected function doValidate($context, $schema) + { + if ($this->vars) { + $vars = $this->vars->resolve($context->rootData(), $context->currentDataPath()); + if (!is_array($vars)) { + $vars = (array)$vars; + } + $vars += $context->globals(); + } else { + $vars = $context->globals(); + } + + $ref = $this->template->resolve($vars); + + $key = isset($ref[32]) ? md5($ref) : $ref; + + if (!array_key_exists($key, $this->cached)) { + $this->cached[$key] = $this->resolveRef($ref, $context->loader(), $schema); + } + + $resolved = $this->cached[$key]; + unset($key); + + if (!$resolved) { + throw new UnresolvedReferenceException($ref, $schema, $context); + } + + return $resolved->validate($this->createContext($context, $schema)); + } + + /** + * @param string $ref + * @param SchemaLoader $repo + * @param Schema $schema + * @return null|Schema + */ + protected function resolveRef($ref, $repo, $schema) + { + if ($ref === '') { + return null; + } + + $baseUri = $schema->info()->idBaseRoot(); + + if ($ref === '#') { + return $repo->loadSchemaById($baseUri); + } + + // Check if is pointer + if ($ref[0] === '#') { + if ($pointer = JsonPointer::parse(substr($ref, 1))) { + if ($pointer->isAbsolute()) { + return $this->resolvePointer($repo, $pointer, $baseUri); + } + unset($pointer); + } + } elseif ($this->allowRelativeJsonPointerInRef && ($pointer = JsonPointer::parse($ref))) { + if ($pointer->isRelative()) { + return $this->resolvePointer($repo, $pointer, $baseUri, $schema->info()->path()); + } + unset($pointer); + } + + $ref = Uri::merge($ref, $baseUri, true); + + if ($ref === null || !$ref->isAbsolute()) { + return null; + } + + return $repo->loadSchemaById($ref); + } +} diff --git a/src/opis/json-schema/src/Keywords/TypeKeyword.php b/src/opis/json-schema/src/Keywords/TypeKeyword.php new file mode 100644 index 00000000..999f5bd8 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/TypeKeyword.php @@ -0,0 +1,60 @@ +type = $type; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $type = $context->currentDataType(); + if ($type && Helper::jsonTypeMatches($type, $this->type)) { + return null; + } + + return $this->error($schema, $context, 'type', 'The data ({type}) must match the type: {expected}', [ + 'expected' => $this->type, + 'type' => $type, + ]); + } +} diff --git a/src/opis/json-schema/src/Keywords/URIRefKeyword.php b/src/opis/json-schema/src/Keywords/URIRefKeyword.php new file mode 100644 index 00000000..32126f5e --- /dev/null +++ b/src/opis/json-schema/src/Keywords/URIRefKeyword.php @@ -0,0 +1,62 @@ +uri = $uri; + } + + /** + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + protected function doValidate($context, $schema) + { + if ($this->resolved === false) { + $this->resolved = $context->loader()->loadSchemaById($this->uri); + } + + if ($this->resolved === null) { + throw new UnresolvedReferenceException((string)$this->uri, $schema, $context); + } + + $this->setLastRefSchema($this->resolved); + + return $this->resolved->validate($this->createContext($context, $schema)); + } +} diff --git a/src/opis/json-schema/src/Keywords/UnevaluatedItemsKeyword.php b/src/opis/json-schema/src/Keywords/UnevaluatedItemsKeyword.php new file mode 100644 index 00000000..c5db7abd --- /dev/null +++ b/src/opis/json-schema/src/Keywords/UnevaluatedItemsKeyword.php @@ -0,0 +1,77 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $unevaluated = $context->getUnevaluatedItems(); + + if (!$unevaluated) { + return null; + } + + if ($this->value === true) { + $context->addEvaluatedItems($unevaluated); + return null; + } + + if ($this->value === false) { + return $this->error($schema, $context, 'unevaluatedItems', + 'Unevaluated array items are not allowed: {indexes}', [ + 'indexes' => $unevaluated, + ]); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $object = $this->createArrayObject($context); + + $error = $this->validateIterableData($schema, $this->value, $context, $unevaluated, + 'unevaluatedItems', 'All unevaluated array items must match schema: {indexes}', [ + 'indexes' => $unevaluated, + ], $object); + + if ($object && $object->count()) { + $context->addEvaluatedItems($object->getArrayCopy()); + } + + return $error; + } +} diff --git a/src/opis/json-schema/src/Keywords/UnevaluatedPropertiesKeyword.php b/src/opis/json-schema/src/Keywords/UnevaluatedPropertiesKeyword.php new file mode 100644 index 00000000..2cc192c0 --- /dev/null +++ b/src/opis/json-schema/src/Keywords/UnevaluatedPropertiesKeyword.php @@ -0,0 +1,79 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $unevaluated = $context->getUnevaluatedProperties(); + + if (!$unevaluated) { + return null; + } + + if ($this->value === true) { + $context->addEvaluatedProperties($unevaluated); + return null; + } + + if ($this->value === false) { + return $this->error($schema, $context, 'unevaluatedProperties', + 'Unevaluated object properties not allowed: {properties}', [ + 'properties' => $unevaluated, + ]); + } + + if (is_object($this->value) && !($this->value instanceof Schema)) { + $this->value = $context->loader()->loadObjectSchema($this->value); + } + + $object = $this->createArrayObject($context); + + $error = $this->validateIterableData($schema, $this->value, $context, $unevaluated, + 'unevaluatedProperties', 'All unevaluated object properties must match schema: {properties}', [ + 'properties' => $unevaluated, + ], $object); + + + if ($object && $object->count()) { + $context->addEvaluatedProperties($object->getArrayCopy()); + } + + return $error; + } +} diff --git a/src/opis/json-schema/src/Keywords/UniqueItemsDataKeyword.php b/src/opis/json-schema/src/Keywords/UniqueItemsDataKeyword.php new file mode 100644 index 00000000..79cb925f --- /dev/null +++ b/src/opis/json-schema/src/Keywords/UniqueItemsDataKeyword.php @@ -0,0 +1,56 @@ +value = $value; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + * @param \Opis\JsonSchema\Schema $schema + */ + public function validate($context, $schema) + { + $value = $this->value->data($context->rootData(), $context->currentDataPath(), $this); + + if ($value === $this || !is_bool($value)) { + return $this->error($schema, $context, 'uniqueItems', 'Invalid $data', [ + 'pointer' => (string)$this->value, + ]); + } + + return $value ? parent::validate($context, $schema) : null; + } +} diff --git a/src/opis/json-schema/src/Keywords/UniqueItemsKeyword.php b/src/opis/json-schema/src/Keywords/UniqueItemsKeyword.php new file mode 100644 index 00000000..8582eddb --- /dev/null +++ b/src/opis/json-schema/src/Keywords/UniqueItemsKeyword.php @@ -0,0 +1,59 @@ +currentData(); + if (!$data) { + return null; + } + + $count = count($data); + + for ($i = 0; $i < $count - 1; $i++) { + for ($j = $i + 1; $j < $count; $j++) { + if (Helper::equals($data[$i], $data[$j])) { + return $this->error($schema, $context, 'uniqueItems', 'Array must have unique items', [ + 'duplicate' => $data[$i], + 'indexes' => [$i, $j], + ]); + } + } + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Parsers/DataKeywordTrait.php b/src/opis/json-schema/src/Parsers/DataKeywordTrait.php new file mode 100644 index 00000000..5e43ae1d --- /dev/null +++ b/src/opis/json-schema/src/Parsers/DataKeywordTrait.php @@ -0,0 +1,59 @@ +{'$data'}) || count(get_object_vars($value)) !== 1) { + return null; + } + + return JsonPointer::parse($value->{'$data'}); + } + + /** + * @param SchemaParser $parser + * @param string|null $keyword + * @return bool + */ + protected function isDataKeywordAllowed($parser, $keyword = null): bool + { + if (!($enabled = $parser->option('allowDataKeyword'))) { + return false; + } + + if ($enabled === true) { + return true; + } + + if ($keyword === null) { + return false; + } + + return is_array($enabled) && in_array($keyword, $enabled); + } +} diff --git a/src/opis/json-schema/src/Parsers/DefaultVocabulary.php b/src/opis/json-schema/src/Parsers/DefaultVocabulary.php new file mode 100644 index 00000000..d05d4280 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/DefaultVocabulary.php @@ -0,0 +1,56 @@ +getKeywordParsers(); + $keywordValidators = $this->getKeywordValidatorParsers(); + $pragmas = $this->getPragmaParsers(); + + if ($extraVocabulary) { + $keywords = array_merge($keywords, $extraVocabulary->keywords()); + $keywordValidators = array_merge($keywordValidators, $extraVocabulary->keywordValidators()); + $pragmas = array_merge($pragmas, $extraVocabulary->pragmas()); + } + + array_unshift($keywords, $this->getRefKeywordParser()); + + parent::__construct($keywords, $keywordValidators, $pragmas); + } + + /** + * @return string + */ + abstract public function version(): string; + + /** + * @return bool + */ + abstract public function allowKeywordsAlongsideRef(): bool; + + /** + * @return bool + */ + abstract public function supportsAnchorId(): bool; + + /** + * @return KeywordParser + */ + abstract protected function getRefKeywordParser(): KeywordParser; + + /** + * @return KeywordParser[] + */ + abstract protected function getKeywordParsers(): array; + + /** + * @return KeywordValidatorParser[] + */ + protected function getKeywordValidatorParsers(): array + { + return []; + } + + /** + * @return PragmaParser[] + */ + protected function getPragmaParsers(): array + { + return []; + } +} diff --git a/src/opis/json-schema/src/Parsers/DraftOptionTrait.php b/src/opis/json-schema/src/Parsers/DraftOptionTrait.php new file mode 100644 index 00000000..22e28153 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/DraftOptionTrait.php @@ -0,0 +1,48 @@ +option($option); + + if (!$value) { + return false; + } + + if ($value === true) { + return true; + } + + if (is_array($value)) { + return in_array($info->draft(), $value); + } + + return $value === $info->draft(); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/Drafts/Draft06.php b/src/opis/json-schema/src/Parsers/Drafts/Draft06.php new file mode 100644 index 00000000..f8a48343 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Drafts/Draft06.php @@ -0,0 +1,145 @@ + '$recursiveRef', 'anchor' => '$recursiveAnchor', 'fragment' => false], + ]); + } + + /** + * @inheritDoc + */ + protected function getKeywordParsers(): array + { + return [ + // Generic + new TypeKeywordParser('type'), + new ConstKeywordParser('const'), + new EnumKeywordParser('enum'), + new FormatKeywordParser('format'), + + // String + new MinLengthKeywordParser('minLength'), + new MaxLengthKeywordParser('maxLength'), + new PatternKeywordParser("pattern"), + new ContentEncodingKeywordParser('contentEncoding'), + new ContentMediaTypeKeywordParser('contentMediaType'), + new ContentSchemaKeywordParser('contentSchema'), + + // Number + new MinimumKeywordParser('minimum', 'exclusiveMinimum'), + new MaximumKeywordParser('maximum', 'exclusiveMaximum'), + new ExclusiveMinimumKeywordParser('exclusiveMinimum'), + new ExclusiveMaximumKeywordParser('exclusiveMaximum'), + new MultipleOfKeywordParser('multipleOf'), + + // Array + new MinItemsKeywordParser('minItems'), + new MaxItemsKeywordParser('maxItems'), + new UniqueItemsKeywordParser('uniqueItems'), + new ContainsKeywordParser('contains', 'minContains', 'maxContains'), + new ItemsKeywordParser('items'), + new AdditionalItemsKeywordParser('additionalItems'), + + // Object + new MinPropertiesKeywordParser('minProperties'), + new MaxPropertiesKeywordParser('maxProperties'), + new RequiredKeywordParser('required'), + new DependenciesKeywordParser('dependencies'), // keep for draft-07 compatibility + new DependentRequiredKeywordParser('dependentRequired'), + new DependentSchemasKeywordParser('dependentSchemas'), + new PropertyNamesKeywordParser('propertyNames'), + new PropertiesKeywordParser('properties'), + new PatternPropertiesKeywordParser('patternProperties'), + new AdditionalPropertiesKeywordParser('additionalProperties'), + + // Conditionals + new IfThenElseKeywordParser('if', 'then', 'else'), + new AnyOfKeywordParser('anyOf'), + new AllOfKeywordParser('allOf'), + new OneOfKeywordParser('oneOf'), + new NotKeywordParser('not'), + + // Unevaluated + new UnevaluatedPropertiesKeywordParser('unevaluatedProperties'), + new UnevaluatedItemsKeywordParser('unevaluatedItems'), + + // Optional + new DefaultKeywordParser('default'), + ]; + } + +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/Drafts/Draft202012.php b/src/opis/json-schema/src/Parsers/Drafts/Draft202012.php new file mode 100644 index 00000000..b4e960ff --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Drafts/Draft202012.php @@ -0,0 +1,163 @@ + '$dynamicRef', 'anchor' => '$dynamicAnchor', 'fragment' => true], + ['ref' => '$recursiveRef', 'anchor' => '$recursiveAnchor', 'fragment' => false], + ]); + } + + /** + * @inheritDoc + */ + protected function getKeywordParsers(): array + { + return [ + // Generic + new TypeKeywordParser('type'), + new ConstKeywordParser('const'), + new EnumKeywordParser('enum'), + new FormatKeywordParser('format'), + + // String + new MinLengthKeywordParser('minLength'), + new MaxLengthKeywordParser('maxLength'), + new PatternKeywordParser("pattern"), + new ContentEncodingKeywordParser('contentEncoding'), + new ContentMediaTypeKeywordParser('contentMediaType'), + new ContentSchemaKeywordParser('contentSchema'), + + // Number + new MinimumKeywordParser('minimum', 'exclusiveMinimum'), + new MaximumKeywordParser('maximum', 'exclusiveMaximum'), + new ExclusiveMinimumKeywordParser('exclusiveMinimum'), + new ExclusiveMaximumKeywordParser('exclusiveMaximum'), + new MultipleOfKeywordParser('multipleOf'), + + // Array + new MinItemsKeywordParser('minItems'), + new MaxItemsKeywordParser('maxItems'), + new UniqueItemsKeywordParser('uniqueItems'), + new ContainsKeywordParser('contains', 'minContains', 'maxContains'), + new ItemsKeywordParser('prefixItems', ItemsKeywordParser::ONLY_ARRAY), + new ItemsKeywordParser('items', ItemsKeywordParser::ONLY_SCHEMA, 'prefixItems'), + // keep for draft-2019-09 compatibility + new AdditionalItemsKeywordParser('additionalItems'), + + // Object + new MinPropertiesKeywordParser('minProperties'), + new MaxPropertiesKeywordParser('maxProperties'), + new RequiredKeywordParser('required'), + new DependenciesKeywordParser('dependencies'), // keep for draft-07 compatibility + new DependentRequiredKeywordParser('dependentRequired'), + new DependentSchemasKeywordParser('dependentSchemas'), + new PropertyNamesKeywordParser('propertyNames'), + new PropertiesKeywordParser('properties'), + new PatternPropertiesKeywordParser('patternProperties'), + new AdditionalPropertiesKeywordParser('additionalProperties'), + + // Conditionals + new IfThenElseKeywordParser('if', 'then', 'else'), + new AnyOfKeywordParser('anyOf'), + new AllOfKeywordParser('allOf'), + new OneOfKeywordParser('oneOf'), + new NotKeywordParser('not'), + + // Unevaluated + new UnevaluatedPropertiesKeywordParser('unevaluatedProperties'), + new UnevaluatedItemsKeywordParser('unevaluatedItems'), + + // Optional + new DefaultKeywordParser('default'), + ]; + } + +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/KeywordParser.php b/src/opis/json-schema/src/Parsers/KeywordParser.php new file mode 100644 index 00000000..0a8fad91 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/KeywordParser.php @@ -0,0 +1,62 @@ +draft(); + return $draft !== '06' && $draft !== '07'; + } +} diff --git a/src/opis/json-schema/src/Parsers/KeywordParserTrait.php b/src/opis/json-schema/src/Parsers/KeywordParserTrait.php new file mode 100644 index 00000000..939b7886 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/KeywordParserTrait.php @@ -0,0 +1,76 @@ +keyword = $keyword; + } + + /** + * @param object|SchemaInfo $schema + * @param string|null $keyword + * @return bool + */ + protected function keywordExists($schema, $keyword = null): bool + { + if ($schema instanceof SchemaInfo) { + $schema = $schema->data(); + } + + return property_exists($schema, $keyword ?? $this->keyword); + } + + /** + * @param object|SchemaInfo $schema + * @param string|null $keyword + * @return mixed + */ + protected function keywordValue($schema, $keyword = null) + { + if ($schema instanceof SchemaInfo) { + $schema = $schema->data(); + } + + return $schema->{$keyword ?? $this->keyword}; + } + + /** + * @param string $message + * @param SchemaInfo $info + * @param string|null $keyword + * @return InvalidKeywordException + */ + protected function keywordException($message, $info, $keyword = null): InvalidKeywordException + { + $keyword = $keyword ?? $this->keyword; + + return new InvalidKeywordException(str_replace('{keyword}', $keyword, $message), $keyword, $info); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/KeywordValidatorParser.php b/src/opis/json-schema/src/Parsers/KeywordValidatorParser.php new file mode 100644 index 00000000..a1c049fe --- /dev/null +++ b/src/opis/json-schema/src/Parsers/KeywordValidatorParser.php @@ -0,0 +1,34 @@ +option('allowPragmas') || !$this->keywordExists($info)) { + return null; + } + + $value = $this->keywordValue($info); + + if (!is_object($value)) { + throw $this->keywordException('{keyword} must be an object', $info); + } + + $list = []; + + $draft = $info->draft() ?? $parser->defaultDraftVersion(); + + $pragmaInfo = new SchemaInfo($value, null, $info->id() ?? $info->base(), $info->root(), + array_merge($info->path(), [$this->keyword]), $draft); + + foreach ($parser->draft($draft)->pragmas() as $pragma) { + if ($handler = $pragma->parse($pragmaInfo, $parser, $shared)) { + $list[] = $handler; + } + } + + return $list ? new PragmaKeywordValidator($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/AdditionalItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/AdditionalItemsKeywordParser.php new file mode 100644 index 00000000..3f527daf --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/AdditionalItemsKeywordParser.php @@ -0,0 +1,66 @@ +option('keepAdditionalItemsKeyword') && $info->draft() === '2020-12') { + return null; + } + + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + if (!property_exists($schema, 'items') || !is_array($schema->items)) { + // Ignore additionalItems + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_bool($value) && !is_object($value)) { + throw $this->keywordException("{keyword} must be a json schema (object or boolean)", $info); + } + + return new AdditionalItemsKeyword($value, count($schema->items)); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/AdditionalPropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/AdditionalPropertiesKeywordParser.php new file mode 100644 index 00000000..bc889504 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/AdditionalPropertiesKeywordParser.php @@ -0,0 +1,57 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_bool($value) && !is_object($value)) { + throw $this->keywordException("{keyword} must be a json schema (object or boolean)", $info); + } + + return new AdditionalPropertiesKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/AllOfKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/AllOfKeywordParser.php new file mode 100644 index 00000000..808b9f6a --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/AllOfKeywordParser.php @@ -0,0 +1,78 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_array($value)) { + throw $this->keywordException("{keyword} should be an array of json schemas", $info); + } + + if (!$value) { + throw $this->keywordException("{keyword} must have at least one element", $info); + } + + $valid = 0; + + foreach ($value as $index => $item) { + if ($item === false) { + throw $this->keywordException("{keyword} contains false schema", $info); + } + if ($item === true) { + $valid++; + continue; + } + if (!is_object($item)) { + throw $this->keywordException("{keyword}[{$index}] must be a json schema", $info); + } elseif (!count(get_object_vars($item))) { + $valid++; + } + } + + return $valid !== count($value) ? new AllOfKeyword($value) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/AnyOfKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/AnyOfKeywordParser.php new file mode 100644 index 00000000..21458985 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/AnyOfKeywordParser.php @@ -0,0 +1,78 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_array($value)) { + throw $this->keywordException("{keyword} should be an array of json schemas", $info); + } + + if (!$value) { + throw $this->keywordException("{keyword} must have at least one element", $info); + } + + $alwaysValid = false; + + foreach ($value as $index => $item) { + if ($item === true) { + $alwaysValid = true; + continue; + } + if ($item === false) { + continue; + } + if (!is_object($item)) { + throw $this->keywordException("{keyword}[{$index}] must be a json schema", $info); + } elseif (!count(get_object_vars($item))) { + $alwaysValid = true; + } + } + + return new AnyOfKeyword($value, $alwaysValid); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ConstKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ConstKeywordParser.php new file mode 100644 index 00000000..8ac34a90 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ConstKeywordParser.php @@ -0,0 +1,77 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new ConstDataKeyword($pointer); + } + } + + $type = Helper::getJsonType($value); + if ($type === null) { + throw $this->keywordException("{keyword} contains unknown json data type", $info); + } + + if (isset($shared->types)) { + if (!Helper::jsonTypeMatches($type, $shared->types)) { + throw $this->keywordException("{keyword} contains a value that doesn't match the type keyword", $info); + } + } else { + $shared->types = [$type]; + } + + return new ConstKeyword($value); + } + +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ContainsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ContainsKeywordParser.php new file mode 100644 index 00000000..37e1de77 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ContainsKeywordParser.php @@ -0,0 +1,94 @@ +minContains = $minContains; + $this->maxContains = $maxContains; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_ARRAY; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_bool($value) && !is_object($value)) { + throw $this->keywordException("{keyword} must be a json schema (object or boolean)", $info); + } + + $min = $max = null; + + if ($this->minContains && $this->keywordExists($schema, $this->minContains)) { + $min = $this->keywordValue($schema, $this->minContains); + if (!is_int($min) || $min < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info, $this->minContains); + } + } + + if ($this->maxContains && $this->keywordExists($schema, $this->maxContains)) { + $max = $this->keywordValue($schema, $this->maxContains); + if (!is_int($max) || $max < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info, $this->maxContains); + } + if ($min !== null && $max < $min) { + throw $this->keywordException("{keyword} must be greater than {$this->minContains}", $info, $this->maxContains); + } + } elseif ($min === 0) { + return null; + } + + return new ContainsKeyword($value, $min, $max); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ContentEncodingKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ContentEncodingKeywordParser.php new file mode 100644 index 00000000..4f55e062 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ContentEncodingKeywordParser.php @@ -0,0 +1,65 @@ +optionAllowedForDraft('decodeContent', $info, $parser)) { + return null; + } + + $schema = $info->data(); + + $resolver = $parser->getContentEncodingResolver(); + + if (!$resolver || !$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_string($value)) { + throw $this->keywordException("{keyword} must be a string", $info); + } + + return new ContentEncodingKeyword(strtolower($value), $resolver); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ContentMediaTypeKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ContentMediaTypeKeywordParser.php new file mode 100644 index 00000000..82a7d8f9 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ContentMediaTypeKeywordParser.php @@ -0,0 +1,65 @@ +optionAllowedForDraft('decodeContent', $info, $parser)) { + return null; + } + + $schema = $info->data(); + + $resolver = $parser->getMediaTypeResolver(); + + if (!$resolver || !$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_string($value)) { + throw $this->keywordException("{keyword} must be a string", $info); + } + + return new ContentMediaTypeKeyword($value, $resolver); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ContentSchemaKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ContentSchemaKeywordParser.php new file mode 100644 index 00000000..08c1c1e9 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ContentSchemaKeywordParser.php @@ -0,0 +1,63 @@ +optionAllowedForDraft('decodeContent', $info, $parser)) { + return null; + } + + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be a valid json schema object", $info); + } + + return new ContentSchemaKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/DefaultKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/DefaultKeywordParser.php new file mode 100644 index 00000000..99f1dcd3 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/DefaultKeywordParser.php @@ -0,0 +1,91 @@ +properties = $properties; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_APPEND; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$parser->option('allowDefaults')) { + return null; + } + + $defaults = null; + + if ($this->keywordExists($schema)) { + $defaults = $this->keywordValue($schema); + + if (is_object($defaults)) { + $defaults = (array)Helper::cloneValue($defaults); + } else { + $defaults = null; + } + } + + if ($this->properties !== null && property_exists($schema, $this->properties) + && is_object($schema->{$this->properties})) { + foreach ($schema->{$this->properties} as $name => $value) { + if (is_object($value) && property_exists($value, $this->keyword)) { + $defaults[$name] = $value->{$this->keyword}; + } + } + } + + if (!$defaults) { + return null; + } + + return new DefaultKeyword($defaults); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/DependenciesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/DependenciesKeywordParser.php new file mode 100644 index 00000000..ca3cc507 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/DependenciesKeywordParser.php @@ -0,0 +1,83 @@ +option('keepDependenciesKeyword') && !in_array($info->draft(), ['06', '07'])) { + return null; + } + + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be an object", $info); + } + + $list = get_object_vars($value); + + foreach ($list as $name => $s) { + if (is_array($s)) { + if (!$s) { + unset($list[$name]); + continue; + } + foreach ($s as $p) { + if (!is_string($p)) { + throw $this->keywordException("{keyword} must be an object containing json schemas or arrays of property names", $info); + } + } + $list[$name] = array_unique($s); + } elseif (is_bool($s)) { + if ($s) { + unset($list[$name]); + } + } elseif (!is_object($s)) { + throw $this->keywordException("{keyword} must be an object containing json schemas or arrays of property names", $info); + } + } + + return $list ? new DependenciesKeyword($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/DependentRequiredKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/DependentRequiredKeywordParser.php new file mode 100644 index 00000000..da237989 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/DependentRequiredKeywordParser.php @@ -0,0 +1,73 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be an object", $info); + } + + $list = []; + foreach ($value as $name => $s) { + if (!is_array($s)) { + throw $this->keywordException("{keyword} must be an object containing json schemas or arrays of property names", $info); + } + if (!$s) { + // Empty array + continue; + } + foreach ($s as $p) { + if (!is_string($p)) { + throw $this->keywordException("{keyword} must be an object containing arrays of property names", $info); + } + } + $list[$name] = array_unique($s); + } + + return $list ? new DependentRequiredKeyword($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/DependentSchemasKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/DependentSchemasKeywordParser.php new file mode 100644 index 00000000..f77f980a --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/DependentSchemasKeywordParser.php @@ -0,0 +1,76 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be an object", $info); + } + + $valid = 0; + $total = 0; + + foreach ($value as $name => $s) { + $total++; + if (is_bool($s)) { + if ($s) { + $valid++; + } + } elseif (!is_object($s)) { + throw $this->keywordException("{keyword} must be an object containing json schemas", $info); + } elseif (!count(get_object_vars($s))) { + $valid++; + } + } + + if (!$total) { + return null; + } + + return $valid !== $total ? new DependentSchemasKeyword($value) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/EnumKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/EnumKeywordParser.php new file mode 100644 index 00000000..2aa6465f --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/EnumKeywordParser.php @@ -0,0 +1,110 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new EnumDataKeyword($pointer); + } + } + + if (!is_array($value) || !$value) { + throw $this->keywordException("{keyword} must be a non-empty array", $info); + } + + $hasConst = property_exists($schema, 'const'); + $constMatched = false; + + $allowedTypes = isset($shared->types) ? $shared->types : null; + $foundTypes = []; + $list = []; + foreach ($value as $item) { + $type = Helper::getJsonType($item); + if ($type === null) { + throw $this->keywordException("{keyword} contains invalid json data type", $info); + } + + if ($allowedTypes && !Helper::jsonTypeMatches($type, $allowedTypes)) { + continue; + } + + if ($hasConst && Helper::equals($item, $schema->const)) { + $constMatched = true; + break; + } + + if (!in_array($type, $foundTypes)) { + $foundTypes[] = $type; + } + + $list[] = $item; + } + + if ($hasConst) { + if ($constMatched) { + return null; + } + throw $this->keywordException("{keyword} does not contain the value of const keyword", $info); + } + + if ($foundTypes) { + if ($allowedTypes === null) { + $shared->types = $foundTypes; + } else { + $shared->types = array_unique(array_merge($shared->types, $foundTypes)); + } + } + + return new EnumKeyword($list); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMaximumKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMaximumKeywordParser.php new file mode 100644 index 00000000..8f3cc97e --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMaximumKeywordParser.php @@ -0,0 +1,73 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (is_bool($value) && $parser->option('allowExclusiveMinMaxAsBool')) { + return null; + } + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new ExclusiveMaximumDataKeyword($pointer); + } + } + + if (!is_int($value) && !is_float($value) || is_nan($value) || !is_finite($value)) { + throw $this->keywordException('{keyword} must contain a valid number', $info); + } + + return new ExclusiveMaximumKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMinimumKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMinimumKeywordParser.php new file mode 100644 index 00000000..0f3ab97b --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ExclusiveMinimumKeywordParser.php @@ -0,0 +1,73 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (is_bool($value) && $parser->option('allowExclusiveMinMaxAsBool')) { + return null; + } + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new ExclusiveMinimumDataKeyword($pointer); + } + } + + if (!is_int($value) && !is_float($value) || is_nan($value) || !is_finite($value)) { + throw $this->keywordException('{keyword} must contain a valid number', $info); + } + + return new ExclusiveMinimumKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/FiltersKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/FiltersKeywordParser.php new file mode 100644 index 00000000..477c6591 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/FiltersKeywordParser.php @@ -0,0 +1,164 @@ +data(); + + if (!$parser->option('allowFilters')) { + return null; + } + + $resolver = $parser->getFilterResolver(); + + if (!$resolver || !$this->keywordExists($schema)) { + return null; + } + + $filters = $this->parseFilters($parser, $resolver, $this->keywordValue($schema), $info); + if (!$filters) { + return null; + } + + return new FiltersKeyword($filters); + } + + /** + * @param SchemaParser $parser + * @param FilterResolver $filterResolver + * @param mixed $filters + * @param SchemaInfo $info + * @return array|null + */ + protected function parseFilters( + $parser, + $filterResolver, + $filters, + $info + ) + { + if (is_string($filters)) { + if ($filters = $this->parseFilter($parser, $filterResolver, $filters, $info)) { + return [$filters]; + } + + return null; + } + + if (is_object($filters)) { + if ($filter = $this->parseFilter($parser, $filterResolver, $filters, $info)) { + return [$filter]; + } + + return null; + } + + if (is_array($filters)) { + if (!$filters) { + return null; + } + $list = []; + foreach ($filters as $filter) { + if ($filter = $this->parseFilter($parser, $filterResolver, $filter, $info)) { + $list[] = $filter; + } + } + + return $list ?: null; + } + + throw $this->keywordException('{keyword} can be a non-empty string, an object or an array of string and objects', $info); + } + + /** + * @param SchemaParser $parser + * @param FilterResolver $resolver + * @param $filter + * @param SchemaInfo $info + * @return object|null + */ + protected function parseFilter( + $parser, + $resolver, + $filter, + $info + ) + { + $vars = null; + if (is_object($filter)) { + if (!property_exists($filter, '$func') || !is_string($filter->{'$func'}) || $filter->{'$func'} === '') { + throw $this->keywordException('$func (for {keyword}) must be a non-empty string', $info); + } + + $vars = get_object_vars($filter); + unset($vars['$func']); + + if (property_exists($filter, '$vars')) { + if (!is_object($filter->{'$vars'})) { + throw $this->keywordException('$vars (for {keyword}) must be a string', $info); + } + unset($vars['$vars']); + $vars = get_object_vars($filter->{'$vars'}) + $vars; + } + + $filter = $filter->{'$func'}; + } elseif (!is_string($filter) || $filter === '') { + throw $this->keywordException('{keyword} can be a non-empty string, an object or an array of string and objects', $info); + } + + $list = $resolver->resolveAll($filter); + if (!$list) { + throw $this->keywordException("{keyword}: {$filter} doesn't exists", $info); + } + + $list = $this->resolveSubTypes($list); + + return (object)[ + 'name' => $filter, + 'args' => $vars ? $this->createVariables($parser, $vars) : null, + 'types' => $list, + ]; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/FormatKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/FormatKeywordParser.php new file mode 100644 index 00000000..4791e05f --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/FormatKeywordParser.php @@ -0,0 +1,74 @@ +data(); + + $resolver = $parser->getFormatResolver(); + + if (!$resolver || !$parser->option('allowFormats') || !$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new FormatDataKeyword($pointer, $resolver); + } + } + + if (!is_string($value)) { + throw $this->keywordException("{keyword} must be a string", $info); + } + + $list = $resolver->resolveAll($value); + + if (!$list) { + return null; + } + + return new FormatKeyword($value, $this->resolveSubTypes($list)); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/IfThenElseKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/IfThenElseKeywordParser.php new file mode 100644 index 00000000..df8862ba --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/IfThenElseKeywordParser.php @@ -0,0 +1,118 @@ +then = $then; + $this->else = $else; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_AFTER; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $if = $this->keywordValue($schema); + if (!$this->isJsonSchema($if)) { + throw $this->keywordException("{keyword} keyword must be a json schema", $info); + } + + $then = true; + if (property_exists($schema, $this->then)) { + $then = $schema->{$this->then}; + } + if (!$this->isJsonSchema($then)) { + throw $this->keywordException("{keyword} keyword must be a json schema", $info, $this->then); + } + + $else = true; + if (property_exists($schema, $this->else)) { + $else = $schema->{$this->else}; + } + if (!$this->isJsonSchema($else)) { + throw $this->keywordException("{keyword} keyword must be a json schema", $info, $this->else); + } + + if ($if === true) { + if ($then === true) { + return null; + } + $else = true; + } elseif ($if === false) { + if ($else === true) { + return null; + } + $then = true; + } elseif ($then === true && $else === true) { + return null; + } + + return new IfThenElseKeyword($if, $then, $else); + } + + /** + * @param $value + * @return bool + */ + protected function isJsonSchema($value): bool + { + return is_bool($value) || is_object($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/ItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/ItemsKeywordParser.php new file mode 100644 index 00000000..d944ee04 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/ItemsKeywordParser.php @@ -0,0 +1,126 @@ +mode = $mode; + $this->startIndexKeyword = $startIndexKeyword; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_ARRAY; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + $alwaysValid = false; + + if (is_bool($value)) { + if ($this->mode === self::ONLY_ARRAY) { + throw $this->keywordException("{keyword} must contain an array of json schemas", $info); + } + if ($value) { + $alwaysValid = true; + } + } elseif (is_array($value)) { + if ($this->mode === self::ONLY_SCHEMA) { + throw $this->keywordException("{keyword} must contain a valid json schema", $info); + } + $valid = 0; + foreach ($value as $index => $v) { + if (is_bool($v)) { + if ($v) { + $valid++; + } + } elseif (!is_object($v)) { + throw $this->keywordException("{keyword}[$index] must contain a valid json schema", $info); + } elseif (!count(get_object_vars($v))) { + $valid++; + } + } + if ($valid === count($value)) { + $alwaysValid = true; + } + } elseif (!is_object($value)) { + if ($this->mode === self::BOTH) { + throw $this->keywordException("{keyword} must be a json schema or an array of json schemas", $info); + } elseif ($this->mode === self::ONLY_ARRAY) { + throw $this->keywordException("{keyword} must contain an array of json schemas", $info); + } else { + throw $this->keywordException("{keyword} must contain a valid json schema", $info); + } + } else { + if ($this->mode === self::ONLY_ARRAY) { + throw $this->keywordException("{keyword} must contain an array of json schemas", $info); + } + if (!count(get_object_vars($value))) { + $alwaysValid = true; + } + } + + $startIndex = 0; + if ($this->startIndexKeyword !== null && $this->keywordExists($schema, $this->startIndexKeyword)) { + $start = $this->keywordValue($schema, $this->startIndexKeyword); + if (is_array($start)) { + $startIndex = count($start); + } + } + + return new ItemsKeyword($value, $alwaysValid, $this->keyword, $startIndex); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MaxItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MaxItemsKeywordParser.php new file mode 100644 index 00000000..f33cd92d --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MaxItemsKeywordParser.php @@ -0,0 +1,66 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MaxItemsDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} most be a positive integer", $info); + } + + return new MaxItemsKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MaxLengthKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MaxLengthKeywordParser.php new file mode 100644 index 00000000..5f691d53 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MaxLengthKeywordParser.php @@ -0,0 +1,66 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MaxLengthDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info); + } + + return new MaxLengthKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MaxPropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MaxPropertiesKeywordParser.php new file mode 100644 index 00000000..dc42ce46 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MaxPropertiesKeywordParser.php @@ -0,0 +1,66 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MaxPropertiesDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info); + } + + return new MaxPropertiesKeywords($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MaximumKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MaximumKeywordParser.php new file mode 100644 index 00000000..e016403d --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MaximumKeywordParser.php @@ -0,0 +1,97 @@ +exclusiveKeyword = $exclusiveKeyword; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_NUMBER; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + $exclusive = false; + if ($parser->option('allowExclusiveMinMaxAsBool') && + $this->exclusiveKeyword !== null && + property_exists($schema, $this->exclusiveKeyword)) { + $exclusive = $schema->{$this->exclusiveKeyword} === true; + } + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return $exclusive + ? new ExclusiveMaximumDataKeyword($pointer) + : new MaximumDataKeyword($pointer); + } + } + + if (!is_int($value) && !is_float($value) || is_nan($value) || !is_finite($value)) { + throw $this->keywordException('{keyword} must contain a valid number', $info); + } + + return $exclusive + ? new ExclusiveMaximumKeyword($value) + : new MaximumKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MinItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MinItemsKeywordParser.php new file mode 100644 index 00000000..2e36c18b --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MinItemsKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MinItemsDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} most be a positive integer", $info); + } + + if ($value === 0) { + return null; + } + + return new MinItemsKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MinLengthKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MinLengthKeywordParser.php new file mode 100644 index 00000000..261330e7 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MinLengthKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MinLengthDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info); + } + + if ($value === 0) { + return null; + } + + return new MinLengthKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MinPropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MinPropertiesKeywordParser.php new file mode 100644 index 00000000..62592705 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MinPropertiesKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MinPropertiesDataKeyword($pointer); + } + } + + if (!is_int($value) || $value < 0) { + throw $this->keywordException("{keyword} must be a non-negative integer", $info); + } + + if ($value === 0) { + return null; + } + + return new MinPropertiesKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MinimumKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MinimumKeywordParser.php new file mode 100644 index 00000000..09fd9119 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MinimumKeywordParser.php @@ -0,0 +1,97 @@ +exclusiveKeyword = $exclusiveKeyword; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_NUMBER; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $schema = $info->data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + $exclusive = false; + if ($parser->option('allowExclusiveMinMaxAsBool') && + $this->exclusiveKeyword !== null && + property_exists($schema, $this->exclusiveKeyword)) { + $exclusive = $schema->{$this->exclusiveKeyword} === true; + } + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return $exclusive + ? new ExclusiveMinimumDataKeyword($pointer) + : new MinimumDataKeyword($pointer); + } + } + + if (!is_int($value) && !is_float($value) || is_nan($value) || !is_finite($value)) { + throw $this->keywordException('{keyword} must contain a valid number', $info); + } + + return $exclusive + ? new ExclusiveMinimumKeyword($value) + : new MinimumKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/MultipleOfKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/MultipleOfKeywordParser.php new file mode 100644 index 00000000..516ca674 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/MultipleOfKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new MultipleOfDataKeyword($pointer); + } + } + + if (!is_int($value) && !is_float($value) || is_nan($value) || !is_finite($value)) { + throw $this->keywordException("{keyword} must be a valid number (integer or float)", $info); + } + + if ($value <= 0) { + throw $this->keywordException("{keyword} must be greater than zero", $info); + } + + return new MultipleOfKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/NotKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/NotKeywordParser.php new file mode 100644 index 00000000..7832bbce --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/NotKeywordParser.php @@ -0,0 +1,61 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (is_bool($value)) { + if (!$value) { + return null; + } + } elseif (!is_object($value)) { + throw $this->keywordException("{keyword} must contain a json schema (object or boolean)", $info); + } + + return new NotKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/OneOfKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/OneOfKeywordParser.php new file mode 100644 index 00000000..a0570f2c --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/OneOfKeywordParser.php @@ -0,0 +1,82 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_array($value)) { + throw $this->keywordException("{keyword} should be an array of json schemas", $info); + } + + if (!$value) { + throw $this->keywordException("{keyword} must have at least one element", $info); + } + + $valid = 0; + + foreach ($value as $index => $item) { + if ($item === false) { + continue; + } + if ($item === true) { + if (++$valid > 1) { + throw $this->keywordException("{keyword} contains multiple true values", $info); + } + continue; + } + if (!is_object($item)) { + throw $this->keywordException("{keyword}[{$index}] must be a json schema", $info); + } elseif (!count(get_object_vars($item))) { + if (++$valid > 1) { + throw $this->keywordException("{keyword} contains multiple true values", $info); + } + } + } + + return new OneOfKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/PatternKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/PatternKeywordParser.php new file mode 100644 index 00000000..2ffd51e4 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/PatternKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new PatternDataKeyword($pointer); + } + } + + if (!is_string($value)) { + throw $this->keywordException("{keyword} value must be a string", $info); + } + + if (!Helper::isValidPattern($value)) { + throw $this->keywordException("{keyword} value must be a valid regex", $info); + } + + return new PatternKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/PatternPropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/PatternPropertiesKeywordParser.php new file mode 100644 index 00000000..c6aff641 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/PatternPropertiesKeywordParser.php @@ -0,0 +1,70 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be an object", $info); + } + + $list = []; + + foreach ($value as $pattern => $item) { + if (!Helper::isValidPattern($pattern)) { + throw $this->keywordException("Each property name from {keyword} must be valid regex", $info); + } + + if (!is_bool($item) && !is_object($item)) { + throw $this->keywordException("{keyword}[{$pattern}] must be a json schema (object or boolean)", $info); + } + + $list[$pattern] = $item; + } + + return $list ? new PatternPropertiesKeyword($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/PropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/PropertiesKeywordParser.php new file mode 100644 index 00000000..faa3312b --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/PropertiesKeywordParser.php @@ -0,0 +1,67 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_object($value)) { + throw $this->keywordException("{keyword} must be an object", $info); + } + + $list = []; + + foreach ($value as $name => $s) { + if (!is_bool($s) && !is_object($s)) { + throw $this->keywordException("{keyword}[{$name}] must be a json schema (object or boolean)", $info); + } + + $list[$name] = $s; + } + + return $list ? new PropertiesKeyword($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/PropertyNamesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/PropertyNamesKeywordParser.php new file mode 100644 index 00000000..8e03e387 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/PropertyNamesKeywordParser.php @@ -0,0 +1,61 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (is_bool($value)) { + if ($value) { + return null; + } + } elseif (!is_object($value)) { + throw $this->keywordException("{keyword} must be a valid json schema (object or boolean)", $info); + } + + return new PropertyNamesKeyword($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/RefKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/RefKeywordParser.php new file mode 100644 index 00000000..554ddcfa --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/RefKeywordParser.php @@ -0,0 +1,234 @@ +variations = $variations; + } + + /** + * @inheritDoc + */ + public function type(): string + { + return self::TYPE_AFTER_REF; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\Info\SchemaInfo $info + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + * @param object $shared + */ + public function parse($info, $parser, $shared) + { + $ref = null; + $recursive = false; + $schema = $info->data(); + $variation = null; + + if ($this->keywordExists($schema)) { + $ref = $this->keywordValue($schema); + if (!is_string($ref) || $ref === '') { + throw $this->keywordException('{keyword} must be a non-empty string', $info); + } + } elseif ($this->variations) { + foreach ($this->variations as $v) { + if (!$this->keywordExists($schema, $v['ref'])) { + continue; + } + $ref = $this->keywordValue($schema, $v['ref']); + if ($v['fragment']) { + if (!preg_match('/^#[a-z][a-z0-9\\-.:_]*/i', $ref)) { + $this->keywordException("{keyword} value is malformed", $info, $v['ref']); + } + } elseif ($ref !== '#') { + $this->keywordException("{keyword} supports only '#' as value", $info, $v['ref']); + } + $variation = $v; + $recursive = true; + break; + } + if (!$recursive) { + return null; + } + } else { + return null; + } + + // Mappers + $mapper = null; + if ($parser->option('allowMappers') && property_exists($schema, '$map')) { + if (!is_object($schema->{'$map'}) && !is_array($schema->{'$map'})) { + throw $this->keywordException('$map keyword must be an object or an array', $info, '$map'); + } + + if (!empty($schema->{'$map'})) { + $mapper = $this->createVariables($parser, $schema->{'$map'}); + } + } + + // Globals + $globals = null; + if ($parser->option('allowGlobals') && property_exists($schema, '$globals')) { + if (!is_object($schema->{'$globals'})) { + throw $this->keywordException('$globals keyword must be an object', $info, '$globals'); + } + + if (!empty($schema->{'$globals'})) { + $globals = $this->createVariables($parser, $schema->{'$globals'}); + } + } + + // Pass slots + $slots = null; + if ($parser->option('allowSlots') && property_exists($schema, '$inject')) { + $slots = $this->parseInjectedSlots($info, $parser, '$inject'); + } + + if ($recursive) { + $ref = $info->idBaseRoot()->resolveRef($ref); + if ($variation['fragment']) { + return new RecursiveRefKeyword($ref->resolveRef('#'), $mapper, $globals, $slots, + $variation['ref'], $variation['anchor'], $ref->fragment()); + } + return new RecursiveRefKeyword($ref, $mapper, $globals, $slots, + $variation['ref'], $variation['anchor'], true); + } + + if ($ref === '#') { + return new URIRefKeyword(Uri::merge('#', $info->idBaseRoot()), $mapper, $globals, $slots, $this->keyword); + } + + if ($parser->option('allowTemplates') && UriTemplate::isTemplate($ref)) { + $tpl = new UriTemplate($ref); + + if ($tpl->hasPlaceholders()) { + $vars = null; + + if (property_exists($schema, '$vars')) { + if (!is_object($schema->{'$vars'})) { + throw $this->keywordException('$vars keyword must be an object', $info, '$vars'); + } + + if (!empty($schema->{'$vars'})) { + $vars = $this->createVariables($parser, $schema->{'$vars'}); + } + } + + return new TemplateRefKeyword( + $tpl, $vars, $mapper, + $globals, $slots, $this->keyword, + $parser->option('allowRelativeJsonPointerInRef') + ); + } + + unset($tpl); + } + + if ($ref[0] === '#') { + if (($pointer = JsonPointer::parse(substr($ref, 1))) && $pointer->isAbsolute()) { + return new PointerRefKeyword($pointer, $mapper, $globals, $slots, $this->keyword); + } + } elseif ($parser->option('allowRelativeJsonPointerInRef') && + ($pointer = JsonPointer::parse($ref)) && $pointer->isRelative()) { + return new PointerRefKeyword($pointer, $mapper, $globals, $slots, $this->keyword); + } + + $ref = Uri::merge($ref, $info->idBaseRoot(), true); + + if ($ref === null || !$ref->isAbsolute()) { + throw $this->keywordException('{keyword} must be a valid uri, uri-reference, uri-template or json-pointer', + $info); + } + + return new URIRefKeyword($ref, $mapper, $globals, $slots, $this->keyword); + } + + /** + * @param SchemaInfo $info + * @param SchemaParser $parser + * @param string $keyword + * @return string[]|object[]|Schema[] + */ + protected function parseInjectedSlots($info, $parser, $keyword) + { + $schema = $info->data(); + + if (!is_object($schema->{$keyword})) { + throw $this->keywordException('{keyword} keyword value must be an object', $info, $keyword); + } + + return $this->getSlotSchemas($info, $parser, $schema->{$keyword}, [$keyword]); + } + + /** + * @param SchemaInfo $info + * @param SchemaParser $parser + * @param object $slots + * @param array $path + * @return null + */ + protected function getSlotSchemas($info, $parser, $slots, $path) + { + $keyword = null; + if ($path) { + $keyword = end($path); + $path = array_merge($info->path(), $path); + } else { + $path = $info->path(); + } + + $list = []; + + foreach ($slots as $name => $value) { + if ($value === null) { + continue; + } + if (is_string($value) || is_object($value)) { + $list[$name] = $value; + } elseif (is_bool($value)) { + $list[$name] = $parser->parseSchema(new SchemaInfo( + $value, null, $info->id() ?? $info->base(), $info->root(), + array_merge($path, [$name]), + $info->draft() ?? $parser->defaultDraftVersion() + )); + } else { + throw $this->keywordException('Slots must contain valid json schemas or slot names', $info, $keyword); + } + } + + return $list ?: null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/RequiredKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/RequiredKeywordParser.php new file mode 100644 index 00000000..b723877f --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/RequiredKeywordParser.php @@ -0,0 +1,110 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + $filter = $this->propertiesFilter($parser, $schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new RequiredDataKeyword($pointer, $filter); + } + } + + if (!is_array($value)) { + throw $this->keywordException("{keyword} must be an array of strings", $info); + } + + foreach ($value as $name) { + if (!is_string($name)) { + throw $this->keywordException("{keyword} must be an array of strings", $info); + } + } + + if ($filter) { + $value = array_filter($value, $filter === null ? function ($value, $key) : bool { + return !empty($value); + } : $filter, $filter === null ? ARRAY_FILTER_USE_BOTH : 0); + } + + return $value ? new RequiredKeyword(array_unique($value)) : null; + } + + /** + * @param SchemaParser $parser + * @param object $schema + * @return callable|null + */ + protected function propertiesFilter($parser, $schema) + { + if (!$parser->option('allowDefaults')) { + return null; + } + + if (!property_exists($schema, 'properties') || !is_object($schema->properties)) { + return null; + } + + $props = $schema->properties; + + return static function (string $name) use ($props) { + if (!property_exists($props, $name)) { + return true; + } + + if (is_object($props->{$name}) && property_exists($props->{$name}, 'default')) { + return false; + } + + return true; + }; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/SlotsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/SlotsKeywordParser.php new file mode 100644 index 00000000..609e750d --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/SlotsKeywordParser.php @@ -0,0 +1,67 @@ +data(); + + if (!$parser->option('allowSlots') || !$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if (!is_object($value)) { + throw $this->keywordException('{keyword} keyword value must be an object', $info); + } + + $slots = []; + foreach ($value as $name => $fallback) { + if (!is_string($name) || $name === '') { + continue; + } + if (is_bool($fallback) || is_string($fallback) || is_object($fallback)) { + $slots[$name] = $fallback; + } + } + + return $slots ? new SlotsKeyword($slots) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/TypeKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/TypeKeywordParser.php new file mode 100644 index 00000000..8aa9d79b --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/TypeKeywordParser.php @@ -0,0 +1,81 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $type = $this->keywordValue($schema); + + if (is_string($type)) { + $type = [$type]; + } elseif (!is_array($type)) { + throw $this->keywordException('{keyword} can only be a string or an array of string', $info); + } + + foreach ($type as $t) { + if (!Helper::isValidJsonType($t)) { + throw $this->keywordException("{keyword} contains invalid json type: {$t}", $info); + } + } + + $type = array_unique($type); + + if (!isset($shared->types)) { + $shared->types = $type; + } else { + $shared->types = array_unique(array_merge($shared->types, $type)); + } + + $count = count($type); + + if ($count === 0) { + throw $this->keywordException("{keyword} cannot be an empty array", $info); + } elseif ($count === 1) { + $type = reset($type); + } + + return new TypeKeyword($type); + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedItemsKeywordParser.php new file mode 100644 index 00000000..ff88103e --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedItemsKeywordParser.php @@ -0,0 +1,79 @@ +data(); + + if (!$this->keywordExists($schema) || !$parser->option('allowUnevaluated')) { + return null; + } + +// if (!$this->makesSense($schema)) { +// return null; +// } + + $value = $this->keywordValue($schema); + + if (!is_bool($value) && !is_object($value)) { + throw $this->keywordException("{keyword} must be a json schema (object or boolean)", $info); + } + + return new UnevaluatedItemsKeyword($value); + } + + /** + * @param object $schema + */ + protected function makesSense($schema): bool + { + if (property_exists($schema, 'additionalItems')) { + return false; + } +// if (property_exists($schema, 'contains')) { +// return false; +// } + if (property_exists($schema, 'items') && !is_array($schema->items)) { + return false; + } + + return true; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedPropertiesKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedPropertiesKeywordParser.php new file mode 100644 index 00000000..953088bb --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/UnevaluatedPropertiesKeywordParser.php @@ -0,0 +1,74 @@ +data(); + + if (!$this->keywordExists($schema) || !$parser->option('allowUnevaluated')) { + return null; + } + +// if (!$this->makesSense($schema)) { +// return null; +// } + + $value = $this->keywordValue($schema); + + if (!is_bool($value) && !is_object($value)) { + throw $this->keywordException("{keyword} must be a json schema (object or boolean)", $info); + } + + return new UnevaluatedPropertiesKeyword($value); + } + + /** + * @param object $schema + */ + protected function makesSense($schema): bool + { + if (property_exists($schema, 'additionalProperties')) { + return false; + } + + return true; + } +} diff --git a/src/opis/json-schema/src/Parsers/Keywords/UniqueItemsKeywordParser.php b/src/opis/json-schema/src/Parsers/Keywords/UniqueItemsKeywordParser.php new file mode 100644 index 00000000..12bbcfbe --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Keywords/UniqueItemsKeywordParser.php @@ -0,0 +1,66 @@ +data(); + + if (!$this->keywordExists($schema)) { + return null; + } + + $value = $this->keywordValue($schema); + + if ($this->isDataKeywordAllowed($parser, $this->keyword)) { + if ($pointer = $this->getDataKeywordPointer($value)) { + return new UniqueItemsDataKeyword($pointer); + } + } + + if (!is_bool($value)) { + throw $this->keywordException("{keyword} must be a boolean", $info); + } + + return $value ? new UniqueItemsKeyword() : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/PragmaParser.php b/src/opis/json-schema/src/Parsers/PragmaParser.php new file mode 100644 index 00000000..1f6a7c72 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/PragmaParser.php @@ -0,0 +1,87 @@ +pragma = $pragma; + } + + /** + * @param SchemaInfo $info + * @param SchemaParser $parser + * @param object $shared + * @return Pragma|null + */ + abstract public function parse($info, $parser, $shared); + + /** + * @param object|SchemaInfo $schema + * @param string|null $pragma + * @return bool + */ + protected function pragmaExists($schema, $pragma = null): bool + { + if ($schema instanceof SchemaInfo) { + $schema = $schema->isObject() ? $schema->data() : null; + } + + return is_object($schema) && property_exists($schema, $pragma ?? $this->pragma); + } + + /** + * @param object|SchemaInfo $schema + * @param string|null $pragma + * @return mixed + */ + protected function pragmaValue($schema, $pragma = null) + { + if ($schema instanceof SchemaInfo) { + $schema = $schema->isObject() ? $schema->data() : null; + } + + return is_object($schema) ? $schema->{$pragma ?? $this->pragma} : null; + } + + /** + * @param string $message + * @param SchemaInfo $info + * @param string|null $pragma + * @return InvalidPragmaException + */ + protected function pragmaException($message, $info, $pragma = null): InvalidPragmaException + { + $pragma = $pragma ?? $this->pragma; + + return new InvalidPragmaException(str_replace('{pragma}', $pragma, $message), $pragma, $info); + } +} diff --git a/src/opis/json-schema/src/Parsers/Pragmas/CastPragmaParser.php b/src/opis/json-schema/src/Parsers/Pragmas/CastPragmaParser.php new file mode 100644 index 00000000..b382520f --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Pragmas/CastPragmaParser.php @@ -0,0 +1,48 @@ +pragmaExists($info)) { + return null; + } + + $value = $this->pragmaValue($info); + + if (!is_string($value) || !Helper::isValidJsonType($value)) { + throw $this->pragmaException('Pragma {pragma} must contain a valid json type name', $info); + } + + return new CastPragma($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Pragmas/GlobalsPragmaParser.php b/src/opis/json-schema/src/Parsers/Pragmas/GlobalsPragmaParser.php new file mode 100644 index 00000000..4bb99d65 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Pragmas/GlobalsPragmaParser.php @@ -0,0 +1,51 @@ +option('allowGlobals') || !$this->pragmaExists($info)) { + return null; + } + + $value = $this->pragmaValue($info); + + if (!is_object($value)) { + throw $this->pragmaException('Pragma {pragma} must be an object', $info); + } + + $value = get_object_vars($value); + + return $value ? new GlobalsPragma($this->createVariables($parser, $value)) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/Pragmas/MaxErrorsPragmaParser.php b/src/opis/json-schema/src/Parsers/Pragmas/MaxErrorsPragmaParser.php new file mode 100644 index 00000000..dcff09e5 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Pragmas/MaxErrorsPragmaParser.php @@ -0,0 +1,47 @@ +pragmaExists($info)) { + return null; + } + + $value = $this->pragmaValue($info); + + if (!is_int($value)) { + throw $this->pragmaException('Pragma {pragma} must be an integer', $info); + } + + return new MaxErrorsPragma($value); + } +} diff --git a/src/opis/json-schema/src/Parsers/Pragmas/SlotsPragmaParser.php b/src/opis/json-schema/src/Parsers/Pragmas/SlotsPragmaParser.php new file mode 100644 index 00000000..4bd68cfe --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Pragmas/SlotsPragmaParser.php @@ -0,0 +1,68 @@ +option('allowSlots') || !$this->pragmaExists($info)) { + return null; + } + + $value = $this->pragmaValue($info); + + if (!is_object($value)) { + throw $this->pragmaException('Pragma {pragma} must be an object', $info); + } + + $list = []; + + foreach ($value as $name => $slot) { + if ($slot === null) { + continue; + } + + if (is_bool($slot)) { + + $list[$name] = $parser->parseSchema(new SchemaInfo( + $slot, null, $info->base(), $info->root(), + array_merge($info->path(), [$this->pragma, $name]), + $info->draft() ?? $parser->defaultDraftVersion() + )); + } elseif (is_string($slot) || is_object($slot)) { + $list[$name] = $slot; + } else { + throw $this->pragmaException('Pragma {pragma} contains invalid value for slot ' . $name, $info); + } + } + + return $list ? new SlotsPragma($list) : null; + } +} diff --git a/src/opis/json-schema/src/Parsers/ResolverTrait.php b/src/opis/json-schema/src/Parsers/ResolverTrait.php new file mode 100644 index 00000000..a4916fda --- /dev/null +++ b/src/opis/json-schema/src/Parsers/ResolverTrait.php @@ -0,0 +1,38 @@ + $super) { + if (!isset($list[$sub]) && isset($list[$super])) { + $list[$sub] = $list[$super]; + } + } + + return $list; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/SchemaParser.php b/src/opis/json-schema/src/Parsers/SchemaParser.php new file mode 100644 index 00000000..2678c622 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/SchemaParser.php @@ -0,0 +1,644 @@ + true, + 'allowFormats' => true, + 'allowMappers' => true, + 'allowTemplates' => true, + 'allowGlobals' => true, + 'allowDefaults' => true, + 'allowSlots' => true, + 'allowKeywordValidators' => true, + 'allowPragmas' => true, + + 'allowDataKeyword' => true, + 'allowKeywordsAlongsideRef' => false, + 'allowUnevaluated' => true, + 'allowRelativeJsonPointerInRef' => true, + 'allowExclusiveMinMaxAsBool' => true, + + 'keepDependenciesKeyword' => true, + 'keepAdditionalItemsKeyword' => true, + + 'decodeContent' => ['06', '07'], + 'defaultDraft' => self::DEFAULT_DRAFT, + + 'varRefKey' => '$ref', + 'varEachKey' => '$each', + 'varDefaultKey' => 'default', + ]; + + /** @var array */ + protected $options; + + /** @var Draft[] */ + protected $drafts; + + /** @var array */ + protected $resolvers; + + /** + * @param array $resolvers + * @param array $options + * @param Vocabulary|null $extraVocabulary + */ + public function __construct( + array $resolvers = [], + array $options = [], + $extraVocabulary = null + ) + { + if ($options) { + $this->options = $options + self::DEFAULT_OPTIONS; + } else { + $this->options = self::DEFAULT_OPTIONS; + } + + $this->resolvers = $this->getResolvers($resolvers); + + $this->drafts = $this->getDrafts($extraVocabulary ?? new DefaultVocabulary()); + } + + /** + * @param Vocabulary|null $extraVocabulary + * @return array + */ + protected function getDrafts($extraVocabulary): array + { + return [ + '06' => new Draft06($extraVocabulary), + '07' => new Draft07($extraVocabulary), + '2019-09' => new Draft201909($extraVocabulary), + '2020-12' => new Draft202012($extraVocabulary), + ]; + } + + /** + * @param array $resolvers + * @return array + */ + protected function getResolvers($resolvers): array + { + if (!array_key_exists('format', $resolvers)) { + $resolvers['format'] = new FormatResolver(); + } + + if (!array_key_exists('contentEncoding', $resolvers)) { + $resolvers['contentEncoding'] = new ContentEncodingResolver(); + } + + if (!array_key_exists('contentMediaType', $resolvers)) { + $resolvers['contentMediaType'] = new ContentMediaTypeResolver(); + } + + if (!array_key_exists('$filters', $resolvers)) { + $resolvers['$filters'] = new FilterResolver(); + } + + return $resolvers; + } + + /** + * @param string $name + * @param null $default + * @return mixed|null + */ + public function option($name, $default = null) + { + return $this->options[$name] ?? $default; + } + + /** + * @param string $name + * @param $value + * @return $this + */ + public function setOption($name, $value): self + { + $this->options[$name] = $value; + + return $this; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * @param string $name + * @param $resolver + * @return $this + */ + public function setResolver($name, $resolver): self + { + $this->resolvers[$name] = $resolver; + + return $this; + } + + /** + * @return null|FilterResolver + */ + public function getFilterResolver() + { + return $this->getResolver('$filters'); + } + + /** + * @param null|FilterResolver $resolver + * @return $this + */ + public function setFilterResolver($resolver): self + { + return $this->setResolver('$filters', $resolver); + } + + /** + * @return null|FormatResolver + */ + public function getFormatResolver() + { + return $this->getResolver('format'); + } + + /** + * @param FormatResolver|null $resolver + * @return $this + */ + public function setFormatResolver($resolver): self + { + return $this->setResolver('format', $resolver); + } + + /** + * @return null|ContentEncodingResolver + */ + public function getContentEncodingResolver() + { + return $this->getResolver('contentEncoding'); + } + + /** + * @param ContentEncodingResolver|null $resolver + * @return $this + */ + public function setContentEncodingResolver($resolver): self + { + return $this->setResolver('contentEncoding', $resolver); + } + + /** + * @return null|ContentMediaTypeResolver + */ + public function getMediaTypeResolver() + { + return $this->getResolver('contentMediaType'); + } + + /** + * @param ContentMediaTypeResolver|null $resolver + * @return $this + */ + public function setMediaTypeResolver($resolver): self + { + return $this->setResolver('contentMediaType', $resolver); + } + + /** + * @return string + */ + public function defaultDraftVersion(): string + { + return $this->option('defaultDraft', self::DEFAULT_DRAFT); + } + + /** + * @param string $draft + * @return $this + */ + public function setDefaultDraftVersion($draft): self + { + return $this->setOption('defaultDraft', $draft); + } + + /** + * @param string $schema + * @return string|null + */ + public function parseDraftVersion($schema) + { + if (!preg_match(self::DRAFT_REGEX, $schema, $m)) { + return null; + } + + return $m[1] ?? null; + } + + /** + * @param object $schema + * @return string|null + */ + public function parseId($schema) + { + if (property_exists($schema, '$id') && is_string($schema->{'$id'})) { + return $schema->{'$id'}; + } + + return null; + } + + /** + * @param object $schema + * @param string $draft + * @return string|null + */ + public function parseAnchor($schema, $draft) + { + if (!property_exists($schema, '$anchor') || + !isset($this->drafts[$draft]) || + !$this->drafts[$draft]->supportsAnchorId()) { + return null; + } + + $anchor = $schema->{'$anchor'}; + + if (!is_string($anchor) || !preg_match(self::ANCHOR_REGEX, $anchor)) { + return null; + } + + return $anchor; + } + + /** + * @param object $schema + * @return string|null + */ + public function parseSchemaDraft($schema) + { + if (!property_exists($schema, '$schema') || !is_string($schema->{'$schema'})) { + return null; + } + + return $this->parseDraftVersion($schema->{'$schema'}); + } + + /** + * @param object $schema + * @param Uri $id + * @param callable $handle_id + * @param callable $handle_object + * @param string|null $draft + * @return Schema|null + */ + public function parseRootSchema( + $schema, + $id, + $handle_id, + $handle_object, + $draft = null + ) + { + $existent = false; + if (property_exists($schema, '$id')) { + $existent = true; + $id = Uri::parse($schema->{'$id'}, true); + } + + if ($id instanceof Uri) { + if ($id->fragment() === null) { + $id = Uri::merge($id, null, true); + } + } else { + throw new ParseException('Root schema id must be an URI', new SchemaInfo($schema, $id)); + } + + if (!$id->isAbsolute()) { + throw new ParseException('Root schema id must be an absolute URI', new SchemaInfo($schema, $id)); + } + + if ($id->fragment() !== '') { + throw new ParseException('Root schema id must have an empty fragment or none', new SchemaInfo($schema, $id)); + } + + // Check if id exists + if ($resolved = $handle_id($id)) { + return $resolved; + } + + if (property_exists($schema, '$schema')) { + if (!is_string($schema->{'$schema'})) { + throw new ParseException('Schema draft must be a string', new SchemaInfo($schema, $id)); + } + $draft = $this->parseDraftVersion($schema->{'$schema'}); + } + + if ($draft === null) { + $draft = $this->defaultDraftVersion(); + } + + if (!$existent) { + $schema->{'$id'} = (string)$id; + } + + $resolved = $handle_object($schema, $id, $draft); + + if (!$existent) { + unset($schema->{'$id'}); + } + + return $resolved; + } + + /** + * @param SchemaInfo $info + * @return Schema + */ + public function parseSchema($info): Schema + { + if ($info->isBoolean()) { + return new BooleanSchema($info); + } + + try { + return $this->parseSchemaObject($info); + } catch (SchemaException $exception) { + return new ExceptionSchema($info, $exception); + } + } + + /** + * @param string $version + * @return Draft|null + */ + public function draft($version) + { + return $this->drafts[$version] ?? null; + } + + /** + * @param Draft $draft + * @return $this + */ + public function addDraft($draft): self + { + $this->drafts[$draft->version()] = $draft; + + return $this; + } + + /** + * @return string[] + */ + public function supportedDrafts(): array + { + return array_keys($this->drafts); + } + + /** + * @param array $options + * @return $this + */ + protected function setOptions($options): self + { + $this->options = $options + $this->options; + + return $this; + } + + /** + * @param string $name + * @return mixed|null + */ + protected function getResolver($name) + { + $resolver = $this->resolvers[$name] ?? null; + + if (!is_object($resolver)) { + return null; + } + + return $resolver; + } + + /** + * @param SchemaInfo $info + * @return Schema + */ + protected function parseSchemaObject($info): Schema + { + $draftObject = $this->draft($info->draft()); + + if ($draftObject === null) { + throw new ParseException("Unsupported draft-{$info->draft()}", $info); + } + + /** @var object $schema */ + $schema = $info->data(); + + // Check id + if (property_exists($schema, '$id')) { + $id = $info->id(); + if ($id === null || !$id->isAbsolute()) { + throw new ParseException('Schema id must be a valid URI', $info); + } + } + + if ($hasRef = property_exists($schema, '$ref')) { + if ($this->option('allowKeywordsAlongsideRef') || $draftObject->allowKeywordsAlongsideRef()) { + $hasRef = false; + } + } + + $shared = (object) []; + + if ($this->option('allowKeywordValidators')) { + $keywordValidator = $this->parseKeywordValidators($info, $draftObject->keywordValidators(), $shared); + } else { + $keywordValidator = null; + } + + return $this->parseSchemaKeywords($info, $keywordValidator, $draftObject->keywords(), $shared, $hasRef); + } + + /** + * @param SchemaInfo $info + * @param KeywordValidatorParser[] $keywordValidators + * @param object $shared + * @return KeywordValidator|null + */ + protected function parseKeywordValidators($info, $keywordValidators, $shared) + { + $last = null; + + while ($keywordValidators) { + /** @var KeywordValidatorParser $keywordValidator */ + $keywordValidator = array_pop($keywordValidators); + if ($keywordValidator && ($keyword = $keywordValidator->parse($info, $this, $shared))) { + $keyword->setNext($last); + $last = $keyword; + unset($keyword); + } + unset($keywordValidator); + } + + return $last; + } + + /** + * @param SchemaInfo $info + * @param KeywordValidator|null $keywordValidator + * @param KeywordParser[] $parsers + * @param object $shared + * @param bool $hasRef + * @return Schema + */ + protected function parseSchemaKeywords($info, $keywordValidator, + $parsers, $shared, $hasRef = false): Schema + { + /** @var Keyword[] $prepend */ + $prepend = []; + /** @var Keyword[] $append */ + $append = []; + /** @var Keyword[] $before */ + $before = []; + /** @var Keyword[] $after */ + $after = []; + /** @var Keyword[][] $types */ + $types = []; + /** @var Keyword[] $ref */ + $ref = []; + + if ($hasRef) { + foreach ($parsers as $parser) { + $kType = $parser->type(); + + if ($kType === KeywordParser::TYPE_APPEND) { + $container = &$append; + } elseif ($kType === KeywordParser::TYPE_AFTER_REF) { + $container = &$ref; + } elseif ($kType === KeywordParser::TYPE_PREPEND) { + $container = &$prepend; + } else { + continue; + } + + if ($keyword = $parser->parse($info, $this, $shared)) { + $container[] = $keyword; + } + + unset($container, $keyword, $kType); + } + } else { + foreach ($parsers as $parser) { + $keyword = $parser->parse($info, $this, $shared); + if ($keyword === null) { + continue; + } + + $kType = $parser->type(); + + switch ($kType) { + case KeywordParser::TYPE_PREPEND: + $prepend[] = $keyword; + break; + case KeywordParser::TYPE_APPEND: + $append[] = $keyword; + break; + case KeywordParser::TYPE_BEFORE: + $before[] = $keyword; + break; + case KeywordParser::TYPE_AFTER: + $after[] = $keyword; + break; + case KeywordParser::TYPE_AFTER_REF: + $ref[] = $keyword; + break; + default: + if (!isset($types[$kType])) { + $types[$kType] = []; + } + $types[$kType][] = $keyword; + break; + + } + } + } + + unset($shared); + + if ($prepend) { + $before = array_merge($prepend, $before); + } + unset($prepend); + + if ($ref) { + $after = array_merge($after, $ref); + } + unset($ref); + + if ($append) { + $after = array_merge($after, $append); + } + unset($append); + + if (empty($before)) { + $before = null; + } + if (empty($after)) { + $after = null; + } + if (empty($types)) { + $types = null; + } + + if (empty($types) && empty($before) && empty($after)) { + return new EmptySchema($info, $keywordValidator); + } + + return new ObjectSchema($info, $keywordValidator, $types, $before, $after); + } +} diff --git a/src/opis/json-schema/src/Parsers/VariablesTrait.php b/src/opis/json-schema/src/Parsers/VariablesTrait.php new file mode 100644 index 00000000..611127ce --- /dev/null +++ b/src/opis/json-schema/src/Parsers/VariablesTrait.php @@ -0,0 +1,41 @@ +option('varRefKey', '$ref'), + $parser->option('varEachKey', '$each'), + $parser->option('varDefaultKey', 'default') + ); + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Parsers/Vocabulary.php b/src/opis/json-schema/src/Parsers/Vocabulary.php new file mode 100644 index 00000000..9d402270 --- /dev/null +++ b/src/opis/json-schema/src/Parsers/Vocabulary.php @@ -0,0 +1,126 @@ +keywords = $keywords; + $this->keywordValidators = $keywordValidators; + $this->pragmas = $pragmas; + } + + /** + * @return KeywordParser[] + */ + public function keywords(): array + { + return $this->keywords; + } + + /** + * @return KeywordValidatorParser[] + */ + public function keywordValidators(): array + { + return $this->keywordValidators; + } + + /** + * @return PragmaParser[] + */ + public function pragmas(): array + { + return $this->pragmas; + } + + /** + * @param KeywordParser $keyword + * @return Vocabulary + */ + public function appendKeyword($keyword): self + { + $this->keywords[] = $keyword; + return $this; + } + + /** + * @param KeywordParser $keyword + * @return Vocabulary + */ + public function prependKeyword($keyword): self + { + array_unshift($this->keywords, $keyword); + return $this; + } + + /** + * @param KeywordValidatorParser $keywordValidatorParser + * @return Vocabulary + */ + public function appendKeywordValidator($keywordValidatorParser): self + { + $this->keywordValidators[] = $keywordValidatorParser; + return $this; + } + + /** + * @param KeywordValidatorParser $keywordValidator + * @return Vocabulary + */ + public function prependKeywordValidator($keywordValidator): self + { + array_unshift($this->keywordValidators, $keywordValidator); + return $this; + } + + /** + * @param PragmaParser $pragma + * @return Vocabulary + */ + public function appendPragma($pragma): self + { + $this->pragmas[] = $pragma; + return $this; + } + + /** + * @param PragmaParser $pragma + * @return Vocabulary + */ + public function prependPragma($pragma): self + { + array_unshift($this->pragmas, $pragma); + return $this; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Pragma.php b/src/opis/json-schema/src/Pragma.php new file mode 100644 index 00000000..22ebbb72 --- /dev/null +++ b/src/opis/json-schema/src/Pragma.php @@ -0,0 +1,33 @@ +cast = $cast; + $this->func = $this->getCastFunction($cast); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function enter($context) + { + $currentType = $context->currentDataType(); + if ($currentType !== null && Helper::jsonTypeMatches($currentType, $this->cast)) { + // Cast not needed + return $this; + } + unset($currentType); + + $currentData = $context->currentData(); + + $context->setCurrentData(($this->func)($currentData)); + + return $currentData; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function leave($context, $data) + { + if ($data !== $this) { + $context->setCurrentData($data); + } + } + + /** + * @param string $type + * @return callable + */ + protected function getCastFunction($type): callable + { + $f = 'toNull'; + + switch ($type) { + case 'integer': + $f = 'toInteger'; + break; + case 'number': + $f = 'toNumber'; + break; + case 'string': + $f = 'toString'; + break; + case 'array': + $f = 'toArray'; + break; + case 'object': + $f = 'toObject'; + break; + case 'boolean': + $f = 'toBoolean'; + break; + } + + return [$this, $f]; + } + + /** + * @param $value + * @return int|null + */ + public function toInteger($value) + { + if ($value === null) { + return 0; + } + + return is_scalar($value) ? intval($value) : null; + } + + /** + * @param $value + * @return float|null + */ + public function toNumber($value) + { + if ($value === null) { + return 0.0; + } + + return is_scalar($value) ? floatval($value) : null; + } + + /** + * @param $value + * @return string|null + */ + public function toString($value) + { + if ($value === null) { + return ''; + } + + if (is_scalar($value)) { + return (string) $value; + } + + return null; + } + + /** + * @param $value + * @return array|null + */ + public function toArray($value) + { + if ($value === null) { + return []; + } + + if (is_scalar($value)) { + return [$value]; + } + + if (is_array($value)) { + return array_values($value); + } + + if (is_object($value)) { + return array_values(get_object_vars($value)); + } + + return null; + } + + /** + * @param $value + * @return object|null + */ + public function toObject($value) + { + if (is_object($value) || is_array($value)) { + return (object) $value; + } + + return null; + } + + /** + * @param $value + * @return bool + */ + public function toBoolean($value): bool + { + if ($value === null) { + return false; + } + if (is_string($value)) { + return !($value === ''); + } + if (is_object($value)) { + return count(get_object_vars($value)) > 0; + } + return boolval($value); + } +} diff --git a/src/opis/json-schema/src/Pragmas/GlobalsPragma.php b/src/opis/json-schema/src/Pragmas/GlobalsPragma.php new file mode 100644 index 00000000..b9b7473c --- /dev/null +++ b/src/opis/json-schema/src/Pragmas/GlobalsPragma.php @@ -0,0 +1,66 @@ +globals = $globals; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function enter($context) + { + $resolved = (array) $this->globals->resolve($context->rootData(), $context->currentDataPath()); + if (!$resolved) { + return null; + } + + $data = $context->globals(); + $context->setGlobals($resolved, false); + return $data; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function leave($context, $data) + { + if ($data === null) { + return; + } + $context->setGlobals($data, true); + } +} diff --git a/src/opis/json-schema/src/Pragmas/MaxErrorsPragma.php b/src/opis/json-schema/src/Pragmas/MaxErrorsPragma.php new file mode 100644 index 00000000..d669929e --- /dev/null +++ b/src/opis/json-schema/src/Pragmas/MaxErrorsPragma.php @@ -0,0 +1,60 @@ +maxErrors = $maxErrors; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function enter($context) + { + $data = $context->maxErrors(); + $context->setMaxErrors($this->maxErrors); + return $data; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function leave($context, $data) + { + if ($data === null) { + return; + } + $context->setMaxErrors($data); + } +} diff --git a/src/opis/json-schema/src/Pragmas/SlotsPragma.php b/src/opis/json-schema/src/Pragmas/SlotsPragma.php new file mode 100644 index 00000000..60d380fd --- /dev/null +++ b/src/opis/json-schema/src/Pragmas/SlotsPragma.php @@ -0,0 +1,57 @@ +slots = $slots; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function enter($context) + { + $data = $context->slots(); + $context->setSlots($data ? $this->slots + $data : $this->slots); + return $data; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function leave($context, $data) + { + $context->setSlots($data); + } +} diff --git a/src/opis/json-schema/src/Resolvers/ContentEncodingResolver.php b/src/opis/json-schema/src/Resolvers/ContentEncodingResolver.php new file mode 100644 index 00000000..40d0f387 --- /dev/null +++ b/src/opis/json-schema/src/Resolvers/ContentEncodingResolver.php @@ -0,0 +1,143 @@ + self::class . '::DecodeBinary', + 'base64' => self::class . '::DecodeBase64', + 'quoted-printable' => self::class . '::DecodeQuotedPrintable', + ]; + + $this->list = $list; + $this->defaultEncoding = $defaultEncoding; + } + + /** + * @param string $name + * @return callable|ContentEncoding|string|null + */ + public function resolve($name) + { + return $this->list[$name] ?? $this->defaultEncoding; + } + + /** + * @param string $name + * @param ContentEncoding $encoding + * @return ContentEncodingResolver + */ + public function register($name, $encoding): self + { + $this->list[$name] = $encoding; + + return $this; + } + + /** + * @param string $name + * @param callable $encoding + * @return ContentEncodingResolver + */ + public function registerCallable($name, $encoding): self + { + $this->list[$name] = $encoding; + + return $this; + } + + /** + * @param string $name + * @return bool + */ + public function unregister($name): bool + { + if (isset($this->list[$name])) { + unset($this->list[$name]); + + return true; + } + + return false; + } + + /** + * @param callable|ContentEncoding|null $handler + * @return $this + */ + public function setDefaultHandler($handler): self + { + $this->defaultEncoding = $handler; + return $this; + } + + public function __serialize(): array + { + return [ + 'list' => $this->list, + 'defaultEncoding' => $this->defaultEncoding, + ]; + } + + public function __unserialize(array $data) + { + $this->list = $data['list']; + $this->defaultEncoding = $data['defaultEncoding'] ?? null; + } + + /** + * @param string $value + */ + public static function DecodeBinary($value) + { + return $value; + } + + /** + * @param string $value + */ + public static function DecodeBase64($value) + { + $value = base64_decode($value, true); + + return is_string($value) ? $value : null; + } + + /** + * @param string $value + */ + public static function DecodeQuotedPrintable($value) + { + return quoted_printable_decode($value); + } +} diff --git a/src/opis/json-schema/src/Resolvers/ContentMediaTypeResolver.php b/src/opis/json-schema/src/Resolvers/ContentMediaTypeResolver.php new file mode 100644 index 00000000..7b86c116 --- /dev/null +++ b/src/opis/json-schema/src/Resolvers/ContentMediaTypeResolver.php @@ -0,0 +1,153 @@ + self::class . '::IsJsonEncoded', + ]; + + $this->media = $media; + $this->defaultMedia = $defaultMedia ?? self::class . '::IsEncodedAsType'; + } + + /** + * @param string $name + * @return callable|ContentMediaType|string|null + */ + public function resolve($name) + { + return $this->media[$name] ?? $this->defaultMedia; + } + + /** + * @param string $name + * @param ContentMediaType $media + * @return ContentMediaTypeResolver + */ + public function register($name, $media): self + { + $this->media[$name] = $media; + + return $this; + } + + /** + * @param string $name + * @param callable $media + * @return ContentMediaTypeResolver + */ + public function registerCallable($name, $media): self + { + $this->media[$name] = $media; + + return $this; + } + + /** + * @param string $name + * @return bool + */ + public function unregister($name): bool + { + if (isset($this->media[$name])) { + unset($this->media[$name]); + + return true; + } + + return false; + } + + /** + * @param callable|ContentMediaType|null $handler + * @return ContentMediaTypeResolver + */ + public function setDefaultHandler($handler): self + { + $this->defaultMedia = $handler; + + return $this; + } + + public function __serialize(): array + { + return [ + 'media' => $this->media, + 'defaultMedia' => $this->defaultMedia, + ]; + } + + public function __unserialize(array $data) + { + $this->media = $data['media']; + $this->defaultMedia = $data['defaultMedia']; + } + + /** + * @param string $value + * @param string $type + */ + public static function IsJsonEncoded($value, + /** @noinspection PhpUnusedParameterInspection */ $type): bool + { + json_decode($value); + + return json_last_error() === JSON_ERROR_NONE; + } + + /** + * @param string $value + * @param string $type + */ + public static function IsEncodedAsType($value, $type): bool + { + /** @var finfo|null|bool $finfo */ + static $finfo = false; + + if ($finfo === false) { + if (!class_exists(finfo::class)) { + $finfo = null; + return false; + } + $finfo = new finfo(FILEINFO_MIME_TYPE); + } elseif (!$finfo) { + return false; + } + + $r = $finfo->buffer($value); + + return $r == $type || $r == 'application/x-empty'; + } +} diff --git a/src/opis/json-schema/src/Resolvers/FilterResolver.php b/src/opis/json-schema/src/Resolvers/FilterResolver.php new file mode 100644 index 00000000..4a670dc7 --- /dev/null +++ b/src/opis/json-schema/src/Resolvers/FilterResolver.php @@ -0,0 +1,271 @@ +separator = $ns_separator; + $this->defaultNS = $default_ns; + + $this->registerDefaultFilters(); + } + + /** + * You can override this to add/remove default filters + */ + protected function registerDefaultFilters() + { + $this->registerMultipleTypes("schema-exists", new SchemaExistsFilter()); + $this->registerMultipleTypes("data-exists", new DataExistsFilter()); + $this->registerMultipleTypes("global-exists", new GlobalVarExistsFilter()); + $this->registerMultipleTypes("slot-exists", new SlotExistsFilter()); + $this->registerMultipleTypes("filter-exists", new FilterExistsFilter()); + $this->registerMultipleTypes("format-exists", new FormatExistsFilter()); + + $cls = DateTimeFilters::class . "::"; + $this->registerCallable("string", "min-date", $cls . "MinDate"); + $this->registerCallable("string", "max-date", $cls . "MaxDate"); + $this->registerCallable("string", "not-date", $cls . "NotDate"); + $this->registerCallable("string", "min-time", $cls . "MinTime"); + $this->registerCallable("string", "max-time", $cls . "MaxTime"); + $this->registerCallable("string", "min-datetime", $cls . "MinDateTime"); + $this->registerCallable("string", "max-datetime", $cls . "MaxDateTime"); + + $cls = CommonFilters::class . "::"; + $this->registerCallable("string", "regex", $cls . "Regex"); + $this->registerMultipleTypes("equals", $cls . "Equals"); + } + + + /** + * @param string $name + * @param string $type + * @return Filter|callable|null + */ + public function resolve($name, $type) + { + list($ns, $name) = $this->parseName($name); + + if (isset($this->filters[$ns][$name])) { + return $this->filters[$ns][$name][$type] ?? null; + } + + if (!isset($this->ns[$ns])) { + return null; + } + + $this->filters[$ns][$name] = $this->ns[$ns]->resolveAll($name); + + return $this->filters[$ns][$name][$type] ?? null; + } + + /** + * @param string $name + * @return Filter[]|callable[]|null + */ + public function resolveAll($name) + { + list($ns, $name) = $this->parseName($name); + + if (isset($this->filters[$ns][$name])) { + return $this->filters[$ns][$name]; + } + + if (!isset($this->ns[$ns])) { + return null; + } + + return $this->filters[$ns][$name] = $this->ns[$ns]->resolveAll($name); + } + + /** + * @param string $type + * @param string $name + * @param Filter $filter + * @return FilterResolver + */ + public function register($type, $name, $filter): self + { + list($ns, $name) = $this->parseName($name); + + $this->filters[$ns][$name][$type] = $filter; + + return $this; + } + + /** + * @param string $name + * @param string|null $type + * @return bool + */ + public function unregister($name, $type = null): bool + { + list($ns, $name) = $this->parseName($name); + if (!isset($this->filters[$ns][$name])) { + return false; + } + + if ($type === null) { + unset($this->filters[$ns][$name]); + + return true; + } + + if (isset($this->filters[$ns][$name][$type])) { + unset($this->filters[$ns][$name][$type]); + + return true; + } + + return false; + } + + /** + * @param string $name + * @param callable|Filter $filter + * @param array|null $types + * @return FilterResolver + */ + public function registerMultipleTypes($name, $filter, $types = null): self + { + list($ns, $name) = $this->parseName($name); + + $types = $types ?? Helper::JSON_TYPES; + + foreach ($types as $type) { + $this->filters[$ns][$name][$type] = $filter; + } + + return $this; + } + + /** + * @param string $type + * @param string $name + * @param callable $filter + * @return FilterResolver + */ + public function registerCallable($type, $name, $filter): self + { + list($ns, $name) = $this->parseName($name); + + $this->filters[$ns][$name][$type] = $filter; + + return $this; + } + + /** + * @param string $ns + * @param FilterResolver $resolver + * @return FilterResolver + */ + public function registerNS($ns, $resolver): self + { + $this->ns[$ns] = $resolver; + + return $this; + } + + /** + * @param string $ns + * @return bool + */ + public function unregisterNS($ns): bool + { + if (isset($this->filters[$ns])) { + unset($this->filters[$ns]); + unset($this->ns[$ns]); + + return true; + } + + if (isset($this->ns[$ns])) { + unset($this->ns[$ns]); + + return true; + } + + return false; + } + + public function __serialize(): array + { + return [ + 'separator' => $this->separator, + 'defaultNS' => $this->defaultNS, + 'ns' => $this->ns, + 'filters' => $this->filters, + ]; + } + + public function __unserialize(array $data) + { + $this->separator = $data['separator']; + $this->defaultNS = $data['defaultNS']; + $this->ns = $data['ns']; + $this->filters = $data['filters']; + } + + /** + * @param string $name + * @return array + */ + protected function parseName($name): array + { + $name = strtolower($name); + + if (strpos($name, $this->separator) === false) { + return [$this->defaultNS, $name]; + } + + return explode($this->separator, $name, 2); + } +} diff --git a/src/opis/json-schema/src/Resolvers/FormatResolver.php b/src/opis/json-schema/src/Resolvers/FormatResolver.php new file mode 100644 index 00000000..0a259ceb --- /dev/null +++ b/src/opis/json-schema/src/Resolvers/FormatResolver.php @@ -0,0 +1,143 @@ +formats = [ + 'string' => [ + 'date' => DateTimeFormats::class . '::date', + 'time' => DateTimeFormats::class . '::time', + 'date-time' => DateTimeFormats::class . '::dateTime', + 'duration' => DateTimeFormats::class . '::duration', + + 'uri' => UriFormats::class . '::uri', + 'uri-reference' => UriFormats::class . '::uriReference', + 'uri-template' => UriFormats::class . '::uriTemplate', + + 'regex' => Helper::class . '::isValidPattern', + 'ipv4' => MiscFormats::class . '::ipv4', + 'ipv6' => MiscFormats::class . '::ipv6', + 'uuid' => MiscFormats::class . '::uuid', + + 'email' => MiscFormats::class . '::email', + 'hostname' => Uri::class . '::isValidHost', + + 'json-pointer' => JsonPointer::class . '::isAbsolutePointer', + 'relative-json-pointer' => JsonPointer::class . '::isRelativePointer', + + 'idn-hostname' => IriFormats::class . '::idnHostname', + 'idn-email' => IriFormats::class . '::idnEmail', + 'iri' => IriFormats::class . '::iri', + 'iri-reference' => IriFormats::class . '::iriReference', + ], + ]; + } + + /** + * @param string $name + * @param string $type + * @return callable|Format|null + */ + public function resolve($name, $type) + { + return $this->formats[$type][$name] ?? null; + } + + /** + * @param string $name + * @return Format[]|callable[]|null + */ + public function resolveAll($name) + { + $list = null; + + foreach ($this->formats as $type => $items) { + if (isset($items[$name])) { + $list[$type] = $items[$name]; + } + } + + return $list; + } + + /** + * @param string $type + * @param string $name + * @param Format $format + * @return FormatResolver + */ + public function register($type, $name, $format): self + { + $this->formats[$type][$name] = $format; + + return $this; + } + + /** + * @param string $type + * @param string $name + * @param callable $format + * @return FormatResolver + */ + public function registerCallable($type, $name, $format): self + { + $this->formats[$type][$name] = $format; + + return $this; + } + + /** + * @param string $type + * @param string $name + * @return bool + */ + public function unregister($type, $name): bool + { + if (isset($this->formats[$type][$name])) { + unset($this->formats[$type][$name]); + + return true; + } + + return false; + } + + public function __serialize(): array + { + return ['formats' => $this->formats]; + } + + public function __unserialize(array $data) + { + $this->formats = $data['formats']; + } +} diff --git a/src/opis/json-schema/src/Resolvers/SchemaResolver.php b/src/opis/json-schema/src/Resolvers/SchemaResolver.php new file mode 100644 index 00000000..cf6b625f --- /dev/null +++ b/src/opis/json-schema/src/Resolvers/SchemaResolver.php @@ -0,0 +1,342 @@ +isAbsolute()) { + return null; + } + + $scheme = $uri->scheme(); + if (isset($this->protocols[$scheme])) { + return ($this->protocols[$scheme])($uri); + } + + $id = (string) $uri; + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + $path = $this->resolvePath($uri); + + if ($path === null || !is_file($path)) { + return null; + } + + $data = file_get_contents($path); + if (!is_string($data)) { + return null; + } + + $data = json_decode($data, false); + + return $data; + } + + /** + * @param bool|object|string $schema + * @param string|null $id + * @return bool + */ + public function registerRaw($schema, $id = null): bool + { + if (is_string($schema)) { + $schema = json_decode($schema, false); + } + + if ($id !== null && strpos($id, '#') === false) { + $id .= '#'; + } + + if (is_bool($schema)) { + if ($id === null) { + return false; + } + $this->raw[$id] = $schema; + return true; + } + + if (!is_object($schema)) { + return false; + } + + + if ($id === null) { + if (!isset($schema->{'$id'}) || !is_string($schema->{'$id'})) { + return false; + } + + $id = $schema->{'$id'}; + if (strpos($id, '#') === false) { + $id .= '#'; + } + } + + $this->raw[$id] = $schema; + + return true; + } + + /** + * @param string $id + * @return bool + */ + public function unregisterRaw($id): bool + { + if (strpos($id, '#') === false) { + $id .= '#'; + } + + if (isset($this->raw[$id])) { + unset($this->raw[$id]); + return true; + } + + return false; + } + + /** + * @param string $id + * @param string $file + * @return SchemaResolver + */ + public function registerFile($id, $file): self + { + if (strpos($id, '#') === false) { + $id .= '#'; + } + + $this->files[$id] = $file; + + return $this; + } + + /** + * @param string $id + * @return bool + */ + public function unregisterFile($id): bool + { + if (strpos($id, '#') === false) { + $id .= '#'; + } + + if (!isset($this->files[$id])) { + return false; + } + + unset($this->files[$id]); + + return true; + } + + /** + * @param string $scheme + * @param callable $handler + * @return SchemaResolver + */ + public function registerProtocol($scheme, $handler): self + { + $this->protocols[$scheme] = $handler; + + return $this; + } + + /** + * @param string $scheme + * @return bool + */ + public function unregisterProtocol($scheme): bool + { + if (isset($this->protocols[$scheme])) { + unset($this->protocols[$scheme]); + + return true; + } + + return false; + } + + /** + * @param string $scheme + * @param string $host + * @param string|null $dir + * @return SchemaResolver + */ + public function registerProtocolDir($scheme, $host, $dir): self + { + if ($dir === null) { + unset($this->dirs[$scheme][$host]); + } else { + $this->dirs[$scheme][$host] = rtrim($dir, '/'); + } + + return $this; + } + + /** + * @param string $scheme + * @return bool + */ + public function unregisterProtocolDirs($scheme): bool + { + if (isset($this->dirs[$scheme])) { + unset($this->dirs[$scheme]); + + return true; + } + + return false; + } + + /** + * @param string $prefix + * @param string $dir + * @return SchemaResolver + */ + public function registerPrefix($prefix, $dir): self + { + $this->prefixes[$prefix] = rtrim($dir, '/'); + + // Sort + uksort($this->prefixes, [$this, 'sortPrefixKeys']); + + return $this; + } + + /** + * @param string $prefix + * @return SchemaResolver + */ + public function unregisterPrefix($prefix): self + { + if (isset($this->prefixes[$prefix])) { + unset($this->prefixes[$prefix]); + // Sort + uksort($this->prefixes, [$this, 'sortPrefixKeys']); + } + + return $this; + } + + + public function __serialize(): array + { + return [ + 'raw' => $this->raw, + 'protocols' => $this->protocols, + 'prefixes' => $this->prefixes, + 'dirs' => $this->dirs, + ]; + } + + public function __unserialize(array $data) + { + $this->raw = $data['raw']; + $this->protocols = $data['protocols']; + $this->prefixes = $data['prefixes']; + $this->dirs = $data['dirs']; + } + + /** + * @param string $a + * @param string $b + * @return int + */ + protected function sortPrefixKeys($a, $b): int + { + $la = strlen($a); + $lb = strlen($b); + + if ($lb > $la) { + return 1; + } + + if ($lb === $la) { + return $b < $a ? 1 : ($b === $a ? 0 : -1); + } + + return -1; + } + + /** + * @param Uri $uri + * @return string|null + */ + protected function resolvePath($uri) + { + $id = (string)$uri; + + if (isset($this->files[$id])) { + return $this->files[$id]; + } + + $scheme = $uri->scheme(); + + if (isset($this->dirs[$scheme])) { + $host = (string)$uri->host(); + if (isset($this->dirs[$scheme][$host])) { + return $this->dirs[$scheme][$host] . '/' . ltrim($uri->path(), '/'); + } + unset($host); + } + + $path = null; + foreach ($this->prefixes as $prefix => $dir) { + if ($prefix === '' || strpos($id, $prefix) === 0) { + $path = substr($id, strlen($prefix)); + if ($path === false || $path === '') { + $path = null; + continue; + } + $path = Uri::parseComponents($path); + if ($path && isset($path['path'])) { + $path = $dir . '/' . ltrim($path['path'], '/'); + break; + } + $path = null; + } + } + + return $path; + } +} diff --git a/src/opis/json-schema/src/Schema.php b/src/opis/json-schema/src/Schema.php new file mode 100644 index 00000000..821a6a02 --- /dev/null +++ b/src/opis/json-schema/src/Schema.php @@ -0,0 +1,28 @@ +dataCache = new SplObjectStorage(); + $this->parser = $parser ?? new SchemaParser(); + $this->resolver = $resolver; + $this->decodeJsonString = $decodeJsonString; + } + + public function baseUri() + { + return $this->base; + } + + /** + * @param \Opis\JsonSchema\Uri|null $uri + */ + public function setBaseUri($uri): self + { + $this->base = $uri; + return $this; + } + + public function parser(): SchemaParser + { + return $this->parser; + } + + /** + * @param \Opis\JsonSchema\Parsers\SchemaParser $parser + */ + public function setParser($parser): self + { + $this->parser = $parser; + + return $this; + } + + public function resolver() + { + return $this->resolver; + } + + /** + * @param \Opis\JsonSchema\Resolvers\SchemaResolver|null $resolver + */ + public function setResolver($resolver): self + { + $this->resolver = $resolver; + + return $this; + } + + /** + * @param object $data + * @param null $id + * @param string|null $draft + * @return Schema + */ + public function loadObjectSchema($data, $id = null, $draft = null): Schema + { + // Check if already loaded + if ($schema = $this->checkExistingObject($data)) { + return $schema; + } + + if (!$id) { + $id = $this->createSchemaId($data); + } + + $handle_id = function (Uri $id) { + return $this->checkExistingUri($id); + }; + + $handle_object = function ($data, Uri $id, string $draft) { + $this->handleObject($data, $id, null, null, [], $draft, (string)$id); + + return $this->checkExistingObject($data); + }; + + return $this->parser->parseRootSchema($data, Uri::parse($id, true), $handle_id, $handle_object, $draft); + } + + /** + * @param bool $data + * @param null $id + * @param string|null $draft + * @return Schema + */ + public function loadBooleanSchema($data, $id = null, $draft = null): Schema + { + if (!$id) { + $id = $this->createSchemaId($data); + } + + return $this->parser->parseSchema(new SchemaInfo($data, Uri::parse($id, true), null, null, [], $draft)); + } + + /** + * @param Uri $uri + * @return Schema|null + */ + public function loadSchemaById($uri) + { + if (!$uri->isAbsolute()) { + if ($this->base === null || !$this->base->isAbsolute()) { + return null; + } + $uri = $this->base->resolveRef($uri); + } + + $fragment = $uri->fragment(); + if ($fragment === null) { + $uri = Uri::merge($uri, null, true); + $fragment = ''; + } + + $schema = $this->checkExistingUri($uri); + + if ($schema !== null) { + return $schema; + } + + if ($fragment === '') { + return $this->resolve($uri); + } + + $root = Uri::merge('#', $uri); + + // Check if already resolved + if (($schema = $this->checkExistingUri($root)) === null) { + // Try to resolve + if (($schema = $this->resolve($root)) === null) { + // Schema not found + return null; + } + } + + // Resolve json pointer + if ($fragment !== '' && $schema && $schema->info()->isObject() && + ($pointer = JsonPointer::parse($fragment)) && $pointer->isAbsolute()) { + $object = $pointer->data($schema->info()->data()); + if (is_bool($object)) { + $schema = $this->loadBooleanSchema($object, $uri, $schema->info()->draft()); + } elseif (is_object($object)) { + $schema = $this->loadObjectSchema($object, $uri, $schema->info()->draft()); + } else { + $schema = null; + } + if ($schema) { + $key = $this->cacheKey((string) $uri); + $this->uriCache[$key] = $schema; + return $schema; + } + } + + // Check fragment + return $this->checkExistingUri($uri); + } + + /** + * Clears internal cache + */ + public function clearCache() + { + $this->dataCache->removeAll($this->dataCache); + $this->uriCache = []; + } + + /** + * @param Uri $uri + * @return null|Schema + */ + protected function resolve($uri) + { + if ($this->resolver === null) { + return null; + } + + $data = $this->resolver->resolve($uri); + + if ($this->decodeJsonString && is_string($data)) { + $data = json_decode($data, false); + } + + if (is_bool($data)) { + $this->handleBoolean($data, $uri, null, null, [], $this->parser->defaultDraftVersion(), (string)$uri); + + return $this->checkExistingUri($uri); + } + + if (is_object($data)) { + if ($data instanceof Schema) { + return $data; + } + + $this->handleObject($data, $uri, null, null, [], $this->parser->defaultDraftVersion(), (string)$uri); + + return $this->checkExistingObject($data); + } + + return null; + } + + /** + * @param object $data + * @return null|Schema + */ + protected function checkExistingObject($data) + { + if (!$this->dataCache->contains($data)) { + return null; + } + + $schema = $this->dataCache[$data]; + + if ($schema instanceof LazySchema) { + $schema = $schema->schema(); + $this->dataCache[$data] = $schema; + } elseif (!($schema instanceof Schema)) { + $schema = null; + } + + return $schema; + } + + /** + * @param Uri $uri + * @return null|Schema + */ + protected function checkExistingUri($uri) + { + if ($uri->fragment() === null || !$uri->isAbsolute()) { + return null; + } + + $key = $this->cacheKey((string)$uri); + + if (!isset($this->uriCache[$key])) { + return null; + } + + $schema = $this->uriCache[$key]; + + if (!($schema instanceof Schema)) { + return $this->uriCache[$key] = $this->checkExistingObject($schema); + } + + if ($schema instanceof LazySchema) { + $schema = $schema->schema(); + $this->uriCache[$key] = $schema; + } + + return $schema; + } + + /** + * @param bool $data + * @param Uri|null $id + * @param Uri|null $base + * @param Uri|null $root + * @param array $path + * @param string $draft + * @param string $pointer + */ + protected function handleBoolean( + $data, + $id, + $base, + $root, + $path, + $draft, + $pointer + ) + { + $key = $this->cacheKey($pointer); + if (isset($this->uriCache[$key])) { + return; + } + + $this->uriCache[$key] = $this->parser->parseSchema(new SchemaInfo($data, $id, $base, $root, $path, $draft)); + } + + /** + * @param array $data + * @param Uri $base + * @param Uri $root + * @param array $path + * @param string $draft + * @param string $pointer + */ + protected function handleArray($data, $base, $root, $path, $draft, $pointer) + { + foreach ($data as $key => $value) { + if (!is_int($key)) { + continue; + } + + if (is_bool($value)) { + $this->handleBoolean($value, null, $base, $root, array_merge($path, [$key]), $draft, + $pointer . '/' . $key); + } elseif (is_array($value)) { + $this->handleArray($value, $base, $root, array_merge($path, [$key]), $draft, $pointer . '/' . $key); + } elseif (is_object($value)) { + $this->handleObject($value, null, $base, $root, array_merge($path, [$key]), $draft, + $pointer . '/' . $key); + } + } + } + + /** + * @param object $data + * @param Uri|null $id + * @param Uri|null $base + * @param Uri|null $root + * @param array $path + * @param string $draft + * @param string $pointer + */ + protected function handleObject( + $data, + $id, + $base, + $root, + $path, + $draft, + $pointer + ) + { + $schema_id = $this->parser->parseId($data); + $schema_anchor = $this->parser->parseAnchor($data, $draft); + $draft = $this->parser->parseSchemaDraft($data) ?? $draft; + + if ($schema_id !== null) { + $id = Uri::merge($schema_id, $base, true); + } elseif ($schema_anchor !== null) { + $id = Uri::merge('#' . $schema_anchor, $base, true); + } + + $lazy = new LazySchema(new SchemaInfo($data, $id, $base, $root, $path, $draft), $this->parser); + + if ($id && $id->isAbsolute()) { + $key = $this->cacheKey((string)$id); + if (isset($this->uriCache[$key])) { + throw new DuplicateSchemaIdException($id, $data); + } + $this->uriCache[$key] = $lazy; + } + + // When $id and $anchor are both present add a reference to the same lazy object + if ($schema_id !== null && $schema_anchor !== null) { + $anchor_id = Uri::merge('#' . $schema_anchor, $id, true); + $key = $this->cacheKey((string)$anchor_id); + if (isset($this->uriCache[$key])) { + throw new DuplicateSchemaIdException($anchor_id, $data); + } + $this->uriCache[$key] = $lazy; + } + + $this->dataCache[$data] = $lazy; + $this->uriCache[$this->cacheKey($pointer)] = $lazy; + + if ($root === null) { + $root = $id; + } + + if ($base === null) { + $base = $id ?? $root; + } elseif ($id !== null) { + $base = $id; + } + + foreach ($data as $key => $value) { + if (!is_string($key)) { + continue; + } + if (is_bool($value)) { + $this->handleBoolean($value, null, $base, $root, array_merge($path, [$key]), $draft, + $pointer . '/' . JsonPointer::encodePath($key)); + } elseif (is_array($value)) { + $this->handleArray($value, $base, $root, array_merge($path, [$key]), $draft, + $pointer . '/' . JsonPointer::encodePath($key)); + } elseif (is_object($value)) { + $this->handleObject($value, null, $base, $root, array_merge($path, [$key]), $draft, + $pointer . '/' . JsonPointer::encodePath($key)); + } + } + } + + /** + * @param string $path + * @return string + */ + protected function cacheKey($path): string + { + return isset($path[32]) ? md5($path) : $path; + } + + /** + * @param bool|object $data + * @return string + */ + protected function createSchemaId($data): string + { + if (is_bool($data)) { + $data = $data ? 'true' : 'false'; + } else { + $data = spl_object_hash($data); + } + + return "schema:///{$data}.json"; + } +} diff --git a/src/opis/json-schema/src/SchemaValidator.php b/src/opis/json-schema/src/SchemaValidator.php new file mode 100644 index 00000000..68c49509 --- /dev/null +++ b/src/opis/json-schema/src/SchemaValidator.php @@ -0,0 +1,29 @@ +info = $info; + } + + /** + * @inheritDoc + */ + public function info(): SchemaInfo + { + return $this->info; + } +} \ No newline at end of file diff --git a/src/opis/json-schema/src/Schemas/BooleanSchema.php b/src/opis/json-schema/src/Schemas/BooleanSchema.php new file mode 100644 index 00000000..884d5dfa --- /dev/null +++ b/src/opis/json-schema/src/Schemas/BooleanSchema.php @@ -0,0 +1,53 @@ +data = $info->data(); + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + if ($this->data) { + return null; + } + + return new ValidationError('', $this, DataInfo::fromContext($context), 'Data not allowed'); + } +} diff --git a/src/opis/json-schema/src/Schemas/EmptySchema.php b/src/opis/json-schema/src/Schemas/EmptySchema.php new file mode 100644 index 00000000..4e158c46 --- /dev/null +++ b/src/opis/json-schema/src/Schemas/EmptySchema.php @@ -0,0 +1,57 @@ +keywordValidator = $keywordValidator; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + if (!$this->keywordValidator) { + return null; + } + + $context->pushSharedObject($this); + $error = $this->keywordValidator->validate($context); + $context->popSharedObject(); + + return $error; + } +} diff --git a/src/opis/json-schema/src/Schemas/ExceptionSchema.php b/src/opis/json-schema/src/Schemas/ExceptionSchema.php new file mode 100644 index 00000000..a1789e9d --- /dev/null +++ b/src/opis/json-schema/src/Schemas/ExceptionSchema.php @@ -0,0 +1,52 @@ +exception = $exception; + } + + /** + * @inheritDoc + * @throws SchemaException + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + throw $this->exception; + } +} diff --git a/src/opis/json-schema/src/Schemas/LazySchema.php b/src/opis/json-schema/src/Schemas/LazySchema.php new file mode 100644 index 00000000..f5ca5995 --- /dev/null +++ b/src/opis/json-schema/src/Schemas/LazySchema.php @@ -0,0 +1,67 @@ +parser = $parser; + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + return $this->schema()->validate($context); + } + + /** + * @return Schema + */ + public function schema(): Schema + { + if ($this->schema === null) { + $this->schema = $this->parser->parseSchema($this->info); + } + + return $this->schema; + } +} diff --git a/src/opis/json-schema/src/Schemas/ObjectSchema.php b/src/opis/json-schema/src/Schemas/ObjectSchema.php new file mode 100644 index 00000000..e2d08174 --- /dev/null +++ b/src/opis/json-schema/src/Schemas/ObjectSchema.php @@ -0,0 +1,124 @@ +types = $types; + $this->before = $before; + $this->after = $after; + $this->keywordValidator = $keywordValidator; + + if ($keywordValidator) { + while ($next = $keywordValidator->next()) { + $keywordValidator = $next; + } + $keywordValidator->setNext(new CallbackKeywordValidator([$this, 'doValidate'])); + } + } + + /** + * @inheritDoc + * @param \Opis\JsonSchema\ValidationContext $context + */ + public function validate($context) + { + $context->pushSharedObject($this); + $error = $this->keywordValidator ? $this->keywordValidator->validate($context) : $this->doValidate($context); + $context->popSharedObject(); + + return $error; + } + + /** + * @param ValidationContext $context + * @return null|ValidationError + *@internal + */ + public function doValidate($context) + { + if ($this->before && ($error = $this->applyKeywords($this->before, $context))) { + return $error; + } + + if ($this->types && ($type = $context->currentDataType())) { + if (isset($this->types[$type]) && ($error = $this->applyKeywords($this->types[$type], $context))) { + return $error; + } + + if (($type = Helper::getJsonSuperType($type)) && isset($this->types[$type])) { + if ($error = $this->applyKeywords($this->types[$type], $context)) { + return $error; + } + } + + unset($type); + } + + if ($this->after && ($error = $this->applyKeywords($this->after, $context))) { + return $error; + } + + return null; + } + + /** + * @param Keyword[] $keywords + * @param ValidationContext $context + * @return ValidationError|null + */ + protected function applyKeywords($keywords, $context) + { + foreach ($keywords as $keyword) { + if ($error = $keyword->validate($context, $this)) { + return $error; + } + } + + return null; + } +} diff --git a/src/opis/json-schema/src/Uri.php b/src/opis/json-schema/src/Uri.php new file mode 100644 index 00000000..1f9b5d38 --- /dev/null +++ b/src/opis/json-schema/src/Uri.php @@ -0,0 +1,94 @@ +__toString(); + } + + /** + * @param string $uri + * @param bool $ensure_fragment + * @return static|null + */ + public static function parse($uri, $ensure_fragment = false) + { + if ($ensure_fragment && strpos($uri, '#') === false) { + $uri .= '#'; + } + + return self::create($uri); + } + + /** + * @param string|array|static $uri + * @param string|array|static $base + * @param bool $ensure_fragment + * @return static|null + */ + public static function merge($uri, $base, $ensure_fragment = false) + { + $uri = self::resolveComponents($uri); + + if ($uri === null) { + return null; + } + + if ($ensure_fragment && !isset($uri['fragment'])) { + $uri['fragment'] = ''; + } + + $base = self::resolveComponents($base); + + if (!$base) { + return new self($uri); + } + + return new self(self::mergeComponents($uri, $base)); + } + + /** + * @param bool $value + */ + public static function useNormalizedComponents($value) + { + self::$useNormalizedComponents = $value; + } +} diff --git a/src/opis/json-schema/src/ValidationContext.php b/src/opis/json-schema/src/ValidationContext.php new file mode 100644 index 00000000..6d6f4928 --- /dev/null +++ b/src/opis/json-schema/src/ValidationContext.php @@ -0,0 +1,743 @@ +sender = $sender; + $this->rootData = $data; + $this->loader = $loader; + $this->parent = $parent; + $this->globals = $globals; + $this->slots = null; + $this->maxErrors = $max_errors; + $this->currentData = [ + [$data, false], + ]; + + if ($slots) { + $this->setSlots($slots); + } + } + + /** + * @param $data + * @param Schema|null $sender + * @param array|null $globals + * @param array|null $slots + * @param int|null $max_errors + * @return self + */ + public function newInstance( + $data, + $sender, + $globals = null, + $slots = null, + $max_errors = null + ): self { + return new self($data, $this->loader, $this, $sender, $globals ?? $this->globals, $slots ?? $this->slots, + $max_errors ?? $this->maxErrors); + } + + /** + * @param \Opis\JsonSchema\Schema $sender + * @param \Opis\JsonSchema\Variables|null $mapper + * @param \Opis\JsonSchema\Variables|null $globals + * @param mixed[]|null $slots + * @param int|null $maxErrors + */ + public function create( + $sender, + $mapper = null, + $globals = null, + $slots = null, + $maxErrors = null + ): self { + if ($globals) { + $globals = $globals->resolve($this->rootData(), $this->currentDataPath()); + if (!is_array($globals)) { + $globals = (array)$globals; + } + $globals += $this->globals; + } else { + $globals = $this->globals; + } + + if ($mapper) { + $data = $mapper->resolve($this->rootData(), $this->currentDataPath()); + } else { + $data = $this->currentData(); + } + + return new self($data, $this->loader, $this, $sender, $globals, $slots ?? $this->slots, + $maxErrors ?? $this->maxErrors); + } + + public function sender() + { + return $this->sender; + } + + /** + * @return self|null + */ + public function parent() + { + return $this->parent; + } + + /** + * @return SchemaLoader + */ + public function loader(): SchemaLoader + { + return $this->loader; + } + + /** + * @return mixed + */ + public function rootData() + { + return $this->rootData; + } + + /** + * @return mixed + */ + public function currentData() + { + return $this->currentData[$this->pathIndex][0]; + } + + /** + * @param $value + */ + public function setCurrentData($value) + { + $this->currentData[$this->pathIndex][0] = $value; + $this->currentData[$this->pathIndex][1] = false; + } + + /** + * @return string|null + */ + public function currentDataType() + { + $type = $this->currentData[$this->pathIndex][1]; + if ($type === false) { + $type = Helper::getJsonType($this->currentData[$this->pathIndex][0]); + $this->currentData[$this->pathIndex][1] = $type; + } + + return $type; + } + + public function fullDataPath(): array + { + if ($this->fullPath === null) { + if ($this->parent === null) { + return $this->currentDataPath; + } + $this->fullPath = array_merge($this->parent->fullDataPath(), $this->currentDataPath); + } + + return $this->fullPath; + } + + /** + * @return int[]|string[] + */ + public function currentDataPath(): array + { + return $this->currentDataPath; + } + + /** + * @param string|int $key + * @return $this + */ + public function pushDataPath($key): self + { + $this->currentDataPath[] = $key; + if ($this->fullPath !== null) { + $this->fullPath[] = $key; + } + + $data = $this->currentData[$this->pathIndex][0]; + + if (is_array($data)) { + $data = $data[$key] ?? null; + } elseif (is_object($data)) { + $data = $data->{$key} ?? null; + } else { + $data = null; + } + + $this->currentData[] = [$data, false]; + $this->pathIndex++; + + return $this; + } + + /** + * @return $this + */ + public function popDataPath(): self + { + if ($this->pathIndex < 1) { + return $this; + } + + if ($this->fullPath !== null) { + array_pop($this->fullPath); + } + array_pop($this->currentDataPath); + array_pop($this->currentData); + $this->pathIndex--; + + return $this; + } + + /** + * @return array + */ + public function globals(): array + { + return $this->globals; + } + + /** + * @param array $globals + * @param bool $overwrite + * @return $this + */ + public function setGlobals($globals, $overwrite = false): self + { + if ($overwrite) { + $this->globals = $globals; + } elseif ($globals) { + $this->globals = $globals + $this->globals; + } + + return $this; + } + + /** + * @return object[]|Schema[]|string[]|null + */ + public function slots() + { + return $this->slots; + } + + /** + * @param array|null $slots + * @return $this + */ + public function setSlots($slots): self + { + if ($slots) { + $list = []; + + foreach ($slots as $name => $value) { + if (is_bool($value)) { + $value = $this->loader->loadBooleanSchema($value); + } elseif (is_object($value)) { + if ($value instanceof Schema) { + $list[$name] = $value; + continue; + } + $value = $this->loader->loadObjectSchema($value); + } elseif (is_string($value)) { + if (isset($this->slots[$value])) { + $value = $this->slots[$value]; + } elseif ($this->parent) { + $value = $this->parent->slot($value); + } + } + + if ($value instanceof Schema) { + $list[$name] = $value; + } + } + + $this->slots = $list; + } else { + $this->slots = null; + } + + return $this; + } + + /** + * @param string $name + * @return Schema|null + */ + public function slot($name) + { + return $this->slots[$name] ?? null; + } + + /** + * @return int + */ + public function maxErrors(): int + { + return $this->maxErrors; + } + + /** + * @param int $max + * @return $this + */ + public function setMaxErrors($max): self + { + $this->maxErrors = $max; + + return $this; + } + + /* --------------------- */ + + /** + * @param Schema $schema + * @return $this + */ + public function pushSharedObject($schema): self + { + $unevaluated = !in_array($schema->info()->draft(), ['06', '07']); + if ($unevaluated && ($parser = $this->loader->parser()) && !$parser->option('allowUnevaluated', true)) { + $unevaluated = false; + } + + $this->shared[] = [ + 'schema' => $schema, + 'unevaluated' => $unevaluated, + 'object' => null, + ]; + $this->sharedIndex++; + + return $this; + } + + /** + * @return $this + */ + public function popSharedObject(): self + { + if ($this->sharedIndex < 0) { + return $this; + } + + $data = array_pop($this->shared); + $this->sharedIndex--; + + if ($data['unevaluated'] && $data['object']) { + if ($this->sharedIndex >= 0) { + $this->mergeUnevaluated($data['object']); + } elseif ($this->parent && $this->parent->sharedIndex >= 0) { + $this->parent->mergeUnevaluated($data['object']); + } + } + + return $this; + } + + /** + * @return object|null + */ + public function sharedObject() + { + if ($this->sharedIndex < 0) { + return null; + } + + return $this->shared[$this->sharedIndex]['object'] = $this->shared[$this->sharedIndex]['object'] ?? (object)[]; + } + + public function schema() + { + return $this->shared[$this->sharedIndex]['schema'] ?? null; + } + + public function trackUnevaluated(): bool + { + return $this->shared[$this->sharedIndex]['unevaluated'] ?? false; + } + + /** + * @param object $obj + */ + protected function mergeUnevaluated($obj) + { + switch ($this->currentDataType()) { + case 'object': + if (isset($obj->evaluatedProperties)) { + $this->addEvaluatedProperties($obj->evaluatedProperties); + } + break; + case 'array': + if (isset($obj->evaluatedItems)) { + $this->addEvaluatedItems($obj->evaluatedItems); + } + break; + } + } + + /* ----------------*/ + + public function getStringLength() + { + if ($this->currentDataType() !== 'string') { + return null; + } + + $shared = $this->sharedObject(); + + if (!isset($shared->stringLength)) { + $shared->stringLength = UnicodeString::from($this->currentData())->length(); + } + + return $shared->stringLength; + } + + /** + * @param string $content + */ + public function setDecodedContent($content): bool + { + if ($this->currentDataType() !== 'string') { + return false; + } + + $this->sharedObject()->decodedContent = $content; + + return true; + } + + public function getDecodedContent() + { + if ($this->currentDataType() !== 'string') { + return null; + } + return $this->sharedObject()->decodedContent ?? $this->currentData(); + } + + public function getObjectProperties() + { + if ($this->currentDataType() !== 'object') { + return null; + } + + return $this->sharedObject()->objectProperties = $this->sharedObject()->objectProperties ?? array_keys(get_object_vars($this->currentData())); + } + + /** + * @param mixed[]|null $properties + */ + public function addCheckedProperties($properties): bool + { + if (!$properties) { + return false; + } + + $shared = $this->sharedObject(); + + if (!isset($shared->checkedProperties)) { + $shared->checkedProperties = $properties; + } else { + $shared->checkedProperties = array_values(array_unique(array_merge($shared->checkedProperties, $properties))); + } + + return true; + } + + public function getCheckedProperties() + { + return $this->sharedObject()->checkedProperties ?? null; + } + + public function getUncheckedProperties() + { + $properties = $this->getObjectProperties(); + if (!$properties) { + return $properties; + } + + $checked = $this->sharedObject()->checkedProperties ?? null; + if (!$checked) { + return $properties; + } + + return array_values(array_diff($properties, $checked)); + } + + public function markAllAsEvaluatedProperties(): bool + { + return $this->addEvaluatedProperties($this->getObjectProperties()); + } + + /** + * @param mixed[]|null $properties + */ + public function addEvaluatedProperties($properties): bool + { + if (!$properties || !($this->currentDataType() === 'object') || !$this->trackUnevaluated()) { + return false; + } + + $shared = $this->sharedObject(); + + if (!isset($shared->evaluatedProperties)) { + $shared->evaluatedProperties = $properties; + } else { + $shared->evaluatedProperties = array_values(array_unique(array_merge($shared->evaluatedProperties, $properties))); + } + + return true; + } + + public function getEvaluatedProperties() + { + return $this->sharedObject()->evaluatedProperties ?? null; + } + + public function getUnevaluatedProperties() + { + $properties = $this->getObjectProperties(); + if (!$properties) { + return $properties; + } + + $evaluated = $this->sharedObject()->evaluatedProperties ?? null; + if (!$evaluated) { + return $properties; + } + + return array_values(array_diff($properties, $evaluated)); + } + + public function markAllAsEvaluatedItems(): bool + { + return $this->addEvaluatedItems(range(0, count($this->currentData()))); + } + + /** + * @param int $count + */ + public function markCountAsEvaluatedItems($count): bool + { + if (!$count) { + return false; + } + + return $this->addEvaluatedItems(range(0, $count)); + } + + /** + * @param mixed[]|null $items + */ + public function addEvaluatedItems($items): bool + { + if (!$items || !($this->currentDataType() === 'array') || !$this->trackUnevaluated()) { + return false; + } + + $shared = $this->sharedObject(); + + if (!isset($shared->evaluatedItems)) { + $shared->evaluatedItems = $items; + } else { + $shared->evaluatedItems = array_values(array_unique(array_merge($shared->evaluatedItems, $items), SORT_NUMERIC)); + } + + return true; + } + + public function getEvaluatedItems() + { + return $this->sharedObject()->evaluatedItems ?? null; + } + + public function getUnevaluatedItems() + { + if ($this->currentDataType() !== 'array') { + return null; + } + + $items = array_keys($this->currentData()); + if (!$items) { + return $items; + } + + $evaluated = $this->sharedObject()->evaluatedItems ?? null; + if (!$evaluated) { + return $items; + } + + return array_values(array_diff($items, $evaluated)); + } + + /** + * @param \Opis\JsonSchema\Schema $schema + * @param int|null $maxErrors + * @param bool $reset_on_error_only + * @param \ArrayObject|null $array + */ + public function validateSchemaWithoutEvaluated( + $schema, + $maxErrors = null, + $reset_on_error_only = false, + $array = null + ) { + $currentMaxErrors = $this->maxErrors; + + $this->maxErrors = $maxErrors ?? $currentMaxErrors; + + if ($this->trackUnevaluated()) { + $shared = $this->sharedObject(); + + $props = $shared->evaluatedProperties ?? null; + $items = $shared->evaluatedItems ?? null; + + $error = $schema->validate($this); + + if ($array) { + $value = null; + + if ($shared->evaluatedProperties ?? null) { + if ($props) { + if ($diff = array_diff($shared->evaluatedProperties, $props)) { + $value['properties'] = $diff; + } + } else { + $value['properties'] = $shared->evaluatedProperties; + } + } + + if ($shared->evaluatedItems ?? null) { + if ($items) { + if ($diff = array_diff($shared->evaluatedItems, $items)) { + $value['items'] = $diff; + } + } else { + $value['items'] = $shared->evaluatedItems; + } + } + + if ($value) { + $array[] = $value; + } + } + + if ($reset_on_error_only) { + if ($error) { + $shared->evaluatedProperties = $props; + $shared->evaluatedItems = $items; + } + } else { + $shared->evaluatedProperties = $props; + $shared->evaluatedItems = $items; + } + } else { + $error = $schema->validate($this); + } + + $this->maxErrors = $currentMaxErrors; + + return $error; + } +} diff --git a/src/opis/json-schema/src/ValidationResult.php b/src/opis/json-schema/src/ValidationResult.php new file mode 100644 index 00000000..2c687ac6 --- /dev/null +++ b/src/opis/json-schema/src/ValidationResult.php @@ -0,0 +1,56 @@ +error = $error; + } + + public function error() + { + return $this->error; + } + + public function isValid(): bool + { + return $this->error === null; + } + + public function hasError(): bool + { + return $this->error !== null; + } + + public function __toString(): string + { + if ($this->error) { + return $this->error->message(); + } + return ''; + } +} diff --git a/src/opis/json-schema/src/Validator.php b/src/opis/json-schema/src/Validator.php new file mode 100644 index 00000000..316179ab --- /dev/null +++ b/src/opis/json-schema/src/Validator.php @@ -0,0 +1,289 @@ +loader = $loader ?? new SchemaLoader(new SchemaParser(), new SchemaResolver(), true); + $this->maxErrors = $max_errors; + } + + /** + * @param $data + * @param bool|string|Uri|Schema|object $schema + * @param array|null $globals + * @param array|null $slots + * @return ValidationResult + */ + public function validate($data, $schema, $globals = null, $slots = null): ValidationResult + { + if (is_string($schema)) { + if ($uri = Uri::parse($schema, true)) { + $schema = $uri; + } else { + $schema = json_decode($schema, false); + } + } + + $error = null; + if (is_bool($schema)) { + $error = $this->dataValidation($data, $schema, $globals, $slots); + } elseif (is_object($schema)) { + if ($schema instanceof Uri) { + $error = $this->uriValidation($data, $schema, $globals, $slots); + } elseif ($schema instanceof Schema) { + $error = $this->schemaValidation($data, $schema, $globals, $slots); + } else { + $error = $this->dataValidation($data, $schema, $globals, $slots); + } + } else { + throw new InvalidArgumentException("Invalid schema"); + } + + return new ValidationResult($error); + } + + /** + * @param $data + * @param Uri|string $uri + * @param array|null $globals + * @param array|null $slots + * @return null|ValidationError + */ + public function uriValidation($data, $uri, $globals = null, $slots = null) + { + if (is_string($uri)) { + $uri = Uri::parse($uri, true); + } + + if (!($uri instanceof Uri)) { + throw new InvalidArgumentException("Invalid uri"); + } + + if ($uri->fragment() === null) { + $uri = Uri::merge($uri, null, true); + } + + $schema = $this->loader->loadSchemaById($uri); + + if ($schema === null) { + throw new RuntimeException("Schema not found: $uri"); + } + + return $this->schemaValidation($data, $schema, $globals, $slots); + } + + /** + * @param $data + * @param string|object|bool $schema + * @param array|null $globals + * @param array|null $slots + * @param string|null $id + * @param string|null $draft + * @return ValidationError|null + */ + public function dataValidation( + $data, + $schema, + $globals = null, + $slots = null, + $id = null, + $draft = null + ) + { + if (is_string($schema)) { + $schema = json_decode($schema, false); + } + + if ($schema === true) { + return null; + } + + if ($schema === false) { + $schema = $this->loader->loadBooleanSchema(false, $id, $draft); + } else { + if (!is_object($schema)) { + throw new InvalidArgumentException("Invalid schema"); + } + + $schema = $this->loader->loadObjectSchema($schema, $id, $draft); + } + + return $this->schemaValidation($data, $schema, $globals, $slots); + } + + /** + * @param $data + * @param Schema $schema + * @param array|null $globals + * @param array|null $slots + * @return null|ValidationError + */ + public function schemaValidation( + $data, + $schema, + $globals = null, + $slots = null + ) + { + return $schema->validate($this->createContext($data, $globals, $slots)); + } + + /** + * @param $data + * @param array|null $globals + * @param array|null $slots + * @return ValidationContext + */ + public function createContext($data, $globals = null, $slots = null): ValidationContext + { + if ($slots) { + $slots = $this->parseSlots($slots); + } + + return new ValidationContext($data, $this->loader, null, null, $globals ?? [], $slots, $this->maxErrors); + } + + /** + * @return SchemaParser + */ + public function parser(): SchemaParser + { + return $this->loader->parser(); + } + + /** + * @param SchemaParser $parser + * @return Validator + */ + public function setParser($parser): self + { + $this->loader->setParser($parser); + + return $this; + } + + /** + * @return SchemaResolver|null + */ + public function resolver() + { + return $this->loader->resolver(); + } + + /** + * @param SchemaResolver|null $resolver + * @return Validator + */ + public function setResolver($resolver): self + { + $this->loader->setResolver($resolver); + + return $this; + } + + /** + * @return SchemaLoader + */ + public function loader(): SchemaLoader + { + return $this->loader; + } + + /** + * @param SchemaLoader $loader + * @return Validator + */ + public function setLoader($loader): self + { + $this->loader = $loader; + + return $this; + } + + /** + * @return int + */ + public function getMaxErrors(): int + { + return $this->maxErrors; + } + + /** + * @param int $max_errors + * @return Validator + */ + public function setMaxErrors($max_errors): self + { + $this->maxErrors = $max_errors; + + return $this; + } + + /** + * @param array $slots + * @return array + */ + protected function parseSlots($slots): array + { + foreach ($slots as $name => &$value) { + if (!is_string($name)) { + unset($slots[$name]); + continue; + } + + if (is_string($value)) { + $value = Uri::parse($value, true); + } + + if ($value instanceof Uri) { + $value = $this->loader->loadSchemaById($value); + } elseif (is_bool($value)) { + $value = $this->loader->loadBooleanSchema($value); + } + + if (!is_object($value)) { + unset($slots[$name]); + } + + unset($value); + } + + return $slots; + } +} diff --git a/src/opis/json-schema/src/Variables.php b/src/opis/json-schema/src/Variables.php new file mode 100644 index 00000000..9837173e --- /dev/null +++ b/src/opis/json-schema/src/Variables.php @@ -0,0 +1,28 @@ +pointer = $pointer; + $this->each = $each; + $this->hasDefault = func_num_args() === 3; + $this->defaultValue = $default; + } + + /** + * @return JsonPointer + */ + public function pointer(): JsonPointer + { + return $this->pointer; + } + + /** + * @return null|Variables + */ + public function each() + { + return $this->each; + } + + /** + * @return bool + */ + public function hasDefaultValue(): bool + { + return $this->hasDefault; + } + + /** + * @return mixed|null + */ + public function defaultValue() + { + return $this->defaultValue; + } + + /** + * @inheritDoc + * @param mixed[] $path + */ + public function resolve($data, $path = []) + { + $resolved = $this->pointer->data($data, $path, $this); + if ($resolved === $this) { + return $this->defaultValue; + } + + if ($this->each && (is_array($resolved) || is_object($resolved))) { + $path = $this->pointer->absolutePath($path); + foreach ($resolved as $key => &$value) { + $path[] = $key; + $value = $this->each->resolve($data, $path); + array_pop($path); + unset($value); + } + } + + return $resolved; + } +} diff --git a/src/opis/json-schema/src/Variables/VariablesContainer.php b/src/opis/json-schema/src/Variables/VariablesContainer.php new file mode 100644 index 00000000..0a627d3c --- /dev/null +++ b/src/opis/json-schema/src/Variables/VariablesContainer.php @@ -0,0 +1,169 @@ +keys = [ + 'ref' => $ref_key, + 'each' => $each_key, + 'default' => $default_key, + ]; + + if ($lazy) { + $this->vars = $data; + } else { + $this->parsed = true; + $this->vars = $this->parse($data); + } + } + + /** + * @inheritdoc + * @param mixed[] $path + */ + public function resolve($data, $path = []) + { + if (!$this->parsed) { + $this->vars = $this->parse($this->vars); + $this->parsed = true; + } + + if (!$this->hasRefs) { + // Nothing to resolve + return $this->vars; + } + + return $this->deepClone($this->vars, $data, $path); + } + + /** + * @param $vars + * @param $data + * @param string[]|int[] $path + * @return array|object|mixed + */ + private function deepClone($vars, $data, array $path) + { + $toObject = false; + if (is_object($vars)) { + if ($vars instanceof Variables) { + return $vars->resolve($data, $path); + } + $vars = get_object_vars($vars); + $toObject = true; + } elseif (!is_array($vars)) { + return $vars; + } + + foreach ($vars as &$var) { + if ($var !== null && !is_scalar($var)) { + $var = $this->deepClone($var, $data, $path); + } + unset($var); + } + + return $toObject ? (object)$vars : $vars; + } + + /** + * @param mixed $data + * @return mixed + */ + private function parse($data) + { + if (is_array($data)) { + return array_map([$this, 'parse'], $data); + } + + if (!is_object($data)) { + return $data; + } + + if ($vars = $this->parseRef($data)) { + $this->hasRefs = true; + + return $vars; + } + + return (object)array_map([$this, 'parse'], get_object_vars($data)); + } + + /** + * @param object $data + * @return null|Variables + */ + private function parseRef($data) + { + if (!property_exists($data, $this->keys['ref'])) { + return null; + } + + $ref = $data->{$this->keys['ref']}; + if (!is_string($ref)) { + return null; + } + + $pointer = JsonPointer::parse($ref); + if ($pointer === null) { + return null; + } + + $each = null; + if (property_exists($data, $this->keys['each']) && is_object($data->{$this->keys['each']})) { + $each = new self($data->{$this->keys['each']}, !$this->parsed, $this->keys['ref'], $this->keys['each'], $this->keys['default']); + } + + if (property_exists($data, $this->keys['default'])) { + return new RefVariablesContainer($pointer, $each, $data->{$this->keys['default']}); + } + + return new RefVariablesContainer($pointer, $each); + } +} diff --git a/src/opis/string/.editorconfig b/src/opis/string/.editorconfig new file mode 100644 index 00000000..131e7a27 --- /dev/null +++ b/src/opis/string/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +charset = utf-8 diff --git a/src/opis/string/CHANGELOG.md b/src/opis/string/CHANGELOG.md new file mode 100644 index 00000000..32fc7214 --- /dev/null +++ b/src/opis/string/CHANGELOG.md @@ -0,0 +1,6 @@ +CHANGELOG +------------- + +### v2.0.0, 2021.04.13 + +* The library was fully refactored diff --git a/src/opis/string/LICENSE b/src/opis/string/LICENSE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/src/opis/string/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/src/opis/string/NOTICE b/src/opis/string/NOTICE new file mode 100644 index 00000000..d63600ed --- /dev/null +++ b/src/opis/string/NOTICE @@ -0,0 +1,9 @@ +Opis String +Copyright 2018-2021 Zindex Software + +This product includes software developed at +Zindex Software (http://zindex.software). + +This software was originally developed by Marius Sarca and Sorin Sarca +(Copyright 2016-2018). The copyright info was changed with the permission +of the original authors. diff --git a/src/opis/string/README.md b/src/opis/string/README.md new file mode 100644 index 00000000..8bca7c7b --- /dev/null +++ b/src/opis/string/README.md @@ -0,0 +1,50 @@ +Opis String +=========== +[![Tests](https://github.com/opis/string/workflows/Tests/badge.svg)](https://github.com/opis/string/actions) +[![Latest Stable Version](https://poser.pugx.org/opis/string/version.png)](https://packagist.org/packages/opis/string) +[![Latest Unstable Version](https://poser.pugx.org/opis/string/v/unstable.png)](https://packagist.org/packages/opis/string) +[![License](https://poser.pugx.org/opis/string/license.png)](https://packagist.org/packages/opis/string) + +Multibyte strings +---------------------------- + +**Opis String** is a tiny library that allows you to work with multibyte encoded strings in an object-oriented manner. +The library has no dependencies to *mb_string* or similar PHP extensions. + +## Documentation + +The full documentation for this library can be found [here][documentation]. + +## License + +**Opis String** is licensed under the [Apache License, Version 2.0][license]. + +## Requirements + +* PHP ^7.4 || ^8.0 +* ext-json +* ext-iconv + +## Installation + +**Opis String** is available on [Packagist] and it can be installed from a +command line interface by using [Composer]. + +```bash +composer require opis/string +``` + +Or you could directly reference it into your `composer.json` file as a dependency + +```json +{ + "require": { + "opis/string": "^2.0" + } +} +``` + +[documentation]: https://opis.io/string +[license]: https://www.apache.org/licenses/LICENSE-2.0 "Apache License" +[Packagist]: https://packagist.org/packages/opis/string "Packagist" +[Composer]: https://getcomposer.org "Composer" diff --git a/src/opis/string/composer.json b/src/opis/string/composer.json new file mode 100644 index 00000000..0b96c635 --- /dev/null +++ b/src/opis/string/composer.json @@ -0,0 +1,40 @@ +{ + "name": "opis/string", + "description": "Multibyte strings as objects", + "keywords": ["opis", "string", "utf-8", "multi-byte", "string manipulation"], + "homepage": "https://opis.io/string", + "license": "Apache-2.0", + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "require": { + "php": "^7.4 || ^8.0", + "ext-json": "*", + "ext-iconv": "*" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Opis\\String\\Test\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + } +} diff --git a/src/opis/string/res/ascii.php b/src/opis/string/res/ascii.php new file mode 100644 index 00000000..000a19fc --- /dev/null +++ b/src/opis/string/res/ascii.php @@ -0,0 +1,751 @@ + 0x28, +0xAA => 0x61, +0xB0 => 0x30, +0xB2 => 0x32, +0xB3 => 0x33, +0xB5 => 0x75, +0xB9 => 0x31, +0xBA => 0x6F, +0xC0 => 0x41, +0xC1 => 0x41, +0xC2 => 0x41, +0xC3 => 0x41, +0xC4 => 0x41, +0xC5 => 0x41, +0xC6 => 0x41, +0xC7 => 0x43, +0xC8 => 0x45, +0xC9 => 0x45, +0xCA => 0x45, +0xCB => 0x45, +0xCC => 0x49, +0xCD => 0x49, +0xCE => 0x49, +0xCF => 0x49, +0xD0 => 0x44, +0xD1 => 0x4E, +0xD2 => 0x4F, +0xD3 => 0x4F, +0xD4 => 0x4F, +0xD5 => 0x4F, +0xD6 => 0x4F, +0xD8 => 0x4F, +0xD9 => 0x55, +0xDA => 0x55, +0xDB => 0x55, +0xDC => 0x55, +0xDD => 0x59, +0xDE => 0x54, +0xDF => 0x73, +0xE0 => 0x61, +0xE1 => 0x61, +0xE2 => 0x61, +0xE3 => 0x61, +0xE4 => 0x61, +0xE5 => 0x61, +0xE6 => 0x61, +0xE7 => 0x63, +0xE8 => 0x65, +0xE9 => 0x65, +0xEA => 0x65, +0xEB => 0x65, +0xEC => 0x69, +0xED => 0x69, +0xEE => 0x69, +0xEF => 0x69, +0xF0 => 0x64, +0xF1 => 0x6E, +0xF2 => 0x6F, +0xF3 => 0x6F, +0xF4 => 0x6F, +0xF5 => 0x6F, +0xF6 => 0x6F, +0xF8 => 0x6F, +0xF9 => 0x75, +0xFA => 0x75, +0xFB => 0x75, +0xFC => 0x75, +0xFD => 0x79, +0xFE => 0x74, +0xFF => 0x79, +0x100 => 0x41, +0x101 => 0x61, +0x102 => 0x41, +0x103 => 0x61, +0x104 => 0x41, +0x105 => 0x61, +0x106 => 0x43, +0x107 => 0x63, +0x108 => 0x43, +0x109 => 0x63, +0x10A => 0x43, +0x10B => 0x63, +0x10C => 0x43, +0x10D => 0x63, +0x10E => 0x44, +0x10F => 0x64, +0x110 => 0x44, +0x111 => 0x64, +0x112 => 0x45, +0x113 => 0x65, +0x114 => 0x45, +0x115 => 0x65, +0x116 => 0x45, +0x117 => 0x65, +0x118 => 0x45, +0x119 => 0x65, +0x11A => 0x45, +0x11B => 0x65, +0x11C => 0x47, +0x11D => 0x67, +0x11E => 0x47, +0x11F => 0x67, +0x120 => 0x47, +0x121 => 0x67, +0x122 => 0x47, +0x123 => 0x67, +0x124 => 0x48, +0x125 => 0x68, +0x126 => 0x48, +0x127 => 0x68, +0x128 => 0x49, +0x129 => 0x69, +0x12A => 0x49, +0x12B => 0x69, +0x12C => 0x49, +0x12D => 0x69, +0x12E => 0x49, +0x12F => 0x69, +0x130 => 0x49, +0x131 => 0x69, +0x132 => 0x49, +0x133 => 0x69, +0x134 => 0x4A, +0x135 => 0x6A, +0x136 => 0x6B, +0x137 => 0x6B, +0x138 => 0x6B, +0x139 => 0x4C, +0x13A => 0x6C, +0x13B => 0x4C, +0x13C => 0x6C, +0x13D => 0x4C, +0x13E => 0x6C, +0x13F => 0x4C, +0x140 => 0x6C, +0x141 => 0x4C, +0x142 => 0x6C, +0x143 => 0x4E, +0x144 => 0x6E, +0x145 => 0x4E, +0x146 => 0x6E, +0x147 => 0x4E, +0x148 => 0x6E, +0x149 => 0x6E, +0x14A => 0x4E, +0x14B => 0x6E, +0x14C => 0x4F, +0x14D => 0x6F, +0x14E => 0x4F, +0x14F => 0x6F, +0x150 => 0x4F, +0x151 => 0x6F, +0x152 => 0x4F, +0x153 => 0x6F, +0x154 => 0x52, +0x155 => 0x72, +0x156 => 0x52, +0x157 => 0x72, +0x158 => 0x52, +0x159 => 0x72, +0x15A => 0x53, +0x15B => 0x73, +0x15C => 0x53, +0x15D => 0x73, +0x15E => 0x53, +0x15F => 0x73, +0x160 => 0x53, +0x161 => 0x73, +0x162 => 0x54, +0x163 => 0x74, +0x164 => 0x54, +0x165 => 0x74, +0x166 => 0x54, +0x167 => 0x74, +0x168 => 0x55, +0x169 => 0x75, +0x16A => 0x55, +0x16B => 0x75, +0x16C => 0x55, +0x16D => 0x75, +0x16E => 0x55, +0x16F => 0x75, +0x170 => 0x55, +0x171 => 0x75, +0x172 => 0x55, +0x173 => 0x75, +0x174 => 0x57, +0x175 => 0x77, +0x176 => 0x59, +0x177 => 0x79, +0x178 => 0x59, +0x179 => 0x5A, +0x17A => 0x7A, +0x17B => 0x5A, +0x17C => 0x7A, +0x17D => 0x5A, +0x17E => 0x7A, +0x17F => 0x73, +0x189 => 0x44, +0x18A => 0x44, +0x18B => 0x44, +0x18C => 0x64, +0x18F => 0x45, +0x192 => 0x66, +0x1A0 => 0x4F, +0x1A1 => 0x6F, +0x1AF => 0x55, +0x1B0 => 0x75, +0x1CD => 0x41, +0x1CE => 0x61, +0x1CF => 0x49, +0x1D0 => 0x69, +0x1D1 => 0x4F, +0x1D2 => 0x6F, +0x1D3 => 0x55, +0x1D4 => 0x75, +0x1D5 => 0x55, +0x1D6 => 0x75, +0x1D7 => 0x55, +0x1D8 => 0x75, +0x1D9 => 0x55, +0x1DA => 0x75, +0x1DB => 0x55, +0x1DC => 0x75, +0x1FA => 0x41, +0x1FB => 0x61, +0x1FC => 0x41, +0x1FD => 0x61, +0x1FE => 0x4F, +0x1FF => 0x6F, +0x218 => 0x53, +0x219 => 0x73, +0x21A => 0x54, +0x21B => 0x74, +0x221 => 0x64, +0x256 => 0x64, +0x257 => 0x64, +0x259 => 0x65, +0x386 => 0x41, +0x388 => 0x45, +0x389 => 0x48, +0x38A => 0x49, +0x38C => 0x4F, +0x38E => 0x59, +0x38F => 0x57, +0x390 => 0x69, +0x391 => 0x41, +0x392 => 0x42, +0x393 => 0x47, +0x394 => 0x44, +0x395 => 0x45, +0x396 => 0x5A, +0x397 => 0x48, +0x398 => 0x4F, +0x399 => 0x49, +0x39A => 0x4B, +0x39B => 0x4C, +0x39C => 0x4D, +0x39D => 0x4E, +0x39E => 0x58, +0x39F => 0x4F, +0x3A0 => 0x50, +0x3A1 => 0x52, +0x3A3 => 0x53, +0x3A4 => 0x54, +0x3A5 => 0x59, +0x3A6 => 0x46, +0x3A7 => 0x58, +0x3A8 => 0x50, +0x3A9 => 0x57, +0x3AA => 0x49, +0x3AB => 0x59, +0x3AC => 0x61, +0x3AD => 0x65, +0x3AE => 0x68, +0x3AF => 0x69, +0x3B0 => 0x79, +0x3B1 => 0x61, +0x3B2 => 0x62, +0x3B3 => 0x67, +0x3B4 => 0x64, +0x3B5 => 0x65, +0x3B6 => 0x7A, +0x3B7 => 0x68, +0x3B8 => 0x6F, +0x3B9 => 0x69, +0x3BA => 0x6B, +0x3BB => 0x6C, +0x3BC => 0x6D, +0x3BD => 0x6E, +0x3BE => 0x78, +0x3BF => 0x6F, +0x3C0 => 0x70, +0x3C1 => 0x72, +0x3C2 => 0x73, +0x3C3 => 0x73, +0x3C4 => 0x74, +0x3C5 => 0x79, +0x3C6 => 0x66, +0x3C7 => 0x78, +0x3C8 => 0x70, +0x3C9 => 0x77, +0x3CA => 0x69, +0x3CB => 0x79, +0x3CC => 0x6F, +0x3CD => 0x79, +0x3CE => 0x77, +0x3D0 => 0x76, +0x3D1 => 0x74, +0x3D2 => 0x49, +0x401 => 0x45, +0x402 => 0x44, +0x404 => 0x45, +0x406 => 0x49, +0x407 => 0x49, +0x408 => 0x6A, +0x409 => 0x4C, +0x40A => 0x4E, +0x40F => 0x44, +0x410 => 0x41, +0x411 => 0x42, +0x412 => 0x56, +0x413 => 0x47, +0x414 => 0x44, +0x415 => 0x45, +0x416 => 0x5A, +0x417 => 0x5A, +0x418 => 0x49, +0x419 => 0x59, +0x41A => 0x4B, +0x41B => 0x4C, +0x41C => 0x4D, +0x41D => 0x4E, +0x41E => 0x4F, +0x41F => 0x50, +0x420 => 0x52, +0x421 => 0x53, +0x422 => 0x54, +0x423 => 0x55, +0x424 => 0x46, +0x425 => 0x4B, +0x426 => 0x54, +0x427 => 0x43, +0x428 => 0x53, +0x429 => 0x53, +0x42A => 0x62, +0x42B => 0x59, +0x42C => 0x62, +0x42D => 0x45, +0x42E => 0x59, +0x42F => 0x59, +0x430 => 0x61, +0x431 => 0x62, +0x432 => 0x76, +0x433 => 0x67, +0x434 => 0x64, +0x435 => 0x65, +0x436 => 0x7A, +0x437 => 0x7A, +0x438 => 0x69, +0x439 => 0x79, +0x43A => 0x6B, +0x43B => 0x6C, +0x43C => 0x6D, +0x43D => 0x6E, +0x43E => 0x6F, +0x43F => 0x70, +0x440 => 0x72, +0x441 => 0x73, +0x442 => 0x74, +0x443 => 0x75, +0x444 => 0x66, +0x445 => 0x6B, +0x446 => 0x74, +0x447 => 0x63, +0x448 => 0x73, +0x449 => 0x73, +0x44B => 0x79, +0x44D => 0x65, +0x44E => 0x79, +0x44F => 0x79, +0x451 => 0x65, +0x452 => 0x64, +0x454 => 0x65, +0x456 => 0x69, +0x457 => 0x69, +0x458 => 0x6A, +0x459 => 0x6C, +0x45A => 0x6E, +0x45F => 0x64, +0x490 => 0x47, +0x491 => 0x67, +0x4E8 => 0x4F, +0x622 => 0x61, +0x623 => 0x61, +0x624 => 0x6F, +0x625 => 0x65, +0x626 => 0x65, +0x627 => 0x61, +0x628 => 0x62, +0x62A => 0x74, +0x62B => 0x74, +0x62C => 0x6A, +0x62D => 0x68, +0x62E => 0x6B, +0x62F => 0x64, +0x630 => 0x74, +0x631 => 0x72, +0x632 => 0x7A, +0x633 => 0x73, +0x634 => 0x73, +0x635 => 0x73, +0x636 => 0x64, +0x637 => 0x74, +0x638 => 0x74, +0x639 => 0x61, +0x63A => 0x67, +0x641 => 0x66, +0x642 => 0x6B, +0x643 => 0x6B, +0x644 => 0x6C, +0x645 => 0x6D, +0x646 => 0x6E, +0x647 => 0x68, +0x648 => 0x6F, +0x64A => 0x79, +0x664 => 0x34, +0x665 => 0x35, +0x666 => 0x36, +0x67E => 0x70, +0x686 => 0x63, +0x698 => 0x7A, +0x6A9 => 0x6B, +0x6AF => 0x67, +0x6CC => 0x69, +0x6F0 => 0x30, +0x6F1 => 0x31, +0x6F2 => 0x32, +0x6F3 => 0x33, +0x6F4 => 0x34, +0x6F5 => 0x35, +0x6F6 => 0x36, +0x6F7 => 0x37, +0x6F8 => 0x38, +0x6F9 => 0x39, +0x905 => 0x61, +0x906 => 0x61, +0x907 => 0x69, +0x908 => 0x69, +0x909 => 0x75, +0x90A => 0x75, +0x90D => 0x65, +0x90F => 0x65, +0x910 => 0x61, +0x911 => 0x6F, +0x912 => 0x6F, +0x913 => 0x6F, +0x92C => 0x42, +0x932 => 0x4C, +0x1000 => 0x6B, +0x1002 => 0x67, +0x1005 => 0x73, +0x1007 => 0x7A, +0x1009 => 0x75, +0x100A => 0x69, +0x100B => 0x74, +0x100D => 0x64, +0x1010 => 0x74, +0x1012 => 0x64, +0x1014 => 0x6E, +0x1015 => 0x70, +0x1017 => 0x62, +0x1019 => 0x6D, +0x101A => 0x79, +0x101C => 0x6C, +0x101D => 0x77, +0x101F => 0x68, +0x1021 => 0x61, +0x1023 => 0x69, +0x1027 => 0x65, +0x102B => 0x61, +0x102C => 0x61, +0x102D => 0x6F, +0x102E => 0x69, +0x102F => 0x75, +0x1030 => 0x75, +0x1031 => 0x65, +0x1032 => 0x65, +0x103D => 0x77, +0x103E => 0x68, +0x10D0 => 0x61, +0x10D1 => 0x62, +0x10D2 => 0x67, +0x10D3 => 0x64, +0x10D4 => 0x65, +0x10D5 => 0x76, +0x10D6 => 0x7A, +0x10D7 => 0x74, +0x10D8 => 0x69, +0x10D9 => 0x6B, +0x10DA => 0x6C, +0x10DB => 0x6D, +0x10DC => 0x6E, +0x10DD => 0x6F, +0x10DE => 0x70, +0x10DF => 0x7A, +0x10E0 => 0x72, +0x10E1 => 0x73, +0x10E2 => 0x74, +0x10E3 => 0x75, +0x10E4 => 0x66, +0x10E5 => 0x6B, +0x10E6 => 0x67, +0x10E7 => 0x71, +0x10E8 => 0x73, +0x10E9 => 0x63, +0x10EA => 0x74, +0x10EB => 0x64, +0x10EC => 0x74, +0x10ED => 0x63, +0x10EE => 0x6B, +0x10EF => 0x6A, +0x10F0 => 0x68, +0x1D05 => 0x44, +0x1D06 => 0x44, +0x1D6D => 0x64, +0x1D81 => 0x64, +0x1D91 => 0x64, +0x1E9E => 0x53, +0x1EA0 => 0x41, +0x1EA1 => 0x61, +0x1EA2 => 0x41, +0x1EA3 => 0x61, +0x1EA4 => 0x41, +0x1EA5 => 0x61, +0x1EA6 => 0x41, +0x1EA7 => 0x61, +0x1EA8 => 0x41, +0x1EA9 => 0x61, +0x1EAA => 0x41, +0x1EAB => 0x61, +0x1EAC => 0x41, +0x1EAD => 0x61, +0x1EAE => 0x41, +0x1EAF => 0x61, +0x1EB0 => 0x41, +0x1EB1 => 0x61, +0x1EB2 => 0x41, +0x1EB3 => 0x61, +0x1EB4 => 0x41, +0x1EB5 => 0x61, +0x1EB6 => 0x41, +0x1EB7 => 0x61, +0x1EB8 => 0x45, +0x1EB9 => 0x65, +0x1EBA => 0x45, +0x1EBB => 0x65, +0x1EBC => 0x45, +0x1EBD => 0x65, +0x1EBE => 0x45, +0x1EBF => 0x65, +0x1EC0 => 0x45, +0x1EC1 => 0x65, +0x1EC2 => 0x45, +0x1EC3 => 0x65, +0x1EC4 => 0x45, +0x1EC5 => 0x65, +0x1EC6 => 0x45, +0x1EC7 => 0x65, +0x1EC8 => 0x49, +0x1EC9 => 0x69, +0x1ECA => 0x49, +0x1ECB => 0x69, +0x1ECC => 0x4F, +0x1ECD => 0x6F, +0x1ECE => 0x4F, +0x1ECF => 0x6F, +0x1ED0 => 0x4F, +0x1ED1 => 0x6F, +0x1ED2 => 0x4F, +0x1ED3 => 0x6F, +0x1ED4 => 0x4F, +0x1ED5 => 0x6F, +0x1ED6 => 0x4F, +0x1ED7 => 0x6F, +0x1ED8 => 0x4F, +0x1ED9 => 0x6F, +0x1EDA => 0x4F, +0x1EDB => 0x6F, +0x1EDC => 0x4F, +0x1EDD => 0x6F, +0x1EDE => 0x4F, +0x1EDF => 0x6F, +0x1EE0 => 0x4F, +0x1EE1 => 0x6F, +0x1EE2 => 0x4F, +0x1EE3 => 0x6F, +0x1EE4 => 0x55, +0x1EE5 => 0x75, +0x1EE6 => 0x55, +0x1EE7 => 0x75, +0x1EE8 => 0x55, +0x1EE9 => 0x75, +0x1EEA => 0x55, +0x1EEB => 0x75, +0x1EEC => 0x55, +0x1EED => 0x75, +0x1EEE => 0x55, +0x1EEF => 0x75, +0x1EF0 => 0x55, +0x1EF1 => 0x75, +0x1EF2 => 0x59, +0x1EF3 => 0x79, +0x1EF4 => 0x59, +0x1EF5 => 0x79, +0x1EF6 => 0x59, +0x1EF7 => 0x79, +0x1EF8 => 0x59, +0x1EF9 => 0x79, +0x1F00 => 0x61, +0x1F01 => 0x61, +0x1F02 => 0x61, +0x1F03 => 0x61, +0x1F04 => 0x61, +0x1F05 => 0x61, +0x1F06 => 0x61, +0x1F07 => 0x61, +0x1F08 => 0x41, +0x1F09 => 0x41, +0x1F0A => 0x41, +0x1F0B => 0x41, +0x1F0C => 0x41, +0x1F0D => 0x41, +0x1F0E => 0x41, +0x1F0F => 0x41, +0x1F10 => 0x65, +0x1F11 => 0x65, +0x1F12 => 0x65, +0x1F13 => 0x65, +0x1F14 => 0x65, +0x1F15 => 0x65, +0x1F18 => 0x45, +0x1F19 => 0x45, +0x1F1A => 0x45, +0x1F1B => 0x45, +0x1F1C => 0x45, +0x1F1D => 0x45, +0x1F30 => 0x69, +0x1F31 => 0x69, +0x1F32 => 0x69, +0x1F33 => 0x69, +0x1F34 => 0x69, +0x1F35 => 0x69, +0x1F36 => 0x69, +0x1F37 => 0x69, +0x1F38 => 0x49, +0x1F39 => 0x49, +0x1F3B => 0x49, +0x1F3C => 0x49, +0x1F3D => 0x49, +0x1F3E => 0x49, +0x1F3F => 0x49, +0x1F40 => 0x6F, +0x1F41 => 0x6F, +0x1F42 => 0x6F, +0x1F43 => 0x6F, +0x1F44 => 0x6F, +0x1F45 => 0x6F, +0x1F48 => 0x4F, +0x1F49 => 0x4F, +0x1F4A => 0x4F, +0x1F4B => 0x4F, +0x1F4C => 0x4F, +0x1F4D => 0x4F, +0x1F70 => 0x61, +0x1F72 => 0x65, +0x1F76 => 0x69, +0x1F78 => 0x6F, +0x1F80 => 0x61, +0x1F81 => 0x61, +0x1F82 => 0x61, +0x1F83 => 0x61, +0x1F84 => 0x61, +0x1F85 => 0x61, +0x1F86 => 0x61, +0x1F87 => 0x61, +0x1F88 => 0x41, +0x1F89 => 0x41, +0x1F8A => 0x41, +0x1F8B => 0x41, +0x1F8C => 0x41, +0x1F8D => 0x41, +0x1F8E => 0x41, +0x1F8F => 0x41, +0x1FB0 => 0x61, +0x1FB1 => 0x61, +0x1FB2 => 0x61, +0x1FB3 => 0x61, +0x1FB4 => 0x61, +0x1FB6 => 0x61, +0x1FB7 => 0x61, +0x1FB8 => 0x41, +0x1FB9 => 0x41, +0x1FBA => 0x41, +0x1FBC => 0x41, +0x1FC8 => 0x45, +0x1FD0 => 0x69, +0x1FD1 => 0x69, +0x1FD2 => 0x69, +0x1FD6 => 0x69, +0x1FD7 => 0x69, +0x1FD8 => 0x49, +0x1FD9 => 0x49, +0x1FDA => 0x49, +0x1FE8 => 0x59, +0x1FE9 => 0x59, +0x1FEA => 0x59, +0x1FF8 => 0x4F, +0x2000 => 0x20, +0x2001 => 0x20, +0x2002 => 0x20, +0x2003 => 0x20, +0x2004 => 0x20, +0x2005 => 0x20, +0x2006 => 0x20, +0x2007 => 0x20, +0x2008 => 0x20, +0x2009 => 0x20, +0x200A => 0x20, +0x202F => 0x20, +0x205F => 0x20, +0x2074 => 0x34, +0x2075 => 0x35, +0x2076 => 0x36, +0x2077 => 0x37, +0x2078 => 0x38, +0x2079 => 0x39, +0x2080 => 0x30, +0x2081 => 0x31, +0x2082 => 0x32, +0x2083 => 0x33, +0x2084 => 0x34, +0x2085 => 0x35, +0x2086 => 0x36, +0x2087 => 0x37, +0x2088 => 0x38, +0x2089 => 0x39, +0x3000 => 0x20, +]; diff --git a/src/opis/string/res/fold.php b/src/opis/string/res/fold.php new file mode 100644 index 00000000..e93f9819 --- /dev/null +++ b/src/opis/string/res/fold.php @@ -0,0 +1,1417 @@ + 0x61, +0x42 => 0x62, +0x43 => 0x63, +0x44 => 0x64, +0x45 => 0x65, +0x46 => 0x66, +0x47 => 0x67, +0x48 => 0x68, +0x49 => 0x69, +0x4A => 0x6A, +0x4B => 0x6B, +0x4C => 0x6C, +0x4D => 0x6D, +0x4E => 0x6E, +0x4F => 0x6F, +0x50 => 0x70, +0x51 => 0x71, +0x52 => 0x72, +0x53 => 0x73, +0x54 => 0x74, +0x55 => 0x75, +0x56 => 0x76, +0x57 => 0x77, +0x58 => 0x78, +0x59 => 0x79, +0x5A => 0x7A, +0xB5 => 0x3BC, +0xC0 => 0xE0, +0xC1 => 0xE1, +0xC2 => 0xE2, +0xC3 => 0xE3, +0xC4 => 0xE4, +0xC5 => 0xE5, +0xC6 => 0xE6, +0xC7 => 0xE7, +0xC8 => 0xE8, +0xC9 => 0xE9, +0xCA => 0xEA, +0xCB => 0xEB, +0xCC => 0xEC, +0xCD => 0xED, +0xCE => 0xEE, +0xCF => 0xEF, +0xD0 => 0xF0, +0xD1 => 0xF1, +0xD2 => 0xF2, +0xD3 => 0xF3, +0xD4 => 0xF4, +0xD5 => 0xF5, +0xD6 => 0xF6, +0xD8 => 0xF8, +0xD9 => 0xF9, +0xDA => 0xFA, +0xDB => 0xFB, +0xDC => 0xFC, +0xDD => 0xFD, +0xDE => 0xFE, +0x100 => 0x101, +0x102 => 0x103, +0x104 => 0x105, +0x106 => 0x107, +0x108 => 0x109, +0x10A => 0x10B, +0x10C => 0x10D, +0x10E => 0x10F, +0x110 => 0x111, +0x112 => 0x113, +0x114 => 0x115, +0x116 => 0x117, +0x118 => 0x119, +0x11A => 0x11B, +0x11C => 0x11D, +0x11E => 0x11F, +0x120 => 0x121, +0x122 => 0x123, +0x124 => 0x125, +0x126 => 0x127, +0x128 => 0x129, +0x12A => 0x12B, +0x12C => 0x12D, +0x12E => 0x12F, +0x132 => 0x133, +0x134 => 0x135, +0x136 => 0x137, +0x139 => 0x13A, +0x13B => 0x13C, +0x13D => 0x13E, +0x13F => 0x140, +0x141 => 0x142, +0x143 => 0x144, +0x145 => 0x146, +0x147 => 0x148, +0x14A => 0x14B, +0x14C => 0x14D, +0x14E => 0x14F, +0x150 => 0x151, +0x152 => 0x153, +0x154 => 0x155, +0x156 => 0x157, +0x158 => 0x159, +0x15A => 0x15B, +0x15C => 0x15D, +0x15E => 0x15F, +0x160 => 0x161, +0x162 => 0x163, +0x164 => 0x165, +0x166 => 0x167, +0x168 => 0x169, +0x16A => 0x16B, +0x16C => 0x16D, +0x16E => 0x16F, +0x170 => 0x171, +0x172 => 0x173, +0x174 => 0x175, +0x176 => 0x177, +0x178 => 0xFF, +0x179 => 0x17A, +0x17B => 0x17C, +0x17D => 0x17E, +0x17F => 0x73, +0x181 => 0x253, +0x182 => 0x183, +0x184 => 0x185, +0x186 => 0x254, +0x187 => 0x188, +0x189 => 0x256, +0x18A => 0x257, +0x18B => 0x18C, +0x18E => 0x1DD, +0x18F => 0x259, +0x190 => 0x25B, +0x191 => 0x192, +0x193 => 0x260, +0x194 => 0x263, +0x196 => 0x269, +0x197 => 0x268, +0x198 => 0x199, +0x19C => 0x26F, +0x19D => 0x272, +0x19F => 0x275, +0x1A0 => 0x1A1, +0x1A2 => 0x1A3, +0x1A4 => 0x1A5, +0x1A6 => 0x280, +0x1A7 => 0x1A8, +0x1A9 => 0x283, +0x1AC => 0x1AD, +0x1AE => 0x288, +0x1AF => 0x1B0, +0x1B1 => 0x28A, +0x1B2 => 0x28B, +0x1B3 => 0x1B4, +0x1B5 => 0x1B6, +0x1B7 => 0x292, +0x1B8 => 0x1B9, +0x1BC => 0x1BD, +0x1C4 => 0x1C6, +0x1C5 => 0x1C6, +0x1C7 => 0x1C9, +0x1C8 => 0x1C9, +0x1CA => 0x1CC, +0x1CB => 0x1CC, +0x1CD => 0x1CE, +0x1CF => 0x1D0, +0x1D1 => 0x1D2, +0x1D3 => 0x1D4, +0x1D5 => 0x1D6, +0x1D7 => 0x1D8, +0x1D9 => 0x1DA, +0x1DB => 0x1DC, +0x1DE => 0x1DF, +0x1E0 => 0x1E1, +0x1E2 => 0x1E3, +0x1E4 => 0x1E5, +0x1E6 => 0x1E7, +0x1E8 => 0x1E9, +0x1EA => 0x1EB, +0x1EC => 0x1ED, +0x1EE => 0x1EF, +0x1F1 => 0x1F3, +0x1F2 => 0x1F3, +0x1F4 => 0x1F5, +0x1F6 => 0x195, +0x1F7 => 0x1BF, +0x1F8 => 0x1F9, +0x1FA => 0x1FB, +0x1FC => 0x1FD, +0x1FE => 0x1FF, +0x200 => 0x201, +0x202 => 0x203, +0x204 => 0x205, +0x206 => 0x207, +0x208 => 0x209, +0x20A => 0x20B, +0x20C => 0x20D, +0x20E => 0x20F, +0x210 => 0x211, +0x212 => 0x213, +0x214 => 0x215, +0x216 => 0x217, +0x218 => 0x219, +0x21A => 0x21B, +0x21C => 0x21D, +0x21E => 0x21F, +0x220 => 0x19E, +0x222 => 0x223, +0x224 => 0x225, +0x226 => 0x227, +0x228 => 0x229, +0x22A => 0x22B, +0x22C => 0x22D, +0x22E => 0x22F, +0x230 => 0x231, +0x232 => 0x233, +0x23A => 0x2C65, +0x23B => 0x23C, +0x23D => 0x19A, +0x23E => 0x2C66, +0x241 => 0x242, +0x243 => 0x180, +0x244 => 0x289, +0x245 => 0x28C, +0x246 => 0x247, +0x248 => 0x249, +0x24A => 0x24B, +0x24C => 0x24D, +0x24E => 0x24F, +0x345 => 0x3B9, +0x370 => 0x371, +0x372 => 0x373, +0x376 => 0x377, +0x37F => 0x3F3, +0x386 => 0x3AC, +0x388 => 0x3AD, +0x389 => 0x3AE, +0x38A => 0x3AF, +0x38C => 0x3CC, +0x38E => 0x3CD, +0x38F => 0x3CE, +0x391 => 0x3B1, +0x392 => 0x3B2, +0x393 => 0x3B3, +0x394 => 0x3B4, +0x395 => 0x3B5, +0x396 => 0x3B6, +0x397 => 0x3B7, +0x398 => 0x3B8, +0x399 => 0x3B9, +0x39A => 0x3BA, +0x39B => 0x3BB, +0x39C => 0x3BC, +0x39D => 0x3BD, +0x39E => 0x3BE, +0x39F => 0x3BF, +0x3A0 => 0x3C0, +0x3A1 => 0x3C1, +0x3A3 => 0x3C3, +0x3A4 => 0x3C4, +0x3A5 => 0x3C5, +0x3A6 => 0x3C6, +0x3A7 => 0x3C7, +0x3A8 => 0x3C8, +0x3A9 => 0x3C9, +0x3AA => 0x3CA, +0x3AB => 0x3CB, +0x3C2 => 0x3C3, +0x3CF => 0x3D7, +0x3D0 => 0x3B2, +0x3D1 => 0x3B8, +0x3D5 => 0x3C6, +0x3D6 => 0x3C0, +0x3D8 => 0x3D9, +0x3DA => 0x3DB, +0x3DC => 0x3DD, +0x3DE => 0x3DF, +0x3E0 => 0x3E1, +0x3E2 => 0x3E3, +0x3E4 => 0x3E5, +0x3E6 => 0x3E7, +0x3E8 => 0x3E9, +0x3EA => 0x3EB, +0x3EC => 0x3ED, +0x3EE => 0x3EF, +0x3F0 => 0x3BA, +0x3F1 => 0x3C1, +0x3F4 => 0x3B8, +0x3F5 => 0x3B5, +0x3F7 => 0x3F8, +0x3F9 => 0x3F2, +0x3FA => 0x3FB, +0x3FD => 0x37B, +0x3FE => 0x37C, +0x3FF => 0x37D, +0x400 => 0x450, +0x401 => 0x451, +0x402 => 0x452, +0x403 => 0x453, +0x404 => 0x454, +0x405 => 0x455, +0x406 => 0x456, +0x407 => 0x457, +0x408 => 0x458, +0x409 => 0x459, +0x40A => 0x45A, +0x40B => 0x45B, +0x40C => 0x45C, +0x40D => 0x45D, +0x40E => 0x45E, +0x40F => 0x45F, +0x410 => 0x430, +0x411 => 0x431, +0x412 => 0x432, +0x413 => 0x433, +0x414 => 0x434, +0x415 => 0x435, +0x416 => 0x436, +0x417 => 0x437, +0x418 => 0x438, +0x419 => 0x439, +0x41A => 0x43A, +0x41B => 0x43B, +0x41C => 0x43C, +0x41D => 0x43D, +0x41E => 0x43E, +0x41F => 0x43F, +0x420 => 0x440, +0x421 => 0x441, +0x422 => 0x442, +0x423 => 0x443, +0x424 => 0x444, +0x425 => 0x445, +0x426 => 0x446, +0x427 => 0x447, +0x428 => 0x448, +0x429 => 0x449, +0x42A => 0x44A, +0x42B => 0x44B, +0x42C => 0x44C, +0x42D => 0x44D, +0x42E => 0x44E, +0x42F => 0x44F, +0x460 => 0x461, +0x462 => 0x463, +0x464 => 0x465, +0x466 => 0x467, +0x468 => 0x469, +0x46A => 0x46B, +0x46C => 0x46D, +0x46E => 0x46F, +0x470 => 0x471, +0x472 => 0x473, +0x474 => 0x475, +0x476 => 0x477, +0x478 => 0x479, +0x47A => 0x47B, +0x47C => 0x47D, +0x47E => 0x47F, +0x480 => 0x481, +0x48A => 0x48B, +0x48C => 0x48D, +0x48E => 0x48F, +0x490 => 0x491, +0x492 => 0x493, +0x494 => 0x495, +0x496 => 0x497, +0x498 => 0x499, +0x49A => 0x49B, +0x49C => 0x49D, +0x49E => 0x49F, +0x4A0 => 0x4A1, +0x4A2 => 0x4A3, +0x4A4 => 0x4A5, +0x4A6 => 0x4A7, +0x4A8 => 0x4A9, +0x4AA => 0x4AB, +0x4AC => 0x4AD, +0x4AE => 0x4AF, +0x4B0 => 0x4B1, +0x4B2 => 0x4B3, +0x4B4 => 0x4B5, +0x4B6 => 0x4B7, +0x4B8 => 0x4B9, +0x4BA => 0x4BB, +0x4BC => 0x4BD, +0x4BE => 0x4BF, +0x4C0 => 0x4CF, +0x4C1 => 0x4C2, +0x4C3 => 0x4C4, +0x4C5 => 0x4C6, +0x4C7 => 0x4C8, +0x4C9 => 0x4CA, +0x4CB => 0x4CC, +0x4CD => 0x4CE, +0x4D0 => 0x4D1, +0x4D2 => 0x4D3, +0x4D4 => 0x4D5, +0x4D6 => 0x4D7, +0x4D8 => 0x4D9, +0x4DA => 0x4DB, +0x4DC => 0x4DD, +0x4DE => 0x4DF, +0x4E0 => 0x4E1, +0x4E2 => 0x4E3, +0x4E4 => 0x4E5, +0x4E6 => 0x4E7, +0x4E8 => 0x4E9, +0x4EA => 0x4EB, +0x4EC => 0x4ED, +0x4EE => 0x4EF, +0x4F0 => 0x4F1, +0x4F2 => 0x4F3, +0x4F4 => 0x4F5, +0x4F6 => 0x4F7, +0x4F8 => 0x4F9, +0x4FA => 0x4FB, +0x4FC => 0x4FD, +0x4FE => 0x4FF, +0x500 => 0x501, +0x502 => 0x503, +0x504 => 0x505, +0x506 => 0x507, +0x508 => 0x509, +0x50A => 0x50B, +0x50C => 0x50D, +0x50E => 0x50F, +0x510 => 0x511, +0x512 => 0x513, +0x514 => 0x515, +0x516 => 0x517, +0x518 => 0x519, +0x51A => 0x51B, +0x51C => 0x51D, +0x51E => 0x51F, +0x520 => 0x521, +0x522 => 0x523, +0x524 => 0x525, +0x526 => 0x527, +0x528 => 0x529, +0x52A => 0x52B, +0x52C => 0x52D, +0x52E => 0x52F, +0x531 => 0x561, +0x532 => 0x562, +0x533 => 0x563, +0x534 => 0x564, +0x535 => 0x565, +0x536 => 0x566, +0x537 => 0x567, +0x538 => 0x568, +0x539 => 0x569, +0x53A => 0x56A, +0x53B => 0x56B, +0x53C => 0x56C, +0x53D => 0x56D, +0x53E => 0x56E, +0x53F => 0x56F, +0x540 => 0x570, +0x541 => 0x571, +0x542 => 0x572, +0x543 => 0x573, +0x544 => 0x574, +0x545 => 0x575, +0x546 => 0x576, +0x547 => 0x577, +0x548 => 0x578, +0x549 => 0x579, +0x54A => 0x57A, +0x54B => 0x57B, +0x54C => 0x57C, +0x54D => 0x57D, +0x54E => 0x57E, +0x54F => 0x57F, +0x550 => 0x580, +0x551 => 0x581, +0x552 => 0x582, +0x553 => 0x583, +0x554 => 0x584, +0x555 => 0x585, +0x556 => 0x586, +0x10A0 => 0x2D00, +0x10A1 => 0x2D01, +0x10A2 => 0x2D02, +0x10A3 => 0x2D03, +0x10A4 => 0x2D04, +0x10A5 => 0x2D05, +0x10A6 => 0x2D06, +0x10A7 => 0x2D07, +0x10A8 => 0x2D08, +0x10A9 => 0x2D09, +0x10AA => 0x2D0A, +0x10AB => 0x2D0B, +0x10AC => 0x2D0C, +0x10AD => 0x2D0D, +0x10AE => 0x2D0E, +0x10AF => 0x2D0F, +0x10B0 => 0x2D10, +0x10B1 => 0x2D11, +0x10B2 => 0x2D12, +0x10B3 => 0x2D13, +0x10B4 => 0x2D14, +0x10B5 => 0x2D15, +0x10B6 => 0x2D16, +0x10B7 => 0x2D17, +0x10B8 => 0x2D18, +0x10B9 => 0x2D19, +0x10BA => 0x2D1A, +0x10BB => 0x2D1B, +0x10BC => 0x2D1C, +0x10BD => 0x2D1D, +0x10BE => 0x2D1E, +0x10BF => 0x2D1F, +0x10C0 => 0x2D20, +0x10C1 => 0x2D21, +0x10C2 => 0x2D22, +0x10C3 => 0x2D23, +0x10C4 => 0x2D24, +0x10C5 => 0x2D25, +0x10C7 => 0x2D27, +0x10CD => 0x2D2D, +0x13F8 => 0x13F0, +0x13F9 => 0x13F1, +0x13FA => 0x13F2, +0x13FB => 0x13F3, +0x13FC => 0x13F4, +0x13FD => 0x13F5, +0x1C80 => 0x432, +0x1C81 => 0x434, +0x1C82 => 0x43E, +0x1C83 => 0x441, +0x1C84 => 0x442, +0x1C85 => 0x442, +0x1C86 => 0x44A, +0x1C87 => 0x463, +0x1C88 => 0xA64B, +0x1C90 => 0x10D0, +0x1C91 => 0x10D1, +0x1C92 => 0x10D2, +0x1C93 => 0x10D3, +0x1C94 => 0x10D4, +0x1C95 => 0x10D5, +0x1C96 => 0x10D6, +0x1C97 => 0x10D7, +0x1C98 => 0x10D8, +0x1C99 => 0x10D9, +0x1C9A => 0x10DA, +0x1C9B => 0x10DB, +0x1C9C => 0x10DC, +0x1C9D => 0x10DD, +0x1C9E => 0x10DE, +0x1C9F => 0x10DF, +0x1CA0 => 0x10E0, +0x1CA1 => 0x10E1, +0x1CA2 => 0x10E2, +0x1CA3 => 0x10E3, +0x1CA4 => 0x10E4, +0x1CA5 => 0x10E5, +0x1CA6 => 0x10E6, +0x1CA7 => 0x10E7, +0x1CA8 => 0x10E8, +0x1CA9 => 0x10E9, +0x1CAA => 0x10EA, +0x1CAB => 0x10EB, +0x1CAC => 0x10EC, +0x1CAD => 0x10ED, +0x1CAE => 0x10EE, +0x1CAF => 0x10EF, +0x1CB0 => 0x10F0, +0x1CB1 => 0x10F1, +0x1CB2 => 0x10F2, +0x1CB3 => 0x10F3, +0x1CB4 => 0x10F4, +0x1CB5 => 0x10F5, +0x1CB6 => 0x10F6, +0x1CB7 => 0x10F7, +0x1CB8 => 0x10F8, +0x1CB9 => 0x10F9, +0x1CBA => 0x10FA, +0x1CBD => 0x10FD, +0x1CBE => 0x10FE, +0x1CBF => 0x10FF, +0x1E00 => 0x1E01, +0x1E02 => 0x1E03, +0x1E04 => 0x1E05, +0x1E06 => 0x1E07, +0x1E08 => 0x1E09, +0x1E0A => 0x1E0B, +0x1E0C => 0x1E0D, +0x1E0E => 0x1E0F, +0x1E10 => 0x1E11, +0x1E12 => 0x1E13, +0x1E14 => 0x1E15, +0x1E16 => 0x1E17, +0x1E18 => 0x1E19, +0x1E1A => 0x1E1B, +0x1E1C => 0x1E1D, +0x1E1E => 0x1E1F, +0x1E20 => 0x1E21, +0x1E22 => 0x1E23, +0x1E24 => 0x1E25, +0x1E26 => 0x1E27, +0x1E28 => 0x1E29, +0x1E2A => 0x1E2B, +0x1E2C => 0x1E2D, +0x1E2E => 0x1E2F, +0x1E30 => 0x1E31, +0x1E32 => 0x1E33, +0x1E34 => 0x1E35, +0x1E36 => 0x1E37, +0x1E38 => 0x1E39, +0x1E3A => 0x1E3B, +0x1E3C => 0x1E3D, +0x1E3E => 0x1E3F, +0x1E40 => 0x1E41, +0x1E42 => 0x1E43, +0x1E44 => 0x1E45, +0x1E46 => 0x1E47, +0x1E48 => 0x1E49, +0x1E4A => 0x1E4B, +0x1E4C => 0x1E4D, +0x1E4E => 0x1E4F, +0x1E50 => 0x1E51, +0x1E52 => 0x1E53, +0x1E54 => 0x1E55, +0x1E56 => 0x1E57, +0x1E58 => 0x1E59, +0x1E5A => 0x1E5B, +0x1E5C => 0x1E5D, +0x1E5E => 0x1E5F, +0x1E60 => 0x1E61, +0x1E62 => 0x1E63, +0x1E64 => 0x1E65, +0x1E66 => 0x1E67, +0x1E68 => 0x1E69, +0x1E6A => 0x1E6B, +0x1E6C => 0x1E6D, +0x1E6E => 0x1E6F, +0x1E70 => 0x1E71, +0x1E72 => 0x1E73, +0x1E74 => 0x1E75, +0x1E76 => 0x1E77, +0x1E78 => 0x1E79, +0x1E7A => 0x1E7B, +0x1E7C => 0x1E7D, +0x1E7E => 0x1E7F, +0x1E80 => 0x1E81, +0x1E82 => 0x1E83, +0x1E84 => 0x1E85, +0x1E86 => 0x1E87, +0x1E88 => 0x1E89, +0x1E8A => 0x1E8B, +0x1E8C => 0x1E8D, +0x1E8E => 0x1E8F, +0x1E90 => 0x1E91, +0x1E92 => 0x1E93, +0x1E94 => 0x1E95, +0x1E9B => 0x1E61, +0x1E9E => 0xDF, +0x1EA0 => 0x1EA1, +0x1EA2 => 0x1EA3, +0x1EA4 => 0x1EA5, +0x1EA6 => 0x1EA7, +0x1EA8 => 0x1EA9, +0x1EAA => 0x1EAB, +0x1EAC => 0x1EAD, +0x1EAE => 0x1EAF, +0x1EB0 => 0x1EB1, +0x1EB2 => 0x1EB3, +0x1EB4 => 0x1EB5, +0x1EB6 => 0x1EB7, +0x1EB8 => 0x1EB9, +0x1EBA => 0x1EBB, +0x1EBC => 0x1EBD, +0x1EBE => 0x1EBF, +0x1EC0 => 0x1EC1, +0x1EC2 => 0x1EC3, +0x1EC4 => 0x1EC5, +0x1EC6 => 0x1EC7, +0x1EC8 => 0x1EC9, +0x1ECA => 0x1ECB, +0x1ECC => 0x1ECD, +0x1ECE => 0x1ECF, +0x1ED0 => 0x1ED1, +0x1ED2 => 0x1ED3, +0x1ED4 => 0x1ED5, +0x1ED6 => 0x1ED7, +0x1ED8 => 0x1ED9, +0x1EDA => 0x1EDB, +0x1EDC => 0x1EDD, +0x1EDE => 0x1EDF, +0x1EE0 => 0x1EE1, +0x1EE2 => 0x1EE3, +0x1EE4 => 0x1EE5, +0x1EE6 => 0x1EE7, +0x1EE8 => 0x1EE9, +0x1EEA => 0x1EEB, +0x1EEC => 0x1EED, +0x1EEE => 0x1EEF, +0x1EF0 => 0x1EF1, +0x1EF2 => 0x1EF3, +0x1EF4 => 0x1EF5, +0x1EF6 => 0x1EF7, +0x1EF8 => 0x1EF9, +0x1EFA => 0x1EFB, +0x1EFC => 0x1EFD, +0x1EFE => 0x1EFF, +0x1F08 => 0x1F00, +0x1F09 => 0x1F01, +0x1F0A => 0x1F02, +0x1F0B => 0x1F03, +0x1F0C => 0x1F04, +0x1F0D => 0x1F05, +0x1F0E => 0x1F06, +0x1F0F => 0x1F07, +0x1F18 => 0x1F10, +0x1F19 => 0x1F11, +0x1F1A => 0x1F12, +0x1F1B => 0x1F13, +0x1F1C => 0x1F14, +0x1F1D => 0x1F15, +0x1F28 => 0x1F20, +0x1F29 => 0x1F21, +0x1F2A => 0x1F22, +0x1F2B => 0x1F23, +0x1F2C => 0x1F24, +0x1F2D => 0x1F25, +0x1F2E => 0x1F26, +0x1F2F => 0x1F27, +0x1F38 => 0x1F30, +0x1F39 => 0x1F31, +0x1F3A => 0x1F32, +0x1F3B => 0x1F33, +0x1F3C => 0x1F34, +0x1F3D => 0x1F35, +0x1F3E => 0x1F36, +0x1F3F => 0x1F37, +0x1F48 => 0x1F40, +0x1F49 => 0x1F41, +0x1F4A => 0x1F42, +0x1F4B => 0x1F43, +0x1F4C => 0x1F44, +0x1F4D => 0x1F45, +0x1F59 => 0x1F51, +0x1F5B => 0x1F53, +0x1F5D => 0x1F55, +0x1F5F => 0x1F57, +0x1F68 => 0x1F60, +0x1F69 => 0x1F61, +0x1F6A => 0x1F62, +0x1F6B => 0x1F63, +0x1F6C => 0x1F64, +0x1F6D => 0x1F65, +0x1F6E => 0x1F66, +0x1F6F => 0x1F67, +0x1F88 => 0x1F80, +0x1F89 => 0x1F81, +0x1F8A => 0x1F82, +0x1F8B => 0x1F83, +0x1F8C => 0x1F84, +0x1F8D => 0x1F85, +0x1F8E => 0x1F86, +0x1F8F => 0x1F87, +0x1F98 => 0x1F90, +0x1F99 => 0x1F91, +0x1F9A => 0x1F92, +0x1F9B => 0x1F93, +0x1F9C => 0x1F94, +0x1F9D => 0x1F95, +0x1F9E => 0x1F96, +0x1F9F => 0x1F97, +0x1FA8 => 0x1FA0, +0x1FA9 => 0x1FA1, +0x1FAA => 0x1FA2, +0x1FAB => 0x1FA3, +0x1FAC => 0x1FA4, +0x1FAD => 0x1FA5, +0x1FAE => 0x1FA6, +0x1FAF => 0x1FA7, +0x1FB8 => 0x1FB0, +0x1FB9 => 0x1FB1, +0x1FBA => 0x1F70, +0x1FBB => 0x1F71, +0x1FBC => 0x1FB3, +0x1FBE => 0x3B9, +0x1FC8 => 0x1F72, +0x1FC9 => 0x1F73, +0x1FCA => 0x1F74, +0x1FCB => 0x1F75, +0x1FCC => 0x1FC3, +0x1FD8 => 0x1FD0, +0x1FD9 => 0x1FD1, +0x1FDA => 0x1F76, +0x1FDB => 0x1F77, +0x1FE8 => 0x1FE0, +0x1FE9 => 0x1FE1, +0x1FEA => 0x1F7A, +0x1FEB => 0x1F7B, +0x1FEC => 0x1FE5, +0x1FF8 => 0x1F78, +0x1FF9 => 0x1F79, +0x1FFA => 0x1F7C, +0x1FFB => 0x1F7D, +0x1FFC => 0x1FF3, +0x2126 => 0x3C9, +0x212A => 0x6B, +0x212B => 0xE5, +0x2132 => 0x214E, +0x2160 => 0x2170, +0x2161 => 0x2171, +0x2162 => 0x2172, +0x2163 => 0x2173, +0x2164 => 0x2174, +0x2165 => 0x2175, +0x2166 => 0x2176, +0x2167 => 0x2177, +0x2168 => 0x2178, +0x2169 => 0x2179, +0x216A => 0x217A, +0x216B => 0x217B, +0x216C => 0x217C, +0x216D => 0x217D, +0x216E => 0x217E, +0x216F => 0x217F, +0x2183 => 0x2184, +0x24B6 => 0x24D0, +0x24B7 => 0x24D1, +0x24B8 => 0x24D2, +0x24B9 => 0x24D3, +0x24BA => 0x24D4, +0x24BB => 0x24D5, +0x24BC => 0x24D6, +0x24BD => 0x24D7, +0x24BE => 0x24D8, +0x24BF => 0x24D9, +0x24C0 => 0x24DA, +0x24C1 => 0x24DB, +0x24C2 => 0x24DC, +0x24C3 => 0x24DD, +0x24C4 => 0x24DE, +0x24C5 => 0x24DF, +0x24C6 => 0x24E0, +0x24C7 => 0x24E1, +0x24C8 => 0x24E2, +0x24C9 => 0x24E3, +0x24CA => 0x24E4, +0x24CB => 0x24E5, +0x24CC => 0x24E6, +0x24CD => 0x24E7, +0x24CE => 0x24E8, +0x24CF => 0x24E9, +0x2C00 => 0x2C30, +0x2C01 => 0x2C31, +0x2C02 => 0x2C32, +0x2C03 => 0x2C33, +0x2C04 => 0x2C34, +0x2C05 => 0x2C35, +0x2C06 => 0x2C36, +0x2C07 => 0x2C37, +0x2C08 => 0x2C38, +0x2C09 => 0x2C39, +0x2C0A => 0x2C3A, +0x2C0B => 0x2C3B, +0x2C0C => 0x2C3C, +0x2C0D => 0x2C3D, +0x2C0E => 0x2C3E, +0x2C0F => 0x2C3F, +0x2C10 => 0x2C40, +0x2C11 => 0x2C41, +0x2C12 => 0x2C42, +0x2C13 => 0x2C43, +0x2C14 => 0x2C44, +0x2C15 => 0x2C45, +0x2C16 => 0x2C46, +0x2C17 => 0x2C47, +0x2C18 => 0x2C48, +0x2C19 => 0x2C49, +0x2C1A => 0x2C4A, +0x2C1B => 0x2C4B, +0x2C1C => 0x2C4C, +0x2C1D => 0x2C4D, +0x2C1E => 0x2C4E, +0x2C1F => 0x2C4F, +0x2C20 => 0x2C50, +0x2C21 => 0x2C51, +0x2C22 => 0x2C52, +0x2C23 => 0x2C53, +0x2C24 => 0x2C54, +0x2C25 => 0x2C55, +0x2C26 => 0x2C56, +0x2C27 => 0x2C57, +0x2C28 => 0x2C58, +0x2C29 => 0x2C59, +0x2C2A => 0x2C5A, +0x2C2B => 0x2C5B, +0x2C2C => 0x2C5C, +0x2C2D => 0x2C5D, +0x2C2E => 0x2C5E, +0x2C60 => 0x2C61, +0x2C62 => 0x26B, +0x2C63 => 0x1D7D, +0x2C64 => 0x27D, +0x2C67 => 0x2C68, +0x2C69 => 0x2C6A, +0x2C6B => 0x2C6C, +0x2C6D => 0x251, +0x2C6E => 0x271, +0x2C6F => 0x250, +0x2C70 => 0x252, +0x2C72 => 0x2C73, +0x2C75 => 0x2C76, +0x2C7E => 0x23F, +0x2C7F => 0x240, +0x2C80 => 0x2C81, +0x2C82 => 0x2C83, +0x2C84 => 0x2C85, +0x2C86 => 0x2C87, +0x2C88 => 0x2C89, +0x2C8A => 0x2C8B, +0x2C8C => 0x2C8D, +0x2C8E => 0x2C8F, +0x2C90 => 0x2C91, +0x2C92 => 0x2C93, +0x2C94 => 0x2C95, +0x2C96 => 0x2C97, +0x2C98 => 0x2C99, +0x2C9A => 0x2C9B, +0x2C9C => 0x2C9D, +0x2C9E => 0x2C9F, +0x2CA0 => 0x2CA1, +0x2CA2 => 0x2CA3, +0x2CA4 => 0x2CA5, +0x2CA6 => 0x2CA7, +0x2CA8 => 0x2CA9, +0x2CAA => 0x2CAB, +0x2CAC => 0x2CAD, +0x2CAE => 0x2CAF, +0x2CB0 => 0x2CB1, +0x2CB2 => 0x2CB3, +0x2CB4 => 0x2CB5, +0x2CB6 => 0x2CB7, +0x2CB8 => 0x2CB9, +0x2CBA => 0x2CBB, +0x2CBC => 0x2CBD, +0x2CBE => 0x2CBF, +0x2CC0 => 0x2CC1, +0x2CC2 => 0x2CC3, +0x2CC4 => 0x2CC5, +0x2CC6 => 0x2CC7, +0x2CC8 => 0x2CC9, +0x2CCA => 0x2CCB, +0x2CCC => 0x2CCD, +0x2CCE => 0x2CCF, +0x2CD0 => 0x2CD1, +0x2CD2 => 0x2CD3, +0x2CD4 => 0x2CD5, +0x2CD6 => 0x2CD7, +0x2CD8 => 0x2CD9, +0x2CDA => 0x2CDB, +0x2CDC => 0x2CDD, +0x2CDE => 0x2CDF, +0x2CE0 => 0x2CE1, +0x2CE2 => 0x2CE3, +0x2CEB => 0x2CEC, +0x2CED => 0x2CEE, +0x2CF2 => 0x2CF3, +0xA640 => 0xA641, +0xA642 => 0xA643, +0xA644 => 0xA645, +0xA646 => 0xA647, +0xA648 => 0xA649, +0xA64A => 0xA64B, +0xA64C => 0xA64D, +0xA64E => 0xA64F, +0xA650 => 0xA651, +0xA652 => 0xA653, +0xA654 => 0xA655, +0xA656 => 0xA657, +0xA658 => 0xA659, +0xA65A => 0xA65B, +0xA65C => 0xA65D, +0xA65E => 0xA65F, +0xA660 => 0xA661, +0xA662 => 0xA663, +0xA664 => 0xA665, +0xA666 => 0xA667, +0xA668 => 0xA669, +0xA66A => 0xA66B, +0xA66C => 0xA66D, +0xA680 => 0xA681, +0xA682 => 0xA683, +0xA684 => 0xA685, +0xA686 => 0xA687, +0xA688 => 0xA689, +0xA68A => 0xA68B, +0xA68C => 0xA68D, +0xA68E => 0xA68F, +0xA690 => 0xA691, +0xA692 => 0xA693, +0xA694 => 0xA695, +0xA696 => 0xA697, +0xA698 => 0xA699, +0xA69A => 0xA69B, +0xA722 => 0xA723, +0xA724 => 0xA725, +0xA726 => 0xA727, +0xA728 => 0xA729, +0xA72A => 0xA72B, +0xA72C => 0xA72D, +0xA72E => 0xA72F, +0xA732 => 0xA733, +0xA734 => 0xA735, +0xA736 => 0xA737, +0xA738 => 0xA739, +0xA73A => 0xA73B, +0xA73C => 0xA73D, +0xA73E => 0xA73F, +0xA740 => 0xA741, +0xA742 => 0xA743, +0xA744 => 0xA745, +0xA746 => 0xA747, +0xA748 => 0xA749, +0xA74A => 0xA74B, +0xA74C => 0xA74D, +0xA74E => 0xA74F, +0xA750 => 0xA751, +0xA752 => 0xA753, +0xA754 => 0xA755, +0xA756 => 0xA757, +0xA758 => 0xA759, +0xA75A => 0xA75B, +0xA75C => 0xA75D, +0xA75E => 0xA75F, +0xA760 => 0xA761, +0xA762 => 0xA763, +0xA764 => 0xA765, +0xA766 => 0xA767, +0xA768 => 0xA769, +0xA76A => 0xA76B, +0xA76C => 0xA76D, +0xA76E => 0xA76F, +0xA779 => 0xA77A, +0xA77B => 0xA77C, +0xA77D => 0x1D79, +0xA77E => 0xA77F, +0xA780 => 0xA781, +0xA782 => 0xA783, +0xA784 => 0xA785, +0xA786 => 0xA787, +0xA78B => 0xA78C, +0xA78D => 0x265, +0xA790 => 0xA791, +0xA792 => 0xA793, +0xA796 => 0xA797, +0xA798 => 0xA799, +0xA79A => 0xA79B, +0xA79C => 0xA79D, +0xA79E => 0xA79F, +0xA7A0 => 0xA7A1, +0xA7A2 => 0xA7A3, +0xA7A4 => 0xA7A5, +0xA7A6 => 0xA7A7, +0xA7A8 => 0xA7A9, +0xA7AA => 0x266, +0xA7AB => 0x25C, +0xA7AC => 0x261, +0xA7AD => 0x26C, +0xA7AE => 0x26A, +0xA7B0 => 0x29E, +0xA7B1 => 0x287, +0xA7B2 => 0x29D, +0xA7B3 => 0xAB53, +0xA7B4 => 0xA7B5, +0xA7B6 => 0xA7B7, +0xA7B8 => 0xA7B9, +0xA7BA => 0xA7BB, +0xA7BC => 0xA7BD, +0xA7BE => 0xA7BF, +0xA7C2 => 0xA7C3, +0xA7C4 => 0xA794, +0xA7C5 => 0x282, +0xA7C6 => 0x1D8E, +0xA7C7 => 0xA7C8, +0xA7C9 => 0xA7CA, +0xA7F5 => 0xA7F6, +0xAB70 => 0x13A0, +0xAB71 => 0x13A1, +0xAB72 => 0x13A2, +0xAB73 => 0x13A3, +0xAB74 => 0x13A4, +0xAB75 => 0x13A5, +0xAB76 => 0x13A6, +0xAB77 => 0x13A7, +0xAB78 => 0x13A8, +0xAB79 => 0x13A9, +0xAB7A => 0x13AA, +0xAB7B => 0x13AB, +0xAB7C => 0x13AC, +0xAB7D => 0x13AD, +0xAB7E => 0x13AE, +0xAB7F => 0x13AF, +0xAB80 => 0x13B0, +0xAB81 => 0x13B1, +0xAB82 => 0x13B2, +0xAB83 => 0x13B3, +0xAB84 => 0x13B4, +0xAB85 => 0x13B5, +0xAB86 => 0x13B6, +0xAB87 => 0x13B7, +0xAB88 => 0x13B8, +0xAB89 => 0x13B9, +0xAB8A => 0x13BA, +0xAB8B => 0x13BB, +0xAB8C => 0x13BC, +0xAB8D => 0x13BD, +0xAB8E => 0x13BE, +0xAB8F => 0x13BF, +0xAB90 => 0x13C0, +0xAB91 => 0x13C1, +0xAB92 => 0x13C2, +0xAB93 => 0x13C3, +0xAB94 => 0x13C4, +0xAB95 => 0x13C5, +0xAB96 => 0x13C6, +0xAB97 => 0x13C7, +0xAB98 => 0x13C8, +0xAB99 => 0x13C9, +0xAB9A => 0x13CA, +0xAB9B => 0x13CB, +0xAB9C => 0x13CC, +0xAB9D => 0x13CD, +0xAB9E => 0x13CE, +0xAB9F => 0x13CF, +0xABA0 => 0x13D0, +0xABA1 => 0x13D1, +0xABA2 => 0x13D2, +0xABA3 => 0x13D3, +0xABA4 => 0x13D4, +0xABA5 => 0x13D5, +0xABA6 => 0x13D6, +0xABA7 => 0x13D7, +0xABA8 => 0x13D8, +0xABA9 => 0x13D9, +0xABAA => 0x13DA, +0xABAB => 0x13DB, +0xABAC => 0x13DC, +0xABAD => 0x13DD, +0xABAE => 0x13DE, +0xABAF => 0x13DF, +0xABB0 => 0x13E0, +0xABB1 => 0x13E1, +0xABB2 => 0x13E2, +0xABB3 => 0x13E3, +0xABB4 => 0x13E4, +0xABB5 => 0x13E5, +0xABB6 => 0x13E6, +0xABB7 => 0x13E7, +0xABB8 => 0x13E8, +0xABB9 => 0x13E9, +0xABBA => 0x13EA, +0xABBB => 0x13EB, +0xABBC => 0x13EC, +0xABBD => 0x13ED, +0xABBE => 0x13EE, +0xABBF => 0x13EF, +0xFF21 => 0xFF41, +0xFF22 => 0xFF42, +0xFF23 => 0xFF43, +0xFF24 => 0xFF44, +0xFF25 => 0xFF45, +0xFF26 => 0xFF46, +0xFF27 => 0xFF47, +0xFF28 => 0xFF48, +0xFF29 => 0xFF49, +0xFF2A => 0xFF4A, +0xFF2B => 0xFF4B, +0xFF2C => 0xFF4C, +0xFF2D => 0xFF4D, +0xFF2E => 0xFF4E, +0xFF2F => 0xFF4F, +0xFF30 => 0xFF50, +0xFF31 => 0xFF51, +0xFF32 => 0xFF52, +0xFF33 => 0xFF53, +0xFF34 => 0xFF54, +0xFF35 => 0xFF55, +0xFF36 => 0xFF56, +0xFF37 => 0xFF57, +0xFF38 => 0xFF58, +0xFF39 => 0xFF59, +0xFF3A => 0xFF5A, +0x10400 => 0x10428, +0x10401 => 0x10429, +0x10402 => 0x1042A, +0x10403 => 0x1042B, +0x10404 => 0x1042C, +0x10405 => 0x1042D, +0x10406 => 0x1042E, +0x10407 => 0x1042F, +0x10408 => 0x10430, +0x10409 => 0x10431, +0x1040A => 0x10432, +0x1040B => 0x10433, +0x1040C => 0x10434, +0x1040D => 0x10435, +0x1040E => 0x10436, +0x1040F => 0x10437, +0x10410 => 0x10438, +0x10411 => 0x10439, +0x10412 => 0x1043A, +0x10413 => 0x1043B, +0x10414 => 0x1043C, +0x10415 => 0x1043D, +0x10416 => 0x1043E, +0x10417 => 0x1043F, +0x10418 => 0x10440, +0x10419 => 0x10441, +0x1041A => 0x10442, +0x1041B => 0x10443, +0x1041C => 0x10444, +0x1041D => 0x10445, +0x1041E => 0x10446, +0x1041F => 0x10447, +0x10420 => 0x10448, +0x10421 => 0x10449, +0x10422 => 0x1044A, +0x10423 => 0x1044B, +0x10424 => 0x1044C, +0x10425 => 0x1044D, +0x10426 => 0x1044E, +0x10427 => 0x1044F, +0x104B0 => 0x104D8, +0x104B1 => 0x104D9, +0x104B2 => 0x104DA, +0x104B3 => 0x104DB, +0x104B4 => 0x104DC, +0x104B5 => 0x104DD, +0x104B6 => 0x104DE, +0x104B7 => 0x104DF, +0x104B8 => 0x104E0, +0x104B9 => 0x104E1, +0x104BA => 0x104E2, +0x104BB => 0x104E3, +0x104BC => 0x104E4, +0x104BD => 0x104E5, +0x104BE => 0x104E6, +0x104BF => 0x104E7, +0x104C0 => 0x104E8, +0x104C1 => 0x104E9, +0x104C2 => 0x104EA, +0x104C3 => 0x104EB, +0x104C4 => 0x104EC, +0x104C5 => 0x104ED, +0x104C6 => 0x104EE, +0x104C7 => 0x104EF, +0x104C8 => 0x104F0, +0x104C9 => 0x104F1, +0x104CA => 0x104F2, +0x104CB => 0x104F3, +0x104CC => 0x104F4, +0x104CD => 0x104F5, +0x104CE => 0x104F6, +0x104CF => 0x104F7, +0x104D0 => 0x104F8, +0x104D1 => 0x104F9, +0x104D2 => 0x104FA, +0x104D3 => 0x104FB, +0x10C80 => 0x10CC0, +0x10C81 => 0x10CC1, +0x10C82 => 0x10CC2, +0x10C83 => 0x10CC3, +0x10C84 => 0x10CC4, +0x10C85 => 0x10CC5, +0x10C86 => 0x10CC6, +0x10C87 => 0x10CC7, +0x10C88 => 0x10CC8, +0x10C89 => 0x10CC9, +0x10C8A => 0x10CCA, +0x10C8B => 0x10CCB, +0x10C8C => 0x10CCC, +0x10C8D => 0x10CCD, +0x10C8E => 0x10CCE, +0x10C8F => 0x10CCF, +0x10C90 => 0x10CD0, +0x10C91 => 0x10CD1, +0x10C92 => 0x10CD2, +0x10C93 => 0x10CD3, +0x10C94 => 0x10CD4, +0x10C95 => 0x10CD5, +0x10C96 => 0x10CD6, +0x10C97 => 0x10CD7, +0x10C98 => 0x10CD8, +0x10C99 => 0x10CD9, +0x10C9A => 0x10CDA, +0x10C9B => 0x10CDB, +0x10C9C => 0x10CDC, +0x10C9D => 0x10CDD, +0x10C9E => 0x10CDE, +0x10C9F => 0x10CDF, +0x10CA0 => 0x10CE0, +0x10CA1 => 0x10CE1, +0x10CA2 => 0x10CE2, +0x10CA3 => 0x10CE3, +0x10CA4 => 0x10CE4, +0x10CA5 => 0x10CE5, +0x10CA6 => 0x10CE6, +0x10CA7 => 0x10CE7, +0x10CA8 => 0x10CE8, +0x10CA9 => 0x10CE9, +0x10CAA => 0x10CEA, +0x10CAB => 0x10CEB, +0x10CAC => 0x10CEC, +0x10CAD => 0x10CED, +0x10CAE => 0x10CEE, +0x10CAF => 0x10CEF, +0x10CB0 => 0x10CF0, +0x10CB1 => 0x10CF1, +0x10CB2 => 0x10CF2, +0x118A0 => 0x118C0, +0x118A1 => 0x118C1, +0x118A2 => 0x118C2, +0x118A3 => 0x118C3, +0x118A4 => 0x118C4, +0x118A5 => 0x118C5, +0x118A6 => 0x118C6, +0x118A7 => 0x118C7, +0x118A8 => 0x118C8, +0x118A9 => 0x118C9, +0x118AA => 0x118CA, +0x118AB => 0x118CB, +0x118AC => 0x118CC, +0x118AD => 0x118CD, +0x118AE => 0x118CE, +0x118AF => 0x118CF, +0x118B0 => 0x118D0, +0x118B1 => 0x118D1, +0x118B2 => 0x118D2, +0x118B3 => 0x118D3, +0x118B4 => 0x118D4, +0x118B5 => 0x118D5, +0x118B6 => 0x118D6, +0x118B7 => 0x118D7, +0x118B8 => 0x118D8, +0x118B9 => 0x118D9, +0x118BA => 0x118DA, +0x118BB => 0x118DB, +0x118BC => 0x118DC, +0x118BD => 0x118DD, +0x118BE => 0x118DE, +0x118BF => 0x118DF, +0x16E40 => 0x16E60, +0x16E41 => 0x16E61, +0x16E42 => 0x16E62, +0x16E43 => 0x16E63, +0x16E44 => 0x16E64, +0x16E45 => 0x16E65, +0x16E46 => 0x16E66, +0x16E47 => 0x16E67, +0x16E48 => 0x16E68, +0x16E49 => 0x16E69, +0x16E4A => 0x16E6A, +0x16E4B => 0x16E6B, +0x16E4C => 0x16E6C, +0x16E4D => 0x16E6D, +0x16E4E => 0x16E6E, +0x16E4F => 0x16E6F, +0x16E50 => 0x16E70, +0x16E51 => 0x16E71, +0x16E52 => 0x16E72, +0x16E53 => 0x16E73, +0x16E54 => 0x16E74, +0x16E55 => 0x16E75, +0x16E56 => 0x16E76, +0x16E57 => 0x16E77, +0x16E58 => 0x16E78, +0x16E59 => 0x16E79, +0x16E5A => 0x16E7A, +0x16E5B => 0x16E7B, +0x16E5C => 0x16E7C, +0x16E5D => 0x16E7D, +0x16E5E => 0x16E7E, +0x16E5F => 0x16E7F, +0x1E900 => 0x1E922, +0x1E901 => 0x1E923, +0x1E902 => 0x1E924, +0x1E903 => 0x1E925, +0x1E904 => 0x1E926, +0x1E905 => 0x1E927, +0x1E906 => 0x1E928, +0x1E907 => 0x1E929, +0x1E908 => 0x1E92A, +0x1E909 => 0x1E92B, +0x1E90A => 0x1E92C, +0x1E90B => 0x1E92D, +0x1E90C => 0x1E92E, +0x1E90D => 0x1E92F, +0x1E90E => 0x1E930, +0x1E90F => 0x1E931, +0x1E910 => 0x1E932, +0x1E911 => 0x1E933, +0x1E912 => 0x1E934, +0x1E913 => 0x1E935, +0x1E914 => 0x1E936, +0x1E915 => 0x1E937, +0x1E916 => 0x1E938, +0x1E917 => 0x1E939, +0x1E918 => 0x1E93A, +0x1E919 => 0x1E93B, +0x1E91A => 0x1E93C, +0x1E91B => 0x1E93D, +0x1E91C => 0x1E93E, +0x1E91D => 0x1E93F, +0x1E91E => 0x1E940, +0x1E91F => 0x1E941, +0x1E920 => 0x1E942, +0x1E921 => 0x1E943, +]; diff --git a/src/opis/string/res/lower.php b/src/opis/string/res/lower.php new file mode 100644 index 00000000..b5697d1d --- /dev/null +++ b/src/opis/string/res/lower.php @@ -0,0 +1,1396 @@ + 0x61, +0x42 => 0x62, +0x43 => 0x63, +0x44 => 0x64, +0x45 => 0x65, +0x46 => 0x66, +0x47 => 0x67, +0x48 => 0x68, +0x49 => 0x69, +0x4A => 0x6A, +0x4B => 0x6B, +0x4C => 0x6C, +0x4D => 0x6D, +0x4E => 0x6E, +0x4F => 0x6F, +0x50 => 0x70, +0x51 => 0x71, +0x52 => 0x72, +0x53 => 0x73, +0x54 => 0x74, +0x55 => 0x75, +0x56 => 0x76, +0x57 => 0x77, +0x58 => 0x78, +0x59 => 0x79, +0x5A => 0x7A, +0xC0 => 0xE0, +0xC1 => 0xE1, +0xC2 => 0xE2, +0xC3 => 0xE3, +0xC4 => 0xE4, +0xC5 => 0xE5, +0xC6 => 0xE6, +0xC7 => 0xE7, +0xC8 => 0xE8, +0xC9 => 0xE9, +0xCA => 0xEA, +0xCB => 0xEB, +0xCC => 0xEC, +0xCD => 0xED, +0xCE => 0xEE, +0xCF => 0xEF, +0xD0 => 0xF0, +0xD1 => 0xF1, +0xD2 => 0xF2, +0xD3 => 0xF3, +0xD4 => 0xF4, +0xD5 => 0xF5, +0xD6 => 0xF6, +0xD8 => 0xF8, +0xD9 => 0xF9, +0xDA => 0xFA, +0xDB => 0xFB, +0xDC => 0xFC, +0xDD => 0xFD, +0xDE => 0xFE, +0x100 => 0x101, +0x102 => 0x103, +0x104 => 0x105, +0x106 => 0x107, +0x108 => 0x109, +0x10A => 0x10B, +0x10C => 0x10D, +0x10E => 0x10F, +0x110 => 0x111, +0x112 => 0x113, +0x114 => 0x115, +0x116 => 0x117, +0x118 => 0x119, +0x11A => 0x11B, +0x11C => 0x11D, +0x11E => 0x11F, +0x120 => 0x121, +0x122 => 0x123, +0x124 => 0x125, +0x126 => 0x127, +0x128 => 0x129, +0x12A => 0x12B, +0x12C => 0x12D, +0x12E => 0x12F, +0x130 => 0x69, +0x132 => 0x133, +0x134 => 0x135, +0x136 => 0x137, +0x139 => 0x13A, +0x13B => 0x13C, +0x13D => 0x13E, +0x13F => 0x140, +0x141 => 0x142, +0x143 => 0x144, +0x145 => 0x146, +0x147 => 0x148, +0x14A => 0x14B, +0x14C => 0x14D, +0x14E => 0x14F, +0x150 => 0x151, +0x152 => 0x153, +0x154 => 0x155, +0x156 => 0x157, +0x158 => 0x159, +0x15A => 0x15B, +0x15C => 0x15D, +0x15E => 0x15F, +0x160 => 0x161, +0x162 => 0x163, +0x164 => 0x165, +0x166 => 0x167, +0x168 => 0x169, +0x16A => 0x16B, +0x16C => 0x16D, +0x16E => 0x16F, +0x170 => 0x171, +0x172 => 0x173, +0x174 => 0x175, +0x176 => 0x177, +0x178 => 0xFF, +0x179 => 0x17A, +0x17B => 0x17C, +0x17D => 0x17E, +0x181 => 0x253, +0x182 => 0x183, +0x184 => 0x185, +0x186 => 0x254, +0x187 => 0x188, +0x189 => 0x256, +0x18A => 0x257, +0x18B => 0x18C, +0x18E => 0x1DD, +0x18F => 0x259, +0x190 => 0x25B, +0x191 => 0x192, +0x193 => 0x260, +0x194 => 0x263, +0x196 => 0x269, +0x197 => 0x268, +0x198 => 0x199, +0x19C => 0x26F, +0x19D => 0x272, +0x19F => 0x275, +0x1A0 => 0x1A1, +0x1A2 => 0x1A3, +0x1A4 => 0x1A5, +0x1A6 => 0x280, +0x1A7 => 0x1A8, +0x1A9 => 0x283, +0x1AC => 0x1AD, +0x1AE => 0x288, +0x1AF => 0x1B0, +0x1B1 => 0x28A, +0x1B2 => 0x28B, +0x1B3 => 0x1B4, +0x1B5 => 0x1B6, +0x1B7 => 0x292, +0x1B8 => 0x1B9, +0x1BC => 0x1BD, +0x1C4 => 0x1C6, +0x1C5 => 0x1C6, +0x1C7 => 0x1C9, +0x1C8 => 0x1C9, +0x1CA => 0x1CC, +0x1CB => 0x1CC, +0x1CD => 0x1CE, +0x1CF => 0x1D0, +0x1D1 => 0x1D2, +0x1D3 => 0x1D4, +0x1D5 => 0x1D6, +0x1D7 => 0x1D8, +0x1D9 => 0x1DA, +0x1DB => 0x1DC, +0x1DE => 0x1DF, +0x1E0 => 0x1E1, +0x1E2 => 0x1E3, +0x1E4 => 0x1E5, +0x1E6 => 0x1E7, +0x1E8 => 0x1E9, +0x1EA => 0x1EB, +0x1EC => 0x1ED, +0x1EE => 0x1EF, +0x1F1 => 0x1F3, +0x1F2 => 0x1F3, +0x1F4 => 0x1F5, +0x1F6 => 0x195, +0x1F7 => 0x1BF, +0x1F8 => 0x1F9, +0x1FA => 0x1FB, +0x1FC => 0x1FD, +0x1FE => 0x1FF, +0x200 => 0x201, +0x202 => 0x203, +0x204 => 0x205, +0x206 => 0x207, +0x208 => 0x209, +0x20A => 0x20B, +0x20C => 0x20D, +0x20E => 0x20F, +0x210 => 0x211, +0x212 => 0x213, +0x214 => 0x215, +0x216 => 0x217, +0x218 => 0x219, +0x21A => 0x21B, +0x21C => 0x21D, +0x21E => 0x21F, +0x220 => 0x19E, +0x222 => 0x223, +0x224 => 0x225, +0x226 => 0x227, +0x228 => 0x229, +0x22A => 0x22B, +0x22C => 0x22D, +0x22E => 0x22F, +0x230 => 0x231, +0x232 => 0x233, +0x23A => 0x2C65, +0x23B => 0x23C, +0x23D => 0x19A, +0x23E => 0x2C66, +0x241 => 0x242, +0x243 => 0x180, +0x244 => 0x289, +0x245 => 0x28C, +0x246 => 0x247, +0x248 => 0x249, +0x24A => 0x24B, +0x24C => 0x24D, +0x24E => 0x24F, +0x370 => 0x371, +0x372 => 0x373, +0x376 => 0x377, +0x37F => 0x3F3, +0x386 => 0x3AC, +0x388 => 0x3AD, +0x389 => 0x3AE, +0x38A => 0x3AF, +0x38C => 0x3CC, +0x38E => 0x3CD, +0x38F => 0x3CE, +0x391 => 0x3B1, +0x392 => 0x3B2, +0x393 => 0x3B3, +0x394 => 0x3B4, +0x395 => 0x3B5, +0x396 => 0x3B6, +0x397 => 0x3B7, +0x398 => 0x3B8, +0x399 => 0x3B9, +0x39A => 0x3BA, +0x39B => 0x3BB, +0x39C => 0x3BC, +0x39D => 0x3BD, +0x39E => 0x3BE, +0x39F => 0x3BF, +0x3A0 => 0x3C0, +0x3A1 => 0x3C1, +0x3A3 => 0x3C3, +0x3A4 => 0x3C4, +0x3A5 => 0x3C5, +0x3A6 => 0x3C6, +0x3A7 => 0x3C7, +0x3A8 => 0x3C8, +0x3A9 => 0x3C9, +0x3AA => 0x3CA, +0x3AB => 0x3CB, +0x3CF => 0x3D7, +0x3D8 => 0x3D9, +0x3DA => 0x3DB, +0x3DC => 0x3DD, +0x3DE => 0x3DF, +0x3E0 => 0x3E1, +0x3E2 => 0x3E3, +0x3E4 => 0x3E5, +0x3E6 => 0x3E7, +0x3E8 => 0x3E9, +0x3EA => 0x3EB, +0x3EC => 0x3ED, +0x3EE => 0x3EF, +0x3F4 => 0x3B8, +0x3F7 => 0x3F8, +0x3F9 => 0x3F2, +0x3FA => 0x3FB, +0x3FD => 0x37B, +0x3FE => 0x37C, +0x3FF => 0x37D, +0x400 => 0x450, +0x401 => 0x451, +0x402 => 0x452, +0x403 => 0x453, +0x404 => 0x454, +0x405 => 0x455, +0x406 => 0x456, +0x407 => 0x457, +0x408 => 0x458, +0x409 => 0x459, +0x40A => 0x45A, +0x40B => 0x45B, +0x40C => 0x45C, +0x40D => 0x45D, +0x40E => 0x45E, +0x40F => 0x45F, +0x410 => 0x430, +0x411 => 0x431, +0x412 => 0x432, +0x413 => 0x433, +0x414 => 0x434, +0x415 => 0x435, +0x416 => 0x436, +0x417 => 0x437, +0x418 => 0x438, +0x419 => 0x439, +0x41A => 0x43A, +0x41B => 0x43B, +0x41C => 0x43C, +0x41D => 0x43D, +0x41E => 0x43E, +0x41F => 0x43F, +0x420 => 0x440, +0x421 => 0x441, +0x422 => 0x442, +0x423 => 0x443, +0x424 => 0x444, +0x425 => 0x445, +0x426 => 0x446, +0x427 => 0x447, +0x428 => 0x448, +0x429 => 0x449, +0x42A => 0x44A, +0x42B => 0x44B, +0x42C => 0x44C, +0x42D => 0x44D, +0x42E => 0x44E, +0x42F => 0x44F, +0x460 => 0x461, +0x462 => 0x463, +0x464 => 0x465, +0x466 => 0x467, +0x468 => 0x469, +0x46A => 0x46B, +0x46C => 0x46D, +0x46E => 0x46F, +0x470 => 0x471, +0x472 => 0x473, +0x474 => 0x475, +0x476 => 0x477, +0x478 => 0x479, +0x47A => 0x47B, +0x47C => 0x47D, +0x47E => 0x47F, +0x480 => 0x481, +0x48A => 0x48B, +0x48C => 0x48D, +0x48E => 0x48F, +0x490 => 0x491, +0x492 => 0x493, +0x494 => 0x495, +0x496 => 0x497, +0x498 => 0x499, +0x49A => 0x49B, +0x49C => 0x49D, +0x49E => 0x49F, +0x4A0 => 0x4A1, +0x4A2 => 0x4A3, +0x4A4 => 0x4A5, +0x4A6 => 0x4A7, +0x4A8 => 0x4A9, +0x4AA => 0x4AB, +0x4AC => 0x4AD, +0x4AE => 0x4AF, +0x4B0 => 0x4B1, +0x4B2 => 0x4B3, +0x4B4 => 0x4B5, +0x4B6 => 0x4B7, +0x4B8 => 0x4B9, +0x4BA => 0x4BB, +0x4BC => 0x4BD, +0x4BE => 0x4BF, +0x4C0 => 0x4CF, +0x4C1 => 0x4C2, +0x4C3 => 0x4C4, +0x4C5 => 0x4C6, +0x4C7 => 0x4C8, +0x4C9 => 0x4CA, +0x4CB => 0x4CC, +0x4CD => 0x4CE, +0x4D0 => 0x4D1, +0x4D2 => 0x4D3, +0x4D4 => 0x4D5, +0x4D6 => 0x4D7, +0x4D8 => 0x4D9, +0x4DA => 0x4DB, +0x4DC => 0x4DD, +0x4DE => 0x4DF, +0x4E0 => 0x4E1, +0x4E2 => 0x4E3, +0x4E4 => 0x4E5, +0x4E6 => 0x4E7, +0x4E8 => 0x4E9, +0x4EA => 0x4EB, +0x4EC => 0x4ED, +0x4EE => 0x4EF, +0x4F0 => 0x4F1, +0x4F2 => 0x4F3, +0x4F4 => 0x4F5, +0x4F6 => 0x4F7, +0x4F8 => 0x4F9, +0x4FA => 0x4FB, +0x4FC => 0x4FD, +0x4FE => 0x4FF, +0x500 => 0x501, +0x502 => 0x503, +0x504 => 0x505, +0x506 => 0x507, +0x508 => 0x509, +0x50A => 0x50B, +0x50C => 0x50D, +0x50E => 0x50F, +0x510 => 0x511, +0x512 => 0x513, +0x514 => 0x515, +0x516 => 0x517, +0x518 => 0x519, +0x51A => 0x51B, +0x51C => 0x51D, +0x51E => 0x51F, +0x520 => 0x521, +0x522 => 0x523, +0x524 => 0x525, +0x526 => 0x527, +0x528 => 0x529, +0x52A => 0x52B, +0x52C => 0x52D, +0x52E => 0x52F, +0x531 => 0x561, +0x532 => 0x562, +0x533 => 0x563, +0x534 => 0x564, +0x535 => 0x565, +0x536 => 0x566, +0x537 => 0x567, +0x538 => 0x568, +0x539 => 0x569, +0x53A => 0x56A, +0x53B => 0x56B, +0x53C => 0x56C, +0x53D => 0x56D, +0x53E => 0x56E, +0x53F => 0x56F, +0x540 => 0x570, +0x541 => 0x571, +0x542 => 0x572, +0x543 => 0x573, +0x544 => 0x574, +0x545 => 0x575, +0x546 => 0x576, +0x547 => 0x577, +0x548 => 0x578, +0x549 => 0x579, +0x54A => 0x57A, +0x54B => 0x57B, +0x54C => 0x57C, +0x54D => 0x57D, +0x54E => 0x57E, +0x54F => 0x57F, +0x550 => 0x580, +0x551 => 0x581, +0x552 => 0x582, +0x553 => 0x583, +0x554 => 0x584, +0x555 => 0x585, +0x556 => 0x586, +0x10A0 => 0x2D00, +0x10A1 => 0x2D01, +0x10A2 => 0x2D02, +0x10A3 => 0x2D03, +0x10A4 => 0x2D04, +0x10A5 => 0x2D05, +0x10A6 => 0x2D06, +0x10A7 => 0x2D07, +0x10A8 => 0x2D08, +0x10A9 => 0x2D09, +0x10AA => 0x2D0A, +0x10AB => 0x2D0B, +0x10AC => 0x2D0C, +0x10AD => 0x2D0D, +0x10AE => 0x2D0E, +0x10AF => 0x2D0F, +0x10B0 => 0x2D10, +0x10B1 => 0x2D11, +0x10B2 => 0x2D12, +0x10B3 => 0x2D13, +0x10B4 => 0x2D14, +0x10B5 => 0x2D15, +0x10B6 => 0x2D16, +0x10B7 => 0x2D17, +0x10B8 => 0x2D18, +0x10B9 => 0x2D19, +0x10BA => 0x2D1A, +0x10BB => 0x2D1B, +0x10BC => 0x2D1C, +0x10BD => 0x2D1D, +0x10BE => 0x2D1E, +0x10BF => 0x2D1F, +0x10C0 => 0x2D20, +0x10C1 => 0x2D21, +0x10C2 => 0x2D22, +0x10C3 => 0x2D23, +0x10C4 => 0x2D24, +0x10C5 => 0x2D25, +0x10C7 => 0x2D27, +0x10CD => 0x2D2D, +0x13A0 => 0xAB70, +0x13A1 => 0xAB71, +0x13A2 => 0xAB72, +0x13A3 => 0xAB73, +0x13A4 => 0xAB74, +0x13A5 => 0xAB75, +0x13A6 => 0xAB76, +0x13A7 => 0xAB77, +0x13A8 => 0xAB78, +0x13A9 => 0xAB79, +0x13AA => 0xAB7A, +0x13AB => 0xAB7B, +0x13AC => 0xAB7C, +0x13AD => 0xAB7D, +0x13AE => 0xAB7E, +0x13AF => 0xAB7F, +0x13B0 => 0xAB80, +0x13B1 => 0xAB81, +0x13B2 => 0xAB82, +0x13B3 => 0xAB83, +0x13B4 => 0xAB84, +0x13B5 => 0xAB85, +0x13B6 => 0xAB86, +0x13B7 => 0xAB87, +0x13B8 => 0xAB88, +0x13B9 => 0xAB89, +0x13BA => 0xAB8A, +0x13BB => 0xAB8B, +0x13BC => 0xAB8C, +0x13BD => 0xAB8D, +0x13BE => 0xAB8E, +0x13BF => 0xAB8F, +0x13C0 => 0xAB90, +0x13C1 => 0xAB91, +0x13C2 => 0xAB92, +0x13C3 => 0xAB93, +0x13C4 => 0xAB94, +0x13C5 => 0xAB95, +0x13C6 => 0xAB96, +0x13C7 => 0xAB97, +0x13C8 => 0xAB98, +0x13C9 => 0xAB99, +0x13CA => 0xAB9A, +0x13CB => 0xAB9B, +0x13CC => 0xAB9C, +0x13CD => 0xAB9D, +0x13CE => 0xAB9E, +0x13CF => 0xAB9F, +0x13D0 => 0xABA0, +0x13D1 => 0xABA1, +0x13D2 => 0xABA2, +0x13D3 => 0xABA3, +0x13D4 => 0xABA4, +0x13D5 => 0xABA5, +0x13D6 => 0xABA6, +0x13D7 => 0xABA7, +0x13D8 => 0xABA8, +0x13D9 => 0xABA9, +0x13DA => 0xABAA, +0x13DB => 0xABAB, +0x13DC => 0xABAC, +0x13DD => 0xABAD, +0x13DE => 0xABAE, +0x13DF => 0xABAF, +0x13E0 => 0xABB0, +0x13E1 => 0xABB1, +0x13E2 => 0xABB2, +0x13E3 => 0xABB3, +0x13E4 => 0xABB4, +0x13E5 => 0xABB5, +0x13E6 => 0xABB6, +0x13E7 => 0xABB7, +0x13E8 => 0xABB8, +0x13E9 => 0xABB9, +0x13EA => 0xABBA, +0x13EB => 0xABBB, +0x13EC => 0xABBC, +0x13ED => 0xABBD, +0x13EE => 0xABBE, +0x13EF => 0xABBF, +0x13F0 => 0x13F8, +0x13F1 => 0x13F9, +0x13F2 => 0x13FA, +0x13F3 => 0x13FB, +0x13F4 => 0x13FC, +0x13F5 => 0x13FD, +0x1C90 => 0x10D0, +0x1C91 => 0x10D1, +0x1C92 => 0x10D2, +0x1C93 => 0x10D3, +0x1C94 => 0x10D4, +0x1C95 => 0x10D5, +0x1C96 => 0x10D6, +0x1C97 => 0x10D7, +0x1C98 => 0x10D8, +0x1C99 => 0x10D9, +0x1C9A => 0x10DA, +0x1C9B => 0x10DB, +0x1C9C => 0x10DC, +0x1C9D => 0x10DD, +0x1C9E => 0x10DE, +0x1C9F => 0x10DF, +0x1CA0 => 0x10E0, +0x1CA1 => 0x10E1, +0x1CA2 => 0x10E2, +0x1CA3 => 0x10E3, +0x1CA4 => 0x10E4, +0x1CA5 => 0x10E5, +0x1CA6 => 0x10E6, +0x1CA7 => 0x10E7, +0x1CA8 => 0x10E8, +0x1CA9 => 0x10E9, +0x1CAA => 0x10EA, +0x1CAB => 0x10EB, +0x1CAC => 0x10EC, +0x1CAD => 0x10ED, +0x1CAE => 0x10EE, +0x1CAF => 0x10EF, +0x1CB0 => 0x10F0, +0x1CB1 => 0x10F1, +0x1CB2 => 0x10F2, +0x1CB3 => 0x10F3, +0x1CB4 => 0x10F4, +0x1CB5 => 0x10F5, +0x1CB6 => 0x10F6, +0x1CB7 => 0x10F7, +0x1CB8 => 0x10F8, +0x1CB9 => 0x10F9, +0x1CBA => 0x10FA, +0x1CBD => 0x10FD, +0x1CBE => 0x10FE, +0x1CBF => 0x10FF, +0x1E00 => 0x1E01, +0x1E02 => 0x1E03, +0x1E04 => 0x1E05, +0x1E06 => 0x1E07, +0x1E08 => 0x1E09, +0x1E0A => 0x1E0B, +0x1E0C => 0x1E0D, +0x1E0E => 0x1E0F, +0x1E10 => 0x1E11, +0x1E12 => 0x1E13, +0x1E14 => 0x1E15, +0x1E16 => 0x1E17, +0x1E18 => 0x1E19, +0x1E1A => 0x1E1B, +0x1E1C => 0x1E1D, +0x1E1E => 0x1E1F, +0x1E20 => 0x1E21, +0x1E22 => 0x1E23, +0x1E24 => 0x1E25, +0x1E26 => 0x1E27, +0x1E28 => 0x1E29, +0x1E2A => 0x1E2B, +0x1E2C => 0x1E2D, +0x1E2E => 0x1E2F, +0x1E30 => 0x1E31, +0x1E32 => 0x1E33, +0x1E34 => 0x1E35, +0x1E36 => 0x1E37, +0x1E38 => 0x1E39, +0x1E3A => 0x1E3B, +0x1E3C => 0x1E3D, +0x1E3E => 0x1E3F, +0x1E40 => 0x1E41, +0x1E42 => 0x1E43, +0x1E44 => 0x1E45, +0x1E46 => 0x1E47, +0x1E48 => 0x1E49, +0x1E4A => 0x1E4B, +0x1E4C => 0x1E4D, +0x1E4E => 0x1E4F, +0x1E50 => 0x1E51, +0x1E52 => 0x1E53, +0x1E54 => 0x1E55, +0x1E56 => 0x1E57, +0x1E58 => 0x1E59, +0x1E5A => 0x1E5B, +0x1E5C => 0x1E5D, +0x1E5E => 0x1E5F, +0x1E60 => 0x1E61, +0x1E62 => 0x1E63, +0x1E64 => 0x1E65, +0x1E66 => 0x1E67, +0x1E68 => 0x1E69, +0x1E6A => 0x1E6B, +0x1E6C => 0x1E6D, +0x1E6E => 0x1E6F, +0x1E70 => 0x1E71, +0x1E72 => 0x1E73, +0x1E74 => 0x1E75, +0x1E76 => 0x1E77, +0x1E78 => 0x1E79, +0x1E7A => 0x1E7B, +0x1E7C => 0x1E7D, +0x1E7E => 0x1E7F, +0x1E80 => 0x1E81, +0x1E82 => 0x1E83, +0x1E84 => 0x1E85, +0x1E86 => 0x1E87, +0x1E88 => 0x1E89, +0x1E8A => 0x1E8B, +0x1E8C => 0x1E8D, +0x1E8E => 0x1E8F, +0x1E90 => 0x1E91, +0x1E92 => 0x1E93, +0x1E94 => 0x1E95, +0x1E9E => 0xDF, +0x1EA0 => 0x1EA1, +0x1EA2 => 0x1EA3, +0x1EA4 => 0x1EA5, +0x1EA6 => 0x1EA7, +0x1EA8 => 0x1EA9, +0x1EAA => 0x1EAB, +0x1EAC => 0x1EAD, +0x1EAE => 0x1EAF, +0x1EB0 => 0x1EB1, +0x1EB2 => 0x1EB3, +0x1EB4 => 0x1EB5, +0x1EB6 => 0x1EB7, +0x1EB8 => 0x1EB9, +0x1EBA => 0x1EBB, +0x1EBC => 0x1EBD, +0x1EBE => 0x1EBF, +0x1EC0 => 0x1EC1, +0x1EC2 => 0x1EC3, +0x1EC4 => 0x1EC5, +0x1EC6 => 0x1EC7, +0x1EC8 => 0x1EC9, +0x1ECA => 0x1ECB, +0x1ECC => 0x1ECD, +0x1ECE => 0x1ECF, +0x1ED0 => 0x1ED1, +0x1ED2 => 0x1ED3, +0x1ED4 => 0x1ED5, +0x1ED6 => 0x1ED7, +0x1ED8 => 0x1ED9, +0x1EDA => 0x1EDB, +0x1EDC => 0x1EDD, +0x1EDE => 0x1EDF, +0x1EE0 => 0x1EE1, +0x1EE2 => 0x1EE3, +0x1EE4 => 0x1EE5, +0x1EE6 => 0x1EE7, +0x1EE8 => 0x1EE9, +0x1EEA => 0x1EEB, +0x1EEC => 0x1EED, +0x1EEE => 0x1EEF, +0x1EF0 => 0x1EF1, +0x1EF2 => 0x1EF3, +0x1EF4 => 0x1EF5, +0x1EF6 => 0x1EF7, +0x1EF8 => 0x1EF9, +0x1EFA => 0x1EFB, +0x1EFC => 0x1EFD, +0x1EFE => 0x1EFF, +0x1F08 => 0x1F00, +0x1F09 => 0x1F01, +0x1F0A => 0x1F02, +0x1F0B => 0x1F03, +0x1F0C => 0x1F04, +0x1F0D => 0x1F05, +0x1F0E => 0x1F06, +0x1F0F => 0x1F07, +0x1F18 => 0x1F10, +0x1F19 => 0x1F11, +0x1F1A => 0x1F12, +0x1F1B => 0x1F13, +0x1F1C => 0x1F14, +0x1F1D => 0x1F15, +0x1F28 => 0x1F20, +0x1F29 => 0x1F21, +0x1F2A => 0x1F22, +0x1F2B => 0x1F23, +0x1F2C => 0x1F24, +0x1F2D => 0x1F25, +0x1F2E => 0x1F26, +0x1F2F => 0x1F27, +0x1F38 => 0x1F30, +0x1F39 => 0x1F31, +0x1F3A => 0x1F32, +0x1F3B => 0x1F33, +0x1F3C => 0x1F34, +0x1F3D => 0x1F35, +0x1F3E => 0x1F36, +0x1F3F => 0x1F37, +0x1F48 => 0x1F40, +0x1F49 => 0x1F41, +0x1F4A => 0x1F42, +0x1F4B => 0x1F43, +0x1F4C => 0x1F44, +0x1F4D => 0x1F45, +0x1F59 => 0x1F51, +0x1F5B => 0x1F53, +0x1F5D => 0x1F55, +0x1F5F => 0x1F57, +0x1F68 => 0x1F60, +0x1F69 => 0x1F61, +0x1F6A => 0x1F62, +0x1F6B => 0x1F63, +0x1F6C => 0x1F64, +0x1F6D => 0x1F65, +0x1F6E => 0x1F66, +0x1F6F => 0x1F67, +0x1F88 => 0x1F80, +0x1F89 => 0x1F81, +0x1F8A => 0x1F82, +0x1F8B => 0x1F83, +0x1F8C => 0x1F84, +0x1F8D => 0x1F85, +0x1F8E => 0x1F86, +0x1F8F => 0x1F87, +0x1F98 => 0x1F90, +0x1F99 => 0x1F91, +0x1F9A => 0x1F92, +0x1F9B => 0x1F93, +0x1F9C => 0x1F94, +0x1F9D => 0x1F95, +0x1F9E => 0x1F96, +0x1F9F => 0x1F97, +0x1FA8 => 0x1FA0, +0x1FA9 => 0x1FA1, +0x1FAA => 0x1FA2, +0x1FAB => 0x1FA3, +0x1FAC => 0x1FA4, +0x1FAD => 0x1FA5, +0x1FAE => 0x1FA6, +0x1FAF => 0x1FA7, +0x1FB8 => 0x1FB0, +0x1FB9 => 0x1FB1, +0x1FBA => 0x1F70, +0x1FBB => 0x1F71, +0x1FBC => 0x1FB3, +0x1FC8 => 0x1F72, +0x1FC9 => 0x1F73, +0x1FCA => 0x1F74, +0x1FCB => 0x1F75, +0x1FCC => 0x1FC3, +0x1FD8 => 0x1FD0, +0x1FD9 => 0x1FD1, +0x1FDA => 0x1F76, +0x1FDB => 0x1F77, +0x1FE8 => 0x1FE0, +0x1FE9 => 0x1FE1, +0x1FEA => 0x1F7A, +0x1FEB => 0x1F7B, +0x1FEC => 0x1FE5, +0x1FF8 => 0x1F78, +0x1FF9 => 0x1F79, +0x1FFA => 0x1F7C, +0x1FFB => 0x1F7D, +0x1FFC => 0x1FF3, +0x2126 => 0x3C9, +0x212A => 0x6B, +0x212B => 0xE5, +0x2132 => 0x214E, +0x2160 => 0x2170, +0x2161 => 0x2171, +0x2162 => 0x2172, +0x2163 => 0x2173, +0x2164 => 0x2174, +0x2165 => 0x2175, +0x2166 => 0x2176, +0x2167 => 0x2177, +0x2168 => 0x2178, +0x2169 => 0x2179, +0x216A => 0x217A, +0x216B => 0x217B, +0x216C => 0x217C, +0x216D => 0x217D, +0x216E => 0x217E, +0x216F => 0x217F, +0x2183 => 0x2184, +0x24B6 => 0x24D0, +0x24B7 => 0x24D1, +0x24B8 => 0x24D2, +0x24B9 => 0x24D3, +0x24BA => 0x24D4, +0x24BB => 0x24D5, +0x24BC => 0x24D6, +0x24BD => 0x24D7, +0x24BE => 0x24D8, +0x24BF => 0x24D9, +0x24C0 => 0x24DA, +0x24C1 => 0x24DB, +0x24C2 => 0x24DC, +0x24C3 => 0x24DD, +0x24C4 => 0x24DE, +0x24C5 => 0x24DF, +0x24C6 => 0x24E0, +0x24C7 => 0x24E1, +0x24C8 => 0x24E2, +0x24C9 => 0x24E3, +0x24CA => 0x24E4, +0x24CB => 0x24E5, +0x24CC => 0x24E6, +0x24CD => 0x24E7, +0x24CE => 0x24E8, +0x24CF => 0x24E9, +0x2C00 => 0x2C30, +0x2C01 => 0x2C31, +0x2C02 => 0x2C32, +0x2C03 => 0x2C33, +0x2C04 => 0x2C34, +0x2C05 => 0x2C35, +0x2C06 => 0x2C36, +0x2C07 => 0x2C37, +0x2C08 => 0x2C38, +0x2C09 => 0x2C39, +0x2C0A => 0x2C3A, +0x2C0B => 0x2C3B, +0x2C0C => 0x2C3C, +0x2C0D => 0x2C3D, +0x2C0E => 0x2C3E, +0x2C0F => 0x2C3F, +0x2C10 => 0x2C40, +0x2C11 => 0x2C41, +0x2C12 => 0x2C42, +0x2C13 => 0x2C43, +0x2C14 => 0x2C44, +0x2C15 => 0x2C45, +0x2C16 => 0x2C46, +0x2C17 => 0x2C47, +0x2C18 => 0x2C48, +0x2C19 => 0x2C49, +0x2C1A => 0x2C4A, +0x2C1B => 0x2C4B, +0x2C1C => 0x2C4C, +0x2C1D => 0x2C4D, +0x2C1E => 0x2C4E, +0x2C1F => 0x2C4F, +0x2C20 => 0x2C50, +0x2C21 => 0x2C51, +0x2C22 => 0x2C52, +0x2C23 => 0x2C53, +0x2C24 => 0x2C54, +0x2C25 => 0x2C55, +0x2C26 => 0x2C56, +0x2C27 => 0x2C57, +0x2C28 => 0x2C58, +0x2C29 => 0x2C59, +0x2C2A => 0x2C5A, +0x2C2B => 0x2C5B, +0x2C2C => 0x2C5C, +0x2C2D => 0x2C5D, +0x2C2E => 0x2C5E, +0x2C60 => 0x2C61, +0x2C62 => 0x26B, +0x2C63 => 0x1D7D, +0x2C64 => 0x27D, +0x2C67 => 0x2C68, +0x2C69 => 0x2C6A, +0x2C6B => 0x2C6C, +0x2C6D => 0x251, +0x2C6E => 0x271, +0x2C6F => 0x250, +0x2C70 => 0x252, +0x2C72 => 0x2C73, +0x2C75 => 0x2C76, +0x2C7E => 0x23F, +0x2C7F => 0x240, +0x2C80 => 0x2C81, +0x2C82 => 0x2C83, +0x2C84 => 0x2C85, +0x2C86 => 0x2C87, +0x2C88 => 0x2C89, +0x2C8A => 0x2C8B, +0x2C8C => 0x2C8D, +0x2C8E => 0x2C8F, +0x2C90 => 0x2C91, +0x2C92 => 0x2C93, +0x2C94 => 0x2C95, +0x2C96 => 0x2C97, +0x2C98 => 0x2C99, +0x2C9A => 0x2C9B, +0x2C9C => 0x2C9D, +0x2C9E => 0x2C9F, +0x2CA0 => 0x2CA1, +0x2CA2 => 0x2CA3, +0x2CA4 => 0x2CA5, +0x2CA6 => 0x2CA7, +0x2CA8 => 0x2CA9, +0x2CAA => 0x2CAB, +0x2CAC => 0x2CAD, +0x2CAE => 0x2CAF, +0x2CB0 => 0x2CB1, +0x2CB2 => 0x2CB3, +0x2CB4 => 0x2CB5, +0x2CB6 => 0x2CB7, +0x2CB8 => 0x2CB9, +0x2CBA => 0x2CBB, +0x2CBC => 0x2CBD, +0x2CBE => 0x2CBF, +0x2CC0 => 0x2CC1, +0x2CC2 => 0x2CC3, +0x2CC4 => 0x2CC5, +0x2CC6 => 0x2CC7, +0x2CC8 => 0x2CC9, +0x2CCA => 0x2CCB, +0x2CCC => 0x2CCD, +0x2CCE => 0x2CCF, +0x2CD0 => 0x2CD1, +0x2CD2 => 0x2CD3, +0x2CD4 => 0x2CD5, +0x2CD6 => 0x2CD7, +0x2CD8 => 0x2CD9, +0x2CDA => 0x2CDB, +0x2CDC => 0x2CDD, +0x2CDE => 0x2CDF, +0x2CE0 => 0x2CE1, +0x2CE2 => 0x2CE3, +0x2CEB => 0x2CEC, +0x2CED => 0x2CEE, +0x2CF2 => 0x2CF3, +0xA640 => 0xA641, +0xA642 => 0xA643, +0xA644 => 0xA645, +0xA646 => 0xA647, +0xA648 => 0xA649, +0xA64A => 0xA64B, +0xA64C => 0xA64D, +0xA64E => 0xA64F, +0xA650 => 0xA651, +0xA652 => 0xA653, +0xA654 => 0xA655, +0xA656 => 0xA657, +0xA658 => 0xA659, +0xA65A => 0xA65B, +0xA65C => 0xA65D, +0xA65E => 0xA65F, +0xA660 => 0xA661, +0xA662 => 0xA663, +0xA664 => 0xA665, +0xA666 => 0xA667, +0xA668 => 0xA669, +0xA66A => 0xA66B, +0xA66C => 0xA66D, +0xA680 => 0xA681, +0xA682 => 0xA683, +0xA684 => 0xA685, +0xA686 => 0xA687, +0xA688 => 0xA689, +0xA68A => 0xA68B, +0xA68C => 0xA68D, +0xA68E => 0xA68F, +0xA690 => 0xA691, +0xA692 => 0xA693, +0xA694 => 0xA695, +0xA696 => 0xA697, +0xA698 => 0xA699, +0xA69A => 0xA69B, +0xA722 => 0xA723, +0xA724 => 0xA725, +0xA726 => 0xA727, +0xA728 => 0xA729, +0xA72A => 0xA72B, +0xA72C => 0xA72D, +0xA72E => 0xA72F, +0xA732 => 0xA733, +0xA734 => 0xA735, +0xA736 => 0xA737, +0xA738 => 0xA739, +0xA73A => 0xA73B, +0xA73C => 0xA73D, +0xA73E => 0xA73F, +0xA740 => 0xA741, +0xA742 => 0xA743, +0xA744 => 0xA745, +0xA746 => 0xA747, +0xA748 => 0xA749, +0xA74A => 0xA74B, +0xA74C => 0xA74D, +0xA74E => 0xA74F, +0xA750 => 0xA751, +0xA752 => 0xA753, +0xA754 => 0xA755, +0xA756 => 0xA757, +0xA758 => 0xA759, +0xA75A => 0xA75B, +0xA75C => 0xA75D, +0xA75E => 0xA75F, +0xA760 => 0xA761, +0xA762 => 0xA763, +0xA764 => 0xA765, +0xA766 => 0xA767, +0xA768 => 0xA769, +0xA76A => 0xA76B, +0xA76C => 0xA76D, +0xA76E => 0xA76F, +0xA779 => 0xA77A, +0xA77B => 0xA77C, +0xA77D => 0x1D79, +0xA77E => 0xA77F, +0xA780 => 0xA781, +0xA782 => 0xA783, +0xA784 => 0xA785, +0xA786 => 0xA787, +0xA78B => 0xA78C, +0xA78D => 0x265, +0xA790 => 0xA791, +0xA792 => 0xA793, +0xA796 => 0xA797, +0xA798 => 0xA799, +0xA79A => 0xA79B, +0xA79C => 0xA79D, +0xA79E => 0xA79F, +0xA7A0 => 0xA7A1, +0xA7A2 => 0xA7A3, +0xA7A4 => 0xA7A5, +0xA7A6 => 0xA7A7, +0xA7A8 => 0xA7A9, +0xA7AA => 0x266, +0xA7AB => 0x25C, +0xA7AC => 0x261, +0xA7AD => 0x26C, +0xA7AE => 0x26A, +0xA7B0 => 0x29E, +0xA7B1 => 0x287, +0xA7B2 => 0x29D, +0xA7B3 => 0xAB53, +0xA7B4 => 0xA7B5, +0xA7B6 => 0xA7B7, +0xA7B8 => 0xA7B9, +0xA7BA => 0xA7BB, +0xA7BC => 0xA7BD, +0xA7BE => 0xA7BF, +0xA7C2 => 0xA7C3, +0xA7C4 => 0xA794, +0xA7C5 => 0x282, +0xA7C6 => 0x1D8E, +0xA7C7 => 0xA7C8, +0xA7C9 => 0xA7CA, +0xA7F5 => 0xA7F6, +0xFF21 => 0xFF41, +0xFF22 => 0xFF42, +0xFF23 => 0xFF43, +0xFF24 => 0xFF44, +0xFF25 => 0xFF45, +0xFF26 => 0xFF46, +0xFF27 => 0xFF47, +0xFF28 => 0xFF48, +0xFF29 => 0xFF49, +0xFF2A => 0xFF4A, +0xFF2B => 0xFF4B, +0xFF2C => 0xFF4C, +0xFF2D => 0xFF4D, +0xFF2E => 0xFF4E, +0xFF2F => 0xFF4F, +0xFF30 => 0xFF50, +0xFF31 => 0xFF51, +0xFF32 => 0xFF52, +0xFF33 => 0xFF53, +0xFF34 => 0xFF54, +0xFF35 => 0xFF55, +0xFF36 => 0xFF56, +0xFF37 => 0xFF57, +0xFF38 => 0xFF58, +0xFF39 => 0xFF59, +0xFF3A => 0xFF5A, +0x10400 => 0x10428, +0x10401 => 0x10429, +0x10402 => 0x1042A, +0x10403 => 0x1042B, +0x10404 => 0x1042C, +0x10405 => 0x1042D, +0x10406 => 0x1042E, +0x10407 => 0x1042F, +0x10408 => 0x10430, +0x10409 => 0x10431, +0x1040A => 0x10432, +0x1040B => 0x10433, +0x1040C => 0x10434, +0x1040D => 0x10435, +0x1040E => 0x10436, +0x1040F => 0x10437, +0x10410 => 0x10438, +0x10411 => 0x10439, +0x10412 => 0x1043A, +0x10413 => 0x1043B, +0x10414 => 0x1043C, +0x10415 => 0x1043D, +0x10416 => 0x1043E, +0x10417 => 0x1043F, +0x10418 => 0x10440, +0x10419 => 0x10441, +0x1041A => 0x10442, +0x1041B => 0x10443, +0x1041C => 0x10444, +0x1041D => 0x10445, +0x1041E => 0x10446, +0x1041F => 0x10447, +0x10420 => 0x10448, +0x10421 => 0x10449, +0x10422 => 0x1044A, +0x10423 => 0x1044B, +0x10424 => 0x1044C, +0x10425 => 0x1044D, +0x10426 => 0x1044E, +0x10427 => 0x1044F, +0x104B0 => 0x104D8, +0x104B1 => 0x104D9, +0x104B2 => 0x104DA, +0x104B3 => 0x104DB, +0x104B4 => 0x104DC, +0x104B5 => 0x104DD, +0x104B6 => 0x104DE, +0x104B7 => 0x104DF, +0x104B8 => 0x104E0, +0x104B9 => 0x104E1, +0x104BA => 0x104E2, +0x104BB => 0x104E3, +0x104BC => 0x104E4, +0x104BD => 0x104E5, +0x104BE => 0x104E6, +0x104BF => 0x104E7, +0x104C0 => 0x104E8, +0x104C1 => 0x104E9, +0x104C2 => 0x104EA, +0x104C3 => 0x104EB, +0x104C4 => 0x104EC, +0x104C5 => 0x104ED, +0x104C6 => 0x104EE, +0x104C7 => 0x104EF, +0x104C8 => 0x104F0, +0x104C9 => 0x104F1, +0x104CA => 0x104F2, +0x104CB => 0x104F3, +0x104CC => 0x104F4, +0x104CD => 0x104F5, +0x104CE => 0x104F6, +0x104CF => 0x104F7, +0x104D0 => 0x104F8, +0x104D1 => 0x104F9, +0x104D2 => 0x104FA, +0x104D3 => 0x104FB, +0x10C80 => 0x10CC0, +0x10C81 => 0x10CC1, +0x10C82 => 0x10CC2, +0x10C83 => 0x10CC3, +0x10C84 => 0x10CC4, +0x10C85 => 0x10CC5, +0x10C86 => 0x10CC6, +0x10C87 => 0x10CC7, +0x10C88 => 0x10CC8, +0x10C89 => 0x10CC9, +0x10C8A => 0x10CCA, +0x10C8B => 0x10CCB, +0x10C8C => 0x10CCC, +0x10C8D => 0x10CCD, +0x10C8E => 0x10CCE, +0x10C8F => 0x10CCF, +0x10C90 => 0x10CD0, +0x10C91 => 0x10CD1, +0x10C92 => 0x10CD2, +0x10C93 => 0x10CD3, +0x10C94 => 0x10CD4, +0x10C95 => 0x10CD5, +0x10C96 => 0x10CD6, +0x10C97 => 0x10CD7, +0x10C98 => 0x10CD8, +0x10C99 => 0x10CD9, +0x10C9A => 0x10CDA, +0x10C9B => 0x10CDB, +0x10C9C => 0x10CDC, +0x10C9D => 0x10CDD, +0x10C9E => 0x10CDE, +0x10C9F => 0x10CDF, +0x10CA0 => 0x10CE0, +0x10CA1 => 0x10CE1, +0x10CA2 => 0x10CE2, +0x10CA3 => 0x10CE3, +0x10CA4 => 0x10CE4, +0x10CA5 => 0x10CE5, +0x10CA6 => 0x10CE6, +0x10CA7 => 0x10CE7, +0x10CA8 => 0x10CE8, +0x10CA9 => 0x10CE9, +0x10CAA => 0x10CEA, +0x10CAB => 0x10CEB, +0x10CAC => 0x10CEC, +0x10CAD => 0x10CED, +0x10CAE => 0x10CEE, +0x10CAF => 0x10CEF, +0x10CB0 => 0x10CF0, +0x10CB1 => 0x10CF1, +0x10CB2 => 0x10CF2, +0x118A0 => 0x118C0, +0x118A1 => 0x118C1, +0x118A2 => 0x118C2, +0x118A3 => 0x118C3, +0x118A4 => 0x118C4, +0x118A5 => 0x118C5, +0x118A6 => 0x118C6, +0x118A7 => 0x118C7, +0x118A8 => 0x118C8, +0x118A9 => 0x118C9, +0x118AA => 0x118CA, +0x118AB => 0x118CB, +0x118AC => 0x118CC, +0x118AD => 0x118CD, +0x118AE => 0x118CE, +0x118AF => 0x118CF, +0x118B0 => 0x118D0, +0x118B1 => 0x118D1, +0x118B2 => 0x118D2, +0x118B3 => 0x118D3, +0x118B4 => 0x118D4, +0x118B5 => 0x118D5, +0x118B6 => 0x118D6, +0x118B7 => 0x118D7, +0x118B8 => 0x118D8, +0x118B9 => 0x118D9, +0x118BA => 0x118DA, +0x118BB => 0x118DB, +0x118BC => 0x118DC, +0x118BD => 0x118DD, +0x118BE => 0x118DE, +0x118BF => 0x118DF, +0x16E40 => 0x16E60, +0x16E41 => 0x16E61, +0x16E42 => 0x16E62, +0x16E43 => 0x16E63, +0x16E44 => 0x16E64, +0x16E45 => 0x16E65, +0x16E46 => 0x16E66, +0x16E47 => 0x16E67, +0x16E48 => 0x16E68, +0x16E49 => 0x16E69, +0x16E4A => 0x16E6A, +0x16E4B => 0x16E6B, +0x16E4C => 0x16E6C, +0x16E4D => 0x16E6D, +0x16E4E => 0x16E6E, +0x16E4F => 0x16E6F, +0x16E50 => 0x16E70, +0x16E51 => 0x16E71, +0x16E52 => 0x16E72, +0x16E53 => 0x16E73, +0x16E54 => 0x16E74, +0x16E55 => 0x16E75, +0x16E56 => 0x16E76, +0x16E57 => 0x16E77, +0x16E58 => 0x16E78, +0x16E59 => 0x16E79, +0x16E5A => 0x16E7A, +0x16E5B => 0x16E7B, +0x16E5C => 0x16E7C, +0x16E5D => 0x16E7D, +0x16E5E => 0x16E7E, +0x16E5F => 0x16E7F, +0x1E900 => 0x1E922, +0x1E901 => 0x1E923, +0x1E902 => 0x1E924, +0x1E903 => 0x1E925, +0x1E904 => 0x1E926, +0x1E905 => 0x1E927, +0x1E906 => 0x1E928, +0x1E907 => 0x1E929, +0x1E908 => 0x1E92A, +0x1E909 => 0x1E92B, +0x1E90A => 0x1E92C, +0x1E90B => 0x1E92D, +0x1E90C => 0x1E92E, +0x1E90D => 0x1E92F, +0x1E90E => 0x1E930, +0x1E90F => 0x1E931, +0x1E910 => 0x1E932, +0x1E911 => 0x1E933, +0x1E912 => 0x1E934, +0x1E913 => 0x1E935, +0x1E914 => 0x1E936, +0x1E915 => 0x1E937, +0x1E916 => 0x1E938, +0x1E917 => 0x1E939, +0x1E918 => 0x1E93A, +0x1E919 => 0x1E93B, +0x1E91A => 0x1E93C, +0x1E91B => 0x1E93D, +0x1E91C => 0x1E93E, +0x1E91D => 0x1E93F, +0x1E91E => 0x1E940, +0x1E91F => 0x1E941, +0x1E920 => 0x1E942, +0x1E921 => 0x1E943, +]; diff --git a/src/opis/string/res/upper.php b/src/opis/string/res/upper.php new file mode 100644 index 00000000..376ce6e6 --- /dev/null +++ b/src/opis/string/res/upper.php @@ -0,0 +1,1413 @@ + 0x41, +0x62 => 0x42, +0x63 => 0x43, +0x64 => 0x44, +0x65 => 0x45, +0x66 => 0x46, +0x67 => 0x47, +0x68 => 0x48, +0x69 => 0x49, +0x6A => 0x4A, +0x6B => 0x4B, +0x6C => 0x4C, +0x6D => 0x4D, +0x6E => 0x4E, +0x6F => 0x4F, +0x70 => 0x50, +0x71 => 0x51, +0x72 => 0x52, +0x73 => 0x53, +0x74 => 0x54, +0x75 => 0x55, +0x76 => 0x56, +0x77 => 0x57, +0x78 => 0x58, +0x79 => 0x59, +0x7A => 0x5A, +0xB5 => 0x39C, +0xE0 => 0xC0, +0xE1 => 0xC1, +0xE2 => 0xC2, +0xE3 => 0xC3, +0xE4 => 0xC4, +0xE5 => 0xC5, +0xE6 => 0xC6, +0xE7 => 0xC7, +0xE8 => 0xC8, +0xE9 => 0xC9, +0xEA => 0xCA, +0xEB => 0xCB, +0xEC => 0xCC, +0xED => 0xCD, +0xEE => 0xCE, +0xEF => 0xCF, +0xF0 => 0xD0, +0xF1 => 0xD1, +0xF2 => 0xD2, +0xF3 => 0xD3, +0xF4 => 0xD4, +0xF5 => 0xD5, +0xF6 => 0xD6, +0xF8 => 0xD8, +0xF9 => 0xD9, +0xFA => 0xDA, +0xFB => 0xDB, +0xFC => 0xDC, +0xFD => 0xDD, +0xFE => 0xDE, +0xFF => 0x178, +0x101 => 0x100, +0x103 => 0x102, +0x105 => 0x104, +0x107 => 0x106, +0x109 => 0x108, +0x10B => 0x10A, +0x10D => 0x10C, +0x10F => 0x10E, +0x111 => 0x110, +0x113 => 0x112, +0x115 => 0x114, +0x117 => 0x116, +0x119 => 0x118, +0x11B => 0x11A, +0x11D => 0x11C, +0x11F => 0x11E, +0x121 => 0x120, +0x123 => 0x122, +0x125 => 0x124, +0x127 => 0x126, +0x129 => 0x128, +0x12B => 0x12A, +0x12D => 0x12C, +0x12F => 0x12E, +0x131 => 0x49, +0x133 => 0x132, +0x135 => 0x134, +0x137 => 0x136, +0x13A => 0x139, +0x13C => 0x13B, +0x13E => 0x13D, +0x140 => 0x13F, +0x142 => 0x141, +0x144 => 0x143, +0x146 => 0x145, +0x148 => 0x147, +0x14B => 0x14A, +0x14D => 0x14C, +0x14F => 0x14E, +0x151 => 0x150, +0x153 => 0x152, +0x155 => 0x154, +0x157 => 0x156, +0x159 => 0x158, +0x15B => 0x15A, +0x15D => 0x15C, +0x15F => 0x15E, +0x161 => 0x160, +0x163 => 0x162, +0x165 => 0x164, +0x167 => 0x166, +0x169 => 0x168, +0x16B => 0x16A, +0x16D => 0x16C, +0x16F => 0x16E, +0x171 => 0x170, +0x173 => 0x172, +0x175 => 0x174, +0x177 => 0x176, +0x17A => 0x179, +0x17C => 0x17B, +0x17E => 0x17D, +0x17F => 0x53, +0x180 => 0x243, +0x183 => 0x182, +0x185 => 0x184, +0x188 => 0x187, +0x18C => 0x18B, +0x192 => 0x191, +0x195 => 0x1F6, +0x199 => 0x198, +0x19A => 0x23D, +0x19E => 0x220, +0x1A1 => 0x1A0, +0x1A3 => 0x1A2, +0x1A5 => 0x1A4, +0x1A8 => 0x1A7, +0x1AD => 0x1AC, +0x1B0 => 0x1AF, +0x1B4 => 0x1B3, +0x1B6 => 0x1B5, +0x1B9 => 0x1B8, +0x1BD => 0x1BC, +0x1BF => 0x1F7, +0x1C5 => 0x1C4, +0x1C6 => 0x1C4, +0x1C8 => 0x1C7, +0x1C9 => 0x1C7, +0x1CB => 0x1CA, +0x1CC => 0x1CA, +0x1CE => 0x1CD, +0x1D0 => 0x1CF, +0x1D2 => 0x1D1, +0x1D4 => 0x1D3, +0x1D6 => 0x1D5, +0x1D8 => 0x1D7, +0x1DA => 0x1D9, +0x1DC => 0x1DB, +0x1DD => 0x18E, +0x1DF => 0x1DE, +0x1E1 => 0x1E0, +0x1E3 => 0x1E2, +0x1E5 => 0x1E4, +0x1E7 => 0x1E6, +0x1E9 => 0x1E8, +0x1EB => 0x1EA, +0x1ED => 0x1EC, +0x1EF => 0x1EE, +0x1F2 => 0x1F1, +0x1F3 => 0x1F1, +0x1F5 => 0x1F4, +0x1F9 => 0x1F8, +0x1FB => 0x1FA, +0x1FD => 0x1FC, +0x1FF => 0x1FE, +0x201 => 0x200, +0x203 => 0x202, +0x205 => 0x204, +0x207 => 0x206, +0x209 => 0x208, +0x20B => 0x20A, +0x20D => 0x20C, +0x20F => 0x20E, +0x211 => 0x210, +0x213 => 0x212, +0x215 => 0x214, +0x217 => 0x216, +0x219 => 0x218, +0x21B => 0x21A, +0x21D => 0x21C, +0x21F => 0x21E, +0x223 => 0x222, +0x225 => 0x224, +0x227 => 0x226, +0x229 => 0x228, +0x22B => 0x22A, +0x22D => 0x22C, +0x22F => 0x22E, +0x231 => 0x230, +0x233 => 0x232, +0x23C => 0x23B, +0x23F => 0x2C7E, +0x240 => 0x2C7F, +0x242 => 0x241, +0x247 => 0x246, +0x249 => 0x248, +0x24B => 0x24A, +0x24D => 0x24C, +0x24F => 0x24E, +0x250 => 0x2C6F, +0x251 => 0x2C6D, +0x252 => 0x2C70, +0x253 => 0x181, +0x254 => 0x186, +0x256 => 0x189, +0x257 => 0x18A, +0x259 => 0x18F, +0x25B => 0x190, +0x25C => 0xA7AB, +0x260 => 0x193, +0x261 => 0xA7AC, +0x263 => 0x194, +0x265 => 0xA78D, +0x266 => 0xA7AA, +0x268 => 0x197, +0x269 => 0x196, +0x26A => 0xA7AE, +0x26B => 0x2C62, +0x26C => 0xA7AD, +0x26F => 0x19C, +0x271 => 0x2C6E, +0x272 => 0x19D, +0x275 => 0x19F, +0x27D => 0x2C64, +0x280 => 0x1A6, +0x282 => 0xA7C5, +0x283 => 0x1A9, +0x287 => 0xA7B1, +0x288 => 0x1AE, +0x289 => 0x244, +0x28A => 0x1B1, +0x28B => 0x1B2, +0x28C => 0x245, +0x292 => 0x1B7, +0x29D => 0xA7B2, +0x29E => 0xA7B0, +0x345 => 0x399, +0x371 => 0x370, +0x373 => 0x372, +0x377 => 0x376, +0x37B => 0x3FD, +0x37C => 0x3FE, +0x37D => 0x3FF, +0x3AC => 0x386, +0x3AD => 0x388, +0x3AE => 0x389, +0x3AF => 0x38A, +0x3B1 => 0x391, +0x3B2 => 0x392, +0x3B3 => 0x393, +0x3B4 => 0x394, +0x3B5 => 0x395, +0x3B6 => 0x396, +0x3B7 => 0x397, +0x3B8 => 0x398, +0x3B9 => 0x399, +0x3BA => 0x39A, +0x3BB => 0x39B, +0x3BC => 0x39C, +0x3BD => 0x39D, +0x3BE => 0x39E, +0x3BF => 0x39F, +0x3C0 => 0x3A0, +0x3C1 => 0x3A1, +0x3C2 => 0x3A3, +0x3C3 => 0x3A3, +0x3C4 => 0x3A4, +0x3C5 => 0x3A5, +0x3C6 => 0x3A6, +0x3C7 => 0x3A7, +0x3C8 => 0x3A8, +0x3C9 => 0x3A9, +0x3CA => 0x3AA, +0x3CB => 0x3AB, +0x3CC => 0x38C, +0x3CD => 0x38E, +0x3CE => 0x38F, +0x3D0 => 0x392, +0x3D1 => 0x398, +0x3D5 => 0x3A6, +0x3D6 => 0x3A0, +0x3D7 => 0x3CF, +0x3D9 => 0x3D8, +0x3DB => 0x3DA, +0x3DD => 0x3DC, +0x3DF => 0x3DE, +0x3E1 => 0x3E0, +0x3E3 => 0x3E2, +0x3E5 => 0x3E4, +0x3E7 => 0x3E6, +0x3E9 => 0x3E8, +0x3EB => 0x3EA, +0x3ED => 0x3EC, +0x3EF => 0x3EE, +0x3F0 => 0x39A, +0x3F1 => 0x3A1, +0x3F2 => 0x3F9, +0x3F3 => 0x37F, +0x3F5 => 0x395, +0x3F8 => 0x3F7, +0x3FB => 0x3FA, +0x430 => 0x410, +0x431 => 0x411, +0x432 => 0x412, +0x433 => 0x413, +0x434 => 0x414, +0x435 => 0x415, +0x436 => 0x416, +0x437 => 0x417, +0x438 => 0x418, +0x439 => 0x419, +0x43A => 0x41A, +0x43B => 0x41B, +0x43C => 0x41C, +0x43D => 0x41D, +0x43E => 0x41E, +0x43F => 0x41F, +0x440 => 0x420, +0x441 => 0x421, +0x442 => 0x422, +0x443 => 0x423, +0x444 => 0x424, +0x445 => 0x425, +0x446 => 0x426, +0x447 => 0x427, +0x448 => 0x428, +0x449 => 0x429, +0x44A => 0x42A, +0x44B => 0x42B, +0x44C => 0x42C, +0x44D => 0x42D, +0x44E => 0x42E, +0x44F => 0x42F, +0x450 => 0x400, +0x451 => 0x401, +0x452 => 0x402, +0x453 => 0x403, +0x454 => 0x404, +0x455 => 0x405, +0x456 => 0x406, +0x457 => 0x407, +0x458 => 0x408, +0x459 => 0x409, +0x45A => 0x40A, +0x45B => 0x40B, +0x45C => 0x40C, +0x45D => 0x40D, +0x45E => 0x40E, +0x45F => 0x40F, +0x461 => 0x460, +0x463 => 0x462, +0x465 => 0x464, +0x467 => 0x466, +0x469 => 0x468, +0x46B => 0x46A, +0x46D => 0x46C, +0x46F => 0x46E, +0x471 => 0x470, +0x473 => 0x472, +0x475 => 0x474, +0x477 => 0x476, +0x479 => 0x478, +0x47B => 0x47A, +0x47D => 0x47C, +0x47F => 0x47E, +0x481 => 0x480, +0x48B => 0x48A, +0x48D => 0x48C, +0x48F => 0x48E, +0x491 => 0x490, +0x493 => 0x492, +0x495 => 0x494, +0x497 => 0x496, +0x499 => 0x498, +0x49B => 0x49A, +0x49D => 0x49C, +0x49F => 0x49E, +0x4A1 => 0x4A0, +0x4A3 => 0x4A2, +0x4A5 => 0x4A4, +0x4A7 => 0x4A6, +0x4A9 => 0x4A8, +0x4AB => 0x4AA, +0x4AD => 0x4AC, +0x4AF => 0x4AE, +0x4B1 => 0x4B0, +0x4B3 => 0x4B2, +0x4B5 => 0x4B4, +0x4B7 => 0x4B6, +0x4B9 => 0x4B8, +0x4BB => 0x4BA, +0x4BD => 0x4BC, +0x4BF => 0x4BE, +0x4C2 => 0x4C1, +0x4C4 => 0x4C3, +0x4C6 => 0x4C5, +0x4C8 => 0x4C7, +0x4CA => 0x4C9, +0x4CC => 0x4CB, +0x4CE => 0x4CD, +0x4CF => 0x4C0, +0x4D1 => 0x4D0, +0x4D3 => 0x4D2, +0x4D5 => 0x4D4, +0x4D7 => 0x4D6, +0x4D9 => 0x4D8, +0x4DB => 0x4DA, +0x4DD => 0x4DC, +0x4DF => 0x4DE, +0x4E1 => 0x4E0, +0x4E3 => 0x4E2, +0x4E5 => 0x4E4, +0x4E7 => 0x4E6, +0x4E9 => 0x4E8, +0x4EB => 0x4EA, +0x4ED => 0x4EC, +0x4EF => 0x4EE, +0x4F1 => 0x4F0, +0x4F3 => 0x4F2, +0x4F5 => 0x4F4, +0x4F7 => 0x4F6, +0x4F9 => 0x4F8, +0x4FB => 0x4FA, +0x4FD => 0x4FC, +0x4FF => 0x4FE, +0x501 => 0x500, +0x503 => 0x502, +0x505 => 0x504, +0x507 => 0x506, +0x509 => 0x508, +0x50B => 0x50A, +0x50D => 0x50C, +0x50F => 0x50E, +0x511 => 0x510, +0x513 => 0x512, +0x515 => 0x514, +0x517 => 0x516, +0x519 => 0x518, +0x51B => 0x51A, +0x51D => 0x51C, +0x51F => 0x51E, +0x521 => 0x520, +0x523 => 0x522, +0x525 => 0x524, +0x527 => 0x526, +0x529 => 0x528, +0x52B => 0x52A, +0x52D => 0x52C, +0x52F => 0x52E, +0x561 => 0x531, +0x562 => 0x532, +0x563 => 0x533, +0x564 => 0x534, +0x565 => 0x535, +0x566 => 0x536, +0x567 => 0x537, +0x568 => 0x538, +0x569 => 0x539, +0x56A => 0x53A, +0x56B => 0x53B, +0x56C => 0x53C, +0x56D => 0x53D, +0x56E => 0x53E, +0x56F => 0x53F, +0x570 => 0x540, +0x571 => 0x541, +0x572 => 0x542, +0x573 => 0x543, +0x574 => 0x544, +0x575 => 0x545, +0x576 => 0x546, +0x577 => 0x547, +0x578 => 0x548, +0x579 => 0x549, +0x57A => 0x54A, +0x57B => 0x54B, +0x57C => 0x54C, +0x57D => 0x54D, +0x57E => 0x54E, +0x57F => 0x54F, +0x580 => 0x550, +0x581 => 0x551, +0x582 => 0x552, +0x583 => 0x553, +0x584 => 0x554, +0x585 => 0x555, +0x586 => 0x556, +0x10D0 => 0x1C90, +0x10D1 => 0x1C91, +0x10D2 => 0x1C92, +0x10D3 => 0x1C93, +0x10D4 => 0x1C94, +0x10D5 => 0x1C95, +0x10D6 => 0x1C96, +0x10D7 => 0x1C97, +0x10D8 => 0x1C98, +0x10D9 => 0x1C99, +0x10DA => 0x1C9A, +0x10DB => 0x1C9B, +0x10DC => 0x1C9C, +0x10DD => 0x1C9D, +0x10DE => 0x1C9E, +0x10DF => 0x1C9F, +0x10E0 => 0x1CA0, +0x10E1 => 0x1CA1, +0x10E2 => 0x1CA2, +0x10E3 => 0x1CA3, +0x10E4 => 0x1CA4, +0x10E5 => 0x1CA5, +0x10E6 => 0x1CA6, +0x10E7 => 0x1CA7, +0x10E8 => 0x1CA8, +0x10E9 => 0x1CA9, +0x10EA => 0x1CAA, +0x10EB => 0x1CAB, +0x10EC => 0x1CAC, +0x10ED => 0x1CAD, +0x10EE => 0x1CAE, +0x10EF => 0x1CAF, +0x10F0 => 0x1CB0, +0x10F1 => 0x1CB1, +0x10F2 => 0x1CB2, +0x10F3 => 0x1CB3, +0x10F4 => 0x1CB4, +0x10F5 => 0x1CB5, +0x10F6 => 0x1CB6, +0x10F7 => 0x1CB7, +0x10F8 => 0x1CB8, +0x10F9 => 0x1CB9, +0x10FA => 0x1CBA, +0x10FD => 0x1CBD, +0x10FE => 0x1CBE, +0x10FF => 0x1CBF, +0x13F8 => 0x13F0, +0x13F9 => 0x13F1, +0x13FA => 0x13F2, +0x13FB => 0x13F3, +0x13FC => 0x13F4, +0x13FD => 0x13F5, +0x1C80 => 0x412, +0x1C81 => 0x414, +0x1C82 => 0x41E, +0x1C83 => 0x421, +0x1C84 => 0x422, +0x1C85 => 0x422, +0x1C86 => 0x42A, +0x1C87 => 0x462, +0x1C88 => 0xA64A, +0x1D79 => 0xA77D, +0x1D7D => 0x2C63, +0x1D8E => 0xA7C6, +0x1E01 => 0x1E00, +0x1E03 => 0x1E02, +0x1E05 => 0x1E04, +0x1E07 => 0x1E06, +0x1E09 => 0x1E08, +0x1E0B => 0x1E0A, +0x1E0D => 0x1E0C, +0x1E0F => 0x1E0E, +0x1E11 => 0x1E10, +0x1E13 => 0x1E12, +0x1E15 => 0x1E14, +0x1E17 => 0x1E16, +0x1E19 => 0x1E18, +0x1E1B => 0x1E1A, +0x1E1D => 0x1E1C, +0x1E1F => 0x1E1E, +0x1E21 => 0x1E20, +0x1E23 => 0x1E22, +0x1E25 => 0x1E24, +0x1E27 => 0x1E26, +0x1E29 => 0x1E28, +0x1E2B => 0x1E2A, +0x1E2D => 0x1E2C, +0x1E2F => 0x1E2E, +0x1E31 => 0x1E30, +0x1E33 => 0x1E32, +0x1E35 => 0x1E34, +0x1E37 => 0x1E36, +0x1E39 => 0x1E38, +0x1E3B => 0x1E3A, +0x1E3D => 0x1E3C, +0x1E3F => 0x1E3E, +0x1E41 => 0x1E40, +0x1E43 => 0x1E42, +0x1E45 => 0x1E44, +0x1E47 => 0x1E46, +0x1E49 => 0x1E48, +0x1E4B => 0x1E4A, +0x1E4D => 0x1E4C, +0x1E4F => 0x1E4E, +0x1E51 => 0x1E50, +0x1E53 => 0x1E52, +0x1E55 => 0x1E54, +0x1E57 => 0x1E56, +0x1E59 => 0x1E58, +0x1E5B => 0x1E5A, +0x1E5D => 0x1E5C, +0x1E5F => 0x1E5E, +0x1E61 => 0x1E60, +0x1E63 => 0x1E62, +0x1E65 => 0x1E64, +0x1E67 => 0x1E66, +0x1E69 => 0x1E68, +0x1E6B => 0x1E6A, +0x1E6D => 0x1E6C, +0x1E6F => 0x1E6E, +0x1E71 => 0x1E70, +0x1E73 => 0x1E72, +0x1E75 => 0x1E74, +0x1E77 => 0x1E76, +0x1E79 => 0x1E78, +0x1E7B => 0x1E7A, +0x1E7D => 0x1E7C, +0x1E7F => 0x1E7E, +0x1E81 => 0x1E80, +0x1E83 => 0x1E82, +0x1E85 => 0x1E84, +0x1E87 => 0x1E86, +0x1E89 => 0x1E88, +0x1E8B => 0x1E8A, +0x1E8D => 0x1E8C, +0x1E8F => 0x1E8E, +0x1E91 => 0x1E90, +0x1E93 => 0x1E92, +0x1E95 => 0x1E94, +0x1E9B => 0x1E60, +0x1EA1 => 0x1EA0, +0x1EA3 => 0x1EA2, +0x1EA5 => 0x1EA4, +0x1EA7 => 0x1EA6, +0x1EA9 => 0x1EA8, +0x1EAB => 0x1EAA, +0x1EAD => 0x1EAC, +0x1EAF => 0x1EAE, +0x1EB1 => 0x1EB0, +0x1EB3 => 0x1EB2, +0x1EB5 => 0x1EB4, +0x1EB7 => 0x1EB6, +0x1EB9 => 0x1EB8, +0x1EBB => 0x1EBA, +0x1EBD => 0x1EBC, +0x1EBF => 0x1EBE, +0x1EC1 => 0x1EC0, +0x1EC3 => 0x1EC2, +0x1EC5 => 0x1EC4, +0x1EC7 => 0x1EC6, +0x1EC9 => 0x1EC8, +0x1ECB => 0x1ECA, +0x1ECD => 0x1ECC, +0x1ECF => 0x1ECE, +0x1ED1 => 0x1ED0, +0x1ED3 => 0x1ED2, +0x1ED5 => 0x1ED4, +0x1ED7 => 0x1ED6, +0x1ED9 => 0x1ED8, +0x1EDB => 0x1EDA, +0x1EDD => 0x1EDC, +0x1EDF => 0x1EDE, +0x1EE1 => 0x1EE0, +0x1EE3 => 0x1EE2, +0x1EE5 => 0x1EE4, +0x1EE7 => 0x1EE6, +0x1EE9 => 0x1EE8, +0x1EEB => 0x1EEA, +0x1EED => 0x1EEC, +0x1EEF => 0x1EEE, +0x1EF1 => 0x1EF0, +0x1EF3 => 0x1EF2, +0x1EF5 => 0x1EF4, +0x1EF7 => 0x1EF6, +0x1EF9 => 0x1EF8, +0x1EFB => 0x1EFA, +0x1EFD => 0x1EFC, +0x1EFF => 0x1EFE, +0x1F00 => 0x1F08, +0x1F01 => 0x1F09, +0x1F02 => 0x1F0A, +0x1F03 => 0x1F0B, +0x1F04 => 0x1F0C, +0x1F05 => 0x1F0D, +0x1F06 => 0x1F0E, +0x1F07 => 0x1F0F, +0x1F10 => 0x1F18, +0x1F11 => 0x1F19, +0x1F12 => 0x1F1A, +0x1F13 => 0x1F1B, +0x1F14 => 0x1F1C, +0x1F15 => 0x1F1D, +0x1F20 => 0x1F28, +0x1F21 => 0x1F29, +0x1F22 => 0x1F2A, +0x1F23 => 0x1F2B, +0x1F24 => 0x1F2C, +0x1F25 => 0x1F2D, +0x1F26 => 0x1F2E, +0x1F27 => 0x1F2F, +0x1F30 => 0x1F38, +0x1F31 => 0x1F39, +0x1F32 => 0x1F3A, +0x1F33 => 0x1F3B, +0x1F34 => 0x1F3C, +0x1F35 => 0x1F3D, +0x1F36 => 0x1F3E, +0x1F37 => 0x1F3F, +0x1F40 => 0x1F48, +0x1F41 => 0x1F49, +0x1F42 => 0x1F4A, +0x1F43 => 0x1F4B, +0x1F44 => 0x1F4C, +0x1F45 => 0x1F4D, +0x1F51 => 0x1F59, +0x1F53 => 0x1F5B, +0x1F55 => 0x1F5D, +0x1F57 => 0x1F5F, +0x1F60 => 0x1F68, +0x1F61 => 0x1F69, +0x1F62 => 0x1F6A, +0x1F63 => 0x1F6B, +0x1F64 => 0x1F6C, +0x1F65 => 0x1F6D, +0x1F66 => 0x1F6E, +0x1F67 => 0x1F6F, +0x1F70 => 0x1FBA, +0x1F71 => 0x1FBB, +0x1F72 => 0x1FC8, +0x1F73 => 0x1FC9, +0x1F74 => 0x1FCA, +0x1F75 => 0x1FCB, +0x1F76 => 0x1FDA, +0x1F77 => 0x1FDB, +0x1F78 => 0x1FF8, +0x1F79 => 0x1FF9, +0x1F7A => 0x1FEA, +0x1F7B => 0x1FEB, +0x1F7C => 0x1FFA, +0x1F7D => 0x1FFB, +0x1F80 => 0x1F88, +0x1F81 => 0x1F89, +0x1F82 => 0x1F8A, +0x1F83 => 0x1F8B, +0x1F84 => 0x1F8C, +0x1F85 => 0x1F8D, +0x1F86 => 0x1F8E, +0x1F87 => 0x1F8F, +0x1F90 => 0x1F98, +0x1F91 => 0x1F99, +0x1F92 => 0x1F9A, +0x1F93 => 0x1F9B, +0x1F94 => 0x1F9C, +0x1F95 => 0x1F9D, +0x1F96 => 0x1F9E, +0x1F97 => 0x1F9F, +0x1FA0 => 0x1FA8, +0x1FA1 => 0x1FA9, +0x1FA2 => 0x1FAA, +0x1FA3 => 0x1FAB, +0x1FA4 => 0x1FAC, +0x1FA5 => 0x1FAD, +0x1FA6 => 0x1FAE, +0x1FA7 => 0x1FAF, +0x1FB0 => 0x1FB8, +0x1FB1 => 0x1FB9, +0x1FB3 => 0x1FBC, +0x1FBE => 0x399, +0x1FC3 => 0x1FCC, +0x1FD0 => 0x1FD8, +0x1FD1 => 0x1FD9, +0x1FE0 => 0x1FE8, +0x1FE1 => 0x1FE9, +0x1FE5 => 0x1FEC, +0x1FF3 => 0x1FFC, +0x214E => 0x2132, +0x2170 => 0x2160, +0x2171 => 0x2161, +0x2172 => 0x2162, +0x2173 => 0x2163, +0x2174 => 0x2164, +0x2175 => 0x2165, +0x2176 => 0x2166, +0x2177 => 0x2167, +0x2178 => 0x2168, +0x2179 => 0x2169, +0x217A => 0x216A, +0x217B => 0x216B, +0x217C => 0x216C, +0x217D => 0x216D, +0x217E => 0x216E, +0x217F => 0x216F, +0x2184 => 0x2183, +0x24D0 => 0x24B6, +0x24D1 => 0x24B7, +0x24D2 => 0x24B8, +0x24D3 => 0x24B9, +0x24D4 => 0x24BA, +0x24D5 => 0x24BB, +0x24D6 => 0x24BC, +0x24D7 => 0x24BD, +0x24D8 => 0x24BE, +0x24D9 => 0x24BF, +0x24DA => 0x24C0, +0x24DB => 0x24C1, +0x24DC => 0x24C2, +0x24DD => 0x24C3, +0x24DE => 0x24C4, +0x24DF => 0x24C5, +0x24E0 => 0x24C6, +0x24E1 => 0x24C7, +0x24E2 => 0x24C8, +0x24E3 => 0x24C9, +0x24E4 => 0x24CA, +0x24E5 => 0x24CB, +0x24E6 => 0x24CC, +0x24E7 => 0x24CD, +0x24E8 => 0x24CE, +0x24E9 => 0x24CF, +0x2C30 => 0x2C00, +0x2C31 => 0x2C01, +0x2C32 => 0x2C02, +0x2C33 => 0x2C03, +0x2C34 => 0x2C04, +0x2C35 => 0x2C05, +0x2C36 => 0x2C06, +0x2C37 => 0x2C07, +0x2C38 => 0x2C08, +0x2C39 => 0x2C09, +0x2C3A => 0x2C0A, +0x2C3B => 0x2C0B, +0x2C3C => 0x2C0C, +0x2C3D => 0x2C0D, +0x2C3E => 0x2C0E, +0x2C3F => 0x2C0F, +0x2C40 => 0x2C10, +0x2C41 => 0x2C11, +0x2C42 => 0x2C12, +0x2C43 => 0x2C13, +0x2C44 => 0x2C14, +0x2C45 => 0x2C15, +0x2C46 => 0x2C16, +0x2C47 => 0x2C17, +0x2C48 => 0x2C18, +0x2C49 => 0x2C19, +0x2C4A => 0x2C1A, +0x2C4B => 0x2C1B, +0x2C4C => 0x2C1C, +0x2C4D => 0x2C1D, +0x2C4E => 0x2C1E, +0x2C4F => 0x2C1F, +0x2C50 => 0x2C20, +0x2C51 => 0x2C21, +0x2C52 => 0x2C22, +0x2C53 => 0x2C23, +0x2C54 => 0x2C24, +0x2C55 => 0x2C25, +0x2C56 => 0x2C26, +0x2C57 => 0x2C27, +0x2C58 => 0x2C28, +0x2C59 => 0x2C29, +0x2C5A => 0x2C2A, +0x2C5B => 0x2C2B, +0x2C5C => 0x2C2C, +0x2C5D => 0x2C2D, +0x2C5E => 0x2C2E, +0x2C61 => 0x2C60, +0x2C65 => 0x23A, +0x2C66 => 0x23E, +0x2C68 => 0x2C67, +0x2C6A => 0x2C69, +0x2C6C => 0x2C6B, +0x2C73 => 0x2C72, +0x2C76 => 0x2C75, +0x2C81 => 0x2C80, +0x2C83 => 0x2C82, +0x2C85 => 0x2C84, +0x2C87 => 0x2C86, +0x2C89 => 0x2C88, +0x2C8B => 0x2C8A, +0x2C8D => 0x2C8C, +0x2C8F => 0x2C8E, +0x2C91 => 0x2C90, +0x2C93 => 0x2C92, +0x2C95 => 0x2C94, +0x2C97 => 0x2C96, +0x2C99 => 0x2C98, +0x2C9B => 0x2C9A, +0x2C9D => 0x2C9C, +0x2C9F => 0x2C9E, +0x2CA1 => 0x2CA0, +0x2CA3 => 0x2CA2, +0x2CA5 => 0x2CA4, +0x2CA7 => 0x2CA6, +0x2CA9 => 0x2CA8, +0x2CAB => 0x2CAA, +0x2CAD => 0x2CAC, +0x2CAF => 0x2CAE, +0x2CB1 => 0x2CB0, +0x2CB3 => 0x2CB2, +0x2CB5 => 0x2CB4, +0x2CB7 => 0x2CB6, +0x2CB9 => 0x2CB8, +0x2CBB => 0x2CBA, +0x2CBD => 0x2CBC, +0x2CBF => 0x2CBE, +0x2CC1 => 0x2CC0, +0x2CC3 => 0x2CC2, +0x2CC5 => 0x2CC4, +0x2CC7 => 0x2CC6, +0x2CC9 => 0x2CC8, +0x2CCB => 0x2CCA, +0x2CCD => 0x2CCC, +0x2CCF => 0x2CCE, +0x2CD1 => 0x2CD0, +0x2CD3 => 0x2CD2, +0x2CD5 => 0x2CD4, +0x2CD7 => 0x2CD6, +0x2CD9 => 0x2CD8, +0x2CDB => 0x2CDA, +0x2CDD => 0x2CDC, +0x2CDF => 0x2CDE, +0x2CE1 => 0x2CE0, +0x2CE3 => 0x2CE2, +0x2CEC => 0x2CEB, +0x2CEE => 0x2CED, +0x2CF3 => 0x2CF2, +0x2D00 => 0x10A0, +0x2D01 => 0x10A1, +0x2D02 => 0x10A2, +0x2D03 => 0x10A3, +0x2D04 => 0x10A4, +0x2D05 => 0x10A5, +0x2D06 => 0x10A6, +0x2D07 => 0x10A7, +0x2D08 => 0x10A8, +0x2D09 => 0x10A9, +0x2D0A => 0x10AA, +0x2D0B => 0x10AB, +0x2D0C => 0x10AC, +0x2D0D => 0x10AD, +0x2D0E => 0x10AE, +0x2D0F => 0x10AF, +0x2D10 => 0x10B0, +0x2D11 => 0x10B1, +0x2D12 => 0x10B2, +0x2D13 => 0x10B3, +0x2D14 => 0x10B4, +0x2D15 => 0x10B5, +0x2D16 => 0x10B6, +0x2D17 => 0x10B7, +0x2D18 => 0x10B8, +0x2D19 => 0x10B9, +0x2D1A => 0x10BA, +0x2D1B => 0x10BB, +0x2D1C => 0x10BC, +0x2D1D => 0x10BD, +0x2D1E => 0x10BE, +0x2D1F => 0x10BF, +0x2D20 => 0x10C0, +0x2D21 => 0x10C1, +0x2D22 => 0x10C2, +0x2D23 => 0x10C3, +0x2D24 => 0x10C4, +0x2D25 => 0x10C5, +0x2D27 => 0x10C7, +0x2D2D => 0x10CD, +0xA641 => 0xA640, +0xA643 => 0xA642, +0xA645 => 0xA644, +0xA647 => 0xA646, +0xA649 => 0xA648, +0xA64B => 0xA64A, +0xA64D => 0xA64C, +0xA64F => 0xA64E, +0xA651 => 0xA650, +0xA653 => 0xA652, +0xA655 => 0xA654, +0xA657 => 0xA656, +0xA659 => 0xA658, +0xA65B => 0xA65A, +0xA65D => 0xA65C, +0xA65F => 0xA65E, +0xA661 => 0xA660, +0xA663 => 0xA662, +0xA665 => 0xA664, +0xA667 => 0xA666, +0xA669 => 0xA668, +0xA66B => 0xA66A, +0xA66D => 0xA66C, +0xA681 => 0xA680, +0xA683 => 0xA682, +0xA685 => 0xA684, +0xA687 => 0xA686, +0xA689 => 0xA688, +0xA68B => 0xA68A, +0xA68D => 0xA68C, +0xA68F => 0xA68E, +0xA691 => 0xA690, +0xA693 => 0xA692, +0xA695 => 0xA694, +0xA697 => 0xA696, +0xA699 => 0xA698, +0xA69B => 0xA69A, +0xA723 => 0xA722, +0xA725 => 0xA724, +0xA727 => 0xA726, +0xA729 => 0xA728, +0xA72B => 0xA72A, +0xA72D => 0xA72C, +0xA72F => 0xA72E, +0xA733 => 0xA732, +0xA735 => 0xA734, +0xA737 => 0xA736, +0xA739 => 0xA738, +0xA73B => 0xA73A, +0xA73D => 0xA73C, +0xA73F => 0xA73E, +0xA741 => 0xA740, +0xA743 => 0xA742, +0xA745 => 0xA744, +0xA747 => 0xA746, +0xA749 => 0xA748, +0xA74B => 0xA74A, +0xA74D => 0xA74C, +0xA74F => 0xA74E, +0xA751 => 0xA750, +0xA753 => 0xA752, +0xA755 => 0xA754, +0xA757 => 0xA756, +0xA759 => 0xA758, +0xA75B => 0xA75A, +0xA75D => 0xA75C, +0xA75F => 0xA75E, +0xA761 => 0xA760, +0xA763 => 0xA762, +0xA765 => 0xA764, +0xA767 => 0xA766, +0xA769 => 0xA768, +0xA76B => 0xA76A, +0xA76D => 0xA76C, +0xA76F => 0xA76E, +0xA77A => 0xA779, +0xA77C => 0xA77B, +0xA77F => 0xA77E, +0xA781 => 0xA780, +0xA783 => 0xA782, +0xA785 => 0xA784, +0xA787 => 0xA786, +0xA78C => 0xA78B, +0xA791 => 0xA790, +0xA793 => 0xA792, +0xA794 => 0xA7C4, +0xA797 => 0xA796, +0xA799 => 0xA798, +0xA79B => 0xA79A, +0xA79D => 0xA79C, +0xA79F => 0xA79E, +0xA7A1 => 0xA7A0, +0xA7A3 => 0xA7A2, +0xA7A5 => 0xA7A4, +0xA7A7 => 0xA7A6, +0xA7A9 => 0xA7A8, +0xA7B5 => 0xA7B4, +0xA7B7 => 0xA7B6, +0xA7B9 => 0xA7B8, +0xA7BB => 0xA7BA, +0xA7BD => 0xA7BC, +0xA7BF => 0xA7BE, +0xA7C3 => 0xA7C2, +0xA7C8 => 0xA7C7, +0xA7CA => 0xA7C9, +0xA7F6 => 0xA7F5, +0xAB53 => 0xA7B3, +0xAB70 => 0x13A0, +0xAB71 => 0x13A1, +0xAB72 => 0x13A2, +0xAB73 => 0x13A3, +0xAB74 => 0x13A4, +0xAB75 => 0x13A5, +0xAB76 => 0x13A6, +0xAB77 => 0x13A7, +0xAB78 => 0x13A8, +0xAB79 => 0x13A9, +0xAB7A => 0x13AA, +0xAB7B => 0x13AB, +0xAB7C => 0x13AC, +0xAB7D => 0x13AD, +0xAB7E => 0x13AE, +0xAB7F => 0x13AF, +0xAB80 => 0x13B0, +0xAB81 => 0x13B1, +0xAB82 => 0x13B2, +0xAB83 => 0x13B3, +0xAB84 => 0x13B4, +0xAB85 => 0x13B5, +0xAB86 => 0x13B6, +0xAB87 => 0x13B7, +0xAB88 => 0x13B8, +0xAB89 => 0x13B9, +0xAB8A => 0x13BA, +0xAB8B => 0x13BB, +0xAB8C => 0x13BC, +0xAB8D => 0x13BD, +0xAB8E => 0x13BE, +0xAB8F => 0x13BF, +0xAB90 => 0x13C0, +0xAB91 => 0x13C1, +0xAB92 => 0x13C2, +0xAB93 => 0x13C3, +0xAB94 => 0x13C4, +0xAB95 => 0x13C5, +0xAB96 => 0x13C6, +0xAB97 => 0x13C7, +0xAB98 => 0x13C8, +0xAB99 => 0x13C9, +0xAB9A => 0x13CA, +0xAB9B => 0x13CB, +0xAB9C => 0x13CC, +0xAB9D => 0x13CD, +0xAB9E => 0x13CE, +0xAB9F => 0x13CF, +0xABA0 => 0x13D0, +0xABA1 => 0x13D1, +0xABA2 => 0x13D2, +0xABA3 => 0x13D3, +0xABA4 => 0x13D4, +0xABA5 => 0x13D5, +0xABA6 => 0x13D6, +0xABA7 => 0x13D7, +0xABA8 => 0x13D8, +0xABA9 => 0x13D9, +0xABAA => 0x13DA, +0xABAB => 0x13DB, +0xABAC => 0x13DC, +0xABAD => 0x13DD, +0xABAE => 0x13DE, +0xABAF => 0x13DF, +0xABB0 => 0x13E0, +0xABB1 => 0x13E1, +0xABB2 => 0x13E2, +0xABB3 => 0x13E3, +0xABB4 => 0x13E4, +0xABB5 => 0x13E5, +0xABB6 => 0x13E6, +0xABB7 => 0x13E7, +0xABB8 => 0x13E8, +0xABB9 => 0x13E9, +0xABBA => 0x13EA, +0xABBB => 0x13EB, +0xABBC => 0x13EC, +0xABBD => 0x13ED, +0xABBE => 0x13EE, +0xABBF => 0x13EF, +0xFF41 => 0xFF21, +0xFF42 => 0xFF22, +0xFF43 => 0xFF23, +0xFF44 => 0xFF24, +0xFF45 => 0xFF25, +0xFF46 => 0xFF26, +0xFF47 => 0xFF27, +0xFF48 => 0xFF28, +0xFF49 => 0xFF29, +0xFF4A => 0xFF2A, +0xFF4B => 0xFF2B, +0xFF4C => 0xFF2C, +0xFF4D => 0xFF2D, +0xFF4E => 0xFF2E, +0xFF4F => 0xFF2F, +0xFF50 => 0xFF30, +0xFF51 => 0xFF31, +0xFF52 => 0xFF32, +0xFF53 => 0xFF33, +0xFF54 => 0xFF34, +0xFF55 => 0xFF35, +0xFF56 => 0xFF36, +0xFF57 => 0xFF37, +0xFF58 => 0xFF38, +0xFF59 => 0xFF39, +0xFF5A => 0xFF3A, +0x10428 => 0x10400, +0x10429 => 0x10401, +0x1042A => 0x10402, +0x1042B => 0x10403, +0x1042C => 0x10404, +0x1042D => 0x10405, +0x1042E => 0x10406, +0x1042F => 0x10407, +0x10430 => 0x10408, +0x10431 => 0x10409, +0x10432 => 0x1040A, +0x10433 => 0x1040B, +0x10434 => 0x1040C, +0x10435 => 0x1040D, +0x10436 => 0x1040E, +0x10437 => 0x1040F, +0x10438 => 0x10410, +0x10439 => 0x10411, +0x1043A => 0x10412, +0x1043B => 0x10413, +0x1043C => 0x10414, +0x1043D => 0x10415, +0x1043E => 0x10416, +0x1043F => 0x10417, +0x10440 => 0x10418, +0x10441 => 0x10419, +0x10442 => 0x1041A, +0x10443 => 0x1041B, +0x10444 => 0x1041C, +0x10445 => 0x1041D, +0x10446 => 0x1041E, +0x10447 => 0x1041F, +0x10448 => 0x10420, +0x10449 => 0x10421, +0x1044A => 0x10422, +0x1044B => 0x10423, +0x1044C => 0x10424, +0x1044D => 0x10425, +0x1044E => 0x10426, +0x1044F => 0x10427, +0x104D8 => 0x104B0, +0x104D9 => 0x104B1, +0x104DA => 0x104B2, +0x104DB => 0x104B3, +0x104DC => 0x104B4, +0x104DD => 0x104B5, +0x104DE => 0x104B6, +0x104DF => 0x104B7, +0x104E0 => 0x104B8, +0x104E1 => 0x104B9, +0x104E2 => 0x104BA, +0x104E3 => 0x104BB, +0x104E4 => 0x104BC, +0x104E5 => 0x104BD, +0x104E6 => 0x104BE, +0x104E7 => 0x104BF, +0x104E8 => 0x104C0, +0x104E9 => 0x104C1, +0x104EA => 0x104C2, +0x104EB => 0x104C3, +0x104EC => 0x104C4, +0x104ED => 0x104C5, +0x104EE => 0x104C6, +0x104EF => 0x104C7, +0x104F0 => 0x104C8, +0x104F1 => 0x104C9, +0x104F2 => 0x104CA, +0x104F3 => 0x104CB, +0x104F4 => 0x104CC, +0x104F5 => 0x104CD, +0x104F6 => 0x104CE, +0x104F7 => 0x104CF, +0x104F8 => 0x104D0, +0x104F9 => 0x104D1, +0x104FA => 0x104D2, +0x104FB => 0x104D3, +0x10CC0 => 0x10C80, +0x10CC1 => 0x10C81, +0x10CC2 => 0x10C82, +0x10CC3 => 0x10C83, +0x10CC4 => 0x10C84, +0x10CC5 => 0x10C85, +0x10CC6 => 0x10C86, +0x10CC7 => 0x10C87, +0x10CC8 => 0x10C88, +0x10CC9 => 0x10C89, +0x10CCA => 0x10C8A, +0x10CCB => 0x10C8B, +0x10CCC => 0x10C8C, +0x10CCD => 0x10C8D, +0x10CCE => 0x10C8E, +0x10CCF => 0x10C8F, +0x10CD0 => 0x10C90, +0x10CD1 => 0x10C91, +0x10CD2 => 0x10C92, +0x10CD3 => 0x10C93, +0x10CD4 => 0x10C94, +0x10CD5 => 0x10C95, +0x10CD6 => 0x10C96, +0x10CD7 => 0x10C97, +0x10CD8 => 0x10C98, +0x10CD9 => 0x10C99, +0x10CDA => 0x10C9A, +0x10CDB => 0x10C9B, +0x10CDC => 0x10C9C, +0x10CDD => 0x10C9D, +0x10CDE => 0x10C9E, +0x10CDF => 0x10C9F, +0x10CE0 => 0x10CA0, +0x10CE1 => 0x10CA1, +0x10CE2 => 0x10CA2, +0x10CE3 => 0x10CA3, +0x10CE4 => 0x10CA4, +0x10CE5 => 0x10CA5, +0x10CE6 => 0x10CA6, +0x10CE7 => 0x10CA7, +0x10CE8 => 0x10CA8, +0x10CE9 => 0x10CA9, +0x10CEA => 0x10CAA, +0x10CEB => 0x10CAB, +0x10CEC => 0x10CAC, +0x10CED => 0x10CAD, +0x10CEE => 0x10CAE, +0x10CEF => 0x10CAF, +0x10CF0 => 0x10CB0, +0x10CF1 => 0x10CB1, +0x10CF2 => 0x10CB2, +0x118C0 => 0x118A0, +0x118C1 => 0x118A1, +0x118C2 => 0x118A2, +0x118C3 => 0x118A3, +0x118C4 => 0x118A4, +0x118C5 => 0x118A5, +0x118C6 => 0x118A6, +0x118C7 => 0x118A7, +0x118C8 => 0x118A8, +0x118C9 => 0x118A9, +0x118CA => 0x118AA, +0x118CB => 0x118AB, +0x118CC => 0x118AC, +0x118CD => 0x118AD, +0x118CE => 0x118AE, +0x118CF => 0x118AF, +0x118D0 => 0x118B0, +0x118D1 => 0x118B1, +0x118D2 => 0x118B2, +0x118D3 => 0x118B3, +0x118D4 => 0x118B4, +0x118D5 => 0x118B5, +0x118D6 => 0x118B6, +0x118D7 => 0x118B7, +0x118D8 => 0x118B8, +0x118D9 => 0x118B9, +0x118DA => 0x118BA, +0x118DB => 0x118BB, +0x118DC => 0x118BC, +0x118DD => 0x118BD, +0x118DE => 0x118BE, +0x118DF => 0x118BF, +0x16E60 => 0x16E40, +0x16E61 => 0x16E41, +0x16E62 => 0x16E42, +0x16E63 => 0x16E43, +0x16E64 => 0x16E44, +0x16E65 => 0x16E45, +0x16E66 => 0x16E46, +0x16E67 => 0x16E47, +0x16E68 => 0x16E48, +0x16E69 => 0x16E49, +0x16E6A => 0x16E4A, +0x16E6B => 0x16E4B, +0x16E6C => 0x16E4C, +0x16E6D => 0x16E4D, +0x16E6E => 0x16E4E, +0x16E6F => 0x16E4F, +0x16E70 => 0x16E50, +0x16E71 => 0x16E51, +0x16E72 => 0x16E52, +0x16E73 => 0x16E53, +0x16E74 => 0x16E54, +0x16E75 => 0x16E55, +0x16E76 => 0x16E56, +0x16E77 => 0x16E57, +0x16E78 => 0x16E58, +0x16E79 => 0x16E59, +0x16E7A => 0x16E5A, +0x16E7B => 0x16E5B, +0x16E7C => 0x16E5C, +0x16E7D => 0x16E5D, +0x16E7E => 0x16E5E, +0x16E7F => 0x16E5F, +0x1E922 => 0x1E900, +0x1E923 => 0x1E901, +0x1E924 => 0x1E902, +0x1E925 => 0x1E903, +0x1E926 => 0x1E904, +0x1E927 => 0x1E905, +0x1E928 => 0x1E906, +0x1E929 => 0x1E907, +0x1E92A => 0x1E908, +0x1E92B => 0x1E909, +0x1E92C => 0x1E90A, +0x1E92D => 0x1E90B, +0x1E92E => 0x1E90C, +0x1E92F => 0x1E90D, +0x1E930 => 0x1E90E, +0x1E931 => 0x1E90F, +0x1E932 => 0x1E910, +0x1E933 => 0x1E911, +0x1E934 => 0x1E912, +0x1E935 => 0x1E913, +0x1E936 => 0x1E914, +0x1E937 => 0x1E915, +0x1E938 => 0x1E916, +0x1E939 => 0x1E917, +0x1E93A => 0x1E918, +0x1E93B => 0x1E919, +0x1E93C => 0x1E91A, +0x1E93D => 0x1E91B, +0x1E93E => 0x1E91C, +0x1E93F => 0x1E91D, +0x1E940 => 0x1E91E, +0x1E941 => 0x1E91F, +0x1E942 => 0x1E920, +0x1E943 => 0x1E921, +]; diff --git a/src/opis/string/src/Exception/InvalidCodePointException.php b/src/opis/string/src/Exception/InvalidCodePointException.php new file mode 100644 index 00000000..f1a3fe96 --- /dev/null +++ b/src/opis/string/src/Exception/InvalidCodePointException.php @@ -0,0 +1,46 @@ +codePoint = $codePoint; + } + + /** + * @return mixed + */ + public function codePoint() + { + return$this->codePoint; + } +} diff --git a/src/opis/string/src/Exception/InvalidStringException.php b/src/opis/string/src/Exception/InvalidStringException.php new file mode 100644 index 00000000..ede9d682 --- /dev/null +++ b/src/opis/string/src/Exception/InvalidStringException.php @@ -0,0 +1,61 @@ +string = $string; + $this->offset = $offset; + } + + /** + * @return string + */ + public function string(): string + { + return $this->string; + } + + /** + * @return int + */ + public function offset(): int + { + return $this->offset; + } +} diff --git a/src/opis/string/src/Exception/UnicodeException.php b/src/opis/string/src/Exception/UnicodeException.php new file mode 100644 index 00000000..22e6d2fa --- /dev/null +++ b/src/opis/string/src/Exception/UnicodeException.php @@ -0,0 +1,25 @@ +codes = $codes; + $this->length = count($codes); + } + + /** + * @return int[] + */ + public function codePoints(): array + { + return $this->codes; + } + + /** + * @return string[] + */ + public function chars(): array + { + if ($this->chars === null) { + $this->chars = self::getCharsFromCodePoints($this->codes); + } + return $this->chars; + } + + /** + * @return int + */ + public function length(): int + { + return $this->length; + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return $this->length === 0; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @return bool + */ + public function equals($text, $ignoreCase = false): bool + { + return $this->compareTo($text, $ignoreCase) === 0; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @return int + */ + public function compareTo($text, $ignoreCase = false): int + { + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $text = self::resolveCodePoints($text, $mode); + + $length = count($text); + + if ($length !== $this->length) { + return $this->length <=> $length; + } + + return $this->getMappedCodes($mode) <=> $text; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @return bool + */ + public function contains($text, $ignoreCase = false): bool + { + return $this->indexOf($text, 0, $ignoreCase) !== -1; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @return bool + */ + public function startsWith($text, $ignoreCase = false): bool + { + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $text = self::resolveCodePoints($text, $mode); + + $len = count($text); + + if ($len === 0 || $len > $this->length) { + return false; + } + + return array_slice($this->getMappedCodes($mode), 0, $len) === $text; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @return bool + */ + public function endsWith($text, $ignoreCase = false): bool + { + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $text = self::resolveCodePoints($text, $mode); + + if (empty($text)) { + return false; + } + + $codes = $this->getMappedCodes($mode); + + $offset = $this->length - count($text); + + if ($offset < 0) { + return false; + } + + return array_slice($codes, $offset) === $text; + } + + /** + * @param string|self|int[]|string[] $text + * @param int $offset + * @param bool $ignoreCase + * @return int + */ + public function indexOf($text, $offset = 0, $ignoreCase = false): int + { + if ($offset < 0) { + $offset += $this->length; + } + if ($offset < 0 || $offset >= $this->length) { + return -1; + } + + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $text = self::resolveCodePoints($text, $mode); + + $len = count($text); + + if ($len === 0 || $offset + $len > $this->length) { + return -1; + } + + return $this->doIndexOf($this->getMappedCodes($mode), $text, $offset); + } + + /** + * @param string|self|int[]|string[] $text + * @param int $offset + * @param bool $ignoreCase + * @return int + */ + public function lastIndexOf($text, $offset = 0, $ignoreCase = false): int + { + if ($offset < 0) { + $start = $this->length + $offset; + if ($start < 0) { + return -1; + } + $last = 0; + } else { + if ($offset >= $this->length) { + return -1; + } + $start = $this->length - 1; + $last = $offset; + } + + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $text = self::resolveCodePoints($text, $mode); + + $len = count($text); + + if ($len === 0) { + return -1; + } + + if ($offset < 0) { + if ($len > $this->length) { + return -1; + } + $start = min($start, $this->length - $len); + } elseif ($offset + $len > $this->length) { + return -1; + } + + $codes = $this->getMappedCodes($mode); + + for ($i = $start; $i >= $last; $i--) { + $match = true; + + for ($j = 0; $j < $len; $j++) { + if ($codes[$i + $j] !== $text[$j]) { + $match = false; + break; + } + } + + if ($match) { + return $i; + } + } + + return -1; + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @param bool $allowPrefixOnly If true the result can contain only the prefix + * @return $this + */ + public function ensurePrefix($text, $ignoreCase = false, $allowPrefixOnly = true): self + { + $text = self::resolveCodePoints($text); + + $len = count($text); + + if ($len === 0) { + return clone $this; + } + + if ($this->length === 0) { + return new static($text); + } + + if ($ignoreCase) { + $prefix = self::getMappedCodePoints($text, self::FOLD_CASE); + } else { + $prefix = &$text; + } + + if ($this->length === $len) { + $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE); + if ($allowPrefixOnly && $part === $prefix) { + return clone $this; + } + // Remove last element to avoid double check + array_pop($part); + } elseif ($this->length < $len) { + $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE); + // Checks if this can be a suffix + if ($allowPrefixOnly && (array_slice($prefix, 0, $this->length) === $part)) { + $text = array_slice($text, $this->length); + return new static(array_merge($this->codes, $text)); + } + } else { + $part = array_slice($this->codes, 0, $len); + if ($ignoreCase) { + $part = self::getMappedCodePoints($part, self::FOLD_CASE); + } + if ($part === $prefix) { + return clone $this; + } + // Remove last element to avoid double check + array_pop($part); + } + + $copy = $len; + + $part_len = count($part); + + while ($part_len) { + if ($part === array_slice($prefix, -$part_len)) { + $copy = $len - $part_len; + break; + } + array_pop($part); + $part_len--; + } + + if ($copy === 0) { + return clone $this; + } + + if ($copy < $len) { + $text = array_slice($text, 0, $copy); + } + + return new static(array_merge($text, $this->codes)); + } + + /** + * @param string|self|int[]|string[] $text + * @param bool $ignoreCase + * @param bool $allowSuffixOnly If true the result can contain only the suffix + * @return static + */ + public function ensureSuffix($text, $ignoreCase = false, $allowSuffixOnly = true): self + { + $text = self::resolveCodePoints($text); + + $len = count($text); + + if ($len === 0) { + return clone $this; + } + + if ($this->length === 0) { + return new static($text); + } + + if ($ignoreCase) { + $suffix = self::getMappedCodePoints($text, self::FOLD_CASE); + } else { + $suffix = &$text; + } + + if ($this->length === $len) { + $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE); + if ($allowSuffixOnly && $part === $suffix) { + return clone $this; + } + // Remove first element to avoid double check + array_shift($part); + } elseif ($this->length < $len) { + $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE); + // Checks if this can be a prefix + if ($allowSuffixOnly && (array_slice($suffix, -$this->length) === $part)) { + $text = array_slice($text, 0, $len - $this->length); + return new static(array_merge($text, $this->codes)); + } + } else { + $part = array_slice($this->codes, -$len); + if ($ignoreCase) { + $part = self::getMappedCodePoints($part, self::FOLD_CASE); + } + if ($part === $suffix) { + return clone $this; + } + // Remove first element to avoid double check + array_shift($part); + } + + $skip = 0; + + $part_len = count($part); + + while ($part_len) { + if ($part === array_slice($suffix, 0, $part_len)) { + $skip = $part_len; + break; + } + array_shift($part); + $part_len--; + } + + if ($skip === $len) { + return clone $this; + } + + if ($skip) { + array_splice($text, 0, $skip); + } + + return new static(array_merge($this->codes, $text)); + } + + /** + * @param string|self|int[]|string[] $text + * @param int $mode + * @return static + */ + public function append($text, $mode = self::KEEP_CASE): self + { + return new static(array_merge($this->codes, self::resolveCodePoints($text, $mode))); + } + + /** + * @param string|self|int[]|string[] $text + * @param int $mode + * @return static + */ + public function prepend($text, $mode = self::KEEP_CASE): self + { + return new static(array_merge(self::resolveCodePoints($text, $mode), $this->codes)); + } + + /** + * @param string|self|int[]|string[] $text + * @param int $offset + * @param int $mode + * @return static + */ + public function insert($text, $offset, $mode = self::KEEP_CASE): self + { + $codes = $this->codes; + + array_splice($codes, $offset, 0, self::resolveCodePoints($text, $mode)); + + return new static($codes); + } + + /** + * @param int $offset + * @param int|null $length + * @return static + */ + public function remove($offset, $length = null): self + { + $codes = $this->codes; + + if ($length === null) { + array_splice($codes, $offset); + } else { + array_splice($codes, $offset, $length); + } + + return new static($codes); + } + + /** + * @param string|self|int[]|string[] $mask + * @return static + */ + public function trim($mask = " \t\n\r\0\x0B"): self + { + return $this->doTrim($mask, true, true); + } + + /** + * @param string|self|int[]|string[] $mask + * @return static + */ + public function trimLeft($mask = " \t\n\r\0\x0B"): self + { + return $this->doTrim($mask, true, false); + } + + /** + * @param string|self|int[]|string[] $mask + * @return static + */ + public function trimRight($mask = " \t\n\r\0\x0B"): self + { + return $this->doTrim($mask, false, true); + } + + /** + * @return static + */ + public function reverse(): self + { + return new static(array_reverse($this->codes)); + } + + /** + * @param int $times + * @return static + */ + public function repeat($times = 1): self + { + if ($times <= 1) { + return clone $this; + } + + $codes = []; + + while ($times--) { + $codes = array_merge($codes, $this->codes); + } + + return new static($codes); + } + + /** + * @param string|self|int[]|string[] $subject + * @param string|self|int[]|string[] $replace + * @param int $offset + * @param bool $ignoreCase + * @return static + */ + public function replace($subject, $replace, $offset = 0, $ignoreCase = false): self + { + if ($offset < 0) { + $offset += $this->length; + } + if ($offset < 0 || $offset >= $this->length) { + return clone $this; + } + + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $subject = self::resolveCodePoints($subject, $mode); + + $len = count($subject); + + if ($len === 0 || $offset + $len > $this->length) { + return clone $this; + } + + $offset = $this->doIndexOf($this->getMappedCodes($mode), $subject, $offset); + + if ($offset === -1) { + return clone $this; + } + + $codes = $this->codes; + + array_splice($codes, $offset, count($subject), self::resolveCodePoints($replace)); + + return new static($codes); + } + + /** + * @param string|self|int[]|string[] $subject + * @param string|self|int[]|string[] $replace + * @param bool $ignoreCase + * @param int $offset + * @return static + */ + public function replaceAll($subject, $replace, $offset = 0, $ignoreCase = false): self + { + if ($offset < 0) { + $offset += $this->length; + } + if ($offset < 0 || $offset >= $this->length) { + return clone $this; + } + + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + + $subject = self::resolveCodePoints($subject, $mode); + + $len = count($subject); + + if ($len === 0 || $offset + $len > $this->length) { + return clone $this; + } + + $replace = self::resolveCodePoints($replace); + + $codes = $this->getMappedCodes($mode); + + $copy = $this->codes; + + $fix = count($replace) - $len; + + $t = 0; + + while (($pos = $this->doIndexOf($codes, $subject, $offset)) >= 0) { + array_splice($copy, $pos + $t * $fix, $len, $replace); + $offset = $pos + $len; + $t++; + } + + return new static($copy); + } + + /** + * @param string|self|int[]|string[] $delimiter + * @param bool $ignoreCase + * @return array + */ + public function split($delimiter = '', $ignoreCase = false): array + { + $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE; + $delimiter = self::resolveCodePoints($delimiter, $mode); + $len = count($delimiter); + + $ret = []; + + if ($len === 0) { + foreach ($this->codes as $code) { + $ret[] = new static([$code]); + } + } else { + $codes = $this->getMappedCodes($mode); + + $offset = 0; + + while (($pos = $this->doIndexOf($codes, $delimiter, $offset)) >= 0) { + $ret[] = new static(array_slice($this->codes, $offset, $pos - $offset)); + $offset = $pos + $len; + } + + $ret[] = new static(array_slice($this->codes, $offset)); + } + + return $ret; + } + + /** + * @param int $start + * @param int|null $length + * @return static + */ + public function substring($start, $length = null): self + { + return new static(array_slice($this->codes, $start, $length)); + } + + /** + * @param int $size If negative then pad left otherwise pad right + * @param self|string|int $char A char or a code point + * @return static + */ + public function pad($size, $char = 0x20): self + { + return new static(array_pad($this->codes, $size, self::resolveFirstCodePoint($char, 0x20))); + } + + /** + * @param int $size + * @param self|string|int $char + * @return static + */ + public function padLeft($size, $char = 0x20): self + { + if ($size > 0) { + $size = -$size; + } + + return $this->pad($size, $char); + } + + /** + * @param int $size + * @param self|string|int $char + * @return static + */ + public function padRight($size, $char = 0x20): self + { + if ($size < 0) { + $size = -$size; + } + + return $this->pad($size, $char); + } + + /** + * @return bool + */ + public function isLowerCase(): bool + { + return $this->isCase(self::LOWER_CASE); + } + + /** + * @return bool + */ + public function isUpperCase(): bool + { + return $this->isCase(self::UPPER_CASE); + } + + /** + * @return bool + */ + public function isAscii(): bool + { + $key = 'i' . self::ASCII_CONV; + + if (!isset($this->cache[$key])) { + $ok = true; + + foreach ($this->codes as $code) { + if ($code >= 0x80) { + $ok = false; + break; + } + } + + $this->cache[$key] = $ok; + } + + return $this->cache[$key]; + } + + /** + * Convert all chars to lower case (where possible) + * @return static + */ + public function toLower(): self + { + if ($this->cache['i' . self::LOWER_CASE] ?? false) { + return clone $this; + } + return new static($this->getMappedCodes(self::LOWER_CASE)); + } + + /** + * Convert all chars to upper case (where possible) + * @return static + */ + public function toUpper(): self + { + if ($this->cache['i' . self::UPPER_CASE] ?? false) { + return clone $this; + } + return new static($this->getMappedCodes(self::UPPER_CASE)); + } + + /** + * Converts all chars to their ASCII equivalent (if any) + * @return static + */ + public function toAscii(): self + { + if ($this->cache['i' . self::ASCII_CONV] ?? false) { + return clone $this; + } + return new static($this->getMappedCodes(self::ASCII_CONV)); + } + + /** + * @param int $index + * @return string + */ + public function charAt($index): string + { + // Allow negative index + if ($index < 0 && $index + $this->length >= 0) { + $index += $this->length; + } + + if ($index < 0 || $index >= $this->length) { + return ''; + } + + return $this->chars()[$index]; + } + + /** + * @param int $index + * @return int + */ + public function codePointAt($index): int + { + // Allow negative index + if ($index < 0 && $index + $this->length >= 0) { + $index += $this->length; + } + + if ($index < 0 || $index >= $this->length) { + return -1; + } + + return $this->codes[$index]; + } + + /** + * @param int $offset + * @return int + */ + public function __invoke(int $offset): int + { + if ($offset < 0) { + if ($offset + $this->length < 0) { + throw new OutOfBoundsException("Undefined offset: {$offset}"); + } + $offset += $this->length; + } elseif ($offset >= $this->length) { + throw new OutOfBoundsException("Undefined offset: {$offset}"); + } + + return $this->codes[$offset]; + } + + /** + * @inheritDoc + */ + public function offsetExists($offset): bool + { + // Allow negative index + if ($offset < 0) { + $offset += $this->length; + } + + return isset($this->codes[$offset]); + } + + /** + * @inheritDoc + */ + public function offsetGet($offset): string + { + if ($offset < 0) { + if ($offset + $this->length < 0) { + throw new OutOfBoundsException("Undefined offset: {$offset}"); + } + $offset += $this->length; + } elseif ($offset >= $this->length) { + throw new OutOfBoundsException("Undefined offset: {$offset}"); + } + + return $this->chars()[$offset]; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + // Allow negative index + if ($offset < 0) { + $offset += $this->length; + } + + if (!isset($this->codes[$offset])) { + return; + } + + + $value = self::resolveFirstCodePoint($value); + if ($value === -1) { + return; + } + + if ($value === $this->codes[$offset]) { + // Same value, nothing to do + return; + } + + $this->codes[$offset] = $value; + + // Clear cache + $this->str = null; + $this->cache = null; + if ($this->chars) { + $this->chars[$offset] = self::getCharFromCodePoint($value); + } + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new RuntimeException("Invalid operation"); + } + + /** + * @inheritDoc + */ + public function count(): int + { + return $this->length; + } + + /** + * @return string + */ + public function __toString(): string + { + if ($this->str === null) { + $this->str = self::getStringFromCodePoints($this->codes); + } + + return $this->str; + } + + /** + * @inheritDoc + */ + public function jsonSerialize(): string + { + return $this->__toString(); + } + + public function __serialize(): array + { + return [ + 'value' => $this->__toString(), + ]; + } + + public function __unserialize(array $data) + { + $this->str = $data['value']; + $this->codes = self::getCodePointsFromString($this->str); + $this->length = count($this->codes); + } + + /** + * Creates an unicode string instance from raw string + * @param string $string + * @param string|null $encoding Defaults to UTF-8 + * @param int $mode + * @return static + * @throws InvalidStringException + */ + public static function from($string, $encoding = null, $mode = self::KEEP_CASE): self + { + if ($encoding !== null && strcasecmp($encoding, 'UTF-8') !== 0) { + if (false === $string = @iconv($encoding, 'UTF-8', $string)) { + throw new UnicodeException("Could not convert string from '$encoding' encoding to UTF-8 encoding"); + } + } + + $instance = new static(self::getCodePointsFromString($string, $mode)); + if ($mode === self::KEEP_CASE) { + $instance->str = $string; + } + return $instance; + } + + /** + * Creates an unicode string instance from code points + * @param int[] $codes + * @param int $mode + * @return static + * @throws InvalidCodePointException + */ + public static function fromCodePoints($codes, $mode = self::KEEP_CASE): self + { + $map = self::getMapByMode($mode); + + foreach ($codes as &$code) { + if (!is_int($codes) || !self::isValidCodePoint($code)) { + throw new InvalidCodePointException($code); + } else { + $code = $map[$code] ?? $code; + } + } + + return new static(array_values($codes)); + } + + /** + * Converts the code point to corresponding char + * @param int $code + * @return string The char or an empty string if code point is invalid + */ + public static function getCharFromCodePoint($code): string + { + if ($code < 0) { + return ''; + } + + if ($code < 0x80) { + return chr($code); + } + + if ($code < 0x800) { + return chr(($code >> 6) + 0xC0) . chr(($code & 0x3F) + 0x80); + } + + if ($code >= 0xD800 && $code <= 0xDFFF) { + /* + The definition of UTF-8 prohibits encoding character numbers between + U+D800 and U+DFFF, which are reserved for use with the UTF-16 + encoding form (as surrogate pairs) and do not directly represent characters. + */ + return ''; + } + + if ($code <= 0xFFFF) { + return + chr(($code >> 12) + 0xE0) . + chr((($code >> 6) & 0x3F) + 0x80) . + chr(($code & 0x3F) + 0x80); + } + + if ($code <= 0x10FFFF) { + return + chr(($code >> 18) + 0xF0) . + chr((($code >> 12) & 0x3F) + 0x80) . + chr((($code >> 6) & 0x3F) + 0x80) . + chr(($code & 0x3F) + 0x80); + } + + /* + Restricted the range of characters to 0000-10FFFF (the UTF-16 accessible range). + */ + + return ''; + } + + /** + * Convert a string to a code point array + * @param string $str + * @param int $mode + * @return array + * @throws InvalidStringException + */ + public static function getCodePointsFromString($str, $mode = self::KEEP_CASE): array + { + // 0x00-0x7F + // 0xC2-0xDF 0x80-0xBF + // 0xE0-0xE0 0xA0-0xBF 0x80-0xBF + // 0xE1-0xEC 0x80-0xBF 0x80-0xBF + // 0xED-0xED 0x80-0x9F 0x80-0xBF + // 0xEE-0xEF 0x80-0xBF 0x80-0xBF + // 0xF0-0xF0 0x90-0xBF 0x80-0xBF 0x80-0xBF + // 0xF1-0xF3 0x80-0xBF 0x80-0xBF 0x80-0xBF + // 0xF4-0xF4 0x80-0x8F 0x80-0xBF 0x80-0xBF + + $codes = []; + $length = strlen($str); + $mode = self::getMapByMode($mode); + + $i = 0; + while ($i < $length) { + $ord0 = ord($str[$i++]); + + if ($ord0 < 0x80) { + $codes[] = $mode[$ord0] ?? $ord0; + continue; + } + + if ($i === $length || $ord0 < 0xC2 || $ord0 > 0xF4) { + throw new InvalidStringException($str, $i - 1); + } + + $ord1 = ord($str[$i++]); + + if ($ord0 < 0xE0) { + if ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + $ord1 = ($ord0 - 0xC0) * 64 + $ord1 - 0x80; + $codes[] = $mode[$ord1] ?? $ord1; + + continue; + } + + if ($i === $length) { + throw new InvalidStringException($str, $i - 1); + } + + $ord2 = ord($str[$i++]); + + if ($ord0 < 0xF0) { + if ($ord0 === 0xE0) { + if ($ord1 < 0xA0 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + } elseif ($ord0 === 0xED) { + if ($ord1 < 0x80 || $ord1 >= 0xA0) { + throw new InvalidStringException($str, $i - 2); + } + } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + + if ($ord2 < 0x80 || $ord2 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + $ord2 = ($ord0 - 0xE0) * 0x1000 + ($ord1 - 0x80) * 64 + $ord2 - 0x80; + $codes[] = $mode[$ord2] ?? $ord2; + + continue; + } + + if ($i === $length) { + throw new InvalidStringException($str, $i - 1); + } + + $ord3 = ord($str[$i++]); + + if ($ord0 < 0xF5) { + if ($ord0 === 0xF0) { + if ($ord1 < 0x90 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 3); + } + } elseif ($ord0 === 0xF4) { + if ($ord1 < 0x80 || $ord1 >= 0x90) { + throw new InvalidStringException($str, $i - 3); + } + } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 3); + } + + if ($ord2 < 0x80 || $ord2 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + + if ($ord3 < 0x80 || $ord3 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + $ord3 = ($ord0 - 0xF0) * 0x40000 + ($ord1 - 0x80) * 0x1000 + ($ord2 - 0x80) * 64 + $ord3 - 0x80; + $codes[] = $mode[$ord3] ?? $ord3; + + continue; + } + + throw new InvalidStringException($str, $i - 1); + } + + return $codes; + } + + /** + * @param string $str + * @return iterable + * + * The key represents the current char index + * Value is a two element array + * - first element is an integer representing the code point + * - second element is an array of integers (length 1 to 4) representing bytes + */ + public static function walkString($str) + { + $i = 0; + $length = strlen($str); + + while ($i < $length) { + $index = $i; + + $ord0 = ord($str[$i++]); + + if ($ord0 < 0x80) { + yield $index => [ + $ord0, + [$ord0] + ]; + continue; + } + + if ($i === $length || $ord0 < 0xC2 || $ord0 > 0xF4) { + throw new InvalidStringException($str, $i - 1); + } + + $ord1 = ord($str[$i++]); + + if ($ord0 < 0xE0) { + if ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + yield $index => [ + ($ord0 - 0xC0) * 64 + $ord1 - 0x80, + [$ord0, $ord1] + ]; + + continue; + } + + if ($i === $length) { + throw new InvalidStringException($str, $i - 1); + } + + $ord2 = ord($str[$i++]); + + if ($ord0 < 0xF0) { + if ($ord0 === 0xE0) { + if ($ord1 < 0xA0 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + } elseif ($ord0 === 0xED) { + if ($ord1 < 0x80 || $ord1 >= 0xA0) { + throw new InvalidStringException($str, $i - 2); + } + } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + + if ($ord2 < 0x80 || $ord2 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + yield $index => [ + ($ord0 - 0xE0) * 0x1000 + ($ord1 - 0x80) * 64 + $ord2 - 0x80, + [$ord0, $ord1, $ord2] + ]; + + continue; + } + + if ($i === $length) { + throw new InvalidStringException($str, $i - 1); + } + + $ord3 = ord($str[$i++]); + + if ($ord0 < 0xF5) { + if ($ord0 === 0xF0) { + if ($ord1 < 0x90 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 3); + } + } elseif ($ord0 === 0xF4) { + if ($ord1 < 0x80 || $ord1 >= 0x90) { + throw new InvalidStringException($str, $i - 3); + } + } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) { + throw new InvalidStringException($str, $i - 3); + } + + if ($ord2 < 0x80 || $ord2 >= 0xC0) { + throw new InvalidStringException($str, $i - 2); + } + + if ($ord3 < 0x80 || $ord3 >= 0xC0) { + throw new InvalidStringException($str, $i - 1); + } + + yield $index => [ + ($ord0 - 0xF0) * 0x40000 + ($ord1 - 0x80) * 0x1000 + ($ord2 - 0x80) * 64 + $ord3 - 0x80, + [$ord0, $ord1, $ord2, $ord3] + ]; + + continue; + } + + throw new InvalidStringException($str, $i - 1); + } + } + + /** + * Converts each code point to a char + * @param array $codes + * @param int $mode + * @return array + * @throws InvalidCodePointException + */ + public static function getCharsFromCodePoints($codes, $mode = self::KEEP_CASE): array + { + $mode = self::getMapByMode($mode); + + foreach ($codes as &$code) { + $char = self::getCharFromCodePoint($mode[$code] ?? $code); + if ($char === '') { + throw new InvalidCodePointException($code); + } else { + $code = $char; + } + } + + return $codes; + } + + /** + * @param string $str + * @param int $mode + * @return string[] + */ + public static function getCharsFromString($str, $mode = self::KEEP_CASE): array + { + return self::getCharsFromCodePoints(self::getCodePointsFromString($str), $mode); + } + + /** + * Converts all code points to chars and returns the string + * Invalid code points are ignored + * @param array $codes + * @param int $mode + * @return string + */ + public static function getStringFromCodePoints($codes, $mode = self::KEEP_CASE): string + { + $str = ''; + + $mode = self::getMapByMode($mode); + + foreach ($codes as $code) { + if (isset($mode[$code])) { + $code = $mode[$code]; + } + + if ($code < 0x80) { + $str .= chr($code); + continue; + } + + if ($code < 0x800) { + $str .= chr(($code >> 6) + 0xC0) . chr(($code & 0x3F) + 0x80); + continue; + } + + if ($code >= 0xD800 && $code <= 0xDFFF) { + continue; + } + + if ($code <= 0xFFFF) { + $str .= + chr(($code >> 12) + 0xE0) . + chr((($code >> 6) & 0x3F) + 0x80) . + chr(($code & 0x3F) + 0x80); + continue; + } + + if ($code <= 0x10FFFF) { + $str .= + chr(($code >> 18) + 0xF0) . + chr((($code >> 12) & 0x3F) + 0x80) . + chr((($code >> 6) & 0x3F) + 0x80) . + chr(($code & 0x3F) + 0x80); + } + } + + return $str; + } + + /** + * @param array $codes + * @param int $mode + * @return array + */ + public static function getMappedCodePoints($codes, $mode): array + { + if ($mode === self::KEEP_CASE) { + return $codes; + } + + $mode = self::getMapByMode($mode); + + if (empty($mode)) { + return $codes; + } + + foreach ($codes as &$code) { + $code = $mode[$code] ?? $code; + } + + return $codes; + } + + /** + * Checks if a code point is valid + * @param int $code + * @return bool + */ + public static function isValidCodePoint($code): bool + { + if ($code < 0 || $code > 0x10FFFF) { + return false; + } + + return $code < 0xD800 || $code > 0xDFFF; + } + + /** + * @param int $mode + * @return int[] + */ + private function getMappedCodes(int $mode): array + { + if ($mode === self::KEEP_CASE || ($this->cache['i' . $mode] ?? false)) { + return $this->codes; + } + + $key = 'm' . $mode; + + if (!isset($this->cache[$key])) { + $this->cache[$key] = self::getMappedCodePoints($this->codes, $mode); + } + + return $this->cache[$key]; + } + + /** + * @param int $mode + * @return bool + */ + private function isCase(int $mode): bool + { + $key = 'i' . $mode; + + if (!isset($this->cache[$key])) { + $list = self::getMapByMode($mode); + foreach ($this->codes as $code) { + if (isset($list[$code])) { + return $this->cache[$key] = false; + } + } + + return $this->cache[$key] = true; + } + + return $this->cache[$key]; + } + + /** + * @param int[] $codes + * @param int[] $text + * @param int $offset + * @return int + */ + private function doIndexOf(array $codes, array $text, int $offset = 0): int + { + $len = count($text); + + for ($i = $offset, $last = count($codes) - $len; $i <= $last; $i++) { + $match = true; + + for ($j = 0; $j < $len; $j++) { + if ($codes[$i + $j] !== $text[$j]) { + $match = false; + break; + } + } + + if ($match) { + return $i; + } + } + + return -1; + } + + /** + * @param string|self|int[]|string[] $mask + * @param bool $left + * @param bool $right + * @return static + */ + private function doTrim($mask, bool $left, bool $right): self + { + if ($this->length === 0) { + return clone $this; + } + + $mask = self::resolveCodePoints($mask); + + if (empty($mask)) { + return clone $this; + } + + $codes = $this->codes; + + if ($left) { + while (in_array($codes[0], $mask, true)) { + array_shift($codes); + if (empty($codes)) { + return new static(); + } + } + } + + if ($right) { + $last = count($codes) - 1; + while (in_array($codes[$last], $mask, true)) { + array_pop($codes); + if (--$last < 0) { + return new static(); + } + } + } + + return new static($codes); + } + + + /** + * @param string|self|int[]|string[] $text + * @param int $mode + * @return array + */ + private static function resolveCodePoints($text, int $mode = self::KEEP_CASE): array + { + if ($text instanceof self) { + return $text->getMappedCodes($mode); + } + + if (is_string($text)) { + return self::getCodePointsFromString($text, $mode); + } + + if ($text && is_array($text) && is_int($text[0])) { + // assume code point array + return self::getMappedCodePoints($text, $mode); + } + + return []; + } + + /** + * @param self|string|int|string[]|int[] $text + * @param int $invalid + * @return int + */ + private static function resolveFirstCodePoint($text, int $invalid = -1): int + { + if ($text instanceof self) { + return $text->length === 0 ? $invalid : $text->codes[0]; + } + + if (is_array($text)) { + if (empty($text)) { + return $invalid; + } + $text = reset($text); + } + + if (is_string($text)) { + if (isset($text[4])) { + $text = substr($text, 0, 4); + } + return self::getCodePointsFromString($text)[0] ?? $invalid; + } + + if (is_int($text)) { + return self::isValidCodePoint($text) ? $text : $invalid; + } + + return $invalid; + } + + /** + * @param int $mode + * @return int[] + */ + private static function getMapByMode(int $mode): array + { + if (isset(self::$maps[$mode])) { + return self::$maps[$mode]; + } + + switch ($mode) { + case self::LOWER_CASE: + $file = 'lower'; + break; + case self::UPPER_CASE: + $file = 'upper'; + break; + case self::ASCII_CONV: + $file = 'ascii'; + break; + case self::FOLD_CASE: + $file = 'fold'; + break; + default: + return []; + } + + /** @noinspection PhpIncludeInspection */ + return self::$maps[$mode] = include(__DIR__ . "/../res/{$file}.php"); + } +} diff --git a/src/opis/uri/LICENSE b/src/opis/uri/LICENSE new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/src/opis/uri/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/src/opis/uri/README.md b/src/opis/uri/README.md new file mode 100644 index 00000000..9d82dca9 --- /dev/null +++ b/src/opis/uri/README.md @@ -0,0 +1,40 @@ +Opis URI +========== +[![Tests](https://github.com/opis/uri/workflows/Tests/badge.svg)](https://github.com/opis/uri/actions) +[![Latest Stable Version](https://poser.pugx.org/opis/uri/version.png)](https://packagist.org/packages/opis/uri) +[![Latest Unstable Version](https://poser.pugx.org/opis/uri/v/unstable.png)](//packagist.org/packages/opis/uri) +[![License](https://poser.pugx.org/opis/uri/license.png)](https://packagist.org/packages/opis/uri) + + +**Opis URI** library allows you to build, parse and validate URIs and URI-templates. + +## License + +**Opis URI** is licensed under the [Apache License, Version 2.0][license]. + +## Requirements + +* PHP ^7.4 || ^8.0 + +## Installation + +**Opis URI** is available on [Packagist] and it can be installed from a +command line interface by using [Composer]. + +```bash +composer require opis/uri +``` + +Or you could directly reference it into your `composer.json` file as a dependency + +```json +{ + "require": { + "opis/uri": "^1.0" + } +} +``` + +[license]: https://www.apache.org/licenses/LICENSE-2.0 "Apache License" +[Packagist]: https://packagist.org/packages/opis/database "Packagist" +[Composer]: https://getcomposer.org "Composer" \ No newline at end of file diff --git a/src/opis/uri/autoload.php b/src/opis/uri/autoload.php new file mode 100644 index 00000000..8aa754c5 --- /dev/null +++ b/src/opis/uri/autoload.php @@ -0,0 +1,42 @@ += $n && $input[$i] < $m) { + $m = $input[$i]; + } + } + + if (($m - $n) > intdiv(self::MAX_INT - $delta, $handled + 1)) { + throw new PunycodeException("Punycode overflow"); + } + + $delta += ($m - $n) * ($handled + 1); + + $n = $m; + + for ($i = 0; $i < $input_len; $i++) { + if ($input[$i] < $n && (++$delta === 0)) { + throw new PunycodeException("Punycode overflow"); + } + + if ($input[$i] === $n) { + $q = $delta; + for ($k = self::BASE; ; $k += self::BASE) { + $t = self::threshold($k, $bias); + if ($q < $t) { + break; + } + + $base_minus_t = self::BASE - $t; + + $q -= $t; + + $output[] = self::encodeDigit($t + ($q % $base_minus_t)); + + $q = intdiv($q, $base_minus_t); + } + + $output[] = self::encodeDigit($q); + + $bias = self::adapt($delta, $handled + 1, $handled === $basic_length); + $delta = 0; + $handled++; + } + } + + $delta++; $n++; + } + + return self::PREFIX . UnicodeString::getStringFromCodePoints($output); + } + + public static function decodePart(string $input): string + { + if (stripos($input, self::PREFIX) !== 0) { + return $input; + } + + $input = UnicodeString::getCodePointsFromString(substr($input, self::PREFIX_LEN), UnicodeString::LOWER_CASE); + $input_len = count($input); + + $pos = array_keys($input, self::DELIMITER, true); + if ($pos) { + $pos = end($pos); + } else { + $pos = -1; + } + + /** @var int $pos */ + + if ($pos === -1) { + $output = []; + $pos = $output_len = 0; + } else { + $output = array_slice($input, 0, ++$pos); + $output_len = $pos; + for ($i = 0; $i < $pos; $i++) { + if ($output[$i] >= 0x80) { + throw new PunycodeException("Non-basic code point is not allowed: {$output[$i]}"); + } + } + } + + $i = 0; + $n = self::INITIAL_N; + $bias = self::INITIAL_BIAS; + + while ($pos < $input_len) { + $old_i = $i; + + for ($w = 1, $k = self::BASE; ; $k += self::BASE) { + if ($pos >= $input_len) { + throw new PunycodeException("Punycode bad input"); + } + + $digit = self::decodeDigit($input[$pos++]); + + if ($digit >= self::BASE || $digit > intdiv(self::MAX_INT - $i, $w)) { + throw new PunycodeException("Punycode overflow"); + } + + $i += $digit * $w; + + $t = self::threshold($k, $bias); + if ($digit < $t) { + break; + } + + $t = self::BASE - $t; + + if ($w > intdiv(self::MAX_INT, $t)) { + throw new PunycodeException("Punycode overflow"); + } + + $w *= $t; + } + + $output_len++; + + if (intdiv($i, $output_len) > self::MAX_INT - $n) { + throw new PunycodeException("Punycode overflow"); + } + + $n += intdiv($i, $output_len); + + $bias = self::adapt($i - $old_i, $output_len, $old_i === 0); + + $i %= $output_len; + + array_splice($output, $i, 0, $n); + + $i++; + } + + return UnicodeString::getStringFromCodePoints($output); + } + + public static function normalizePart(string $input): string + { + $input = strtolower($input); + + if (strpos($input, self::DELIMITER) === 0) { + self::decodePart($input); // just validate + return $input; + } + + return self::encodePart($input); + } + + private static function encodeDigit(int $digit): int + { + return $digit + 0x16 + ($digit < 0x1A ? 0x4B: 0x00); + } + + private static function decodeDigit(int $code): int + { + if ($code < 0x3A) { + return $code - 0x16; + } + if ($code < 0x5B) { + return $code - 0x41; + } + if ($code < 0x7B) { + return $code - 0x61; + } + + return self::BASE; + } + + private static function threshold(int $k, int $bias): int + { + $d = $k - $bias; + + if ($d <= self::TMIN) { + return self::TMIN; + } + + if ($d >= self::TMAX) { + return self::TMAX; + } + + return $d; + } + + private static function adapt(int $delta, int $num_points, bool $first_time = false): int + { + $delta = intdiv($delta, $first_time ? self::DAMP : 2); + $delta += intdiv($delta, $num_points); + + $k = 0; + $base_tmin_diff = self::BASE - self::TMIN; + $lim = $base_tmin_diff * self::TMAX / 2; + + while ($delta > $lim) { + $delta = intdiv($delta, $base_tmin_diff); + $k += self::BASE; + } + + $k += intdiv(($base_tmin_diff + 1) * $delta, $delta + self::SKEW); + + return $k; + } +} diff --git a/src/opis/uri/src/PunycodeException.php b/src/opis/uri/src/PunycodeException.php new file mode 100644 index 00000000..6eb1c129 --- /dev/null +++ b/src/opis/uri/src/PunycodeException.php @@ -0,0 +1,25 @@ +[^:]+)(?::(?.*))?$`'; + + const HOST_LABEL_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-]+)*$`i'; + + const AUTHORITY_REGEX = '`^(?:(?[^@]+)\@)?(?(\[[a-f0-9:]+\]|[^:]+))(?::(?\d+))?$`i'; + + const PATH_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'()*+,;=:@/]+)*$`i'; + + const QUERY_OR_FRAGMENT_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'"()\[\]*+,;=:@?/%]+)*$`i'; + + /** + * @var mixed[] + */ + protected $components; + + /** + * @var string|null + */ + protected $str; + + /** + * @param array $components An array of normalized components + */ + public function __construct(array $components) + { + $this->components = $components + [ + 'scheme' => null, + 'user' => null, + 'pass' => null, + 'host' => null, + 'port' => null, + 'path' => null, + 'query' => null, + 'fragment' => null, + ]; + } + + /** + * @return string|null + */ + public function scheme() + { + return $this->components['scheme']; + } + + /** + * @return string|null + */ + public function user() + { + return $this->components['user']; + } + + /** + * @return string|null + */ + public function pass() + { + return $this->components['pass']; + } + + /** + * @return string|null + */ + public function userInfo() + { + if ($this->components['user'] === null) { + return null; + } + + if ($this->components['pass'] === null) { + return $this->components['user']; + } + + return $this->components['user'] . ':' . $this->components['pass']; + } + + /** + * @return string|null + */ + public function host() + { + return $this->components['host']; + } + + /** + * @return int|null + */ + public function port() + { + return $this->components['port']; + } + + /** + * @return string|null + */ + public function authority() + { + if ($this->components['host'] === null) { + return null; + } + + $authority = $this->userInfo(); + if ($authority !== null) { + $authority .= '@'; + } + + $authority .= $this->components['host']; + + if ($this->components['port'] !== null) { + $authority .= ':' . $this->components['port']; + } + + return $authority; + } + + /** + * @return string|null + */ + public function path() + { + return $this->components['path']; + } + + /** + * @return string|null + */ + public function query() + { + return $this->components['query']; + } + + /** + * @return string|null + */ + public function fragment() + { + return $this->components['fragment']; + } + + /** + * @return array|null[] + */ + public function components(): array + { + return $this->components; + } + + /** + * @return bool + */ + public function isAbsolute(): bool + { + return $this->components['scheme'] !== null; + } + + /** + * Use this URI as base to resolve the reference + * @param static|string|array $ref + * @param bool $normalize + * @return $this|null + */ + public function resolveRef($ref, $normalize = false) + { + $ref = self::resolveComponents($ref); + if ($ref === null) { + return $this; + } + + return new static(self::mergeComponents($ref, $this->components, $normalize)); + } + + /** + * Resolve this URI reference using a base URI + * @param static|string|array $base + * @param bool $normalize + * @return static + */ + public function resolve($base, $normalize = false): self + { + if ($this->isAbsolute()) { + return $this; + } + + $base = self::resolveComponents($base); + + if ($base === null) { + return $this; + } + + return new static(self::mergeComponents($this->components, $base, $normalize)); + } + + /** + * @return string + */ + public function __toString(): string + { + if ($this->str !== null) { + return $this->str; + } + + $str = ''; + + if ($this->components['scheme'] !== null) { + $str .= $this->components['scheme'] . ':'; + } + + if ($this->components['host'] !== null) { + $str .= '//' . $this->authority(); + } + + $str .= $this->components['path']; + + if ($this->components['query'] !== null) { + $str .= '?' . $this->components['query']; + } + + if ($this->components['fragment'] !== null) { + $str .= '#' . $this->components['fragment']; + } + + return $this->str = $str; + } + + /** + * @param string $uri + * @param bool $normalize + * @return static|null + */ + public static function create($uri, $normalize = false) + { + $comp = self::parseComponents($uri); + if (!$comp) { + return null; + } + + if ($normalize) { + $comp = self::normalizeComponents($comp); + } + + return new static($comp); + } + + /** + * Checks if the scheme contains valid chars + * @param string $scheme + * @return bool + */ + public static function isValidScheme($scheme): bool + { + return (bool)preg_match(self::SCHEME_REGEX, $scheme); + } + + /** + * Checks if user contains valid chars + * @param string $user + * @return bool + */ + public static function isValidUser($user): bool + { + return (bool)preg_match(self::USER_OR_PASS_REGEX, $user); + } + + /** + * Checks if pass contains valid chars + * @param string $pass + * @return bool + */ + public static function isValidPass($pass): bool + { + return (bool)preg_match(self::USER_OR_PASS_REGEX, $pass); + } + + /** + * @param string $userInfo + * @return bool + */ + public static function isValidUserInfo($userInfo): bool + { + /** @var array|string $userInfo */ + + if (!preg_match(self::USERINFO_REGEX, $userInfo, $userInfo)) { + return false; + } + + if (!self::isValidUser($userInfo['user'])) { + return false; + } + + if (isset($userInfo['pass'])) { + return self::isValidPass($userInfo['pass']); + } + + return true; + } + + /** + * Checks if host is valid + * @param string $host + * @return bool + */ + public static function isValidHost($host): bool + { + // min and max length + if ($host === '' || isset($host[253])) { + return false; + } + + // check ipv6 + if ($host[0] === '[') { + if ($host[-1] !== ']') { + return false; + } + + return filter_var( + substr($host, 1, -1), + \FILTER_VALIDATE_IP, + \FILTER_FLAG_IPV6 + ) !== false; + } + + // check ipv4 + if (preg_match('`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\$`', $host)) { + return \filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) !== false; + } + + foreach (explode('.', $host) as $host) { + // empty or too long label + if ($host === '' || isset($host[63])) { + return false; + } + if ($host[0] === '-' || $host[-1] === '-') { + return false; + } + if (!preg_match(self::HOST_LABEL_REGEX, $host)) { + return false; + } + } + + return true; + } + + /** + * Checks if the port is valid + * @param int $port + * @return bool + */ + public static function isValidPort($port): bool + { + return $port >= 0 && $port <= 65535; + } + + /** + * Checks if authority contains valid chars + * @param string $authority + * @return bool + */ + public static function isValidAuthority($authority): bool + { + if ($authority === '') { + return true; + } + + /** @var array|string $authority */ + + if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) { + return false; + } + + if (isset($authority['port']) && !self::isValidPort((int)$authority['port'])) { + return false; + } + + if (isset($authority['userinfo']) && !self::isValidUserInfo($authority['userinfo'])) { + return false; + } + + return self::isValidHost($authority['host']); + } + + /** + * Checks if the path contains valid chars + * @param string $path + * @return bool + */ + public static function isValidPath($path): bool + { + return $path === '' || (bool)preg_match(self::PATH_REGEX, $path); + } + + /** + * Checks if the query string contains valid chars + * @param string $query + * @return bool + */ + public static function isValidQuery($query): bool + { + return $query === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $query); + } + + /** + * Checks if the fragment contains valid chars + * @param string $fragment + * @return bool + */ + public static function isValidFragment($fragment): bool + { + return $fragment === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $fragment); + } + + /** + * @param string $uri + * @param bool $expand_authority + * @param bool $validate + * @return array|null + */ + public static function parseComponents($uri, $expand_authority = true, $validate = true) + { + if (!preg_match(self::URI_REGEX, $uri, $uri)) { + return null; + } + + $comp = []; + + // scheme + if (isset($uri[2]) && $uri[2] !== '') { + if ($validate && !self::isValidScheme($uri[2])) { + return null; + } + $comp['scheme'] = $uri[2]; + } + + // authority + if (isset($uri[4]) && isset($uri[3][0])) { + if ($uri[4] === '') { + if ($expand_authority) { + $comp['host'] = ''; + } else { + $comp['authority'] = ''; + } + } elseif ($expand_authority) { + $au = self::parseAuthorityComponents($uri[4], $validate); + if ($au === null) { + return null; + } + $comp += $au; + unset($au); + } else { + if ($validate && !self::isValidAuthority($uri[4])) { + return null; + } + $comp['authority'] = $uri[4]; + } + } + + // path + if (isset($uri[5])) { + if ($validate && !self::isValidPath($uri[5])) { + return null; + } + $comp['path'] = $uri[5]; + // not a relative uri, remove dot segments + if (isset($comp['scheme']) || isset($comp['authority']) || isset($comp['host'])) { + $comp['path'] = self::removeDotSegmentsFromPath($comp['path']); + } + } + + // query + if (isset($uri[7]) && isset($uri[6][0])) { + if ($validate && !self::isValidQuery($uri[7])) { + return null; + } + $comp['query'] = $uri[7]; + } + + // fragment + if (isset($uri[9]) && isset($uri[8][0])) { + if ($validate && !self::isValidFragment($uri[9])) { + return null; + } + $comp['fragment'] = $uri[9]; + } + + return $comp; + } + + /** + * @param self|string|array $uri + * @return array|null + */ + public static function resolveComponents($uri) + { + if ($uri instanceof self) { + return $uri->components; + } + + if (is_string($uri)) { + return self::parseComponents($uri); + } + + if (is_array($uri)) { + if (isset($uri['host'])) { + unset($uri['authority']); + } elseif (isset($uri['authority'])) { + $au = self::parseAuthorityComponents($uri['authority']); + unset($uri['authority']); + if ($au !== null) { + unset($uri['user'], $uri['pass'], $uri['host'], $uri['port']); + $uri += $au; + } + } + return $uri; + } + + return null; + } + + /** + * @param string $authority + * @param bool $validate + * @return array|null + */ + public static function parseAuthorityComponents($authority, $validate = true) + { + /** @var array|string $authority */ + + if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) { + return null; + } + + $comp = []; + + // userinfo + if (isset($authority['userinfo']) && $authority['userinfo'] !== '') { + if (!preg_match(self::USERINFO_REGEX, $authority['userinfo'], $ui)) { + return null; + } + + // user + if ($validate && !self::isValidUser($ui['user'])) { + return null; + } + $comp['user'] = $ui['user']; + + // pass + if (isset($ui['pass']) && $ui['pass'] !== '') { + if ($validate && !self::isValidPass($ui['pass'])) { + return null; + } + $comp['pass'] = $ui['pass']; + } + + unset($ui); + } + + // host + if ($validate && !self::isValidHost($authority['host'])) { + return null; + } + $comp['host'] = $authority['host']; + + + // port + if (isset($authority['port'])) { + $authority['port'] = (int)$authority['port']; + if (!self::isValidPort($authority['port'])) { + return null; + } + $comp['port'] = $authority['port']; + } + + return $comp; + } + + /** + * @param array $ref + * @param array $base + * @param bool $normalize + * @return array + */ + public static function mergeComponents($ref, $base, $normalize = false): array + { + if (isset($ref['scheme'])) { + $dest = $ref; + } else { + $dest = []; + + $dest['scheme'] = $base['scheme'] ?? null; + + if (isset($ref['authority']) || isset($ref['host'])) { + $dest += $ref; + } else { + if (isset($base['authority'])) { + $dest['authority'] = $base['authority']; + } else { + $dest['user'] = $base['user'] ?? null; + $dest['pass'] = $base['pass'] ?? null; + $dest['host'] = $base['host'] ?? null; + $dest['port'] = $base['port'] ?? null; + } + + if (!isset($ref['path'])) { + $ref['path'] = ''; + } + if (!isset($base['path'])) { + $base['path'] = ''; + } + + if ($ref['path'] === '') { + $dest['path'] = $base['path']; + $dest['query'] = $ref['query'] ?? $base['query'] ?? null; + } else { + if ($ref['path'][0] === '/') { + $dest['path'] = $ref['path']; + } else { + if ((isset($base['authority']) || isset($base['host'])) && $base['path'] === '') { + $dest['path'] = '/' . $ref['path']; + } else { + $dest['path'] = $base['path']; + + if ($dest['path'] !== '') { + $pos = strrpos($dest['path'], '/'); + if ($pos === false) { + $dest['path'] = ''; + } else { + $dest['path'] = substr($dest['path'], 0, $pos); + } + + unset($pos); + } + $dest['path'] .= '/' . $ref['path']; + } + } + + $dest['query'] = $ref['query'] ?? null; + } + } + } + + $dest['fragment'] = $ref['fragment'] ?? null; + + if ($normalize) { + return self::normalizeComponents($dest); + } + + if (isset($dest['path'])) { + $dest['path'] = self::removeDotSegmentsFromPath($dest['path']); + } + + return $dest; + } + + /** + * @param mixed[] $components + */ + public static function normalizeComponents($components): array + { + if (isset($components['scheme'])) { + $components['scheme'] = strtolower($components['scheme']); + // Remove default port + if (isset($components['port']) && self::getSchemePort($components['scheme']) === $components['port']) { + $components['port'] = null; + } + } + + if (isset($components['host'])) { + $components['host'] = strtolower($components['host']); + } + + if (isset($components['path'])) { + $components['path'] = self::removeDotSegmentsFromPath($components['path']); + } + + if (isset($components['query'])) { + $components['query'] = self::normalizeQueryString($components['query']); + } + + return $components; + } + + /** + * Removes dot segments from path + * @param string $path + * @return string + */ + public static function removeDotSegmentsFromPath($path): string + { + // Fast check common simple paths + if ($path === '' || $path === '/') { + return $path; + } + + $output = ''; + $last_slash = 0; + + $len = strlen($path); + $i = 0; + + while ($i < $len) { + if ($path[$i] === '.') { + $j = $i + 1; + // search for . + if ($j >= $len) { + break; + } + + // search for ./ + if ($path[$j] === '/') { + $i = $j + 1; + continue; + } + + // search for ../ + if ($path[$j] === '.') { + $k = $j + 1; + if ($k >= $len) { + break; + } + if ($path[$k] === '/') { + $i = $k + 1; + continue; + } + } + } elseif ($path[$i] === '/') { + $j = $i + 1; + if ($j >= $len) { + $output .= '/'; + break; + } + + // search for /. + if ($path[$j] === '.') { + $k = $j + 1; + if ($k >= $len) { + $output .= '/'; + break; + } + // search for /./ + if ($path[$k] === '/') { + $i = $k; + continue; + } + // search for /.. + if ($path[$k] === '.') { + $n = $k + 1; + if ($n >= $len) { + // keep the slash + $output = substr($output, 0, $last_slash + 1); + break; + } + // search for /../ + if ($path[$n] === '/') { + $output = substr($output, 0, $last_slash); + $last_slash = (int)strrpos($output, '/'); + $i = $n; + continue; + } + } + } + } + + $pos = strpos($path, '/', $i + 1); + + if ($pos === false) { + $output .= substr($path, $i); + break; + } + + $last_slash = strlen($output); + $output .= substr($path, $i, $pos - $i); + + $i = $pos; + } + + return $output; + } + + /** + * @param string|null $query + * @return array + */ + public static function parseQueryString($query): array + { + if ($query === null) { + return []; + } + + $list = []; + + foreach (explode('&', $query) as $name) { + $value = null; + if (($pos = strpos($name, '=')) !== false) { + $value = self::decodeComponent(substr($name, $pos + 1)); + $name = self::decodeComponent(substr($name, 0, $pos)); + } else { + $name = self::decodeComponent($name); + } + $list[$name] = $value; + } + + return $list; + } + + /** + * @param array $qs + * @param string|null $prefix + * @param string $separator + * @param bool $sort + * @return string + */ + public static function buildQueryString($qs, $prefix = null, + $separator = '&', $sort = false): string + { + $isIndexed = static function (array $array): bool { + for ($i = 0, $max = count($array); $i < $max; $i++) { + if (!array_key_exists($i, $array)) { + return false; + } + } + return true; + }; + + $f = static function (array $arr, $prefix = null) use (&$f, &$isIndexed) { + $indexed = $prefix !== null && $isIndexed($arr); + + foreach ($arr as $key => $value) { + if ($prefix !== null) { + $key = $prefix . ($indexed ? "[]" : "[{$key}]"); + } + if (is_array($value)) { + yield from $f($value, $key); + } else { + yield $key => $value; + } + } + }; + + $data = []; + + foreach ($f($qs, $prefix) as $key => $value) { + $item = is_string($key) ? self::encodeComponent($key) : $key; + if ($value !== null) { + $item .= '='; + $item .= is_string($value) ? self::encodeComponent($value) : $value; + } + if ($item === '' || $item === '=') { + continue; + } + $data[] = $item; + } + + if (!$data) { + return ''; + } + + if ($sort) { + sort($data); + } + + return implode($separator, $data); + } + + /** + * @param string $query + * @return string + */ + public static function normalizeQueryString($query): string + { + return static::buildQueryString(self::parseQueryString($query), null, '&', true); + } + + /** + * @param string $component + */ + public static function decodeComponent($component): string + { + return rawurldecode($component); + } + + /** + * @param string $component + * @param mixed[]|null $skip + */ + public static function encodeComponent($component, $skip = null): string + { + if (!$skip) { + return rawurlencode($component); + } + + $str = ''; + + foreach (UnicodeString::walkString($component) as list($cp, $chars)) { + if ($cp < 0x80) { + if ($cp === 0x2D || $cp === 0x2E || + $cp === 0x5F || $cp === 0x7E || + ($cp >= 0x41 && $cp <= 0x5A) || + ($cp >= 0x61 && $cp <= 0x7A) || + ($cp >= 0x30 && $cp <= 0x39) || + in_array($cp, $skip, true) + ) { + $str .= chr($cp); + } else { + $str .= '%' . strtoupper(dechex($cp)); + } + } else { + $i = 0; + while (isset($chars[$i])) { + $str .= '%' . strtoupper(dechex($chars[$i++])); + } + } + } + + return $str; + } + + /** + * @param string $scheme + * @param int|null $port + */ + public static function setSchemePort($scheme, $port) + { + $scheme = strtolower($scheme); + + if ($port === null) { + unset(self::$KNOWN_PORTS[$scheme]); + } else { + self::$KNOWN_PORTS[$scheme] = $port; + } + } + + /** + * @param string $scheme + */ + public static function getSchemePort($scheme) + { + return self::$KNOWN_PORTS[strtolower($scheme)] ?? null; + } + + /** + * @var mixed[] + */ + protected static $KNOWN_PORTS = [ + 'ftp' => 21, + 'ssh' => 22, + 'telnet' => 23, + 'smtp' => 25, + 'tftp' => 69, + 'http' => 80, + 'pop' => 110, + 'sftp' => 115, + 'imap' => 143, + 'irc' => 194, + 'ldap' => 389, + 'https' => 443, + 'ldaps' => 636, + 'telnets' => 992, + 'imaps' => 993, + 'ircs' => 994, + 'pops' => 995, + ]; +} diff --git a/src/opis/uri/src/UriTemplate.php b/src/opis/uri/src/UriTemplate.php new file mode 100644 index 00000000..3de35d6e --- /dev/null +++ b/src/opis/uri/src/UriTemplate.php @@ -0,0 +1,523 @@ +[a-zA-Z0-9\_\%\.]+)(?:(?\*)?|\:(?\d+))?$~'; + + /** @var string */ + const TEMPLATE_REGEX = <<<'REGEX' +~\{ +(?[+#./;&=,!@|\?])? +(? + (?:(?P>varspec),)* + (?(?: + [a-zA-Z0-9\_\%\.]+ + (?:\*|\:\d+)? + )) +) +\}~x +REGEX; + + /** @var array */ + const TEMPLATE_TABLE = [ + '' => [ + 'first' => '', + 'sep' => ',', + 'named' => false, + 'ifemp' => '', + 'allow' => false, + ], + '+' => [ + 'first' => '', + 'sep' => ',', + 'named' => false, + 'ifemp' => '', + 'allow' => true, + ], + '.' => [ + 'first' => '.', + 'sep' => '.', + 'named' => false, + 'ifemp' => '', + 'allow' => false, + ], + '/' => [ + 'first' => '/', + 'sep' => '/', + 'named' => false, + 'ifemp' => '', + 'allow' => false, + ], + ';' => [ + 'first' => ';', + 'sep' => ';', + 'named' => true, + 'ifemp' => '', + 'allow' => false, + ], + '?' => [ + 'first' => '?', + 'sep' => '&', + 'named' => true, + 'ifemp' => '=', + 'allow' => false, + ], + '&' => [ + 'first' => '&', + 'sep' => '&', + 'named' => true, + 'ifemp' => '=', + 'allow' => false, + ], + '#' => [ + 'first' => '#', + 'sep' => ',', + 'named' => false, + 'ifemp' => '', + 'allow' => true, + ], + ]; + + /** + * @var string + */ + protected $uri; + + /** @var bool|null|array */ + protected $parsed = false; + + /** + * UriTemplate constructor. + * @param string $uri_template + */ + public function __construct(string $uri_template) + { + $this->uri = $uri_template; + } + + /** + * @param array $vars + * @return string + */ + public function resolve($vars): string + { + if ($this->parsed === false) { + $this->parsed = $this->parse($this->uri); + } + if ($this->parsed === null || !$vars) { + return $this->uri; + } + + $data = ''; + $vars = $this->prepareVars($vars); + + foreach ($this->parsed as $item) { + if (!is_array($item)) { + $data .= $item; + continue; + } + + $data .= $this->parseTemplateExpression( + self::TEMPLATE_TABLE[$item['operator']], + $this->resolveVars($item['vars'], $vars) + ); + } + + return $data; + } + + /** + * @return bool + */ + public function hasPlaceholders(): bool + { + if ($this->parsed === false) { + $this->parse($this->uri); + } + + return $this->parsed !== null; + } + + /** + * @param string $uri + * @return array|null + */ + protected function parse($uri) + { + $placeholders = null; + preg_match_all(self::TEMPLATE_REGEX, $uri, $placeholders, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); + + if (!$placeholders) { + return null; + } + + $dataIndex = -1; + $data = []; + + $hasVars = false; + $nextOffset = 0; + foreach ($placeholders as &$p) { + $offset = $p[0][1]; + if ($nextOffset < $offset) { + $data[] = substr($uri, $nextOffset, $offset - $nextOffset); + $dataIndex++; + } + $matched = $p[0][0]; + $nextOffset = $offset + strlen($matched); + + $operator = $p['operator'][0] ?? null; + if ($operator === null || !isset(self::TEMPLATE_TABLE[$operator])) { + if ($dataIndex >= 0 && is_string($data[$dataIndex])) { + $data[$dataIndex] .= $matched; + } else { + $data[] = $matched; + $dataIndex++; + } + continue; + } + + $varList = $p['varlist'][0] ?? ''; + $varList = $varList === '' ? [] : explode(',', $varList); + $p = null; + + $varData = []; + + foreach ($varList as $var) { + if (!preg_match(self::TEMPLATE_VARSPEC_REGEX, $var, $spec)) { + continue; + } + + $varData[] = [ + 'name' => $spec['varname'], + 'explode' => isset($spec['explode']) && $spec['explode'] === '*', + 'prefix' => isset($spec['prefix']) ? (int)$spec['prefix'] : 0, + ]; + + unset($var, $spec); + } + + if ($varData) { + $hasVars = true; + $data[] = [ + 'operator' => $operator, + 'vars' => $varData, + ]; + $dataIndex++; + } else { + if ($dataIndex >= 0 && is_string($data[$dataIndex])) { + $data[$dataIndex] .= $matched; + } else { + $data[] = $matched; + $dataIndex++; + } + } + + unset($varData, $varList, $operator); + } + + if (!$hasVars) { + return null; + } + + $matched = substr($uri, $nextOffset); + if ($matched !== false && $matched !== '') { + if ($dataIndex >= 0 && is_string($data[$dataIndex])) { + $data[$dataIndex] .= $matched; + } else { + $data[] = $matched; + } + } + + return $data; + } + + /** + * Convert assoc arrays to objects + * @param array $vars + * @return array + */ + protected function prepareVars($vars): array + { + foreach ($vars as &$value) { + if (is_scalar($value)) { + if (!is_string($value)) { + $value = (string)$value; + } + continue; + } + + if (!is_array($value)) { + continue; + } + + $len = count($value); + for ($i = 0; $i < $len; $i++) { + if (!array_key_exists($i, $value)) { + $value = (object)$value; + break; + } + } + } + + return $vars; + } + + /** + * @param array $vars + * @param array $data + * @return array + */ + protected function resolveVars($vars, $data): array + { + $resolved = []; + + foreach ($vars as $info) { + $name = $info['name']; + + if (!isset($data[$name])) { + continue; + } + + $resolved[] = $info + ['value' => &$data[$name]]; + } + + return $resolved; + } + + /** + * @param array $table + * @param array $data + * @return string + */ + protected function parseTemplateExpression($table, $data): string + { + $result = []; + foreach ($data as $var) { + $str = ""; + if (is_string($var['value'])) { + if ($table['named']) { + $str .= $var['name']; + if ($var['value'] === '') { + $str .= $table['ifemp']; + } else { + $str .= '='; + } + } + if ($var['prefix']) { + $str .= $this->encodeTemplateString(self::prefix($var['value'], $var['prefix']), $table['allow']); + } else { + $str .= $this->encodeTemplateString($var['value'], $table['allow']); + } + } elseif ($var['explode']) { + $list = []; + if ($table['named']) { + if (is_array($var['value'])) { + foreach ($var['value'] as $v) { + if (is_null($v) || !is_scalar($v)) { + continue; + } + $v = $this->encodeTemplateString((string)$v, $table['allow']); + if ($v === '') { + $list[] = $var['name'] . $table['ifemp']; + } else { + $list[] = $var['name'] . '=' . $v; + } + } + } elseif (is_object($var['value'])) { + foreach ($var['value'] as $prop => $v) { + if (is_null($v) || !is_scalar($v)) { + continue; + } + $v = $this->encodeTemplateString((string)$v, $table['allow']); + $prop = $this->encodeTemplateString((string)$prop, $table['allow']); + if ($v === '') { + $list[] = $prop . $table['ifemp']; + } else { + $list[] = $prop . '=' . $v; + } + } + } + } else { + if (is_array($var['value'])) { + foreach ($var['value'] as $v) { + if (is_null($v) || !is_scalar($v)) { + continue; + } + $list[] = $this->encodeTemplateString($v, $table['allow']); + } + } elseif (is_object($var['value'])) { + foreach ($var['value'] as $prop => $v) { + if (is_null($v) || !is_scalar($v)) { + continue; + } + $v = $this->encodeTemplateString((string)$v, $table['allow']); + $prop = $this->encodeTemplateString((string)$prop, $table['allow']); + $list[] = $prop . '=' . $v; + } + } + } + + if ($list) { + $str .= implode($table['sep'], $list); + } + unset($list); + } else { + if ($table['named']) { + $str .= $var['name']; + if ($var['value'] === '') { + $str .= $table['ifemp']; + } else { + $str .= '='; + } + } + $list = []; + if (is_array($var['value'])) { + foreach ($var['value'] as $v) { + $list[] = $this->encodeTemplateString($v, $table['allow']); + } + } elseif (is_object($var['value'])) { + foreach ($var['value'] as $prop => $v) { + $list[] = $this->encodeTemplateString((string)$prop, $table['allow']); + $list[] = $this->encodeTemplateString((string)$v, $table['allow']); + } + } + if ($list) { + $str .= implode(',', $list); + } + unset($list); + } + + if ($str !== '') { + $result[] = $str; + } + } + + if (!$result) { + return ''; + } + + $result = implode($table['sep'], $result); + + if ($result !== '') { + $result = $table['first'] . $result; + } + + return $result; + } + + /** + * @param string $data + * @param bool $reserved + * @return string + */ + protected function encodeTemplateString($data, $reserved): string + { + $skip = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~'; + + if ($reserved) { + $skip .= ':/?#[]@!$&\'()*+,;='; + } + + $result = ''; + $temp = ''; + for ($i = 0, $len = strlen($data); $i < $len; $i++) { + if (strpos($skip, $data[$i]) !== false) { + if ($temp !== '') { + $result .= Uri::encodeComponent($temp); + $temp = ''; + } + $result .= $data[$i]; + continue; + } + if ($reserved && $data[$i] === '%') { + if (isset($data[$i + 1]) && isset($data[$i + 2]) + && strpos('ABCDEF0123456789', $data[$i + 1]) !== false + && strpos('ABCDEF0123456789', $data[$i + 2]) !== false) { + if ($temp !== '') { + $result .= Uri::encodeComponent($temp); + } + $result .= '%' . $data[$i + 1] . $data[$i + 2]; + $i += 3; + continue; + } + } + $temp .= $data[$i]; + } + + if ($temp !== '') { + $result .= Uri::encodeComponent($temp); + } + + return $result; + } + + /** + * @return string + */ + public function value(): string + { + return $this->uri; + } + + public function __toString(): string + { + return $this->uri; + } + + /** + * @param string $uri + * @return bool + */ + public static function isTemplate($uri): bool + { + $open = substr_count($uri, '{'); + if ($open === 0) { + return false; + } + $close = substr_count($uri, '}'); + if ($open !== $close) { + return false; + } + + return (bool)preg_match(self::TEMPLATE_REGEX, $uri); + } + + /** + * @param string $str + * @param int $len + * @return string + */ + protected static function prefix($str, $len): string + { + if ($len === 0) { + return ''; + } + + if ($len >= strlen($str)) { + // Prefix is longer than string length + return $str; + } + + return (string)UnicodeString::from($str)->substring(0, $len); + } +}