From f05be6c606e13237fd87e3373f2857a092085779 Mon Sep 17 00:00:00 2001 From: Thoriq Firdaus <2067467+tfirdaus@users.noreply.github.com> Date: Fri, 29 Dec 2023 17:09:15 +0000 Subject: [PATCH] Initial commit --- .devcontainer/devcontainer.json | 28 ++ .editorconfig | 17 + .gitattributes | 11 + .github/dependabot.yml | 24 + .github/workflows/php.yml | 84 ++++ .gitignore | 27 ++ .vscode/extensions.json | 14 + .vscode/settings.json | 103 ++++ CODE_OF_CONDUCT.md | 132 ++++++ LICENSE | 21 + README.md | 42 ++ app/CaseConverter/CaseConverter.php | 23 + app/CaseConverter/functions.php | 87 ++++ app/Validator/Validator.php | 24 + app/Validator/functions.php | 173 +++++++ codecov.yml | 3 + composer.json | 84 ++++ phpcs.xml.dist | 39 ++ phpstan.neon.dist | 4 + phpunit.xml.dist | 22 + tests/CaseConverterTest.php | 163 +++++++ tests/ValidatorTest.php | 698 ++++++++++++++++++++++++++++ 22 files changed, 1823 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/php.yml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/CaseConverter/CaseConverter.php create mode 100644 app/CaseConverter/functions.php create mode 100644 app/Validator/Validator.php create mode 100644 app/Validator/functions.php create mode 100644 codecov.yml create mode 100644 composer.json create mode 100644 phpcs.xml.dist create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 tests/CaseConverterTest.php create mode 100644 tests/ValidatorTest.php diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..7b4b342 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "syntatis/php-utils", + "image": "ghcr.io/syntatis/php:7.4-fpm", + "customizations": { + "vscode": { + "settings": { + "php.validate.executablePath": "/usr/local/bin/php", + "terminal.integrated.profiles.linux": { + "bash": { + "path": "/bin/bash", + "args": [ + "-l" + ] + } + }, + "terminal.integrated.defaultProfile.linux": "bash" + }, + "extensions": [ + "bmewburn.vscode-intelephense-client", + "christian-kohler.path-intellisense", + "editorConfig.editorConfig", + "neilbrayfield.php-docblocker", + "SanderRonde.phpstan-vscode", + "wongjn.php-sniffer" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..03c9d82 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,neon,neon.dist,yml,md}] +indent_size = 2 + +[*.{yml,md,neon,neon.dist}] +indent_style = space diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b5bfc95 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +/.devcontainer export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.vscode export-ignore +/docs export-ignore +/phpstan.neon.dist export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c24ad21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + + - package-ecosystem: "composer" + versioning-strategy: increase + directory: "/" + schedule: + interval: "weekly" + day: "sunday" + groups: + composer-require: + dependency-type: "production" + update-types: + - "minor" + - "patch" + composer-require-dev: + dependency-type: "development" + update-types: + - "minor" + - "patch" diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..385cb35 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,84 @@ +name: php + +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - ".github/workflows/php.yml" + - "**.php" + - "composer.json" + - "composer.lock" + - "phpcs.xml.dist" + - "phpstan.neon.dist" + - "phpunit.xml.dist" + push: + branches: + - main + paths: + - ".github/workflows/php.yml" + - "**.php" + - "composer.json" + - "composer.lock" + - "phpcs.xml.dist" + - "phpstan.neon.dist" + - "phpunit.xml.dist" + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + + - name: Install dependencies + run: composer install --prefer-dist --no-ansi --no-interaction --no-progress + + - name: Run linter + run: composer phpcs + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + version: [7.4, 8.0, 8.1, 8.2] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Setup Composer cache + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ matrix.version }}-${{ hashFiles('**/composer.json') }} + restore-keys: php-${{ matrix.version }}-composer- + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.version }} + tools: composer:v2 + + - name: Install dependencies + run: composer update --prefer-dist --no-ansi --no-interaction --no-progress + + - name: Run test + run: | + composer phpstan + vendor/bin/phpunit --testdox --coverage-clover coverage.xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4df777d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +#OS +.DS_* + +# Composer +/vendor +composer.lock +auth.json + +# Log, cache, and tmp files +*.cache +*.log +*.meta +tmp/ + +# Dotenv +.env.* +!.env + +# Tests # +############# +artifacts/ +phpunit.xml +phpstan.neon +phpcs.xml + +# Docker +docker-composer.override.yml diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..887c373 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + "recommendations": [ + "bmewburn.vscode-intelephense-client", + "christian-kohler.path-intellisense", + "editorconfig.editorconfig", + "kasik96.latte", + "mikestead.dotenv", + "ms-vscode-remote.remote-containers", + "neilbrayfield.php-docblocker", + "sanderronde.phpstan-vscode", + "wmaurer.change-case", + "dotjoshjohnson.xml" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f4731d4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,103 @@ +{ + "intelephense.stubs": [ + "apache", + "bcmath", + "bz2", + "calendar", + "com_dotnet", + "Core", + "ctype", + "curl", + "date", + "dba", + "dom", + "enchant", + "exif", + "FFI", + "fileinfo", + "filter", + "fpm", + "ftp", + "gd", + "gettext", + "gmp", + "hash", + "iconv", + "imap", + "intl", + "json", + "ldap", + "libxml", + "mbstring", + "meta", + "mysqli", + "oci8", + "odbc", + "openssl", + "pcntl", + "pcre", + "pdo_ibm", + "pdo_mysql", + "pdo_pgsql", + "pdo_sqlite", + "PDO", + "pgsql", + "Phar", + "posix", + "pspell", + "readline", + "redis", + "Reflection", + "session", + "shmop", + "SimpleXML", + "snmp", + "soap", + "sockets", + "sodium", + "SPL", + "sqlite3", + "standard", + "superglobals", + "sysvmsg", + "sysvsem", + "sysvshm", + "tidy", + "tokenizer", + "wordpress", + "xml", + "xmlreader", + "xmlrpc", + "xmlwriter", + "xsl", + "Zend OPcache", + "zip", + "zlib" + ], + "[json]": { + "editor.defaultFormatter": "vscode.json-language-features" + }, + "[php]": { + "editor.defaultFormatter": "wongjn.php-sniffer" + }, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/.DS_Store": true, + "**/Thumbs.db": true + }, + "search.exclude": { + "**/dist": true + }, + "intelephense.files.exclude": [ + "**/.git/**", + "**/.svn/**", + "**/.hg/**", + "**/.DS_Store/**", + "**/.history/**", + "**/CVS/**" + ], + "phpSniffer.autoDetect": true, + "phpSniffer.run": "onSave" +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ddb2790 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2a8e318 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Syntatis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8a3e49 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +
+ 📦 utils-php +

Handy functions for PHP

+ + [![php](https://github.com/syntatis/utils-php/actions/workflows/php.yml/badge.svg)](https://github.com/syntatis/utils-php/actions/workflows/php.yml) [![codecov](https://codecov.io/gh/syntatis/utils-php/graph/badge.svg?token=1DJG88CULL)](https://codecov.io/gh/syntatis/utils-php) ![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/syntatis/utils/php) +
+ +--- + +## Installation + +```bash +composer require syntatis/utils +``` + +## Usage + +This PHP package includes a number of functions to perform common value validation, such as if a value is an email, URL, or not being blank. + +| Function | Description | +| --- | --- | +| `is_blank` | Validates whether a value is blank or empty. | +| `is_email` | Validates whether a value is a valid email address. | +| `is_url` | Validates whether a value is a valid URL. | +| `is_uuid` | Validates whether a value is a valid UUID. | +| `is_semver` | Validates whether a value is a valid SemVer format. | +| `is_ip_address` | Validates whether a value is a valid IPv4 or IPv6 address. | +| `is_unique` | Validates that all elements in the provided collection are unique. | + +For examples: + +```php +use function Syntatis\Utils\is_blank; +use function Syntatis\Utils\is_email; + +// Whether a value is blank or empty. +is_blank(''); // `true`. +is_blank(' '); // `true`. +is_blank('foo '); // `false`. +``` + +For other functions and examples, please refer to the [Wiki](https://github.com/syntatis/utils-php/wiki). diff --git a/app/CaseConverter/CaseConverter.php b/app/CaseConverter/CaseConverter.php new file mode 100644 index 0000000..374f93b --- /dev/null +++ b/app/CaseConverter/CaseConverter.php @@ -0,0 +1,23 @@ +convert($value)->toCamel(); +} + +/** + * Convert a string to "kebab-case" format. + */ +function kebabcased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toKebab(); +} + +/** + * Convert a string to "snake_case" format. + */ +function snakecased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toSnake(); +} + +/** + * Convert a string to "PascalCase" format. + */ +function pascalcased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toPascal(); +} + +/** + * Convert a string to "Title case" format. + */ +function titlecased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toTitle(); +} + +/** + * Convert a string to "lowercased" format. + */ +function lowercased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toLower(); +} + +/** + * Convert a string to "UPPERCASE" format. + */ +function uppercased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toUpper(); +} + +/** + * Convert a string to "MACRO_CASED" format. + */ +function macrocased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toMacro(); +} + +/** + * Convert a string to "COBOL-CASED" format. + */ +function cobolcased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toCobol(); +} + +/** + * Convert a string to "Sentence case" format. + */ +function sentencecased(string $value): string +{ + return CaseConverter::instance()->convert($value)->toSentence(); +} diff --git a/app/Validator/Validator.php b/app/Validator/Validator.php new file mode 100644 index 0000000..9fe34fa --- /dev/null +++ b/app/Validator/Validator.php @@ -0,0 +1,24 @@ +validate( + $value, + new Sequentially( + [ + new NotBlank(null, null, null, 'trim'), + new Email(null, null, Email::VALIDATION_MODE_STRICT), + ], + ), + )->count() <= 0; +} + +/** + * Validates that a value is a valid Universally unique identifier (UUID) per RFC 4122 + * {@link https://www.rfc-editor.org/rfc/rfc4122} + * + * @param mixed $value + * @param array|null $versions + * + * @phpstan-param array>|null $versions + */ +function is_uuid($value, ?array $versions = null): bool +{ + return Validator::instance() + ->validate( + $value, + new Uuid( + null, + null, + $versions, // Versions. + true, // Stricts. + ), + )->count() <= 0; +} + +/** + * @param mixed ...$value + * + * @phpstan-assert-if-true ''|array{}|false|null $value + */ +function is_blank(...$value): bool +{ + foreach ($value as $k => $v) { + if (Validator::instance()->validate($v, new NotBlank(null, null, null, 'trim'))->count() >= 1) { + continue; + } + + return false; + } + + return true; +} + +/** + * @param mixed $value + * @param array $protocols + * + * @phpstan-assert-if-true non-empty-string $value + */ +function is_url($value, array $protocols = ['http', 'https']): bool +{ + return Validator::instance()->validate( + $value, + [ + new Sequentially( + [ + new NotBlank(null, null, null, 'trim'), + new Url(null, null, $protocols, null, 'trim'), + ], + ), + ], + )->count() <= 0; +} + +/** + * @param mixed $value + * + * @phpstan-assert-if-true non-empty-string $value + */ +function is_semver($value): bool +{ + return Validator::instance()->validate( + $value, + new Regex( + /** @see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string */ + '/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/', + ), + )->count() <= 0; +} + +/** + * Validate if the value is a valid IPv4 or IPv6 address. + * + * @param mixed $value + * + * @phpstan-assert-if-true non-empty-string $value + */ +function is_ip_address($value): bool +{ + return Validator::instance()->validate( + $value, + new Sequentially( + [ + new NotBlank(null, null, null, 'trim'), + new Ip([ + 'normalizer' => 'trim', + 'version' => Ip::ALL, + ]), + ], + ), + )->count() <= 0; +} + +/** + * Validates that all elements in the provided collection are unique, employing + * strict comparison by default (treating '7' and 7 as distinct elements). + * + * @param mixed $value + * @param string|array $fields Specifies the key or keys in a collection + * to be examined for uniqueness. + * Required PHP 8.1 or higher. + * + * @phpstan-assert-if-true non-empty-array $value + */ +function is_unique($value, $fields = []): bool +{ + $notBlank = new NotBlank(null, null, null); + + if (PHP_VERSION_ID < 80100) { + return Validator::instance()->validate( + $value, + new Sequentially([ + $notBlank, + new Unique(), + ]), + )->count() <= 0; + } + + return Validator::instance()->validate( + $value, + new Sequentially([ + $notBlank, + new Unique(['fields' => $fields]), + ]), + )->count() <= 0; +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..3f356b0 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +comment: + layout: "condensed_header, condensed_files, condensed_footer" + hide_project_coverage: true diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..005ef56 --- /dev/null +++ b/composer.json @@ -0,0 +1,84 @@ +{ + "name": "syntatis/utils", + "license": "MIT", + "description": "Handy functions for PHP", + "keywords": [ + "utilities", + "validator", + "case-converter" + ], + "homepage": "https://github.com/syntatis/utils-php#README", + "authors": [ + { + "name": "Thoriq Firdaus", + "homepage": "https://github.com/tfirdaus", + "role": "Developer" + } + ], + "config": { + "allow-plugins": { + "composer/installers": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpstan/extension-installer": true + }, + "cache-dir": "tmp/composer/cache", + "preferred-install": "dist", + "process-timeout": 0, + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "files": [ + "app/CaseConverter/functions.php", + "app/Validator/functions.php" + ], + "psr-4": { + "Syntatis\\Utils\\": "app" + } + }, + "autoload-dev": { + "files": [ + "app/CaseConverter/functions.php", + "app/Validator/functions.php" + ], + "psr-4": { + "Syntatis\\Utils\\Tests\\": "tests" + } + }, + "scripts": { + "phpcs:fix": "vendor/bin/phpcbf", + "phpcs": "vendor/bin/phpcs", + "phpstan": "vendor/bin/phpstan analyse --xdebug", + "phpunit:coverage": [ + "@putenv XDEBUG_MODE=coverage", + "vendor/bin/phpunit" + ], + "phpunit": "vendor/bin/phpunit --no-coverage", + "test": [ + "@phpcs", + "@phpstan", + "@phpunit" + ] + }, + "require": { + "php": "^7.4 || ^8.0", + "egulias/email-validator": "^3.2 || ^4.0", + "jawira/case-converter": "^3.5", + "symfony/validator": "^5.4 || ^6.1 || ^7.0" + }, + "require-dev": { + "composer/installers": "^2.2", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-deprecation-rules": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.8", + "symfony/var-dumper": "^5.4 || ^6.1 || ^7.0", + "syntatis/coding-standard": "^1.0" + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..24f1567 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,39 @@ + + + PHP Coding Standard + + + + + + + + + ./app/ + ./tests/ + + + + + + + + + + + + + + + + + + + + + + /cache/ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..3ad371d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,4 @@ +parameters: + level: 9 + paths: + - app diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..61e22f3 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + ./tests/ + + + + + ./app/ + + + + + + diff --git a/tests/CaseConverterTest.php b/tests/CaseConverterTest.php new file mode 100644 index 0000000..83f28e9 --- /dev/null +++ b/tests/CaseConverterTest.php @@ -0,0 +1,163 @@ +assertEquals($expect, camelcased($value)); + } + + /** + * @dataProvider dataKebabCased + * @testdox it can convert string to camelcase + */ + public function testKebabCased(string $value, string $expect): void + { + $this->assertEquals($expect, kebabcased($value)); + } + + /** + * @dataProvider dataSnakeCased + * @testdox it can convert string to snakecase + */ + public function testSnakeCased(string $value, string $expect): void + { + $this->assertEquals($expect, snakecased($value)); + } + + /** + * @dataProvider dataPascalCased + * @testdox it can convert string to pascalcase + */ + public function testPascalCased(string $value, string $expect): void + { + $this->assertEquals($expect, pascalcased($value)); + } + + /** + * @dataProvider dataTitleCased + * @testdox it can convert string to titlecase + */ + public function testTitleCased(string $value, string $expect): void + { + $this->assertEquals($expect, titlecased($value)); + } + + /** + * @dataProvider dataLowerCased + * @testdox it can convert string to lowercase + */ + public function testLowerCased(string $value, string $expect): void + { + $this->assertEquals($expect, lowercased($value)); + } + + /** + * @dataProvider dataUpperCased + * @testdox it can convert string to uppercase + */ + public function testUpperCased(string $value, string $expect): void + { + $this->assertEquals($expect, uppercased($value)); + } + + /** + * @dataProvider dataMacroCased + * @testdox it can convert string to macrocase + */ + public function testMacroCased(string $value, string $expect): void + { + $this->assertEquals($expect, macrocased($value)); + } + + /** + * @dataProvider dataCobolCased + * @testdox it can convert string to cobolcase + */ + public function testCobolCased(string $value, string $expect): void + { + $this->assertEquals($expect, cobolcased($value)); + } + + /** + * @dataProvider dataSentenceCased + * @testdox it can convert string to sentencecase + */ + public function testSentenceCased(string $value, string $expect): void + { + $this->assertEquals($expect, sentencecased($value)); + } + + public function dataCamelCased(): iterable + { + return [['foo_bar', 'fooBar'], ['foo-bar', 'fooBar'], ['foo bar', 'fooBar'], ['fooBar', 'fooBar'], ['FooBar', 'fooBar']]; + } + + public function dataKebabCased(): iterable + { + return [['foo_bar', 'foo-bar'], ['foo-bar', 'foo-bar'], ['foo bar', 'foo-bar'], ['fooBar', 'foo-bar'], ['FooBar', 'foo-bar']]; + } + + public function dataSnakeCased(): iterable + { + return [['foo_bar', 'foo_bar'], ['foo-bar', 'foo_bar'], ['foo bar', 'foo_bar'], ['fooBar', 'foo_bar'], ['FooBar', 'foo_bar']]; + } + + public function dataPascalCased(): iterable + { + return [['foo_bar', 'FooBar'], ['foo-bar', 'FooBar'], ['foo bar', 'FooBar'], ['fooBar', 'FooBar'], ['FooBar', 'FooBar']]; + } + + public function dataTitleCased(): iterable + { + return [['foo_bar', 'Foo Bar'], ['foo-bar', 'Foo Bar'], ['foo bar', 'Foo Bar'], ['fooBar', 'Foo Bar'], ['FooBar', 'Foo Bar']]; + } + + public function dataLowerCased(): iterable + { + return [['foo_bar', 'foo bar'], ['foo-bar', 'foo bar'], ['foo bar', 'foo bar'], ['fooBar', 'foo bar'], ['FooBar', 'foo bar']]; + } + + public function dataUpperCased(): iterable + { + return [['foo_bar', 'FOO BAR'], ['foo-bar', 'FOO BAR'], ['foo bar', 'FOO BAR'], ['fooBar', 'FOO BAR'], ['FooBar', 'FOO BAR']]; + } + + public function dataMacroCased(): iterable + { + return [['foo_bar', 'FOO_BAR'], ['foo-bar', 'FOO_BAR'], ['foo bar', 'FOO_BAR'], ['fooBar', 'FOO_BAR'], ['FooBar', 'FOO_BAR']]; + } + + public function dataCobolCased(): iterable + { + return [['foo_bar', 'FOO-BAR'], ['foo-bar', 'FOO-BAR'], ['foo bar', 'FOO-BAR'], ['fooBar', 'FOO-BAR'], ['FooBar', 'FOO-BAR']]; + } + + public function dataSentenceCased(): iterable + { + return [['foo_bar', 'Foo bar'], ['foo-bar', 'Foo bar'], ['foo bar', 'Foo bar'], ['fooBar', 'Foo bar'], ['FooBar', 'Foo bar']]; + } +} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php new file mode 100644 index 0000000..71a3f8d --- /dev/null +++ b/tests/ValidatorTest.php @@ -0,0 +1,698 @@ +assertTrue(is_email($value)); + } + + /** + * @dataProvider dataIsEmailInvalid + * @testdox it can validate a invalid email + * + * @param mixed $value + */ + public function testIsEmailInvalid($value): void + { + $this->assertFalse(is_email($value)); + } + + /** + * @dataProvider dataIsURLValid + * @testdox it can validate a valid url + * + * @param mixed $value + */ + public function testIsURLValid($value): void + { + $this->assertTrue(is_url($value)); + } + + /** + * @dataProvider dataIsURLInvalid + * @testdox it can validate an invalid url + * + * @param mixed $value + */ + public function testIsURLInvalid($value): void + { + $this->assertFalse(is_url($value)); + } + + /** + * @dataProvider dataIsBlank + * @testdox it can validate a blank value + * + * @param mixed $value + */ + public function testIsBlank($value): void + { + $this->assertTrue(is_blank($value)); + } + + /** @testdox it can validate a multiple blank value */ + public function testIsBlankMultiple(): void + { + $this->assertTrue(is_blank([], '', ' ', false, null)); + $this->assertFalse(is_blank([], 'this is not blank', ' ', false, null)); + } + + /** + * @dataProvider dataIsNotBlank + * @testdox it can validate that is a not blank value + * + * @param mixed $value + */ + public function testIsNotBlank($value): void + { + $this->assertFalse(is_blank($value)); + } + + /** + * @dataProvider dataIsUUID + * @testdox it can validate a uuid value + * + * @param mixed $value + */ + public function testIsUUID($value): void + { + $this->assertTrue(is_uuid($value)); + } + + /** + * @dataProvider dataIsSemver + * @testdox it can validate a semver value + * + * @param mixed $value + */ + public function testIsSemver($value): void + { + $this->assertTrue(is_semver($value)); + } + + /** + * @dataProvider dataIsNotSemver + * @testdox it can validate an invalid semver value + * + * @param mixed $value + */ + public function testIsNotSemver($value): void + { + $this->assertFalse(is_semver($value)); + } + + /** + * @dataProvider dataIsIpAddress + * @testdox it can validate a valid ip address + * + * @param mixed $value + */ + public function testIsIpAddress($value): void + { + $this->assertTrue(is_ip_address($value)); + } + + /** + * @dataProvider dataIsNotIpAddress + * @testdox it can validate a invalid ip address + * + * @param mixed $value + */ + public function testIsNotIpAddress($value): void + { + $this->assertFalse(is_ip_address($value)); + } + + /** + * @dataProvider dataIsUnique + * @testdox it can validate unique collection + * + * @param mixed $value + */ + public function testIsUnique($value): void + { + $this->assertTrue(is_unique($value)); + } + + /** + * @dataProvider dataIsUniqueOptionFields + * @requires PHP 8.1 + * @testdox it can validate unique collection + * + * @param mixed $value + * @param string|array $fields The list of fields to check uniqueness. + */ + public function testIsUniqueOptionFields($value, $fields = []): void + { + $this->assertTrue(is_unique($value, $fields)); + } + + /** + * @dataProvider dataIsNotUnique + * @testdox it can validate non-unique collection + * + * @param mixed $value + */ + public function testIsNotUnique($value): void + { + $this->assertFalse(is_unique($value)); + } + + public static function dataIsEmailValid(): array + { + return [ + ['â@iana.org'], + ['fabien@symfony.com'], + ['example@example.co.uk'], + ['fabien_potencier@example.fr'], + ['fab\'ien@symfony.com'], + ['fab\ ien@symfony.com'], + ['fabien+a@symfony.com'], + ['exampl=e@example.com'], + ['инфо@письмо.рф'], + ['müller@möller.de'], + ['1500111@профи-инвест.рф'], + [sprintf('example@%s.com', str_repeat('ъ', 40))], + ]; + } + + /** @see https://github.com/symfony/validator/blob/46c4193ec1eefdd7f9568261c685450d6f3b32c9/Tests/Constraints/EmailValidatorTest.php#L342 */ + public static function dataIsEmailInvalid(): array + { + return [ + [' '], + [''], + [null], + ['"""@iana.org'], + ['""@iana.org'], + ['"\""@iana.org'], + ['"\"@iana.org'], + ['"\a"@iana.org'], + ['"test""test"@iana.org'], + ['"test"' . chr(0) . '@iana.org'], + ['"test"."test"@iana.org'], + ['"test".test@iana.org'], + ['"test"test@iana.org'], + ['"test\ test"@iana.org'], + ['"test\"@iana.org'], + ['"user name"@example.com'], + ['"user,name"@example.com'], + ['"user@name"@example.com'], + ['"user\"name"@example.com'], + ['"username"@example.com'], + ['(test_exampel@example.fr'], + ['.example@localhost'], + ['0'], + ['\r\n \r\n test@iana.org'], + ['\r\n \r\ntest@iana.org'], + ['\r\n test@iana.org'], + ['\r\ntest@iana.org'], + ['ex\ample@localhost'], + ['example((example))@fakedfake.co.uk'], + ['example(example]example@example.co.uk'], + ['example.@example.co.uk'], + ['example@example@example.co.uk'], + ['examp║le@symfony.com'], + ['test;123@foobar.com'], + ['test@example.com test'], + ['user name@example.com'], + ['user name@example.com'], + ['user name@example.com'], + ['user[na]me@example.com'], + ['usern,ame@example.com'], + [chr(226) . '@iana.org'], + ['"""@iana.org'], + ['"\"@iana.org'], + ['"test""test"@iana.org'], + ['"test"' . chr(0) . '@iana.org'], + ['"test"."test"@iana.org'], + ['"test".test@iana.org'], + ['"test"test@iana.org'], + ['"test\"@iana.org'], + ['(test_exampel@example.fr)'], + ['.example@localhost'], + ['\r\n \r\n test@iana.org'], + ['\r\n \r\ntest@iana.org'], + ['\r\n test@iana.org'], + ['\r\ntest@iana.org'], + ['email.email@email."'], + ['ex\ample@localhost'], + ['example(example)example@example.co.uk'], + ['example.@example.co.uk'], + ['example@(fake).com'], + ['example@(fake.com'], + ['example@example@example.co.uk'], + ['example@local\host'], + ['example@localhost.'], + ['test;123@foobar.com'], + ['test@' . chr(226) . '.org'], + ['test@email<'], + ['test@email>'], + ['test@email{'], + ['test@example..com'], + ['test@foo;bar.com'], + ['test@iana.org \r\n\r\n '], + ['test@iana.org \r\n '], + ['test@iana.org \r\n \r\n'], + ['test@iana.org \r\n'], + ['test@iana.org \r\n\r\n'], + ['test@iana/icann.org'], + ['user name@example.com'], + ['user name@example.com'], + ['user name@example.com'], + ['user[na]me@example.com'], + ['usern,ame@example.com'], + ['username@ example . com'], + ['username@example,com'], + [chr(226) . '@iana.org'], + [str_repeat('x', 254) . '@example.com'], // email with warnings + ]; + } + + /** @see https://github.com/symfony/validator/blob/46c4193ec1eefdd7f9568261c685450d6f3b32c9/Tests/Constraints/UrlValidatorTest.php#L98 */ + public static function dataIsURLValid(): array + { + return [ + ['http://a.pl'], + ['http://www.example.com'], + ['http://tt.example.com'], + ['http://m.example.com'], + ['http://m.m.m.example.com'], + ['http://example.m.example.com'], + ['https://long-string_with+symbols.m.example.com'], + ['http://www.example.com.'], + ['http://www.example.museum'], + ['https://example.com/'], + ['https://example.com:80/'], + ['http://examp_le.com'], + ['http://www.sub_domain.examp_le.com'], + ['http://www.example.coop/'], + ['http://www.test-example.com/'], + ['http://www.symfony.com/'], + ['http://symfony.fake/blog/'], + ['http://symfony.com/?'], + ['http://symfony.com/search?type=&q=url+validator'], + ['http://symfony.com/#'], + ['http://symfony.com/#?'], + ['http://www.symfony.com/doc/current/book/validation.html#supported-constraints'], + ['http://very.long.domain.name.com/'], + ['http://localhost/'], + ['http://myhost123/'], + ['http://internal-api'], + ['http://internal-api.'], + ['http://internal-api/'], + ['http://internal-api/path'], + ['http://127.0.0.1/'], + ['http://127.0.0.1:80/'], + ['http://[::1]/'], + ['http://[::1]:80/'], + ['http://[1:2:3::4:5:6:7]/'], + ['http://sãopaulo.com/'], + ['http://xn--sopaulo-xwa.com/'], + ['http://sãopaulo.com.br/'], + ['http://xn--sopaulo-xwa.com.br/'], + ['http://пример.испытание/'], + ['http://xn--e1afmkfd.xn--80akhbyknj4f/'], + ['http://مثال.إختبار/'], + ['http://xn--mgbh0fb.xn--kgbechtv/'], + ['http://例子.测试/'], + ['http://xn--fsqu00a.xn--0zwm56d/'], + ['http://例子.測試/'], + ['http://xn--fsqu00a.xn--g6w251d/'], + ['http://例え.テスト/'], + ['http://xn--r8jz45g.xn--zckzah/'], + ['http://مثال.آزمایشی/'], + ['http://xn--mgbh0fb.xn--hgbk6aj7f53bba/'], + ['http://실례.테스트/'], + ['http://xn--9n2bp8q.xn--9t4b11yi5a/'], + ['http://العربية.idn.icann.org/'], + ['http://xn--ogb.idn.icann.org/'], + ['http://xn--e1afmkfd.xn--80akhbyknj4f.xn--e1afmkfd/'], + ['http://xn--espaa-rta.xn--ca-ol-fsay5a/'], + ['http://xn--d1abbgf6aiiy.xn--p1ai/'], + ['http://☎.com/'], + ['http://username:password@symfony.com'], + ['http://user.name:password@symfony.com'], + ['http://user_name:pass_word@symfony.com'], + ['http://username:pass.word@symfony.com'], + ['http://user.name:pass.word@symfony.com'], + ['http://user-name@symfony.com'], + ['http://user_name@symfony.com'], + ['http://u%24er:password@symfony.com'], + ['http://user:pa%24%24word@symfony.com'], + ['http://symfony.com?'], + ['http://symfony.com?query=1'], + ['http://symfony.com/?query=1'], + ['http://symfony.com#'], + ['http://symfony.com#fragment'], + ['http://symfony.com/#fragment'], + ['http://symfony.com/#one_more%20test'], + ['http://example.com/exploit.html?hello[0]=test'], + ['http://বিডিআইএ.বাংলা'], + + // Valid URL with whitespace. + '\x20http://www.example.com' => ["\x20http://www.example.com"], + '\x09\x09http://www.example.com.' => ["\x09\x09http://www.example.com."], + 'http://symfony.fake/blog/\x0A' => ["http://symfony.fake/blog/\x0A"], + 'http://symfony.com/search?type=&q=url+validator\x0D\x0D' => ["http://symfony.com/search?type=&q=url+validator\x0D\x0D"], + '\x00https://example.com:80\x00' => ["\x00https://example.com:80\x00"], + '\x0B\x0Bhttp://username:password@symfony.com\x0B\x0B' => ["\x0B\x0Bhttp://username:password@symfony.com\x0B\x0B"], + ]; + } + + public static function dataIsURLInvalid(): array + { + return [ + "' '" => [' '], + "''" => [''], + 'null' => [null], + 'example.com' => ['example.com'], + '://example.com' => ['://example.com'], + 'http ://example.com' => ['http ://example.com'], + 'http:/example.com' => ['http:/example.com'], + 'http://example.com::aa' => ['http://example.com::aa'], + 'http://example.com:aa' => ['http://example.com:aa'], + 'ftp://example.fr' => ['ftp://example.fr'], + 'faked://example.fr' => ['faked://example.fr'], + 'http://127.0.0.1:aa/' => ['http://127.0.0.1:aa/'], + 'ftp://[::1]/' => ['ftp://[::1]/'], + 'http://[::1' => ['http://[::1'], + 'http://☎' => ['http://☎'], + 'http://☎.' => ['http://☎.'], + 'http://☎/' => ['http://☎/'], + 'http://☎/path' => ['http://☎/path'], + 'http://hello.☎' => ['http://hello.☎'], + 'http://hello.☎.' => ['http://hello.☎.'], + 'http://hello.☎/' => ['http://hello.☎/'], + 'http://hello.☎/path' => ['http://hello.☎/path'], + 'http://:password@symfony.com' => ['http://:password@symfony.com'], + 'http://:password@@symfony.com' => ['http://:password@@symfony.com'], + 'http://username:passwordsymfony.com' => ['http://username:passwordsymfony.com'], + 'http://usern@me:password@symfony.com' => ['http://usern@me:password@symfony.com'], + 'http://nota%hex:password@symfony.com' => ['http://nota%hex:password@symfony.com'], + 'http://username:nota%hex@symfony.com' => ['http://username:nota%hex@symfony.com'], + 'http://example.com/exploit.html?' => ['http://example.com/exploit.html?'], + 'http://example.com/exploit.html?hel lo' => ['http://example.com/exploit.html?hel lo'], + 'http://example.com/exploit.html?not_a%hex' => ['http://example.com/exploit.html?not_a%hex'], + 'http://' => ['http://'], + 'http://www..com' => ['http://www..com'], + 'http://www..example.com' => ['http://www..example.com'], + 'http://www..m.example.com' => ['http://www..m.example.com'], + 'http://.m.example.com' => ['http://.m.example.com'], + 'http://wwww.example..com' => ['http://wwww.example..com'], + 'http://.www.example.com' => ['http://.www.example.com'], + 'http://example.co-' => ['http://example.co-'], + 'http://example.co-/path' => ['http://example.co-/path'], + 'http:///path' => ['http:///path'], + + // Relative URLs. + '/example.com' => ['/example.com'], + '//example.com::aa' => ['//example.com::aa'], + '//example.com:aa' => ['//example.com:aa'], + '//127.0.0.1:aa/' => ['//127.0.0.1:aa/'], + '//[::1' => ['//[::1'], + '//hello.☎/' => ['//hello.☎/'], + '//:password@symfony.com' => ['//:password@symfony.com'], + '//:password@@symfony.com' => ['//:password@@symfony.com'], + '//username:passwordsymfony.com' => ['//username:passwordsymfony.com'], + '//usern@me:password@symfony.com' => ['//usern@me:password@symfony.com'], + '//example.com/exploit.html?' => ['//example.com/exploit.html?'], + '//example.com/exploit.html?hel lo' => ['//example.com/exploit.html?hel lo'], + '//example.com/exploit.html?not_a%hex' => ['//example.com/exploit.html?not_a%hex'], + '//' => ['//'], + ]; + } + + public static function dataIsBlank(): array + { + return [ + "''" => [''], + "' '" => [' '], + '[]' => [[]], + 'null' => [null], + 'false' => [false], + + // Whitespace characters. + '\x20' => ["\x20"], + '\x09\x09' => ["\x09\x09"], + '\x0A' => ["\x0A"], + '\x0D\x0D' => ["\x0D\x0D"], + '\x00' => ["\x00"], + '\x0B\x0B' => ["\x0B\x0B"], + ]; + } + + public static function dataIsNotBlank(): array + { + return [ + 'foobar' => ['foobar'], + '0' => [0], + '0.0' => [0.0], + "'0'" => ['0'], + '1234' => [1234], + '[1234]' => [[1234]], + 'true' => [true], + ]; + } + + public static function dataIsUUID(): array + { + return [ + '216fff40-98d9-11e3-a5e2-0800200c9a66' => ['216fff40-98d9-11e3-a5e2-0800200c9a66'], + 'e22a9860-8e9f-11ed-95f6-5d3ec56dc459' => ['e22a9860-8e9f-11ed-95f6-5d3ec56dc459'], + ]; + } + + public static function dataIsNotUUID(): array + { + return [ + "' '" => [' '], + "''" => [''], + 'null' => [null], + '{216fff40-98d9-11e3-a5e2-0800200c9a66}' => ['{216fff40-98d9-11e3-a5e2-0800200c9a66}'], + '216fff4098d911e3a5e20800200c9a66' => ['216fff4098d911e3a5e20800200c9a66'], + '216f-ff40-98d9-11e3-a5e2-0800-200c-9a66' => ['216f-ff40-98d9-11e3-a5e2-0800-200c-9a66'], + ]; + } + + public static function dataIsSemver(): array + { + return [ + '0.0.4' => ['0.0.4'], + '1.2.3' => ['1.2.3'], + '10.20.30' => ['10.20.30'], + '1.1.2-prerelease+meta' => ['1.1.2-prerelease+meta'], + '1.1.2+meta' => ['1.1.2+meta'], + '1.1.2+meta-valid' => ['1.1.2+meta-valid'], + '1.0.0-alpha' => ['1.0.0-alpha'], + '1.0.0-beta' => ['1.0.0-beta'], + '1.0.0-alpha.beta' => ['1.0.0-alpha.beta'], + '1.0.0-alpha.beta.1' => ['1.0.0-alpha.beta.1'], + '1.0.0-alpha.1' => ['1.0.0-alpha.1'], + '1.0.0-alpha0.valid' => ['1.0.0-alpha0.valid'], + '1.0.0-alpha.0valid' => ['1.0.0-alpha.0valid'], + '1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' => ['1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay'], + '1.0.0-rc.1+build.1' => ['1.0.0-rc.1+build.1'], + '2.0.0-rc.1+build.123' => ['2.0.0-rc.1+build.123'], + '1.2.3-beta' => ['1.2.3-beta'], + '10.2.3-DEV-SNAPSHOT' => ['10.2.3-DEV-SNAPSHOT'], + '1.2.3-SNAPSHOT-123' => ['1.2.3-SNAPSHOT-123'], + '1.0.0' => ['1.0.0'], + '2.0.0' => ['2.0.0'], + '1.1.7' => ['1.1.7'], + '2.0.0+build.1848' => ['2.0.0+build.1848'], + '2.0.1-alpha.1227' => ['2.0.1-alpha.1227'], + '1.0.0-alpha+beta' => ['1.0.0-alpha+beta'], + '1.2.3----RC-SNAPSHOT.12.9.1--.12+788' => ['1.2.3----RC-SNAPSHOT.12.9.1--.12+788'], + '1.2.3----R-S.12.9.1--.12+meta' => ['1.2.3----R-S.12.9.1--.12+meta'], + '1.2.3----RC-SNAPSHOT.12.9.1--.12' => ['1.2.3----RC-SNAPSHOT.12.9.1--.12'], + '1.0.0+0.build.1-rc.10000aaa-kk-0.1' => ['1.0.0+0.build.1-rc.10000aaa-kk-0.1'], + '99999999999999999999999.999999999999999999.99999999999999999' => ['99999999999999999999999.999999999999999999.99999999999999999'], + '1.0.0-0A.is.legal' => ['1.0.0-0A.is.legal'], + + // With v* prefix + 'v0.0.4' => ['v0.0.4'], + 'v1.2.3' => ['v1.2.3'], + 'v10.20.30' => ['v10.20.30'], + 'v1.1.2-prerelease+meta' => ['v1.1.2-prerelease+meta'], + 'v1.1.2+meta' => ['v1.1.2+meta'], + 'v1.1.2+meta-valid' => ['v1.1.2+meta-valid'], + 'v1.0.0-alpha' => ['v1.0.0-alpha'], + 'v1.0.0-beta' => ['v1.0.0-beta'], + 'v1.0.0-alpha.beta' => ['v1.0.0-alpha.beta'], + 'v1.0.0-alpha.beta.1' => ['v1.0.0-alpha.beta.1'], + 'v1.0.0-alpha.1' => ['v1.0.0-alpha.1'], + 'v1.0.0-alpha0.valid' => ['v1.0.0-alpha0.valid'], + 'v1.0.0-alpha.0valid' => ['v1.0.0-alpha.0valid'], + 'v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay' => ['v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay'], + 'v1.0.0-rc.1+build.1' => ['v1.0.0-rc.1+build.1'], + 'v2.0.0-rc.1+build.123' => ['v2.0.0-rc.1+build.123'], + 'v1.2.3-beta' => ['v1.2.3-beta'], + 'v10.2.3-DEV-SNAPSHOT' => ['v10.2.3-DEV-SNAPSHOT'], + 'v1.2.3-SNAPSHOT-123' => ['v1.2.3-SNAPSHOT-123'], + 'v1.0.0' => ['v1.0.0'], + 'v2.0.0' => ['v2.0.0'], + 'v1.1.7' => ['v1.1.7'], + 'v2.0.0+build.1848' => ['v2.0.0+build.1848'], + 'v2.0.1-alpha.1227' => ['v2.0.1-alpha.1227'], + 'v1.0.0-alpha+beta' => ['v1.0.0-alpha+beta'], + 'v1.2.3----RC-SNAPSHOT.12.9.1--.12+788' => ['v1.2.3----RC-SNAPSHOT.12.9.1--.12+788'], + 'v1.2.3----R-S.12.9.1--.12+meta' => ['v1.2.3----R-S.12.9.1--.12+meta'], + 'v1.2.3----RC-SNAPSHOT.12.9.1--.12' => ['v1.2.3----RC-SNAPSHOT.12.9.1--.12'], + 'v1.0.0+0.build.1-rc.10000aaa-kk-0.1' => ['v1.0.0+0.build.1-rc.10000aaa-kk-0.1'], + 'v99999999999999999999999.999999999999999999.99999999999999999' => ['v99999999999999999999999.999999999999999999.99999999999999999'], + 'v1.0.0-0A.is.legal' => ['v1.0.0-0A.is.legal'], + ]; + } + + public static function dataIsNotSemver(): array + { + return [ + '1' => ['1'], + '1.2' => ['1.2'], + '1.2.3-0123' => ['1.2.3-0123'], + '1.2.3-0123.0123' => ['1.2.3-0123.0123'], + '1.1.2+.123' => ['1.1.2+.123'], + '+invalid' => ['+invalid'], + '-invalid' => ['-invalid'], + '-invalid+invalid' => ['-invalid+invalid'], + '-invalid.01' => ['-invalid.01'], + 'alpha' => ['alpha'], + 'alpha.beta' => ['alpha.beta'], + 'alpha.beta.1' => ['alpha.beta.1'], + 'alpha.1' => ['alpha.1'], + 'alpha+beta' => ['alpha+beta'], + 'alpha_beta' => ['alpha_beta'], + 'alpha.' => ['alpha.'], + 'alpha..' => ['alpha..'], + 'beta' => ['beta'], + '1.0.0-alpha_beta' => ['1.0.0-alpha_beta'], + '-alpha.' => ['-alpha.'], + '1.0.0-alpha..' => ['1.0.0-alpha..'], + '1.0.0-alpha..1' => ['1.0.0-alpha..1'], + '1.0.0-alpha...1' => ['1.0.0-alpha...1'], + '1.0.0-alpha....1' => ['1.0.0-alpha....1'], + '1.0.0-alpha.....1' => ['1.0.0-alpha.....1'], + '1.0.0-alpha......1' => ['1.0.0-alpha......1'], + '1.0.0-alpha.......1' => ['1.0.0-alpha.......1'], + '01.1.1' => ['01.1.1'], + '1.01.1' => ['1.01.1'], + '1.1.01' => ['1.1.01'], + '1.2' => ['1.2'], + '1.2.3.DEV' => ['1.2.3.DEV'], + '1.2-SNAPSHOT' => ['1.2-SNAPSHOT'], + '1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788' => ['1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788'], + '1.2-RC-SNAPSHOT' => ['1.2-RC-SNAPSHOT'], + '-1.0.3-gamma+b7718' => ['-1.0.3-gamma+b7718'], + '+justmeta' => ['+justmeta'], + '9.8.7+meta+meta' => ['9.8.7+meta+meta'], + '9.8.7-whatever+meta+meta' => ['9.8.7-whatever+meta+meta'], + '99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12' => ['99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12'], + ]; + } + + public static function dataIsIpAddress(): array + { + return [ + '127.0.0.1' => ['127.0.0.1'], + '1.2.3.4' => ['1.2.3.4'], + '2001:db8:3333:4444:5555:6666:7777:8888' => ['2001:db8:3333:4444:5555:6666:7777:8888'], + '2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF' => ['2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF'], + '::' => ['::'], + '2001:db8::' => ['2001:db8::'], + '::1234:5678' => ['::1234:5678'], + '2001:db8::1234:5678' => ['2001:db8::1234:5678'], + '2001:0db8:0001:0000:0000:0ab9:C0A8:0102' => ['2001:0db8:0001:0000:0000:0ab9:C0A8:0102'], + '2001:db8:3333:4444:5555:6666:1.2.3.4' => ['2001:db8:3333:4444:5555:6666:1.2.3.4'], + '::11.22.33.44' => ['::11.22.33.44'], + '2001:db8::123.123.123.123' => ['2001:db8::123.123.123.123'], + '::1234:5678:91.123.4.56' => ['::1234:5678:91.123.4.56'], + '::1234:5678:1.2.3.4' => ['::1234:5678:1.2.3.4'], + '2001:db8::1234:5678:5.6.7.8' => ['2001:db8::1234:5678:5.6.7.8'], + ]; + } + + public static function dataIsNotIpAddress(): array + { + return [ + "''" => [''], + "' '" => [' '], + 'null' => [null], + 'false' => [false], + 'zero' => [0], + 'float' => [0.1], + 'array' => [[]], + 'object' => [new stdClass()], + '99999999999999999999999' => ['99999999999999999999999'], + '1.2.3.4.5' => ['1.2.3.4.5'], + '1,2,3,4' => ['1,2,3,4'], + '270.0.0.1' => ['270.0.0.1'], + ]; + } + + public static function dataIsUnique(): array + { + return [ + 'sequential' => [[1, 2, 3]], + 'associative' => [['a' => 1, 'b' => 2, 'c' => 3]], + 'multidimensional' => [['a' => 1, 'b' => [1, 2], 'c' => ['a' => 1, 'b' => 2]]], + ]; + } + + public static function dataIsUniqueOptionFields(): array + { + return [ + [ + [ + [ + 'label' => 'foo', + 'latitude' => '1', + 'longitude' => '2', + ], + [ + 'label' => 'bar', + 'latitude' => '3', + 'longitude' => '4', + ], + ], + ['latitude', 'longitude'], + ], + ]; + } + + public static function dataIsNotUnique(): array + { + return [ + 'sequential' => [[1, 2, 2]], // index 1 and 2 are not unique. + 'associative' => [['a' => 1, 'b' => 2, 'c' => 2]], // b and c are not unique. + 'multidimensional' => [['a' => 1, 'b' => [1, 2], 'c' => [1, 2]]], // b and c are not unique. + ]; + } +}