diff --git a/.travis.yml b/.travis.yml
index d94b108..8ca00f3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,4 +20,7 @@ install:
- travis_retry composer update --prefer-dist --no-interaction --prefer-stable --no-suggest
script:
- - composer test
+ - composer test -- --coverage-clover=coverage.xml
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/README.md b/README.md
index 681afdb..222b303 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Apitizer
[![Build Status](https://travis-ci.org/drtheuns/apitizer_php.svg?branch=master)](https://travis-ci.org/drtheuns/apitizer_php)
+[![codecov](https://codecov.io/gh/drtheuns/apitizer_php/branch/master/graph/badge.svg)](https://codecov.io/gh/drtheuns/apitizer_php)
Apitizer is a Laravel library that primarily offers a Query Builder that allows
you to easily create documented API endpoints that are capable of filtering,
diff --git a/composer.json b/composer.json
index 248f95e..84bb905 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,8 @@
"require-dev": {
"phpunit/phpunit": "^8.5",
"orchestra/testbench": "^4.0",
- "mockery/mockery": "^1.3"
+ "mockery/mockery": "^1.3",
+ "nunomaduro/larastan": "^0.5.2"
},
"autoload": {
"psr-4": {
@@ -38,6 +39,7 @@
},
"scripts": {
"test": "./vendor/bin/phpunit",
- "coverage": "./vendor/bin/phpunit --coverage-html coverage"
+ "coverage": "./vendor/bin/phpunit --coverage-html coverage",
+ "analyse": "./vendor/bin/phpstan analyse"
}
}
diff --git a/composer.lock b/composer.lock
index e911a49..099fe3a 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "1029f669ac7e87e8fee5fe897dcac979",
+ "content-hash": "a77599ab6321454c92b5fdae0f35739e",
"packages": [
{
"name": "doctrine/inflector",
@@ -2694,6 +2694,307 @@
}
],
"packages-dev": [
+ {
+ "name": "composer/ca-bundle",
+ "version": "1.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/ca-bundle.git",
+ "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/47fe531de31fca4a1b997f87308e7d7804348f7e",
+ "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-pcre": "*",
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
+ "psr/log": "^1.0",
+ "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\CaBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
+ "keywords": [
+ "cabundle",
+ "cacert",
+ "certificate",
+ "ssl",
+ "tls"
+ ],
+ "time": "2020-01-13T10:02:55+00:00"
+ },
+ {
+ "name": "composer/composer",
+ "version": "1.9.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/composer.git",
+ "reference": "1291a16ce3f48bfdeca39d64fca4875098af4d7b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/composer/zipball/1291a16ce3f48bfdeca39d64fca4875098af4d7b",
+ "reference": "1291a16ce3f48bfdeca39d64fca4875098af4d7b",
+ "shasum": ""
+ },
+ "require": {
+ "composer/ca-bundle": "^1.0",
+ "composer/semver": "^1.0",
+ "composer/spdx-licenses": "^1.2",
+ "composer/xdebug-handler": "^1.1",
+ "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0",
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0",
+ "seld/jsonlint": "^1.4",
+ "seld/phar-utils": "^1.0",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0",
+ "symfony/finder": "^2.7 || ^3.0 || ^4.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0"
+ },
+ "conflict": {
+ "symfony/console": "2.8.38"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7",
+ "phpunit/phpunit-mock-objects": "^2.3 || ^3.0"
+ },
+ "suggest": {
+ "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+ "ext-zip": "Enabling the zip extension allows you to unzip archives",
+ "ext-zlib": "Allow gzip compression of HTTP requests"
+ },
+ "bin": [
+ "bin/composer"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\": "src/Composer"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
+ "homepage": "https://getcomposer.org/",
+ "keywords": [
+ "autoload",
+ "dependency",
+ "package"
+ ],
+ "time": "2020-02-04T11:58:49+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "1.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+ "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.5 || ^5.0.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "time": "2020-01-13T12:06:48+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "0c3e51e1880ca149682332770e25977c70cf9dae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae",
+ "reference": "0c3e51e1880ca149682332770e25977c70cf9dae",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Spdx\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "SPDX licenses list and validation library.",
+ "keywords": [
+ "license",
+ "spdx",
+ "validator"
+ ],
+ "time": "2020-02-14T07:44:31+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "cbe23383749496fe0f373345208b79568e4bc248"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/cbe23383749496fe0f373345208b79568e4bc248",
+ "reference": "cbe23383749496fe0f373345208b79568e4bc248",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "time": "2019-11-06T16:40:04+00:00"
+ },
{
"name": "doctrine/instantiator",
"version": "1.3.0",
@@ -2848,6 +3149,72 @@
],
"time": "2016-01-20T08:20:44+00:00"
},
+ {
+ "name": "justinrainbow/json-schema",
+ "version": "5.2.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/justinrainbow/json-schema.git",
+ "reference": "44c6787311242a979fa15c704327c20e7221a0e4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4",
+ "reference": "44c6787311242a979fa15c704327c20e7221a0e4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+ "json-schema/json-schema-test-suite": "1.2.0",
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "bin": [
+ "bin/validate-json"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "JsonSchema\\": "src/JsonSchema/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bruno Prieto Reis",
+ "email": "bruno.p.reis@gmail.com"
+ },
+ {
+ "name": "Justin Rainbow",
+ "email": "justin.rainbow@gmail.com"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Robert Schönthal",
+ "email": "seroscho@googlemail.com"
+ }
+ ],
+ "description": "A library to validate a json schema.",
+ "homepage": "https://github.com/justinrainbow/json-schema",
+ "keywords": [
+ "json",
+ "schema"
+ ],
+ "time": "2019-09-25T14:49:45+00:00"
+ },
{
"name": "mockery/mockery",
"version": "1.3.1",
@@ -2961,6 +3328,78 @@
],
"time": "2019-12-15T19:12:40+00:00"
},
+ {
+ "name": "nunomaduro/larastan",
+ "version": "v0.5.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nunomaduro/larastan.git",
+ "reference": "a4cc1d5861123ea4c6b5c966a092063fb70a144b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/a4cc1d5861123ea4c6b5c966a092063fb70a144b",
+ "reference": "a4cc1d5861123ea4c6b5c966a092063fb70a144b",
+ "shasum": ""
+ },
+ "require": {
+ "composer/composer": "^1.0",
+ "ext-json": "*",
+ "illuminate/console": "^6.0 || ^7.0",
+ "illuminate/container": "^6.0 || ^7.0",
+ "illuminate/contracts": "^6.0 || ^7.0",
+ "illuminate/database": "^6.0 || ^7.0",
+ "illuminate/http": "^6.0 || ^7.0",
+ "illuminate/pipeline": "^6.0 || ^7.0",
+ "illuminate/support": "^6.0 || ^7.0",
+ "mockery/mockery": "^0.9 || ^1.0",
+ "php": "^7.2",
+ "phpstan/phpstan": "^0.12",
+ "symfony/process": "^4.3 || ^5.0"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^4.0 || ^5.0",
+ "phpunit/phpunit": "^7.3 || ^8.2"
+ },
+ "suggest": {
+ "orchestra/testbench": "^4.0 || ^5.0"
+ },
+ "type": "phpstan-extension",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "NunoMaduro\\Larastan\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan wrapper for Laravel",
+ "keywords": [
+ "PHPStan",
+ "code analyse",
+ "code analysis",
+ "larastan",
+ "laravel",
+ "package",
+ "php",
+ "static analysis"
+ ],
+ "time": "2020-02-10T12:49:03+00:00"
+ },
{
"name": "orchestra/testbench",
"version": "v4.5.0",
@@ -3392,6 +3831,45 @@
],
"time": "2019-12-22T21:05:45+00:00"
},
+ {
+ "name": "phpstan/phpstan",
+ "version": "0.12.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "ca5f2b7cf81c6d8fba74f9576970399c5817e03b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ca5f2b7cf81c6d8fba74f9576970399c5817e03b",
+ "reference": "ca5f2b7cf81c6d8fba74f9576970399c5817e03b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.12-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "time": "2020-02-16T14:00:29+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "7.0.10",
@@ -4342,6 +4820,149 @@
"homepage": "https://github.com/sebastianbergmann/version",
"time": "2016-10-03T07:35:21+00:00"
},
+ {
+ "name": "seld/jsonlint",
+ "version": "1.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e2e5d290e4d2a4f0eb449f510071392e00e10d19",
+ "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ },
+ "bin": [
+ "bin/jsonlint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Seld\\JsonLint\\": "src/Seld/JsonLint/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "JSON Linter",
+ "keywords": [
+ "json",
+ "linter",
+ "parser",
+ "validator"
+ ],
+ "time": "2019-10-24T14:27:39+00:00"
+ },
+ {
+ "name": "seld/phar-utils",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/phar-utils.git",
+ "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8800503d56b9867d43d9c303b9cbcc26016e82f0",
+ "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Seld\\PharUtils\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ }
+ ],
+ "description": "PHAR file format utilities, for when PHP phars you up",
+ "keywords": [
+ "phar"
+ ],
+ "time": "2020-02-14T15:25:33+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v4.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "266c9540b475f26122b61ef8b23dd9198f5d1cfd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/266c9540b475f26122b61ef8b23dd9198f5d1cfd",
+ "reference": "266c9540b475f26122b61ef8b23dd9198f5d1cfd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "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": "Symfony Filesystem Component",
+ "homepage": "https://symfony.com",
+ "time": "2020-01-21T08:20:44+00:00"
+ },
{
"name": "theseer/tokenizer",
"version": "1.1.3",
diff --git a/config/apitizer.php b/config/apitizer.php
index 665966a..1d8f20a 100644
--- a/config/apitizer.php
+++ b/config/apitizer.php
@@ -23,6 +23,14 @@
*/
'route_prefix' => 'apidoc',
+ /*
+ * Common validation settings.
+ */
+ 'validation' => [
+ 'date_format' => 'Y-m-d',
+ 'datetime_format' => DATE_ATOM,
+ ],
+
/*
* Register the query builders of this project.
*/
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..17286bc
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,15 @@
+includes:
+ - ./vendor/nunomaduro/larastan/extension.neon
+parameters:
+ paths:
+ - src
+ # We can only starts increasing this number once we support only PHP 7.4+ due
+ # to the type hints on the return types in several builders:
+ # https://www.php.net/manual/en/language.oop5.variance.php
+ level: 8
+ ignoreErrors:
+ - '#Unsafe usage of new static#'
+ excludes_analyse:
+ # This class does string-building with class-strings, which is more hassle
+ # to type-hint and annotate than it's worth.
+ - src/Apitizer/Support/ClassFilter.php
diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php
new file mode 100644
index 0000000..45d71bf
--- /dev/null
+++ b/resources/lang/en/validation.php
@@ -0,0 +1,57 @@
+ 'must be a "true" boolean value: yes, on, 1, or true',
+ 'active_url' => 'must have a valid A or AAAA DNS record',
+ 'after' => 'must be after :date',
+ 'after_or_equal' => 'must be equal to or after :date',
+ 'alpha' => 'must consist entirely of alphabetic characters',
+ 'alpha_dash' => 'must consist entirely of alpha-numeric characters',
+ 'before' => 'must be before :date',
+ 'before_or_equal' => 'must be equal to or before :date',
+ 'between' => 'must be between :min and :max',
+ 'confirmed' => 'There must be a field :field with the same value',
+ 'date_equals' => 'The date must be equal to :date',
+ 'different' => 'must be different from the value in field :field',
+ 'digits' => 'must be exactly :length digits long',
+ 'digits_between' => 'must be between :min and :max digits long',
+ 'dimensions' => 'The image must abide by the constraints',
+ 'distinct' => 'The array may not contain duplicate values',
+ 'email' => 'must be valid email',
+ 'ends_with' => 'must end with one of',
+ 'exists' => 'must exist on the server side',
+ 'image' => 'The file must be an image',
+ 'filled' => 'may not be empty if present',
+ 'gt' => 'must be greater than the value in :field',
+ 'gte' => 'must be greater than or equal to the value in :field',
+ 'lt' => 'must be less than the value in :field',
+ 'lte' => 'must be less than or equal to the value in :field',
+ 'in' => 'must be one of',
+ 'in_array' => 'must be in the array of field :field',
+ 'ip' => 'must be a valid IP address',
+ 'ipv4' => 'must be a valid IPv4 address',
+ 'ipv6' => 'must be a valid IPv6 address',
+ 'json' => 'must be a valid json string',
+ 'max' => 'must be less than or equal to :max',
+ 'min' => 'must be greater than or equal to :min',
+ 'mimetypes' => 'The file must have one of mimetypes',
+ 'mimes' => 'The file must have one of mimes',
+ 'not_in' => 'must not be included in: :values',
+ 'not_regex' => 'must not match the regex: :regex',
+ 'nullable' => 'may be nullable',
+ 'present' => 'The field must be present',
+ 'regex' => 'must match the regex: :regex',
+ 'required_unless' => 'is required unless :field has value: :value',
+ 'required_if' => 'The field is required if: :reason',
+ 'required_with' => 'The field is required only if :fields are present',
+ 'required_with_all' => 'The field is required only if all of :fields are present',
+ 'required_without' => 'The field is required only when :fields are not present',
+ 'required_without_all' => 'The field is required only when :fields are not present',
+ 'same' => 'must be the same as the value of field :field',
+ 'size' => 'must have a matching size with :size',
+ 'starts_with' => 'must start with one of',
+ 'timezone' => 'must be a valid timezone (e.g. Europe/Amsterdam)',
+ 'unique' => 'must be a unique resource on the server',
+ 'url' => 'must be a valid url',
+ 'uuid' => 'must be a valid UUID',
+];
diff --git a/resources/views/documentation.blade.php b/resources/views/documentation.blade.php
index a3e163d..89e8ca2 100644
--- a/resources/views/documentation.blade.php
+++ b/resources/views/documentation.blade.php
@@ -37,6 +37,7 @@
--separator-color: #dedede;
--link-color: #1f5688;
--link-hover-background-color: var(--darken);
+ --button-hover-color: var(--darken);
--sidebar-width: 300px;
--sidebar-background-color: var(--primary-color);
@@ -193,10 +194,10 @@
.attributes-title, assoc-title {
font-size: 18px;
}
- .attribute-name, .assoc-name {
+ .attribute-name, .assoc-name, .object-field-name {
font-weight: 700;
}
- .attribute-type, .attribute-name {
+ .attribute-type, .attribute-name, .object-field-name, .object-field-type {
font-family: monospace;
}
.attribute {
@@ -221,6 +222,12 @@
.topic-content > .topic-section ~ .topic-section {
margin-top: 40px;
}
+ .endpoints-title {
+ font-size: 22px;
+ }
+ .endpoint-name {
+ font-size: 18px;
+ }
.menu {
position: fixed;
top: 0;
@@ -288,6 +295,54 @@
cursor: pointer;
background-color: var(--lighten-lg);
}
+ .object-rules {
+ border-radius: 5px;
+ margin-top: 10px;
+ }
+ .object-rules .object-rules {
+ border: 1px solid black;
+ padding: 10px;
+ }
+ .object-rules .object-meta {
+ display: inline-block;
+ padding: 10px 0;
+ font-size: 14px;
+ }
+ .object-field-name.required::after {
+ content: '*';
+ color: red;
+ }
+ .validation-rules {
+ list-style: inside;
+ margin-left: 20px;
+ }
+ .tabs .tab-content {
+ display: none;
+ }
+ .tabs .tab [type="radio"]:checked ~ .tab-content {
+ display: initial;
+ }
+ .tabs [type="radio"] {
+ display: none;
+ }
+ .tabs .tab-toggle {
+ padding: 10px;
+ border: 1px solid var(--separator-color);
+ margin: 0;
+ flex-grow: 1;
+ }
+ .tabs .tab-toggle:hover {
+ background-color: var(--button-hover-color);
+ cursor: pointer;
+ }
+ .tabs .tab-toggle ~ .tab-toggle {
+ margin-left: -1px;
+ }
+ .tab-headings {
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ }
@media (min-width: 768px) {
.content {
diff --git a/resources/views/resource_section.blade.php b/resources/views/resource_section.blade.php
index 99008d1..69c8d41 100644
--- a/resources/views/resource_section.blade.php
+++ b/resources/views/resource_section.blade.php
@@ -88,5 +88,34 @@
@endif
+ @if ($doc->hasRules())
+ Validation
+ @foreach ($doc->getValidationBuilders() as $actionName => $builder)
+ {{ $doc->humanizeActionName($actionName) }}
+
+interface {{ $resourceName }}{{ $actionName }} {{ \Apitizer\Support\TsViewHelper::printObject($builder, $depth) }} +diff --git a/resources/views/ts_object.blade.php b/resources/views/ts_object.blade.php new file mode 100644 index 0000000..d501c2c --- /dev/null +++ b/resources/views/ts_object.blade.php @@ -0,0 +1,5 @@ +{ +@foreach ($builder->getChildren() as $field) +{{ str_repeat(' ', $depth) }}{{ $field->getFieldName() }}@if (!$field->isRequired())?@endif: {{ \Apitizer\Support\TsViewHelper::printableType($field, $depth) }}; +@endforeach +{{ str_repeat(' ', $depth - 1) }}} \ No newline at end of file diff --git a/resources/views/validation_field.blade.php b/resources/views/validation_field.blade.php new file mode 100644 index 0000000..096fb68 --- /dev/null +++ b/resources/views/validation_field.blade.php @@ -0,0 +1,21 @@ +
{$this->min}
",
+ 'max' => "{$this->max}
",
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/ConfirmedRule.php b/src/Apitizer/Validation/Rules/ConfirmedRule.php
new file mode 100644
index 0000000..7901e29
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/ConfirmedRule.php
@@ -0,0 +1,21 @@
+field = $field . '_confirmed';
+ }
+
+ public function getName(): string
+ {
+ return 'confirmed';
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName();
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/Constraint.php b/src/Apitizer/Validation/Rules/Constraint.php
new file mode 100644
index 0000000..e33b516
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/Constraint.php
@@ -0,0 +1,48 @@
+name = $name;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.'. $this->getName());
+ }
+
+ public function getParameters(): array
+ {
+ return [];
+ }
+
+ public function toValidationRule()
+ {
+ return $this->name;
+ }
+
+ public function __toString()
+ {
+ return $this->getDocumentation() ?? '';
+ }
+
+ public function toHtml(): string
+ {
+ return (string) $this;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/DateEqualsRule.php b/src/Apitizer/Validation/Rules/DateEqualsRule.php
new file mode 100644
index 0000000..e5e61d3
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/DateEqualsRule.php
@@ -0,0 +1,11 @@
+format = $format;
+ }
+
+ public function getName(): string
+ {
+ return 'date_format';
+ }
+
+ public function getParameters(): array
+ {
+ return ['format' => $this->format];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.date_format', ['format' => $this->format]);
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ":{$this->format}";
+ }
+
+ public function toHtml(): string
+ {
+ return trans('apitizer::validation.date_format', [
+ 'format' => "{$this->format}
"
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/DateRule.php b/src/Apitizer/Validation/Rules/DateRule.php
new file mode 100644
index 0000000..fb586c8
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/DateRule.php
@@ -0,0 +1,58 @@
+date = $date;
+ $this->format = $format;
+ $this->formatted = $date->format($format);
+ }
+
+ public function getParameters(): array
+ {
+ return [
+ 'date' => $this->date,
+ 'format' => $this->format,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans("apitizer::validation.{$this->getName()}", [
+ 'date' => $this->formatted,
+ ]);
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . $this->formatted;
+ }
+
+ public function toHtml(): string
+ {
+ return trans("apitizer::validation.{$this->getName()}", [
+ 'date' => "{$this->formatted}
",
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/DifferentRule.php b/src/Apitizer/Validation/Rules/DifferentRule.php
new file mode 100644
index 0000000..ab18baf
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/DifferentRule.php
@@ -0,0 +1,11 @@
+min = $min;
+ $this->max = $max;
+ }
+
+ public function getName(): string
+ {
+ return 'digits_between';
+ }
+
+ public function getParameters(): array
+ {
+ return [
+ 'min' => $this->min,
+ 'max' => $this->max,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.digits_between', $this->getParameters());
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . $this->min . ',' . $this->max;
+ }
+
+ public function toHtml(): string
+ {
+ return trans('apitizer::validation.digits_between', [
+ 'min' => "{$this->min}
",
+ 'max' => "{$this->max}
",
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/DigitsRule.php b/src/Apitizer/Validation/Rules/DigitsRule.php
new file mode 100644
index 0000000..f04ade5
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/DigitsRule.php
@@ -0,0 +1,43 @@
+digits = $digits;
+ }
+
+ public function getName(): string
+ {
+ return 'digits';
+ }
+
+ public function getParameters(): array
+ {
+ return ['digits' => $this->digits];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.digits', ['length' => $this->digits]);
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . $this->digits;
+ }
+
+ public function toHtml(): string
+ {
+ return $this->getDocumentation() ?? '';
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/DimensionsRule.php b/src/Apitizer/Validation/Rules/DimensionsRule.php
new file mode 100644
index 0000000..4e2ae0f
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/DimensionsRule.php
@@ -0,0 +1,38 @@
+constraints;
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.dimensions');
+ }
+
+ public function toValidationRule()
+ {
+ return $this;
+ }
+
+ public function toHtml(): string
+ {
+ $list = collect($this->constraints)->map(function ($constraint, $key) {
+ return "{$this->field}
",
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/GtRule.php b/src/Apitizer/Validation/Rules/GtRule.php
new file mode 100644
index 0000000..1fe18eb
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/GtRule.php
@@ -0,0 +1,11 @@
+field = rtrim($field, '.*');
+ }
+
+ public function getName(): string
+ {
+ return 'in_array';
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . $this->field . '.*';
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/InRule.php b/src/Apitizer/Validation/Rules/InRule.php
new file mode 100644
index 0000000..6f4fe17
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/InRule.php
@@ -0,0 +1,40 @@
+ $this->values,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans("apitizer::validation.{$this->getName()}");
+ }
+
+ public function toValidationRule()
+ {
+ return (string) $this;
+ }
+
+ public function toHtml(): string
+ {
+ $values = collect($this->values)->map(function ($value) {
+ return '' . $value . '
';
+ })->implode(', ');
+
+ return $this->getDocumentation() . ': ' . $values;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/LtRule.php b/src/Apitizer/Validation/Rules/LtRule.php
new file mode 100644
index 0000000..6f1e068
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/LtRule.php
@@ -0,0 +1,11 @@
+ $this->size];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.max', [
+ 'max' => $this->suffixUnit($this->size),
+ ]);
+ }
+
+ public function toHtml(): string
+ {
+ return trans('apitizer::validation.max', [
+ 'max' => "{$this->suffixUnit($this->size)}
"
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/MimesRule.php b/src/Apitizer/Validation/Rules/MimesRule.php
new file mode 100644
index 0000000..a4c0b54
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/MimesRule.php
@@ -0,0 +1,50 @@
+mimes = $mimes;
+ }
+
+ public function getName(): string
+ {
+ return 'mimes';
+ }
+
+ public function getParameters(): array
+ {
+ return ['mimes' => $this->mimes];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.mimes');
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . implode(',', $this->mimes);
+ }
+
+ public function toHtml(): string
+ {
+ $values = collect($this->mimes)->map(function ($value) {
+ return '' . $value . '
';
+ })->implode(', ');
+
+ return $this->getDocumentation() . ': ' . $values;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/MimetypesRule.php b/src/Apitizer/Validation/Rules/MimetypesRule.php
new file mode 100644
index 0000000..55bda75
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/MimetypesRule.php
@@ -0,0 +1,50 @@
+mimetypes = $mimetypes;
+ }
+
+ public function getName(): string
+ {
+ return 'mimetypes';
+ }
+
+ public function getParameters(): array
+ {
+ return ['mimetypes' => $this->mimetypes];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.mimetypes');
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . implode(',', $this->mimetypes);
+ }
+
+ public function toHtml(): string
+ {
+ $values = collect($this->mimetypes)->map(function ($value) {
+ return '' . $value . '
';
+ })->implode(', ');
+
+ return $this->getDocumentation() . ': ' . $values;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/MinRule.php b/src/Apitizer/Validation/Rules/MinRule.php
new file mode 100644
index 0000000..9ddbb03
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/MinRule.php
@@ -0,0 +1,30 @@
+ $this->size];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.min', [
+ 'min' => $this->suffixUnit($this->size),
+ ]);
+ }
+
+ public function toHtml(): string
+ {
+ return trans('apitizer::validation.min', [
+ 'min' => "{$this->suffixUnit($this->size)}
"
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/NotInRule.php b/src/Apitizer/Validation/Rules/NotInRule.php
new file mode 100644
index 0000000..1aa9433
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/NotInRule.php
@@ -0,0 +1,11 @@
+regex = trim($regex, '/');
+ }
+
+ public function getName(): string
+ {
+ return 'regex';
+ }
+
+ public function getParameters(): array
+ {
+ return ['regex' => $this->regex];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans("apitizer::validation.{$this->getName()}", $this->getParameters());
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . "/{$this->regex}/";
+ }
+
+ public function toHtml(): string
+ {
+ return trans("apitizer::validation.{$this->getName()}", [
+ 'regex' => "{$this->regex}
",
+ ]);
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/RequiredIfRule.php b/src/Apitizer/Validation/Rules/RequiredIfRule.php
new file mode 100644
index 0000000..4d8cc5f
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/RequiredIfRule.php
@@ -0,0 +1,49 @@
+condition = $condition;
+ $this->explanation = $explanation;
+ }
+
+ public function getName(): string
+ {
+ return 'required_if';
+ }
+
+ public function getParameters(): array
+ {
+ return [];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.required_if', ['reason' => $this->explanation]);
+ }
+
+ public function toValidationRule()
+ {
+ return $this;
+ }
+
+ public function toHtml(): string
+ {
+ return $this->getDocumentation() ?? '';
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/RequiredWithAllRule.php b/src/Apitizer/Validation/Rules/RequiredWithAllRule.php
new file mode 100644
index 0000000..2cddc20
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/RequiredWithAllRule.php
@@ -0,0 +1,11 @@
+fields = $fields;
+ }
+
+ public function getName(): string
+ {
+ return 'required_with';
+ }
+
+ public function getParameters(): array
+ {
+ return ['fields' => $this->fields];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans("apitizer::validation.{$this->getName()}");
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . implode(',', $this->fields);
+ }
+
+ public function toHtml(): string
+ {
+ $values = collect($this->fields)->map(function ($field) {
+ return "$field
";
+ })->implode(', ');
+
+ return $this->getDocumentation() . ': ' . $values;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/RequiredWithoutAllRule.php b/src/Apitizer/Validation/Rules/RequiredWithoutAllRule.php
new file mode 100644
index 0000000..5fbd438
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/RequiredWithoutAllRule.php
@@ -0,0 +1,11 @@
+size = $size;
+ $this->type = $type;
+ }
+
+ public function getName(): string
+ {
+ return 'size';
+ }
+
+ public function getParameters(): array
+ {
+ return [
+ 'size' => $this->size,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.size', [
+ 'size' => $this->suffixUnit($this->size)
+ ]);
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . $this->size;
+ }
+
+ public function toHtml(): string
+ {
+ return trans('apitizer::validation.size', [
+ 'size' => "{$this->suffixUnit($this->size)}
"
+ ]);
+ }
+
+ /**
+ * @param int|float $value
+ */
+ protected function suffixUnit($value): string
+ {
+ $suffix = '';
+
+ switch ($this->type) {
+ case 'string':
+ $suffix = $value === 1 ? 'character' : 'characters';
+ break;
+ case 'file':
+ $suffix = $value === 1 ? 'byte' : 'bytes';
+ break;
+ case 'array':
+ $suffix = $value === 1 ? 'element' : 'elements';
+ }
+
+ $value = (string) $value;
+
+ return empty($suffix) ? $value : "$value $suffix";
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/StartsWithRule.php b/src/Apitizer/Validation/Rules/StartsWithRule.php
new file mode 100644
index 0000000..6f4e889
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/StartsWithRule.php
@@ -0,0 +1,52 @@
+values = $values;
+ }
+
+ public function getName(): string
+ {
+ return 'starts_with';
+ }
+
+ public function getParameters(): array
+ {
+ return [
+ 'values' => $this->values,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans("apitizer::validation.{$this->getName()}");
+ }
+
+ public function toValidationRule()
+ {
+ return $this->getName() . ':' . implode(',', $this->values);
+ }
+
+ public function toHtml(): string
+ {
+ $values = collect($this->values)->map(function ($value) {
+ return '' . $value . '
';
+ })->implode(', ');
+
+ return trans("apitizer::validation.{$this->getName()}") . ': ' . $values;
+ }
+}
diff --git a/src/Apitizer/Validation/Rules/UniqueRule.php b/src/Apitizer/Validation/Rules/UniqueRule.php
new file mode 100644
index 0000000..b0d8d92
--- /dev/null
+++ b/src/Apitizer/Validation/Rules/UniqueRule.php
@@ -0,0 +1,37 @@
+ $this->table,
+ 'column' => $this->column,
+ ];
+ }
+
+ public function getDocumentation(): ?string
+ {
+ return trans('apitizer::validation.unique');
+ }
+
+ public function toValidationRule()
+ {
+ return $this;
+ }
+
+ public function toHtml(): string
+ {
+ return $this->getDocumentation() ?? '';
+ }
+}
diff --git a/src/Apitizer/Validation/StringRules.php b/src/Apitizer/Validation/StringRules.php
new file mode 100644
index 0000000..cdd1d1d
--- /dev/null
+++ b/src/Apitizer/Validation/StringRules.php
@@ -0,0 +1,176 @@
+addConstraint('active_url');
+ }
+
+ /**
+ * Validate that the string consists only of alphabetic characters.
+ */
+ public function alpha(): self
+ {
+ return $this->addConstraint('alpha');
+ }
+
+ /**
+ * Validate that the string consists only of alphanumeric characters,
+ * dashes, and underscores.
+ */
+ public function alphaDash(): self
+ {
+ return $this->addConstraint('alpha_dash');
+ }
+
+ /**
+ * Validate that the string consists only of alphanumeric characters.
+ */
+ public function alphaNum(): self
+ {
+ return $this->addConstraint('alpha_num');
+ }
+
+ /**
+ * Validate that the string is numeric consisting of exactly $length digits.
+ */
+ public function digits(int $length): self
+ {
+ return $this->addRule(new DigitsRule($length));
+ }
+
+ /**
+ * Validate that the string is numeric consisting between $min and $max digits.
+ */
+ public function digitsBetween(int $min, int $max): self
+ {
+ return $this->addRule(new DigitsBetweenRule($min, $max));
+ }
+
+ /**
+ * Validate that the string is a valid email according to the
+ * egulias/email-validator package.
+ *
+ * @param string[] $validationStyle the validation styles to apply. One of:
+ *
+ * - rfc: RFCValidation
+ * - strict: NoRFCWarningsValidation
+ * - dns: DNSCheckValidation
+ * - spoof: SpoofCheckValidation
+ * - filter: FilterEmailValidation
+ */
+ public function email(array $validationStyle = ['rfc']): self
+ {
+ return $this->addRule(new EmailRule($validationStyle));
+ }
+
+ /**
+ * Validate that the string ends with one of the given values.
+ *
+ * @param string[] $values
+ *
+ * @see \Illuminate\Support\Str::endsWith
+ */
+ public function endsWith(array $values): self
+ {
+ return $this->addRule(new EndsWithRule($values));
+ }
+
+ /**
+ * Validate that the string is a valid IP address.
+ */
+ public function ip(): self
+ {
+ return $this->addConstraint('ip');
+ }
+
+ /**
+ * Validate that the string is a valid IPv4 address.
+ */
+ public function ipv4(): self
+ {
+ return $this->addConstraint('ipv4');
+ }
+
+ /**
+ * Validate that the string is a valid IPv6 address.
+ */
+ public function ipv6(): self
+ {
+ return $this->addConstraint('ipv6');
+ }
+
+ /**
+ * Validate that the string is a valid json string.
+ */
+ public function json(): self
+ {
+ return $this->addConstraint('json');
+ }
+
+ /**
+ * Validate that the string is numeric.
+ */
+ public function numeric(): self
+ {
+ return $this->addConstraint('numeric');
+ }
+
+ /**
+ * Validate that the string starts with one of the given values.
+ *
+ * @param string[] $values
+ *
+ * @see \Illuminate\Support\Str::startsWith
+ */
+ public function startsWith(array $values): self
+ {
+ return $this->addRule(new StartsWithRule($values));
+ }
+
+ /**
+ * Validate that the string is a valid timezone.
+ *
+ * @see \DateTimeZone::listIdentifiers
+ */
+ public function timezone(): self
+ {
+ return $this->addConstraint('timezone');
+ }
+
+ /**
+ * Validate that the string is a valid url.
+ */
+ public function url(): self
+ {
+ return $this->addConstraint('url');
+ }
+
+ /**
+ * Validate that the string is a valid UUID
+ */
+ public function uuid(): self
+ {
+ return $this->addConstraint('uuid');
+ }
+
+ public function getType(): string
+ {
+ return 'string';
+ }
+}
diff --git a/src/Apitizer/Validation/TypedRuleBuilder.php b/src/Apitizer/Validation/TypedRuleBuilder.php
new file mode 100644
index 0000000..6cbcb89
--- /dev/null
+++ b/src/Apitizer/Validation/TypedRuleBuilder.php
@@ -0,0 +1,53 @@
+
+ */
+ public function getParameters(): array;
+
+ /**
+ * Convert the rule to a validation rule that Laravel's Validator can
+ * understand.
+ *
+ * @return string|Rule
+ */
+ public function toValidationRule();
+
+ /**
+ * Renders the HTML that will be used by the generated documentation.
+ *
+ * This should be kept to a minimum to ensure maximum compatibility with
+ * custom rendered documentation pages. It should therefore not contain
+ * anything other than plain inline-tags without additional attributes.
+ */
+ public function toHtml(): string;
+}
diff --git a/tests/Support/Builders/CommentBuilder.php b/tests/Support/Builders/CommentBuilder.php
index f30bd85..956a54d 100644
--- a/tests/Support/Builders/CommentBuilder.php
+++ b/tests/Support/Builders/CommentBuilder.php
@@ -2,11 +2,10 @@
namespace Tests\Support\Builders;
-use Apitizer\QueryBuilder;
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\Models\Comment;
-class CommentBuilder extends QueryBuilder
+class CommentBuilder extends EmptyBuilder
{
public function fields(): array
{
diff --git a/tests/Support/Builders/EmptyBuilder.php b/tests/Support/Builders/EmptyBuilder.php
index f791e72..d35e5de 100644
--- a/tests/Support/Builders/EmptyBuilder.php
+++ b/tests/Support/Builders/EmptyBuilder.php
@@ -3,6 +3,7 @@
namespace Tests\Support\Builders;
use Apitizer\QueryBuilder;
+use Apitizer\Validation\Rules;
use Tests\Feature\Models\User;
use Illuminate\Database\Eloquent\Model;
@@ -27,4 +28,8 @@ public function model(): Model
{
return new User();
}
+
+ public function rules(Rules $rules)
+ {
+ }
}
diff --git a/tests/Support/Builders/PostBuilder.php b/tests/Support/Builders/PostBuilder.php
index aee631f..7c0054b 100644
--- a/tests/Support/Builders/PostBuilder.php
+++ b/tests/Support/Builders/PostBuilder.php
@@ -2,12 +2,11 @@
namespace Tests\Support\Builders;
-use Apitizer\QueryBuilder;
use Apitizer\Types\Apidoc;
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\Models\Post;
-class PostBuilder extends QueryBuilder
+class PostBuilder extends EmptyBuilder
{
const DESCRIPTION = 'A blog post';
@@ -49,7 +48,7 @@ public function model(): Model
return new Post();
}
- public function apidoc(Apidoc $apidoc)
+ public function apidoc(Apidoc $apidoc): void
{
$apidoc->setDescription(self::DESCRIPTION);
}
diff --git a/tests/Support/Builders/TagBuilder.php b/tests/Support/Builders/TagBuilder.php
index 193a2ab..b94d7fe 100644
--- a/tests/Support/Builders/TagBuilder.php
+++ b/tests/Support/Builders/TagBuilder.php
@@ -2,11 +2,10 @@
namespace Tests\Support\Builders;
-use Apitizer\QueryBuilder;
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\Models\Tag;
-class TagBuilder extends QueryBuilder
+class TagBuilder extends EmptyBuilder
{
public function fields(): array
{
diff --git a/tests/Support/Builders/UserBuilder.php b/tests/Support/Builders/UserBuilder.php
index 22ea144..f60572e 100644
--- a/tests/Support/Builders/UserBuilder.php
+++ b/tests/Support/Builders/UserBuilder.php
@@ -2,11 +2,10 @@
namespace Tests\Support\Builders;
-use Apitizer\QueryBuilder;
use Illuminate\Database\Eloquent\Model;
use Tests\Feature\Models\User;
-class UserBuilder extends QueryBuilder
+class UserBuilder extends EmptyBuilder
{
public function fields(): array
{
diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php
index 2b214d3..1488d1e 100644
--- a/tests/Unit/TestCase.php
+++ b/tests/Unit/TestCase.php
@@ -4,4 +4,11 @@
class TestCase extends \Tests\TestCase
{
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ // Ensure translations are available in our tests
+ $this->app['translator']->addNamespace('apitizer', __DIR__.'/../../resources/lang');
+ }
}
diff --git a/tests/Unit/Validation/ArrayRulesTest.php b/tests/Unit/Validation/ArrayRulesTest.php
new file mode 100644
index 0000000..cf6657c
--- /dev/null
+++ b/tests/Unit/Validation/ArrayRulesTest.php
@@ -0,0 +1,83 @@
+assertRules([
+ 't1' => ['array', 'distinct'],
+ ], function (ObjectRules $rules) {
+ $rules->array('t1')->distinct();
+ });
+ }
+
+ /** @test */
+ public function it_nests_validation_rules()
+ {
+ $this->assertRules([
+ 't1' => ['required', 'array'],
+ 't1.*' => ['string', 'uuid'],
+ 't2' => ['array'],
+ 't2.*' => ['array'],
+ 't2.*.*' => ['string'],
+ 't3' => ['array'],
+ 't3.*' => [],
+ 't3.*.id' => ['string', 'uuid'],
+ 't3.*.tags' => ['array'],
+ 't3.*.tags.*' => ['string', 'uuid'],
+ ], function (ObjectRules $rules) {
+ $rules->array('t1')->required()->whereEach()->uuid();
+ $rules->array('t2')->whereEach()->array()->whereEach()->string();
+ $rules->array('t3')->whereEach()->object(function (ObjectRules $rules) {
+ $rules->uuid('id');
+ $rules->array('tags')->whereEach()->uuid();
+ });
+ });
+ }
+
+ /** @test */
+ public function it_supports_all_types_for_the_array_elements()
+ {
+ $this->assertRules([
+ 't1' => ['array'],
+ 't1.*' => ['string'],
+ 't2' => ['array'],
+ 't2.*' => ['string', 'uuid'],
+ 't3' => ['array'],
+ 't3.*' => ['boolean'],
+ 't4' => ['array'],
+ 't4.*' => ['date'],
+ 't5' => ['array'],
+ 't5.*' => ['date_format:' . DATE_ATOM],
+ 't6' => ['array'],
+ 't6.*' => ['numeric'],
+ 't7' => ['array'],
+ 't7.*' => ['integer'],
+ 't8' => ['array'],
+ 't8.*' => ['file'],
+ 't9' => ['array'],
+ 't9.*' => ['file', 'image'],
+ 't10' => ['array'],
+ 't10.*' => [],
+ 't10.*.name' => ['string'],
+ ], function (ObjectRules $rules) {
+ $rules->array('t1')->whereEach()->string();
+ $rules->array('t2')->whereEach()->uuid();
+ $rules->array('t3')->whereEach()->boolean();
+ $rules->array('t4')->whereEach()->date();
+ $rules->array('t5')->whereEach()->datetime();
+ $rules->array('t6')->whereEach()->number();
+ $rules->array('t7')->whereEach()->integer();
+ $rules->array('t8')->whereEach()->file();
+ $rules->array('t9')->whereEach()->image();
+ $rules->array('t10')->whereEach()->object(function (ObjectRules $rules) {
+ $rules->string('name');
+ });
+ });
+ }
+}
diff --git a/tests/Unit/Validation/BooleanRulesTest.php b/tests/Unit/Validation/BooleanRulesTest.php
new file mode 100644
index 0000000..56b905d
--- /dev/null
+++ b/tests/Unit/Validation/BooleanRulesTest.php
@@ -0,0 +1,20 @@
+assertRules([
+ 't1' => ['required', 'boolean'],
+ 't2' => ['boolean', 'accepted'],
+ ], function (ObjectRules $rules) {
+ $rules->boolean('t1')->required();
+ $rules->boolean('t2')->accepted();
+ });
+ }
+}
diff --git a/tests/Unit/Validation/CommonRulesTest.php b/tests/Unit/Validation/CommonRulesTest.php
new file mode 100644
index 0000000..c71b891
--- /dev/null
+++ b/tests/Unit/Validation/CommonRulesTest.php
@@ -0,0 +1,62 @@
+assertRules([
+ 't1' => ['string', 'required_with:t1,t2'],
+ 't2' => ['string', 'required_with_all:t1,t2'],
+ 't3' => ['string', 'required_without:t1,t2'],
+ 't4' => ['string', 'required_without_all:t1,t2'],
+ 't5' => ['string', 'nullable'],
+ 't6' => ['string', 'size:50'],
+ 't7' => ['string', 'max:50'],
+ 't8' => ['string', 'min:20'],
+ 't9' => ['string', 'bail'],
+ 't10' => ['string', 'confirmed'],
+ 't11' => ['string', 'between:5,10'],
+ 't12' => ['string', 'different:t1'],
+ 't13' => ['string', 'same:t1'],
+ 't14' => ['string', 'regex:/hello/'],
+ 't15' => ['string', 'not_regex:/hello/'],
+ 't16' => ['string', 'gt:t1'],
+ 't17' => ['string', 'gte:t1'],
+ 't18' => ['string', 'lt:t1'],
+ 't19' => ['string', 'lte:t1'],
+ 't20' => ['string', 'in_array:t1.*'],
+ 't21' => ['string', 'filled'],
+ 't22' => ['string', 'sometimes'],
+ 't23' => ['string', 'present'],
+ ], function (ObjectRules $rules) {
+ $rules->string('t1')->requiredWith(['t1', 't2']);
+ $rules->string('t2')->requiredWithAll(['t1', 't2']);
+ $rules->string('t3')->requiredWithout(['t1', 't2']);
+ $rules->string('t4')->requiredWithoutAll(['t1', 't2']);
+ $rules->string('t5')->nullable();
+ $rules->string('t6')->size(50);
+ $rules->string('t7')->max(50);
+ $rules->string('t8')->min(20);
+ $rules->string('t9')->bail();
+ $rules->string('t10')->confirmed();
+ $rules->string('t11')->between(5, 10);
+ $rules->string('t12')->different('t1');
+ $rules->string('t13')->same('t1');
+ $rules->string('t14')->regex('/hello/');
+ $rules->string('t15')->notRegex('/hello/');
+ $rules->string('t16')->gt('t1');
+ $rules->string('t17')->gte('t1');
+ $rules->string('t18')->lt('t1');
+ $rules->string('t19')->lte('t1');
+ $rules->string('t20')->inArray('t1');
+ $rules->string('t21')->filled();
+ $rules->string('t22')->sometimes();
+ $rules->string('t23')->present();
+ });
+ }
+}
diff --git a/tests/Unit/Validation/DateRulesTest.php b/tests/Unit/Validation/DateRulesTest.php
new file mode 100644
index 0000000..7f69118
--- /dev/null
+++ b/tests/Unit/Validation/DateRulesTest.php
@@ -0,0 +1,87 @@
+assertRules([
+ 't1' => ['date'],
+ 't2' => ['date_format:' . DATE_ATOM],
+ ], function (ObjectRules $rules) {
+ $rules->date('t1');
+ $rules->datetime('t2');
+ });
+ }
+
+ /** @test */
+ public function it_validates_after()
+ {
+ $tomorrow = Carbon::tomorrow();
+
+ $this->assertRules([
+ 't1' => ['date', 'after:' . $tomorrow->format('Y-m-d')],
+ 't2' => ['date_format:'.DATE_ATOM, 'after:' . $tomorrow->format(DATE_ATOM)],
+ ], function (ObjectRules $rules) use ($tomorrow) {
+ $rules->date('t1')->after($tomorrow);
+ $rules->datetime('t2')->after($tomorrow);
+ });
+ }
+
+ /** @test */
+ public function it_validates_after_or_equal()
+ {
+ $tomorrow = Carbon::tomorrow();
+
+ $this->assertRules([
+ 't1' => ['date', 'after_or_equal:' . $tomorrow->format('Y-m-d')],
+ 't2' => ['date_format:' . DATE_ATOM, 'after_or_equal:' . $tomorrow->format(DATE_ATOM)],
+ ], function (ObjectRules $rules) use ($tomorrow) {
+ $rules->date('t1')->afterOrEqual($tomorrow);
+ $rules->datetime('t2')->afterOrEqual($tomorrow);
+ });
+ }
+
+ /** @test */
+ public function it_validates_before()
+ {
+ $tomorrow = Carbon::tomorrow();
+
+ $this->assertRules([
+ 't1' => ['date', 'before:' . $tomorrow->format('Y-m-d')],
+ 't2' => ['date_format:' . DATE_ATOM, 'before:' . $tomorrow->format(DATE_ATOM)],
+ ], function (ObjectRules $rules) use ($tomorrow) {
+ $rules->date('t1')->before($tomorrow);
+ $rules->datetime('t2')->before($tomorrow);
+ });
+ }
+
+ /** @test */
+ public function it_validates_before_or_equal()
+ {
+ $tomorrow = Carbon::tomorrow();
+
+ $this->assertRules([
+ 't1' => ['date', 'before_or_equal:' . $tomorrow->format('Y-m-d')],
+ 't2' => ['date_format:' . DATE_ATOM, 'before_or_equal:' . $tomorrow->format(DATE_ATOM)],
+ ], function (ObjectRules $rules) use ($tomorrow) {
+ $rules->date('t1')->beforeOrEqual($tomorrow);
+ $rules->datetime('t2')->beforeOrEqual($tomorrow);
+ });
+ }
+
+ /** @test */
+ public function it_validates_the_equals_rule()
+ {
+ $this->assertRules([
+ 't1' => ['date', 'date_equals:2020-01-01'],
+ ], function (ObjectRules $rules) {
+ $rules->date('t1')->equals(Carbon::parse('2020-01-01'));
+ });
+ }
+}
diff --git a/tests/Unit/Validation/FileRulesTest.php b/tests/Unit/Validation/FileRulesTest.php
new file mode 100644
index 0000000..733c48a
--- /dev/null
+++ b/tests/Unit/Validation/FileRulesTest.php
@@ -0,0 +1,53 @@
+assertRules([
+ 't1' => ['file'],
+ 't2' => ['file', 'image'],
+ ], function (ObjectRules $rules) {
+ $rules->file('t1');
+ $rules->image('t2');
+ });
+ }
+
+ /** @test */
+ public function it_validates_mimetypes()
+ {
+ $this->assertRules([
+ 't1' => ['file', 'mimetypes:image/jpeg'],
+ 't2' => ['file', 'mimes:jpeg,csv'],
+ ], function (ObjectRules $rules) {
+ $rules->file('t1')->mimetypes(['image/jpeg']);
+ $rules->file('t2')->mimes(['jpeg', 'csv']);
+ });
+ }
+
+ /** @test */
+ public function it_validates_dimensions()
+ {
+ $dimensions = (new DimensionsRule())
+ ->height(100)
+ ->width(200)
+ ->maxHeight(300)
+ ->maxWidth(400)
+ ->ratio(1 / 2);
+
+ $this->assertRules([
+ 't1' => [
+ 'file', 'image',
+ 'dimensions:height=100,width=200,max_height=300,max_width=400,ratio=0.5'
+ ]
+ ], function (ObjectRules $rules) use ($dimensions) {
+ $rules->image('t1')->dimensions($dimensions);
+ });
+ }
+}
diff --git a/tests/Unit/Validation/NumberRulesTest.php b/tests/Unit/Validation/NumberRulesTest.php
new file mode 100644
index 0000000..c711e7b
--- /dev/null
+++ b/tests/Unit/Validation/NumberRulesTest.php
@@ -0,0 +1,44 @@
+assertRules([
+ 't1' => ['numeric'],
+ 't2' => ['integer'],
+ ], function (ObjectRules $rules) {
+ $rules->number('t1');
+ $rules->integer('t2');
+ });
+ }
+
+ /** @test */
+ public function it_validates_starts_and_ends_with()
+ {
+ $this->assertRules([
+ 't1' => ['numeric', 'ends_with:name,wow'],
+ 't1' => ['numeric', 'starts_with:name,wow'],
+ ], function (ObjectRules $rules) {
+ $rules->number('t1')->endsWith(['name', 'wow']);
+ $rules->number('t1')->startsWith(['name', 'wow']);
+ });
+ }
+
+ /** @test */
+ public function it_validates_digits()
+ {
+ $this->assertRules([
+ 't1' => ['numeric', 'digits:5'],
+ 't2' => ['numeric', 'digits_between:14,34'],
+ ], function (ObjectRules $rules) {
+ $rules->number('t1')->digits(5);
+ $rules->number('t2')->digitsBetween(14, 34);
+ });
+ }
+}
diff --git a/tests/Unit/Validation/RulesTest.php b/tests/Unit/Validation/RulesTest.php
new file mode 100644
index 0000000..616a9cd
--- /dev/null
+++ b/tests/Unit/Validation/RulesTest.php
@@ -0,0 +1,87 @@
+define('store', function (ObjectRules $rules) {
+ $rules->string('name');
+ });
+
+ $storeRules = $rules->getBuilder('store');
+
+ $this->assertInstanceOf(ObjectRules::class, $storeRules);
+ $this->assertCount(1, $storeRules->getChildren());
+ }
+
+ /** @test */
+ public function it_returns_an_empty_object_when_none_is_defined()
+ {
+ $rules = new Rules();
+ $object = $rules->getBuilder('store');
+
+ $this->assertInstanceOf(ObjectRules::class, $object);
+ $this->assertEmpty($object->getChildren());
+ }
+
+ /** @test */
+ public function it_resolves_all_builders_when_all_rules_are_requested()
+ {
+ $rules = new Rules();
+ $rules->storeRules(function (ObjectRules $builder) {});
+ $rules->updateRules(function (ObjectRules $builder) {});
+
+ $rules = $rules->getValidationRules();
+
+ $this->assertCount(2, $rules);
+ $this->assertEquals([
+ 'store' => [],
+ 'update' => []
+ ], $rules);
+ }
+
+ /** @test */
+ public function it_resolves_a_single_builder_when_only_one_is_requested()
+ {
+ $rules = new Rules();
+ $rules->storeRules(function (ObjectRules $builder) {});
+ $rules->updateRules(function (ObjectRules $builder) {
+ $this->fail("The update rules should not be resolved");
+ });
+
+ $rules->getValidationRulesForAction('store');
+
+ // Pass the test, since "fail" was never called.
+ // We cant use "expectNotToPerformAssertions" because those tests are
+ // removed from coverage.
+ $this->addToAssertionCount(1);
+ }
+
+ /** @test */
+ public function it_only_resolves_rules_once_for_each_action()
+ {
+ $count = 0;
+ $rules = new Rules();
+ $rules->storeRules(function (ObjectRules $builder) use (&$count) {
+ if (++$count > 1) {
+ $this->fail();
+ }
+ });
+
+ // Call it twice.
+ $rules->getValidationRulesForAction('store');
+ $rules->getValidationRulesForAction('store');
+
+ // Pass the test, since "fail" was never called.
+ // We cant use "expectNotToPerformAssertions" because those tests are
+ // removed from coverage.
+ $this->addToAssertionCount(1);
+ }
+}
diff --git a/tests/Unit/Validation/StringRulesTest.php b/tests/Unit/Validation/StringRulesTest.php
new file mode 100644
index 0000000..c758111
--- /dev/null
+++ b/tests/Unit/Validation/StringRulesTest.php
@@ -0,0 +1,52 @@
+assertRules([
+ 't1' => ['string', 'active_url'],
+ 't2' => ['string', 'alpha'],
+ 't3' => ['string', 'alpha_dash'],
+ 't4' => ['string', 'alpha_num'],
+ 't5' => ['string', 'ip'],
+ 't6' => ['string', 'ipv4'],
+ 't7' => ['string', 'ipv6'],
+ 't8' => ['string', 'json'],
+ 't9' => ['string', 'numeric'],
+ 't10' => ['string', 'timezone'],
+ 't11' => ['string', 'url'],
+ 't12' => ['string', 'uuid'],
+ 't13' => ['string', 'digits:5'],
+ 't14' => ['string', 'digits_between:5,10'],
+ 't15' => ['string', 'starts_with:name'],
+ 't16' => ['string', 'ends_with:name'],
+ 't17' => ['string', 'email:rfc'],
+ 't18' => ['string', 'email:spoof,filter,dns'],
+ ], function (ObjectRules $rules) {
+ $rules->string('t1')->activeUrl();
+ $rules->string('t2')->alpha();
+ $rules->string('t3')->alphaDash();
+ $rules->string('t4')->alphaNum();
+ $rules->string('t5')->ip();
+ $rules->string('t6')->ipv4();
+ $rules->string('t7')->ipv6();
+ $rules->string('t8')->json();
+ $rules->string('t9')->numeric();
+ $rules->string('t10')->timezone();
+ $rules->string('t11')->url();
+ $rules->string('t12')->uuid();
+ $rules->string('t13')->digits(5);
+ $rules->string('t14')->digitsBetween(5, 10);
+ $rules->string('t15')->startsWith(['name']);
+ $rules->string('t16')->endsWith(['name']);
+ $rules->string('t17')->email();
+ $rules->string('t18')->email(['spoof', 'filter', 'dns']);
+ });
+ }
+}
diff --git a/tests/Unit/Validation/TestCase.php b/tests/Unit/Validation/TestCase.php
new file mode 100644
index 0000000..d46771b
--- /dev/null
+++ b/tests/Unit/Validation/TestCase.php
@@ -0,0 +1,24 @@
+resolve();
+ $rules = RuleInterpreter::rulesFrom($object);
+
+ PHPUnit::assertEquals(
+ $expected,
+ $rules,
+ "The generated rules did not match the expected rules"
+ );
+ }
+}