diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index fcdbf71d..44d3f8b1 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -101,7 +101,8 @@ jobs: --rm jbzoo/csv-blueprint \ validate:csv \ --csv=/parent-host/tests/fixtures/batch/*.csv \ - --schema=/parent-host/tests/schemas/demo_valid.yml + --schema=/parent-host/tests/schemas/demo_valid.yml \ + --ansi - name: 👎 Invalid CSV file run: | @@ -110,64 +111,5 @@ jobs: --rm jbzoo/csv-blueprint \ validate:csv \ --csv=/parent-host/tests/fixtures/batch/*.csv \ - --schema=/parent-host/tests/schemas/demo_invalid.yml - - - phar: - name: Phar - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.3 - tools: composer - - - name: Build the project - run: make build --no-print-directory - - - name: 👍 Valid CSV file - run: | - ./build/csv-blueprint.phar \ - validate:csv \ - --csv=./tests/fixtures/batch/*.csv \ - --schema=./tests/schemas/demo_valid.yml - - - name: 👎 Invalid CSV file - run: | - ! ./build/csv-blueprint.phar \ - validate:csv \ - --csv=./tests/fixtures/batch/*.csv \ - --schema=./tests/schemas/demo_invalid.yml - - - php: - name: Pure PHP - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.3 - tools: composer - - - name: Build the Project - run: make build-install --no-print-directory - - - name: 👍 Valid CSV file - run: | - ./csv-blueprint \ - validate:csv \ - --csv=./tests/fixtures/batch/*.csv \ - --schema=./tests/schemas/demo_valid.yml - - - name: 👎 Invalid CSV file - run: | - ! ./csv-blueprint \ - validate:csv \ - --csv=./tests/fixtures/batch/*.csv \ - --schema=./tests/schemas/demo_invalid.yml + --schema=/parent-host/tests/schemas/demo_invalid.yml \ + --ansi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89eb356c..4d76443f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -139,8 +139,40 @@ jobs: name: Reports - ${{ matrix.php-version }} path: build/ - phar: - name: Phar + + test-php-binary: + name: Verify PHP binary + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + tools: composer + + - name: Build the Project + run: make build-install --no-print-directory + + - name: 👍 Valid CSV file + run: | + ./csv-blueprint \ + validate:csv \ + --csv=./tests/fixtures/batch/*.csv \ + --schema=./tests/schemas/demo_valid.yml + + - name: 👎 Invalid CSV file + run: | + ! ./csv-blueprint \ + validate:csv \ + --csv=./tests/fixtures/batch/*.csv \ + --schema=./tests/schemas/demo_invalid.yml + + + test-phar: + name: Verify PHAR runs-on: ubuntu-latest strategy: matrix: @@ -148,26 +180,35 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 - with: - fetch-depth: 0 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} - coverage: xdebug tools: composer - extensions: ast - name: Build the project run: make build --no-print-directory - - name: Building Phar binary file - run: make build-phar --no-print-directory - - - name: Trying to use the phar file + - name: Test help and logo run: ./build/csv-blueprint.phar + - name: 👍 Valid CSV file + run: | + ./build/csv-blueprint.phar \ + validate:csv \ + --csv=./tests/fixtures/batch/*.csv \ + --schema=./tests/schemas/demo_valid.yml \ + --ansi + + - name: 👎 Invalid CSV file + run: | + ! ./build/csv-blueprint.phar \ + validate:csv \ + --csv=./tests/fixtures/batch/*.csv \ + --schema=./tests/schemas/demo_invalid.yml \ + --ansi + - name: Upload Artifacts uses: actions/upload-artifact@v3 continue-on-error: true @@ -177,7 +218,7 @@ jobs: docker: - name: Docker + name: Verify Docker runs-on: ubuntu-latest steps: - name: Checkout code @@ -186,9 +227,25 @@ jobs: - name: 🐳 Building Docker Image run: make build-docker - - name: Trying to use the Docker Image + - name: Test help and logo run: docker run --rm jbzoo/csv-blueprint --ansi - - name: Reporting example via Docker - run: make demo-docker --no-print-directory - continue-on-error: true + - name: 👍 Valid CSV file + run: | + docker run --rm \ + -v `pwd`:/parent-host \ + jbzoo/csv-blueprint \ + validate:csv \ + --csv=/parent-host/tests/fixtures/demo.csv \ + --schema=/parent-host/tests/schemas/demo_valid.yml \ + --ansi + + - name: 👎 Invalid CSV file + run: | + ! docker run --rm \ + -v `pwd`:/parent-host \ + jbzoo/csv-blueprint \ + validate:csv \ + --csv=/parent-host/tests/fixtures/demo.csv \ + --schema=/parent-host/tests/schemas/demo_invalid.yml \ + --ansi diff --git a/Makefile b/Makefile index b41f5a70..352a8c37 100644 --- a/Makefile +++ b/Makefile @@ -12,11 +12,13 @@ .PHONY: build +REPORT ?= table +COLUMNS_TEST ?= 150 + ifneq (, $(wildcard ./vendor/jbzoo/codestyle/src/init.Makefile)) include ./vendor/jbzoo/codestyle/src/init.Makefile endif -REPORT ?= table build: ##@Project Install all 3rd party dependencies $(call title,"Install/Update all 3rd party dependencies") diff --git a/README.md b/README.md index c88fea3e..cbd1252c 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,6 @@ [![Stable Version](https://poser.pugx.org/jbzoo/csv-blueprint/version)](https://packagist.org/packages/jbzoo/csv-blueprint/) [![Total Downloads](https://poser.pugx.org/jbzoo/csv-blueprint/downloads)](https://packagist.org/packages/jbzoo/csv-blueprint/stats) [![Docker Pulls](https://img.shields.io/docker/pulls/jbzoo/csv-blueprint.svg)](https://hub.docker.com/r/jbzoo/csv-blueprint) [![Dependents](https://poser.pugx.org/jbzoo/csv-blueprint/dependents)](https://packagist.org/packages/jbzoo/csv-blueprint/dependents?order_by=downloads) [![GitHub License](https://img.shields.io/github/license/jbzoo/csv-blueprint)](https://github.com/JBZoo/Csv-Blueprint/blob/master/LICENSE) - -* [Introduction](#introduction) -* [Why validate CSV files in CI?](#why-validate-csv-files-in-ci) -* [Features](#features) -* [Live Demo](#live-demo) -* [Usage](#usage) - * [As GitHub Action](#as-github-action) - * [As Docker container](#as-docker-container) - * [As PHP binary](#as-php-binary) - * [As PHP project](#as-php-project) - * [CLI Help Message](#cli-help-message) - * [Report examples](#report-examples) - * [Schema Definition](#schema-definition) - * [Schema file examples](#schema-file-examples) -* [Coming soon](#coming-soon) -* [Disadvantages?](#disadvantages) -* [Contributing](#contributing) -* [License](#license) -* [See Also](#see-also) - - ## Introduction The JBZoo/Csv-Blueprint tool is a powerful and flexible utility designed for validating CSV files against @@ -232,35 +211,30 @@ Schema: ./tests/schemas/demo_invalid.yml Found CSV files: 3 (1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv -+------+------------------+--------------+ demo-1.csv ------------------------------------------+ -| Line | id:Column | Rule | Message | -+------+------------------+--------------+------------------------------------------------------+ -| 3 | 2:Float | max | Value "74605.944" is greater than "74605" | -| 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | -| | | | "green", "Blue"] | -+------+------------------+--------------+ demo-1.csv ------------------------------------------+ ++------+------------------+--------------+--------- demo-1.csv --------------------------------------------------+ +| Line | id:Column | Rule | Message | ++------+------------------+--------------+-----------------------------------------------------------------------+ +| 3 | 2:Float | max | Value "74605.944" is greater than "74605" | +| 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"] | ++------+------------------+--------------+--------- demo-1.csv --------------------------------------------------+ (2/3) Invalid file: ./tests/fixtures/batch/demo-2.csv -+------+------------+------------+----- demo-2.csv ---------------------------------------+ -| Line | id:Column | Rule | Message | -+------+------------+------------+--------------------------------------------------------+ -| 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | -| 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | -| | | | "1955-05-15T00:00:00.000+00:00" | -| 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | -| | | | "1955-05-15T00:00:00.000+00:00" | -| 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | -| | | | "2009-01-01T00:00:00.000+00:00" | -| 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | -+------+------------+------------+----- demo-2.csv ---------------------------------------+ ++------+------------+------------+------------------ demo-2.csv ----------------------------------------------------+ +| Line | id:Column | Rule | Message | ++------+------------+------------+----------------------------------------------------------------------------------+ +| 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | +| 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | +| 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | +| 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date "2009-01-01T00:00:00.000+00:00" | +| 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | ++------+------------+------------+------------------ demo-2.csv ----------------------------------------------------+ (3/3) Invalid file: ./tests/fixtures/batch/sub/demo-3.csv -+------+-----------+------------------+---- demo-3.csv -------------------------------------------+ -| Line | id:Column | Rule | Message | -+------+-----------+------------------+-----------------------------------------------------------+ -| 0 | | filename_pattern | Filename "./tests/fixtures/batch/sub/demo-3.csv" does not | -| | | | match pattern: "/demo-[12].csv$/i" | -+------+-----------+------------------+---- demo-3.csv -------------------------------------------+ ++------+-----------+------------------+---------------------- demo-3.csv ------------------------------------------------------------+ +| Line | id:Column | Rule | Message | ++------+-----------+------------------+----------------------------------------------------------------------------------------------+ +| 0 | | filename_pattern | Filename "./tests/fixtures/batch/sub/demo-3.csv" does not match pattern: "/demo-[12].csv$/i" | ++------+-----------+------------------+---------------------- demo-3.csv ------------------------------------------------------------+ Found 8 issues in 3 out of 3 CSV files. @@ -311,6 +285,8 @@ This gives you great flexibility when validating CSV files. ### Schema file examples +Available formats: [YAML](schema-examples/full.yml), [JSON](schema-examples/full.json), [PHP](schema-examples/full.php). + ```yml # It's a full example of the CSV schema file in YAML format. @@ -349,11 +325,20 @@ columns: only_lowercase: true # String is only lower-case. Example: "hello world" only_uppercase: true # String is only upper-case. Example: "HELLO WORLD" only_capitalize: true # String is only capitalized. Example: "Hello World" + word_count: 10 # Integer only. Exact count of words in the string. Example: "Hello World, 123" - 2 words only (123 is not a word) + min_word_count: 1 # Integer only. Min count of words in the string. Example: "Hello World. 123" - 2 words only (123 is not a word) + max_word_count: 5 # Integer only. Max count of words in the string Example: "Hello World! 123" - 2 words only (123 is not a word) + at_least_contains: [ a, b ] # At least one of the string must be in the CSV value. Case-sensitive. + all_must_contain: [ a, b, c ] # All the strings must be part of a CSV value. Case-sensitive. + str_ends_with: " suffix" # Case-sensitive. Example: "Hello World suffix" + str_starts_with: "prefix " # Case-sensitive. Example: "prefix Hello World" # Decimal and integer numbers min: 10 # Can be integer or float, negative and positive max: 100.50 # Can be integer or float, negative and positive - precision: 2 # Strict(!) number of digits after the decimal point + precision: 3 # Strict(!) number of digits after the decimal point + min_precision: 2 # Min number of digits after the decimal point (with zeros) + max_precision: 4 # Max number of digits after the decimal point (with zeros) # Dates date_format: Y-m-d # See: https://www.php.net/manual/en/datetime.format.php @@ -371,6 +356,7 @@ columns: is_uuid4: true # Only UUID4 format. Example: "550e8400-e29b-41d4-a716-446655440000" is_latitude: true # Can be integer or float. Example: 50.123456 is_longitude: true # Can be integer or float. Example: -89.123456 + is_alias: true # Only alias format. Example: "my-alias-123" cardinal_direction: true # Valid cardinal direction. Examples: "N", "S", "NE", "SE", "none", "" usa_market_name: true # Check if the value is a valid USA market name. Example: "New York, NY" @@ -379,130 +365,6 @@ columns: ``` -
- Click to see: JSON Format - -```json -{ - "filename_pattern" : "/demo(-\\d+)?\\.csv$/i", - "csv" : { - "header" : true, - "delimiter" : ",", - "quote_char" : "\\", - "enclosure" : "\"", - "encoding" : "utf-8", - "bom" : false - }, - "columns" : [ - { - "name" : "csv_header_name", - "description" : "Lorem ipsum", - "rules" : { - "not_empty" : true, - "exact_value" : "Some string", - "allow_values" : ["y", "n", ""], - "regex" : "\/^[\\d]{2}$\/", - "min_length" : 1, - "max_length" : 10, - "only_trimed" : true, - "only_lowercase" : true, - "only_uppercase" : true, - "only_capitalize" : true, - "min" : 10, - "max" : 100.5, - "precision" : 2, - "date_format" : "Y-m-d", - "min_date" : "2000-01-02", - "max_date" : "+1 day", - "is_bool" : true, - "is_int" : true, - "is_float" : true, - "is_ip" : true, - "is_url" : true, - "is_email" : true, - "is_domain" : true, - "is_uuid4" : true, - "is_latitude" : true, - "is_longitude" : true, - "cardinal_direction" : true, - "usa_market_name" : true - } - }, - {"name" : "another_column"} - ] -} - -``` - -
- - - - -
- Click to see: PHP Format - -```php - '/demo(-\\d+)?\\.csv$/i', - - 'csv' => [ - 'header' => true, - 'delimiter' => ',', - 'quote_char' => '\\', - 'enclosure' => '"', - 'encoding' => 'utf-8', - 'bom' => false, - ], - - 'columns' => [ - [ - 'name' => 'csv_header_name', - 'description' => 'Lorem ipsum', - 'rules' => [ - 'not_empty' => true, - 'exact_value' => 'Some string', - 'allow_values' => ['y', 'n', ''], - 'regex' => '/^[\\d]{2}$/', - 'min_length' => 1, - 'max_length' => 10, - 'only_trimed' => true, - 'only_lowercase' => true, - 'only_uppercase' => true, - 'only_capitalize' => true, - 'min' => 10, - 'max' => 100.5, - 'precision' => 2, - 'date_format' => 'Y-m-d', - 'min_date' => '2000-01-02', - 'max_date' => '+1 day', - 'is_bool' => true, - 'is_int' => true, - 'is_float' => true, - 'is_ip' => true, - 'is_url' => true, - 'is_email' => true, - 'is_domain' => true, - 'is_uuid4' => true, - 'is_latitude' => true, - 'is_longitude' => true, - 'cardinal_direction' => true, - 'usa_market_name' => true, - ], - ], - ['name' => 'another_column'], - ], -]; - -``` - -
- - - ## Coming soon It's random ideas and plans. No orderings and deadlines. But batch processing is the priority #1. @@ -517,6 +379,8 @@ Batch processing Validation * [x] ~~`filename_pattern` validation with regex (like "all files in the folder should be in the format `/^[\d]{4}-[\d]{2}-[\d]{2}\.csv$/`").~~ +* [ ] Flag to ignore file name pattern. It's useful when you have a lot of files and you don't want to validate the file name. +* [ ] Keyword for null value. Configurable. By default, it's an empty string. But you can use `null`, `nil`, `none`, `empty`, etc. * [ ] Agregate rules (like "at least one of the fields should be not empty" or "all values must be unique"). * [ ] Handle empty files and files with only a header row, or only with one line of data. One column wthout header is also possible. * [ ] Using multiple schemas for one csv file. @@ -531,6 +395,8 @@ Release workflow * [ ] Build and release Docker image [via GitHub Actions, tags and labels](https://docs.docker.com/build/ci/github-actions/manage-tags-labels/). Review it. * [ ] Upgrading to PHP 8.3.x * [ ] Build phar file and release via GitHub Actions. +* [ ] Auto insert tool version into the Docker image and phar file. It's important to know the version of the tool you are using. +* [ ] Show version as part of output. Performance and optimization * [ ] Parallel validation of really-really large files (1GB+ ?). I know you have them and not so much memory. @@ -545,6 +411,7 @@ Mock data generation Reporting * [ ] More report formats (like JSON, XML, etc). Any ideas? * [ ] Gitlab and JUnit reports must be as one structure. It's not so easy to implement. But it's a good idea. +* [ ] Merge reports from multiple CSV files into one report. It's useful when you have a lot of files and you want to see all errors in one place. Especially for GitLab and JUnit reports. Misc * [ ] Use it as PHP SDK. Examples in Readme. diff --git a/composer.lock b/composer.lock index ff94acda..703111e6 100644 --- a/composer.lock +++ b/composer.lock @@ -4355,16 +4355,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.60", + "version": "1.10.62", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe" + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe", - "reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/cd5c8a1660ed3540b211407c77abf4af193a6af9", + "reference": "cd5c8a1660ed3540b211407c77abf4af193a6af9", "shasum": "" }, "require": { @@ -4413,7 +4413,7 @@ "type": "tidelift" } ], - "time": "2024-03-07T13:30:19+00:00" + "time": "2024-03-13T12:27:20+00:00" }, { "name": "phpstan/phpstan-strict-rules", @@ -5198,12 +5198,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "bb15a6dc9a8493ace041a6de2929eb63ba0809ef" + "reference": "eedc674d89085b0199bd96bfad410404fb2f5dbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/bb15a6dc9a8493ace041a6de2929eb63ba0809ef", - "reference": "bb15a6dc9a8493ace041a6de2929eb63ba0809ef", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/eedc674d89085b0199bd96bfad410404fb2f5dbf", + "reference": "eedc674d89085b0199bd96bfad410404fb2f5dbf", "shasum": "" }, "conflict": { @@ -5930,7 +5930,7 @@ "type": "tidelift" } ], - "time": "2024-03-10T05:04:21+00:00" + "time": "2024-03-13T21:04:41+00:00" }, { "name": "sabre/event", @@ -6851,6 +6851,7 @@ "type": "github" } ], + "abandoned": true, "time": "2020-09-28T06:45:17+00:00" }, { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9ca3124e..bb58f1ed 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -50,4 +50,8 @@ + + + + diff --git a/schema-examples/full.json b/schema-examples/full.json index 17131304..fe841bf1 100644 --- a/schema-examples/full.json +++ b/schema-examples/full.json @@ -23,9 +23,18 @@ "only_lowercase" : true, "only_uppercase" : true, "only_capitalize" : true, + "word_count" : 10, + "min_word_count" : 1, + "max_word_count" : 5, + "at_least_contains" : ["a", "b"], + "all_must_contain" : ["a", "b", "c"], + "str_ends_with" : " suffix", + "str_starts_with" : "prefix ", "min" : 10, "max" : 100.5, - "precision" : 2, + "precision" : 3, + "min_precision" : 2, + "max_precision" : 4, "date_format" : "Y-m-d", "min_date" : "2000-01-02", "max_date" : "+1 day", @@ -39,6 +48,7 @@ "is_uuid4" : true, "is_latitude" : true, "is_longitude" : true, + "is_alias" : true, "cardinal_direction" : true, "usa_market_name" : true } diff --git a/schema-examples/full.php b/schema-examples/full.php index 66c7542a..f336b00f 100644 --- a/schema-examples/full.php +++ b/schema-examples/full.php @@ -41,9 +41,18 @@ 'only_lowercase' => true, 'only_uppercase' => true, 'only_capitalize' => true, + 'word_count' => 10, + 'min_word_count' => 1, + 'max_word_count' => 5, + 'at_least_contains' => ['a', 'b'], + 'all_must_contain' => ['a', 'b', 'c'], + 'str_ends_with' => ' suffix', + 'str_starts_with' => 'prefix ', 'min' => 10, 'max' => 100.5, - 'precision' => 2, + 'precision' => 3, + 'min_precision' => 2, + 'max_precision' => 4, 'date_format' => 'Y-m-d', 'min_date' => '2000-01-02', 'max_date' => '+1 day', @@ -57,6 +66,7 @@ 'is_uuid4' => true, 'is_latitude' => true, 'is_longitude' => true, + 'is_alias' => true, 'cardinal_direction' => true, 'usa_market_name' => true, ], diff --git a/schema-examples/full.yml b/schema-examples/full.yml index 98036c1c..136e73cb 100644 --- a/schema-examples/full.yml +++ b/schema-examples/full.yml @@ -47,11 +47,20 @@ columns: only_lowercase: true # String is only lower-case. Example: "hello world" only_uppercase: true # String is only upper-case. Example: "HELLO WORLD" only_capitalize: true # String is only capitalized. Example: "Hello World" + word_count: 10 # Integer only. Exact count of words in the string. Example: "Hello World, 123" - 2 words only (123 is not a word) + min_word_count: 1 # Integer only. Min count of words in the string. Example: "Hello World. 123" - 2 words only (123 is not a word) + max_word_count: 5 # Integer only. Max count of words in the string Example: "Hello World! 123" - 2 words only (123 is not a word) + at_least_contains: [ a, b ] # At least one of the string must be in the CSV value. Case-sensitive. + all_must_contain: [ a, b, c ] # All the strings must be part of a CSV value. Case-sensitive. + str_ends_with: " suffix" # Case-sensitive. Example: "Hello World suffix" + str_starts_with: "prefix " # Case-sensitive. Example: "prefix Hello World" # Decimal and integer numbers min: 10 # Can be integer or float, negative and positive max: 100.50 # Can be integer or float, negative and positive - precision: 2 # Strict(!) number of digits after the decimal point + precision: 3 # Strict(!) number of digits after the decimal point + min_precision: 2 # Min number of digits after the decimal point (with zeros) + max_precision: 4 # Max number of digits after the decimal point (with zeros) # Dates date_format: Y-m-d # See: https://www.php.net/manual/en/datetime.format.php @@ -69,6 +78,7 @@ columns: is_uuid4: true # Only UUID4 format. Example: "550e8400-e29b-41d4-a716-446655440000" is_latitude: true # Can be integer or float. Example: 50.123456 is_longitude: true # Can be integer or float. Example: -89.123456 + is_alias: true # Only alias format. Example: "my-alias-123" cardinal_direction: true # Valid cardinal direction. Examples: "N", "S", "NE", "SE", "none", "" usa_market_name: true # Check if the value is a valid USA market name. Example: "New York, NY" diff --git a/src/Csv/Column.php b/src/Csv/Column.php index 1cc88b66..3dc820a8 100644 --- a/src/Csv/Column.php +++ b/src/Csv/Column.php @@ -17,8 +17,8 @@ namespace JBZoo\CsvBlueprint\Csv; use JBZoo\CsvBlueprint\Utils; +use JBZoo\CsvBlueprint\Validators\ColumnValidator; use JBZoo\CsvBlueprint\Validators\ErrorSuite; -use JBZoo\CsvBlueprint\Validators\Validator; use JBZoo\Data\Data; final class Column @@ -111,7 +111,7 @@ public function getInherit(): string public function validate(string $cellValue, int $line): ErrorSuite { - return (new Validator($this))->validate($cellValue, $line); + return (new ColumnValidator($this))->validate($cellValue, $line); } private function prepareRuleSet(string $schemaKey): array diff --git a/src/Csv/CsvFile.php b/src/Csv/CsvFile.php index 65d52eb1..543ce522 100644 --- a/src/Csv/CsvFile.php +++ b/src/Csv/CsvFile.php @@ -17,8 +17,7 @@ namespace JBZoo\CsvBlueprint\Csv; use JBZoo\CsvBlueprint\Schema; -use JBZoo\CsvBlueprint\Utils; -use JBZoo\CsvBlueprint\Validators\Error; +use JBZoo\CsvBlueprint\Validators\CsvValidator; use JBZoo\CsvBlueprint\Validators\ErrorSuite; use League\Csv\Reader as LeagueReader; use League\Csv\Statement; @@ -81,15 +80,7 @@ public function getRecordsChunk(int $offset = 0, int $limit = -1): TabularDataRe public function validate(bool $quickStop = false): ErrorSuite { - $errors = new ErrorSuite($this->getCsvFilename()); - - $errors - ->addErrorSuit($this->validateFile($quickStop)) - ->addErrorSuit($this->validateHeader($quickStop)) - ->addErrorSuit($this->validateEachCell($quickStop)) - ->addErrorSuit(self::validateAggregateRules($quickStop)); - - return $errors; + return (new CsvValidator($this, $this->schema))->validate($quickStop); } private function prepareReader(): LeagueReader @@ -108,93 +99,4 @@ private function prepareReader(): LeagueReader return $reader; } - - private function validateHeader(bool $quickStop = false): ErrorSuite - { - $errors = new ErrorSuite(); - - if (!$this->getCsvStructure()->isHeader()) { - return $errors; - } - - foreach ($this->schema->getColumns() as $column) { - if ($column->getName() === '') { - $error = new Error( - 'csv.header', - "Property \"name\" is not defined in schema: \"{$this->schema->getFilename()}\"", - $column->getHumanName(), - 1, - ); - - $errors->addError($error); - } - - if ($quickStop && $errors->count() > 0) { - return $errors; - } - } - - return $errors; - } - - private function validateEachCell(bool $quickStop = false): ErrorSuite - { - $errors = new ErrorSuite(); - - foreach ($this->getRecords() as $line => $record) { - $columns = $this->schema->getColumnsMappedByHeader($this->getHeader()); - - foreach ($columns as $column) { - if ($column === null) { - continue; - } - - $errors->addErrorSuit($column->validate($record[$column->getKey()], (int)$line + 1)); - if ($quickStop && $errors->count() > 0) { - return $errors; - } - } - } - - return $errors; - } - - private function validateFile(bool $quickStop = false): ErrorSuite - { - $errors = new ErrorSuite(); - - $filenamePattern = $this->schema->getFilenamePattern(); - if ( - $filenamePattern !== null - && $filenamePattern !== '' - && \preg_match($filenamePattern, $this->csvFilename) === 0 - ) { - $error = new Error( - 'filename_pattern', - 'Filename "' . Utils::cutPath($this->csvFilename) . - "\" does not match pattern: \"{$filenamePattern}\"", - '', - 0, - ); - - $errors->addError($error); - - if ($quickStop && $errors->count() > 0) { - return $errors; - } - } - - return $errors; - } - - private static function validateAggregateRules(bool $quickStop = false): ErrorSuite - { - $errors = new ErrorSuite(); - - if ($quickStop && $errors->count() > 0) { - return $errors; - } - - return new ErrorSuite(); - } } diff --git a/src/Validators/Rules/AbstarctRule.php b/src/Rules/AbstarctRule.php similarity index 92% rename from src/Validators/Rules/AbstarctRule.php rename to src/Rules/AbstarctRule.php index d97bcb05..8da530e0 100644 --- a/src/Validators/Rules/AbstarctRule.php +++ b/src/Rules/AbstarctRule.php @@ -14,7 +14,7 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; use JBZoo\CsvBlueprint\Utils; use JBZoo\CsvBlueprint\Validators\Error; @@ -33,7 +33,7 @@ abstract class AbstarctRule private string $columnNameId; private string $ruleCode; - abstract public function validateRule(?string $cellValue): ?string; + abstract public function validateRule(string $cellValue): ?string; public function __construct(string $columnNameId, null|array|bool|float|int|string $options = null) { @@ -42,7 +42,7 @@ public function __construct(string $columnNameId, null|array|bool|float|int|stri $this->ruleCode = $this->getRuleCode(); } - public function validate(?string $cellValue, int $line = 0): ?Error + public function validate(string $cellValue, int $line = 0): ?Error { $error = $this->validateRule($cellValue); if ($error !== null) { diff --git a/src/Rules/AllMustContain.php b/src/Rules/AllMustContain.php new file mode 100644 index 00000000..9a50a1aa --- /dev/null +++ b/src/Rules/AllMustContain.php @@ -0,0 +1,37 @@ +getOptionAsArray(); + if (\count($inclusions) === 0) { + return 'Rule must contain at least one inclusion value in schema file.'; + } + + foreach ($inclusions as $inclusion) { + if (\strpos($cellValue, (string)$inclusion) === false) { + return "Value \"{$cellValue}\" must contain all of the following:" . + ' "["' . \implode('", "', $inclusions) . '"]"'; + } + } + + return null; + } +} diff --git a/src/Validators/Rules/AllowValues.php b/src/Rules/AllowValues.php similarity index 88% rename from src/Validators/Rules/AllowValues.php rename to src/Rules/AllowValues.php index 11dbcf21..c2f602c5 100644 --- a/src/Validators/Rules/AllowValues.php +++ b/src/Rules/AllowValues.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; class AllowValues extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $allowedValues = $this->getOptionAsArray(); diff --git a/src/Rules/AtLeastContains.php b/src/Rules/AtLeastContains.php new file mode 100644 index 00000000..e93161e6 --- /dev/null +++ b/src/Rules/AtLeastContains.php @@ -0,0 +1,37 @@ +getOptionAsArray(); + if (\count($inclusions) === 0) { + return 'Rule must contain at least one inclusion value in schema file.'; + } + + foreach ($inclusions as $inclusion) { + if (\strpos($cellValue, (string)$inclusion) !== false) { + return null; + } + } + + return "Value \"{$cellValue}\" must contain one of the following:" . + ' "["' . \implode('", "', $inclusions) . '"]"'; + } +} diff --git a/src/Validators/Rules/CardinalDirection.php b/src/Rules/CardinalDirection.php similarity index 74% rename from src/Validators/Rules/CardinalDirection.php rename to src/Rules/CardinalDirection.php index f1cf9be9..60ca6d92 100644 --- a/src/Validators/Rules/CardinalDirection.php +++ b/src/Rules/CardinalDirection.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; -class CardinalDirection extends AllowValues +final class CardinalDirection extends AllowValues { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - return parent::validateRule((string)$cellValue); + return parent::validateRule($cellValue); } public function getOptionAsArray(): array diff --git a/src/Validators/Rules/DateFormat.php b/src/Rules/DateFormat.php similarity index 86% rename from src/Validators/Rules/DateFormat.php rename to src/Rules/DateFormat.php index ec3f80da..232e1046 100644 --- a/src/Validators/Rules/DateFormat.php +++ b/src/Rules/DateFormat.php @@ -14,18 +14,18 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class DateFormat extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $expectedDateFormat = $this->getOptionAsString(); if ($expectedDateFormat === '') { return 'Date format is not defined'; } - if ($cellValue === null || $cellValue === '') { + if ($cellValue === '') { return 'Date format of value "" is not valid. Expected format: "' . $expectedDateFormat . '"'; } diff --git a/src/Validators/Rules/ExactValue.php b/src/Rules/ExactValue.php similarity index 78% rename from src/Validators/Rules/ExactValue.php rename to src/Rules/ExactValue.php index 317aa0cb..2c3aa3d9 100644 --- a/src/Validators/Rules/ExactValue.php +++ b/src/Rules/ExactValue.php @@ -14,13 +14,13 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class ExactValue extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { - if ($this->getOptionAsString() !== (string)$cellValue) { + if ($this->getOptionAsString() !== $cellValue) { return "Value \"{$cellValue}\" is not strict equal to " . "\"{$this->getOptionAsString()}\""; } diff --git a/src/Rules/IsAlias.php b/src/Rules/IsAlias.php new file mode 100644 index 00000000..885c0c7b --- /dev/null +++ b/src/Rules/IsAlias.php @@ -0,0 +1,36 @@ +getOptionAsBool()) { + return null; + } + + $alias = Filter::alias($cellValue); + if ($cellValue !== $alias) { + return "Value \"{$cellValue}\" is not a valid alias. Expected \"{$alias}\"."; + } + + return null; + } +} diff --git a/src/Validators/Rules/IsBool.php b/src/Rules/IsBool.php similarity index 77% rename from src/Validators/Rules/IsBool.php rename to src/Rules/IsBool.php index e4260f5b..f1c08a79 100644 --- a/src/Validators/Rules/IsBool.php +++ b/src/Rules/IsBool.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsBool extends AllowValues { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - return parent::validateRule(\strtolower((string)$cellValue)); + return parent::validateRule(\strtolower($cellValue)); } public function getOptionAsArray(): array diff --git a/src/Validators/Rules/IsDomain.php b/src/Rules/IsDomain.php similarity index 80% rename from src/Validators/Rules/IsDomain.php rename to src/Rules/IsDomain.php index e815db76..1234350e 100644 --- a/src/Validators/Rules/IsDomain.php +++ b/src/Rules/IsDomain.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsDomain extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; @@ -26,7 +26,7 @@ public function validateRule(?string $cellValue): ?string $domainPattern = '/^(?!-)[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(\.[A-Za-z]{2,})$/'; - if (\preg_match($domainPattern, (string)$cellValue) === 0) { + if (\preg_match($domainPattern, $cellValue) === 0) { return "Value \"{$cellValue}\" is not a valid domain"; } diff --git a/src/Validators/Rules/IsEmail.php b/src/Rules/IsEmail.php similarity index 86% rename from src/Validators/Rules/IsEmail.php rename to src/Rules/IsEmail.php index f26cc9dc..d59fbb95 100644 --- a/src/Validators/Rules/IsEmail.php +++ b/src/Rules/IsEmail.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsEmail extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; diff --git a/src/Validators/Rules/IsFloat.php b/src/Rules/IsFloat.php similarity index 77% rename from src/Validators/Rules/IsFloat.php rename to src/Rules/IsFloat.php index 644b11aa..ab2f8402 100644 --- a/src/Validators/Rules/IsFloat.php +++ b/src/Rules/IsFloat.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; class IsFloat extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if (\preg_match('/^-?\d+(\.\d+)?$/', (string)$cellValue) === 0) { + if (\preg_match('/^-?\d+(\.\d+)?$/', $cellValue) === 0) { return "Value \"{$cellValue}\" is not a float number"; } diff --git a/src/Validators/Rules/IsInt.php b/src/Rules/IsInt.php similarity index 78% rename from src/Validators/Rules/IsInt.php rename to src/Rules/IsInt.php index 4581f04a..478376b8 100644 --- a/src/Validators/Rules/IsInt.php +++ b/src/Rules/IsInt.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsInt extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if (\preg_match('/^-?\d+$/', (string)$cellValue) === 0) { + if (\preg_match('/^-?\d+$/', $cellValue) === 0) { return "Value \"{$cellValue}\" is not an integer"; } diff --git a/src/Validators/Rules/IsIp.php b/src/Rules/IsIp.php similarity index 86% rename from src/Validators/Rules/IsIp.php rename to src/Rules/IsIp.php index ccc5fc98..6ed5cf1e 100644 --- a/src/Validators/Rules/IsIp.php +++ b/src/Rules/IsIp.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsIp extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; diff --git a/src/Validators/Rules/IsLatitude.php b/src/Rules/IsLatitude.php similarity index 89% rename from src/Validators/Rules/IsLatitude.php rename to src/Rules/IsLatitude.php index 7a670fb6..27b19b53 100644 --- a/src/Validators/Rules/IsLatitude.php +++ b/src/Rules/IsLatitude.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsLatitude extends IsFloat { private float $min = -90.0; private float $max = 90.0; - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; diff --git a/src/Validators/Rules/IsLongitude.php b/src/Rules/IsLongitude.php similarity index 90% rename from src/Validators/Rules/IsLongitude.php rename to src/Rules/IsLongitude.php index 31f2a53b..0188bf5f 100644 --- a/src/Validators/Rules/IsLongitude.php +++ b/src/Rules/IsLongitude.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsLongitude extends IsFloat { private float $min = -180.0; private float $max = 180.0; - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; diff --git a/src/Validators/Rules/IsUrl.php b/src/Rules/IsUrl.php similarity index 86% rename from src/Validators/Rules/IsUrl.php rename to src/Rules/IsUrl.php index f2fba082..d57580c9 100644 --- a/src/Validators/Rules/IsUrl.php +++ b/src/Rules/IsUrl.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsUrl extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; diff --git a/src/Validators/Rules/IsUuid4.php b/src/Rules/IsUuid4.php similarity index 80% rename from src/Validators/Rules/IsUuid4.php rename to src/Rules/IsUuid4.php index 86a5b55b..37c35fab 100644 --- a/src/Validators/Rules/IsUuid4.php +++ b/src/Rules/IsUuid4.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class IsUuid4 extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; @@ -26,7 +26,7 @@ public function validateRule(?string $cellValue): ?string $uuid4 = '/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89ABab][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/'; - if (\preg_match($uuid4, (string)$cellValue) === 0) { + if (\preg_match($uuid4, $cellValue) === 0) { return 'Value is not a valid UUID v4'; } diff --git a/src/Validators/Rules/Max.php b/src/Rules/Max.php similarity index 87% rename from src/Validators/Rules/Max.php rename to src/Rules/Max.php index 7a0cb9bc..b99d1f72 100644 --- a/src/Validators/Rules/Max.php +++ b/src/Rules/Max.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class Max extends IsFloat { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $result = parent::validateRule($cellValue); if ($result !== null) { diff --git a/src/Validators/Rules/MaxDate.php b/src/Rules/MaxDate.php similarity index 81% rename from src/Validators/Rules/MaxDate.php rename to src/Rules/MaxDate.php index 3dc0612f..834c7471 100644 --- a/src/Validators/Rules/MaxDate.php +++ b/src/Rules/MaxDate.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class MaxDate extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $minDate = $this->getOptionAsDate(); - $cellDate = new \DateTimeImmutable((string)$cellValue); + $cellDate = new \DateTimeImmutable($cellValue); if ($cellDate->getTimestamp() > $minDate->getTimestamp()) { return "Value \"{$cellValue}\" is more than the maximum " . diff --git a/src/Validators/Rules/MaxLength.php b/src/Rules/MaxLength.php similarity index 81% rename from src/Validators/Rules/MaxLength.php rename to src/Rules/MaxLength.php index 515cdb74..e34ff02b 100644 --- a/src/Validators/Rules/MaxLength.php +++ b/src/Rules/MaxLength.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class MaxLength extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $minLength = $this->getOptionAsInt(); - $length = \mb_strlen((string)$cellValue); + $length = \mb_strlen($cellValue); if ($length > $minLength) { return "Value \"{$cellValue}\" (length: {$length}) is too long. " . diff --git a/src/Rules/MaxPrecision.php b/src/Rules/MaxPrecision.php new file mode 100644 index 00000000..1dea055f --- /dev/null +++ b/src/Rules/MaxPrecision.php @@ -0,0 +1,32 @@ + $this->getOptionAsInt()) { + return "Value \"{$cellValue}\" has a precision of {$valuePrecision} " . + "but should have a max precision of {$this->getOptionAsInt()}"; + } + + return null; + } +} diff --git a/src/Rules/MaxWordCount.php b/src/Rules/MaxWordCount.php new file mode 100644 index 00000000..99925603 --- /dev/null +++ b/src/Rules/MaxWordCount.php @@ -0,0 +1,33 @@ +getOptionAsInt(); + $count = \str_word_count($cellValue); + + if ($count > $wordCount) { + return "Value \"{$cellValue}\" has {$count} words, " . + "but must have no more than {$wordCount} words"; + } + + return null; + } +} diff --git a/src/Validators/Rules/Min.php b/src/Rules/Min.php similarity index 87% rename from src/Validators/Rules/Min.php rename to src/Rules/Min.php index ee340fcb..33e7271c 100644 --- a/src/Validators/Rules/Min.php +++ b/src/Rules/Min.php @@ -14,11 +14,11 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class Min extends IsFloat { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $result = parent::validateRule($cellValue); if ($result !== null) { diff --git a/src/Validators/Rules/MinDate.php b/src/Rules/MinDate.php similarity index 81% rename from src/Validators/Rules/MinDate.php rename to src/Rules/MinDate.php index e9435a6b..174f73d9 100644 --- a/src/Validators/Rules/MinDate.php +++ b/src/Rules/MinDate.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class MinDate extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $minDate = $this->getOptionAsDate(); - $cellDate = new \DateTimeImmutable((string)$cellValue); + $cellDate = new \DateTimeImmutable($cellValue); if ($cellDate->getTimestamp() < $minDate->getTimestamp()) { return "Value \"{$cellValue}\" is less than the minimum " . diff --git a/src/Validators/Rules/MinLength.php b/src/Rules/MinLength.php similarity index 81% rename from src/Validators/Rules/MinLength.php rename to src/Rules/MinLength.php index 10120131..0d6b6fe0 100644 --- a/src/Validators/Rules/MinLength.php +++ b/src/Rules/MinLength.php @@ -14,14 +14,14 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class MinLength extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $minLength = $this->getOptionAsInt(); - $length = \mb_strlen((string)$cellValue); + $length = \mb_strlen($cellValue); if ($length < $minLength) { return "Value \"{$cellValue}\" (length: {$length}) is too short. " . diff --git a/src/Rules/MinPrecision.php b/src/Rules/MinPrecision.php new file mode 100644 index 00000000..90ceda3e --- /dev/null +++ b/src/Rules/MinPrecision.php @@ -0,0 +1,32 @@ +getOptionAsInt()) { + return "Value \"{$cellValue}\" has a precision of {$valuePrecision} " . + "but should have a min precision of {$this->getOptionAsInt()}"; + } + + return null; + } +} diff --git a/src/Rules/MinWordCount.php b/src/Rules/MinWordCount.php new file mode 100644 index 00000000..a7fe08a8 --- /dev/null +++ b/src/Rules/MinWordCount.php @@ -0,0 +1,33 @@ +getOptionAsInt(); + $count = \str_word_count($cellValue); + + if ($count < $wordCount) { + return "Value \"{$cellValue}\" has {$count} words, " . + "but must have at least {$wordCount} words"; + } + + return null; + } +} diff --git a/src/Validators/Rules/NotEmpty.php b/src/Rules/NotEmpty.php similarity index 78% rename from src/Validators/Rules/NotEmpty.php rename to src/Rules/NotEmpty.php index b99339d7..cfa623ca 100644 --- a/src/Validators/Rules/NotEmpty.php +++ b/src/Rules/NotEmpty.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class NotEmpty extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if ($cellValue === null || $cellValue === '') { + if ($cellValue === '') { return 'Value is empty'; } diff --git a/src/Validators/Rules/OnlyCapitalize.php b/src/Rules/OnlyCapitalize.php similarity index 77% rename from src/Validators/Rules/OnlyCapitalize.php rename to src/Rules/OnlyCapitalize.php index 99b011f2..e25f747d 100644 --- a/src/Validators/Rules/OnlyCapitalize.php +++ b/src/Rules/OnlyCapitalize.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class OnlyCapitalize extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if ($cellValue !== null && $cellValue !== \ucfirst($cellValue)) { + if ($cellValue !== \ucfirst($cellValue)) { return "Value \"{$cellValue}\" should be in capitalize"; } diff --git a/src/Validators/Rules/OnlyLowercase.php b/src/Rules/OnlyLowercase.php similarity index 77% rename from src/Validators/Rules/OnlyLowercase.php rename to src/Rules/OnlyLowercase.php index 54bd838e..cf36c7f7 100644 --- a/src/Validators/Rules/OnlyLowercase.php +++ b/src/Rules/OnlyLowercase.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class OnlyLowercase extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if ($cellValue !== null && $cellValue !== \mb_strtolower($cellValue)) { + if ($cellValue !== \mb_strtolower($cellValue)) { return "Value \"{$cellValue}\" should be in lowercase"; } diff --git a/src/Validators/Rules/OnlyTrimed.php b/src/Rules/OnlyTrimed.php similarity index 78% rename from src/Validators/Rules/OnlyTrimed.php rename to src/Rules/OnlyTrimed.php index 60d3b0c8..d8621755 100644 --- a/src/Validators/Rules/OnlyTrimed.php +++ b/src/Rules/OnlyTrimed.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class OnlyTrimed extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if (\trim((string)$cellValue) !== (string)$cellValue) { + if (\trim($cellValue) !== $cellValue) { return "Value \"{$cellValue}\" is not trimmed"; } diff --git a/src/Validators/Rules/OnlyUppercase.php b/src/Rules/OnlyUppercase.php similarity index 76% rename from src/Validators/Rules/OnlyUppercase.php rename to src/Rules/OnlyUppercase.php index 6546547e..a1034e5e 100644 --- a/src/Validators/Rules/OnlyUppercase.php +++ b/src/Rules/OnlyUppercase.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; final class OnlyUppercase extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if ($cellValue !== null && \mb_strtoupper($cellValue, 'UTF-8') !== $cellValue) { + if (\mb_strtoupper($cellValue, 'UTF-8') !== $cellValue) { return "Value \"{$cellValue}\" is not uppercase"; } diff --git a/src/Validators/Rules/Precision.php b/src/Rules/Precision.php similarity index 63% rename from src/Validators/Rules/Precision.php rename to src/Rules/Precision.php index f6106e31..697a5b80 100644 --- a/src/Validators/Rules/Precision.php +++ b/src/Rules/Precision.php @@ -14,15 +14,15 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; -final class Precision extends AbstarctRule +class Precision extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $valuePrecision = self::getFloatPrecision($cellValue); - if ($this->getOptionAsInt() !== $valuePrecision) { + if ($valuePrecision !== $this->getOptionAsInt()) { return "Value \"{$cellValue}\" has a precision of {$valuePrecision} " . "but should have a precision of {$this->getOptionAsInt()}"; } @@ -30,15 +30,13 @@ public function validateRule(?string $cellValue): ?string return null; } - private static function getFloatPrecision(?string $cellValue): int + protected static function getFloatPrecision(string $cellValue): int { - $floatAsString = (string)$cellValue; - $dotPosition = \strpos($floatAsString, '.'); - + $dotPosition = \strpos($cellValue, '.'); if ($dotPosition === false) { return 0; } - return \strlen($floatAsString) - $dotPosition - 1; + return \strlen($cellValue) - $dotPosition - 1; } } diff --git a/src/Validators/Rules/Regex.php b/src/Rules/Regex.php similarity index 82% rename from src/Validators/Rules/Regex.php rename to src/Rules/Regex.php index 356cbdb1..f07ca4d8 100644 --- a/src/Validators/Rules/Regex.php +++ b/src/Rules/Regex.php @@ -14,13 +14,13 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; use JBZoo\CsvBlueprint\Utils; final class Regex extends AbstarctRule { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { $regex = Utils::prepareRegex($this->getOptionAsString()); @@ -28,7 +28,7 @@ public function validateRule(?string $cellValue): ?string return 'Regex pattern is not defined'; } - if (\preg_match($regex, (string)$cellValue) === 0) { + if (\preg_match($regex, $cellValue) === 0) { return "Value \"{$cellValue}\" does not match the pattern \"{$regex}\""; } diff --git a/src/Validators/Rules/RuleException.php b/src/Rules/RuleException.php similarity index 90% rename from src/Validators/Rules/RuleException.php rename to src/Rules/RuleException.php index 48ed9cb6..d0e06b41 100644 --- a/src/Validators/Rules/RuleException.php +++ b/src/Rules/RuleException.php @@ -14,7 +14,7 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; class RuleException extends \JBZoo\CsvBlueprint\Exception { diff --git a/src/Rules/StrEndsWith.php b/src/Rules/StrEndsWith.php new file mode 100644 index 00000000..aa5dd3ad --- /dev/null +++ b/src/Rules/StrEndsWith.php @@ -0,0 +1,34 @@ +getOptionAsString(); + if ($prefix === '') { + return 'Rule must contain a suffix value in schema file.'; + } + + if (!\str_ends_with($cellValue, $prefix)) { + return "Value \"{$cellValue}\" must end with \"{$prefix}\""; + } + + return null; + } +} diff --git a/src/Rules/StrStartsWith.php b/src/Rules/StrStartsWith.php new file mode 100644 index 00000000..a16e8f32 --- /dev/null +++ b/src/Rules/StrStartsWith.php @@ -0,0 +1,34 @@ +getOptionAsString(); + if ($prefix === '') { + return 'Rule must contain a prefix value in schema file.'; + } + + if (!\str_starts_with($cellValue, $prefix)) { + return "Value \"{$cellValue}\" must start with \"{$prefix}\""; + } + + return null; + } +} diff --git a/src/Validators/Rules/UsaMarketName.php b/src/Rules/UsaMarketName.php similarity index 74% rename from src/Validators/Rules/UsaMarketName.php rename to src/Rules/UsaMarketName.php index 0979b6ea..0626d44e 100644 --- a/src/Validators/Rules/UsaMarketName.php +++ b/src/Rules/UsaMarketName.php @@ -14,17 +14,17 @@ declare(strict_types=1); -namespace JBZoo\CsvBlueprint\Validators\Rules; +namespace JBZoo\CsvBlueprint\Rules; -class UsaMarketName extends AllowValues +final class UsaMarketName extends AllowValues { - public function validateRule(?string $cellValue): ?string + public function validateRule(string $cellValue): ?string { if (!$this->getOptionAsBool()) { return null; } - if (\preg_match('/^[A-Za-z0-9\s-]+, [A-Z]{2}$/u', (string)$cellValue) === 0) { + if (\preg_match('/^[A-Za-z0-9\s-]+, [A-Z]{2}$/u', $cellValue) === 0) { return "Invalid market name format for value \"{$cellValue}\". " . 'Market name must have format "New York, NY"'; } diff --git a/src/Rules/WordCount.php b/src/Rules/WordCount.php new file mode 100644 index 00000000..dc020c6f --- /dev/null +++ b/src/Rules/WordCount.php @@ -0,0 +1,33 @@ +getOptionAsInt(); + $count = \str_word_count($cellValue); + + if ($count !== $wordCount) { + return "Value \"{$cellValue}\" has {$count} words, " . + "but must have exactly {$wordCount} words"; + } + + return null; + } +} diff --git a/src/Utils.php b/src/Utils.php index 02b99f78..57beb1cc 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -95,6 +95,12 @@ public static function findFiles(array $paths): array public static function cutPath(string $fullpath): string { - return \str_replace((string)\getcwd(), '.', $fullpath); + $pwd = (string)\getcwd(); + + if (\strlen($pwd) <= 1) { + return $fullpath; + } + + return \str_replace($pwd, '.', $fullpath); } } diff --git a/src/Validators/Validator.php b/src/Validators/ColumnValidator.php similarity index 87% rename from src/Validators/Validator.php rename to src/Validators/ColumnValidator.php index 2d8c857c..be91b832 100644 --- a/src/Validators/Validator.php +++ b/src/Validators/ColumnValidator.php @@ -18,7 +18,7 @@ use JBZoo\CsvBlueprint\Csv\Column; -final class Validator +final class ColumnValidator { private Ruleset $ruleset; @@ -27,7 +27,7 @@ public function __construct(Column $column) $this->ruleset = new Ruleset($column->getRules(), $column->getHumanName()); } - public function validate(?string $cellValue, int $line): ErrorSuite + public function validate(string $cellValue, int $line): ErrorSuite { return $this->ruleset->validate($cellValue, $line); } diff --git a/src/Validators/CsvValidator.php b/src/Validators/CsvValidator.php new file mode 100644 index 00000000..8169b3bc --- /dev/null +++ b/src/Validators/CsvValidator.php @@ -0,0 +1,134 @@ +csv = $csv; + $this->schema = $schema; + $this->errors = new ErrorSuite($this->csv->getCsvFilename()); + } + + public function validate(bool $quickStop = false): ErrorSuite + { + return $this->errors + ->addErrorSuit($this->validateFile($quickStop)) + ->addErrorSuit($this->validateHeader($quickStop)) + ->addErrorSuit($this->validateEachCell($quickStop)) + ->addErrorSuit(self::validateAggregateRules($quickStop)); + } + + private function validateHeader(bool $quickStop = false): ErrorSuite + { + $errors = new ErrorSuite(); + + if (!$this->schema->getCsvStructure()->isHeader()) { + return $errors; + } + + foreach ($this->schema->getColumns() as $column) { + if ($column->getName() === '') { + $error = new Error( + 'csv.header', + 'Property "name" is not defined in schema: ' . + "\"{$this->schema->getFilename()}\"", + $column->getHumanName(), + 1, + ); + + $errors->addError($error); + } + + if ($quickStop && $errors->count() > 0) { + return $errors; + } + } + + return $errors; + } + + private function validateEachCell(bool $quickStop = false): ErrorSuite + { + $errors = new ErrorSuite(); + + foreach ($this->csv->getRecords() as $line => $record) { + $columns = $this->schema->getColumnsMappedByHeader($this->csv->getHeader()); + + foreach ($columns as $column) { + if ($column === null) { + continue; + } + + $errors->addErrorSuit($column->validate($record[$column->getKey()], (int)$line + 1)); + if ($quickStop && $errors->count() > 0) { + return $errors; + } + } + } + + return $errors; + } + + private function validateFile(bool $quickStop = false): ErrorSuite + { + $errors = new ErrorSuite(); + + $filenamePattern = $this->schema->getFilenamePattern(); + if ( + $filenamePattern !== null + && $filenamePattern !== '' + && \preg_match($filenamePattern, $this->csv->getCsvFilename()) === 0 + ) { + $error = new Error( + 'filename_pattern', + 'Filename "' . Utils::cutPath($this->csv->getCsvFilename()) . + "\" does not match pattern: \"{$filenamePattern}\"", + '', + 0, + ); + + $errors->addError($error); + + if ($quickStop && $errors->count() > 0) { + return $errors; + } + } + + return $errors; + } + + private static function validateAggregateRules(bool $quickStop = false): ErrorSuite + { + $errors = new ErrorSuite(); + + if ($quickStop && $errors->count() > 0) { + return $errors; + } + + return new ErrorSuite(); + } +} diff --git a/src/Validators/ErrorSuite.php b/src/Validators/ErrorSuite.php index 1384688c..4a625448 100644 --- a/src/Validators/ErrorSuite.php +++ b/src/Validators/ErrorSuite.php @@ -21,6 +21,9 @@ use JBZoo\CIReportConverter\Converters\JUnitConverter; use JBZoo\CIReportConverter\Converters\TeamCityTestsConverter; use JBZoo\CIReportConverter\Formats\Source\SourceSuite; +use JBZoo\Utils\Cli; +use JBZoo\Utils\Env; +use JBZoo\Utils\Vars; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Output\BufferedOutput; @@ -139,15 +142,17 @@ private function renderPlainText(): string private function renderTable(): string { + $floatingSizes = self::getTableSize(); + $buffer = new BufferedOutput(); $table = (new Table($buffer)) ->setHeaderTitle($this->getTestcaseName()) ->setFooterTitle($this->getTestcaseName()) ->setHeaders(['Line', 'id:Column', 'Rule', 'Message']) - ->setColumnMaxWidth(0, 10) - ->setColumnMaxWidth(1, 20) - ->setColumnMaxWidth(2, 20) - ->setColumnMaxWidth(3, 60); + ->setColumnMaxWidth(0, $floatingSizes['line']) + ->setColumnMaxWidth(1, $floatingSizes['column']) + ->setColumnMaxWidth(2, $floatingSizes['rule']) + ->setColumnMaxWidth(3, $floatingSizes['message']); foreach ($this->errors as $error) { $table->addRow([ @@ -182,4 +187,38 @@ private function getTestcaseName(): string { return \pathinfo((string)\realpath((string)$this->csvFilename), \PATHINFO_BASENAME); } + + /** + * Retrieves the size configuration for a table. + * + * @return int[] + */ + private static function getTableSize(): array + { + $floatingSizes = [ + 'line' => 10, + 'column' => 20, + 'rule' => 20, + 'min' => 90, + 'max' => 150, + 'reserve' => 5, // So that the table does not rest on the very edge of the terminal. Just in case. + ]; + + // Fallback to 80 if the terminal width cannot be determined. + // env.COLUMNS_TEST usually not defined so we use it only for testing purposes. + $maxAutoDetected = Env::int('COLUMNS_TEST', Cli::getNumberOfColumns()); + + $maxWindowWidth = Vars::limit( + $maxAutoDetected, + $floatingSizes['min'], + $floatingSizes['max'], + ) - $floatingSizes['reserve']; + + $floatingSizes['message'] = $maxWindowWidth + - $floatingSizes['line'] + - $floatingSizes['column'] + - $floatingSizes['rule']; + + return $floatingSizes; + } } diff --git a/src/Validators/Ruleset.php b/src/Validators/Ruleset.php index 42efff96..9bccd8c0 100644 --- a/src/Validators/Ruleset.php +++ b/src/Validators/Ruleset.php @@ -16,8 +16,8 @@ namespace JBZoo\CsvBlueprint\Validators; +use JBZoo\CsvBlueprint\Rules\AbstarctRule; use JBZoo\CsvBlueprint\Utils; -use JBZoo\CsvBlueprint\Validators\Rules\AbstarctRule; final class Ruleset { @@ -41,7 +41,7 @@ public function __construct(array $rules, string $columnNameId) */ public function createRule(string $ruleName, null|array|bool|float|int|string $options = null): AbstarctRule { - $classname = __NAMESPACE__ . '\\Rules\\' . Utils::kebabToCamelCase($ruleName); + $classname = '\\JBZoo\\CsvBlueprint\\Rules\\' . Utils::kebabToCamelCase($ruleName); if (\class_exists($classname)) { // @phpstan-ignore-next-line return new $classname($this->columnNameId, $options); @@ -50,7 +50,7 @@ public function createRule(string $ruleName, null|array|bool|float|int|string $o throw new Exception("Rule \"{$ruleName}\" not found. Expected class: \"{$classname}\""); } - public function validate(?string $cellValue, int $line): ErrorSuite + public function validate(string $cellValue, int $line): ErrorSuite { $errors = new ErrorSuite(); diff --git a/tests/Blueprint/MiscTest.php b/tests/Blueprint/MiscTest.php index 04b0d277..2ac6f844 100644 --- a/tests/Blueprint/MiscTest.php +++ b/tests/Blueprint/MiscTest.php @@ -61,7 +61,7 @@ public function testFullListOfRules(): void $finder = (new Finder()) ->files() - ->in(PROJECT_ROOT . '/src/Validators/Rules') + ->in(PROJECT_ROOT . '/src/Rules') ->ignoreDotFiles(false) ->ignoreVCS(true) ->name('/\\.php$/'); @@ -82,7 +82,13 @@ public function testFullListOfRules(): void } \sort($rulesInCode); - isSame($rulesInCode, $rulesInConfig); + $diffAsErrMessage = \array_reduce( + \array_diff($rulesInCode, $rulesInConfig), + static fn (string $carry, string $item) => $carry . "{$item}: FIXME\n", + '', + ); + + isSame($rulesInCode, $rulesInConfig, $diffAsErrMessage); } public function testCsvStrutureDefaultValues(): void @@ -105,15 +111,15 @@ public function testCheckYmlSchemaExampleInReadme(): void ); } - public function testCheckPhpSchemaExampleInReadme(): void - { - $this->testCheckExampleInReadme(PROJECT_ROOT . '/schema-examples/full.php', 'php', 'PHP Format', 14); - } - - public function testCheckJsonSchemaExampleInReadme(): void - { - $this->testCheckExampleInReadme(PROJECT_ROOT . '/schema-examples/full.json', 'json', 'JSON Format', 0); - } + // public function testCheckPhpSchemaExampleInReadme(): void + // { + // $this->testCheckExampleInReadme(PROJECT_ROOT . '/schema-examples/full.php', 'php', 'PHP Format', 14); + // } + // + // public function testCheckJsonSchemaExampleInReadme(): void + // { + // $this->testCheckExampleInReadme(PROJECT_ROOT . '/schema-examples/full.json', 'json', 'JSON Format', 0); + // } public function testCompareExamplesWithOrig(): void { @@ -125,8 +131,8 @@ public function testCompareExamplesWithOrig(): void // file_put_contents("{$basepath}.php", (string)phpArray($origYml)); // file_put_contents("{$basepath}.json", (string)json($origYml)); - isSame($origYml, phpArray("{$basepath}.php")->getArrayCopy(), 'PHP config is invalid'); - isSame($origYml, json("{$basepath}.json")->getArrayCopy(), 'JSON config is invalid'); + isSame((string)phpArray($origYml), (string)phpArray("{$basepath}.php"), 'PHP config is invalid'); + isSame((string)json($origYml), (string)json("{$basepath}.json"), 'JSON config is invalid'); } public function testFindFiles(): void diff --git a/tests/Blueprint/RulesTest.php b/tests/Blueprint/RulesTest.php index 69f97883..d7f741d3 100644 --- a/tests/Blueprint/RulesTest.php +++ b/tests/Blueprint/RulesTest.php @@ -16,33 +16,43 @@ namespace JBZoo\PHPUnit\Blueprint; -use JBZoo\CsvBlueprint\Validators\Rules\AllowValues; -use JBZoo\CsvBlueprint\Validators\Rules\CardinalDirection; -use JBZoo\CsvBlueprint\Validators\Rules\DateFormat; -use JBZoo\CsvBlueprint\Validators\Rules\ExactValue; -use JBZoo\CsvBlueprint\Validators\Rules\IsBool; -use JBZoo\CsvBlueprint\Validators\Rules\IsDomain; -use JBZoo\CsvBlueprint\Validators\Rules\IsEmail; -use JBZoo\CsvBlueprint\Validators\Rules\IsFloat; -use JBZoo\CsvBlueprint\Validators\Rules\IsInt; -use JBZoo\CsvBlueprint\Validators\Rules\IsIp; -use JBZoo\CsvBlueprint\Validators\Rules\IsLatitude; -use JBZoo\CsvBlueprint\Validators\Rules\IsLongitude; -use JBZoo\CsvBlueprint\Validators\Rules\IsUrl; -use JBZoo\CsvBlueprint\Validators\Rules\IsUuid4; -use JBZoo\CsvBlueprint\Validators\Rules\Max; -use JBZoo\CsvBlueprint\Validators\Rules\MaxDate; -use JBZoo\CsvBlueprint\Validators\Rules\MaxLength; -use JBZoo\CsvBlueprint\Validators\Rules\Min; -use JBZoo\CsvBlueprint\Validators\Rules\MinDate; -use JBZoo\CsvBlueprint\Validators\Rules\MinLength; -use JBZoo\CsvBlueprint\Validators\Rules\NotEmpty; -use JBZoo\CsvBlueprint\Validators\Rules\OnlyCapitalize; -use JBZoo\CsvBlueprint\Validators\Rules\OnlyLowercase; -use JBZoo\CsvBlueprint\Validators\Rules\OnlyUppercase; -use JBZoo\CsvBlueprint\Validators\Rules\Precision; -use JBZoo\CsvBlueprint\Validators\Rules\Regex; -use JBZoo\CsvBlueprint\Validators\Rules\UsaMarketName; +use JBZoo\CsvBlueprint\Rules\AllMustContain; +use JBZoo\CsvBlueprint\Rules\AllowValues; +use JBZoo\CsvBlueprint\Rules\AtLeastContains; +use JBZoo\CsvBlueprint\Rules\CardinalDirection; +use JBZoo\CsvBlueprint\Rules\DateFormat; +use JBZoo\CsvBlueprint\Rules\ExactValue; +use JBZoo\CsvBlueprint\Rules\IsAlias; +use JBZoo\CsvBlueprint\Rules\IsBool; +use JBZoo\CsvBlueprint\Rules\IsDomain; +use JBZoo\CsvBlueprint\Rules\IsEmail; +use JBZoo\CsvBlueprint\Rules\IsFloat; +use JBZoo\CsvBlueprint\Rules\IsInt; +use JBZoo\CsvBlueprint\Rules\IsIp; +use JBZoo\CsvBlueprint\Rules\IsLatitude; +use JBZoo\CsvBlueprint\Rules\IsLongitude; +use JBZoo\CsvBlueprint\Rules\IsUrl; +use JBZoo\CsvBlueprint\Rules\IsUuid4; +use JBZoo\CsvBlueprint\Rules\Max; +use JBZoo\CsvBlueprint\Rules\MaxDate; +use JBZoo\CsvBlueprint\Rules\MaxLength; +use JBZoo\CsvBlueprint\Rules\MaxPrecision; +use JBZoo\CsvBlueprint\Rules\MaxWordCount; +use JBZoo\CsvBlueprint\Rules\Min; +use JBZoo\CsvBlueprint\Rules\MinDate; +use JBZoo\CsvBlueprint\Rules\MinLength; +use JBZoo\CsvBlueprint\Rules\MinPrecision; +use JBZoo\CsvBlueprint\Rules\MinWordCount; +use JBZoo\CsvBlueprint\Rules\NotEmpty; +use JBZoo\CsvBlueprint\Rules\OnlyCapitalize; +use JBZoo\CsvBlueprint\Rules\OnlyLowercase; +use JBZoo\CsvBlueprint\Rules\OnlyUppercase; +use JBZoo\CsvBlueprint\Rules\Precision; +use JBZoo\CsvBlueprint\Rules\Regex; +use JBZoo\CsvBlueprint\Rules\StrEndsWith; +use JBZoo\CsvBlueprint\Rules\StrStartsWith; +use JBZoo\CsvBlueprint\Rules\UsaMarketName; +use JBZoo\CsvBlueprint\Rules\WordCount; use JBZoo\PHPUnit\PHPUnit; use JBZoo\Utils\Str; @@ -456,13 +466,9 @@ public function testNotEmpty(): void '"not_empty" at line 0, column "prop". Value is empty.', \strip_tags((string)$rule->validate('')), ); - isSame( - '"not_empty" at line 0, column "prop". Value is empty.', - \strip_tags((string)$rule->validate(null)), - ); $rule = new NotEmpty('prop', false); - isSame(null, $rule->validate(null)); + isSame(null, $rule->validate('')); } public function testOnlyCapitalize(): void @@ -575,6 +581,75 @@ public function testPrecision(): void ); } + public function testMinPrecision(): void + { + $rule = new MinPrecision('prop', 0); + isSame(null, $rule->validate('0')); + isSame(null, $rule->validate('0.0')); + isSame(null, $rule->validate('0.1')); + isSame(null, $rule->validate('-1.0')); + isSame(null, $rule->validate('10.01')); + isSame(null, $rule->validate('-10.0001')); + + $rule = new MinPrecision('prop', 1); + isSame(null, $rule->validate('0.0')); + isSame(null, $rule->validate('10.0')); + isSame(null, $rule->validate('-10.0')); + + isSame( + '"min_precision" at line 0, column "prop". ' . + 'Value "2" has a precision of 0 but should have a min precision of 1.', + \strip_tags((string)$rule->validate('2')), + ); + + $rule = new MinPrecision('prop', 2); + isSame(null, $rule->validate('10.01')); + isSame(null, $rule->validate('-10.0001')); + + isSame( + '"min_precision" at line 0, column "prop". ' . + 'Value "2" has a precision of 0 but should have a min precision of 2.', + \strip_tags((string)$rule->validate('2')), + ); + + isSame( + '"min_precision" at line 0, column "prop". ' . + 'Value "2.0" has a precision of 1 but should have a min precision of 2.', + \strip_tags((string)$rule->validate('2.0')), + ); + } + + public function testMaxPrecision(): void + { + $rule = new MaxPrecision('prop', 0); + isSame(null, $rule->validate('0')); + isSame(null, $rule->validate('10')); + isSame(null, $rule->validate('-10')); + + isSame( + '"max_precision" at line 0, column "prop". ' . + 'Value "2.0" has a precision of 1 but should have a max precision of 0.', + \strip_tags((string)$rule->validate('2.0')), + ); + + $rule = new MaxPrecision('prop', 1); + isSame(null, $rule->validate('0.0')); + isSame(null, $rule->validate('10.0')); + isSame(null, $rule->validate('-10.0')); + + isSame( + '"max_precision" at line 0, column "prop". ' . + 'Value "-2.003" has a precision of 3 but should have a max precision of 1.', + \strip_tags((string)$rule->validate('-2.003')), + ); + + isSame( + '"max_precision" at line 0, column "prop". ' . + 'Value "2.00000" has a precision of 5 but should have a max precision of 1.', + \strip_tags((string)$rule->validate('2.00000')), + ); + } + public function testRegex(): void { $rule = new Regex('prop', '/^a/'); @@ -643,4 +718,179 @@ public function testIsUuid4(): void $rule = new IsUuid4('prop', false); isSame(null, $rule->validate('123')); } + + public function testMustContain(): void + { + $rule = new AtLeastContains('prop', []); + isSame( + '"at_least_contains" at line 0, column "prop". ' . + 'Rule must contain at least one inclusion value in schema file.', + \strip_tags((string)$rule->validate('123')), + ); + + $rule = new AtLeastContains('prop', ['a', 'b', 'c']); + isSame(null, $rule->validate('a')); + isSame(null, $rule->validate('abc')); + isSame(null, $rule->validate('adasdasdasdc')); + + isSame( + '"at_least_contains" at line 0, column "prop". ' . + 'Value "123" must contain one of the following: "["a", "b", "c"]".', + \strip_tags((string)$rule->validate('123')), + ); + } + + public function testAllMustContain(): void + { + $rule = new AllMustContain('prop', []); + isSame( + '"all_must_contain" at line 0, column "prop". ' . + 'Rule must contain at least one inclusion value in schema file.', + \strip_tags((string)$rule->validate('ac')), + ); + + $rule = new AllMustContain('prop', ['a', 'b', 'c']); + isSame(null, $rule->validate('abc')); + isSame(null, $rule->validate('abdasadasdasdc')); + + isSame( + '"all_must_contain" at line 0, column "prop". ' . + 'Value "ab" must contain all of the following: "["a", "b", "c"]".', + \strip_tags((string)$rule->validate('ab')), + ); + isSame( + '"all_must_contain" at line 0, column "prop". ' . + 'Value "ac" must contain all of the following: "["a", "b", "c"]".', + \strip_tags((string)$rule->validate('ac')), + ); + } + + public function testStrStartsWith(): void + { + $rule = new StrStartsWith('prop', 'a'); + isSame(null, $rule->validate('a')); + isSame(null, $rule->validate('abc')); + + isSame( + '"str_starts_with" at line 0, column "prop". Value "" must start with "a".', + \strip_tags((string)$rule->validate('')), + ); + + isSame( + '"str_starts_with" at line 0, column "prop". Value " a" must start with "a".', + \strip_tags((string)$rule->validate(' a')), + ); + + $rule = new StrStartsWith('prop', ''); + isSame( + '"str_starts_with" at line 0, column "prop". Rule must contain a prefix value in schema file.', + \strip_tags((string)$rule->validate('a ')), + ); + } + + public function testStrEndsWith(): void + { + $rule = new StrEndsWith('prop', 'a'); + isSame(null, $rule->validate('a')); + isSame(null, $rule->validate('cba')); + + isSame( + '"str_ends_with" at line 0, column "prop". Value "" must end with "a".', + \strip_tags((string)$rule->validate('')), + ); + + isSame( + '"str_ends_with" at line 0, column "prop". Value "a " must end with "a".', + \strip_tags((string)$rule->validate('a ')), + ); + + $rule = new StrEndsWith('prop', ''); + isSame( + '"str_ends_with" at line 0, column "prop". Rule must contain a suffix value in schema file.', + \strip_tags((string)$rule->validate('a ')), + ); + } + + public function testStrWordCount(): void + { + $rule = new WordCount('prop', 0); + isSame(null, $rule->validate('')); + isSame( + '"word_count" at line 0, column "prop". ' . + 'Value "cba" has 1 words, but must have exactly 0 words.', + \strip_tags((string)$rule->validate('cba')), + ); + + $rule = new WordCount('prop', 2); + isSame(null, $rule->validate('asd, asdasd')); + isSame( + '"word_count" at line 0, column "prop". ' . + 'Value "cba" has 1 words, but must have exactly 2 words.', + \strip_tags((string)$rule->validate('cba')), + ); + isSame( + '"word_count" at line 0, column "prop". ' . + 'Value "cba 123, 123123" has 1 words, but must have exactly 2 words.', + \strip_tags((string)$rule->validate('cba 123, 123123')), + ); + + isSame( + '"word_count" at line 0, column "prop". Value "a b c" has 3 words, but must have exactly 2 words.', + \strip_tags((string)$rule->validate('a b c')), + ); + } + + public function testMinWordCount(): void + { + $rule = new MinWordCount('prop', 0); + isSame(null, $rule->validate('cba')); + + $rule = new MinWordCount('prop', 2); + isSame(null, $rule->validate('asd, asdasd')); + isSame(null, $rule->validate('asd, asdasd asd')); + isSame(null, $rule->validate('asd, asdasd 1232 asdas')); + isSame( + '"min_word_count" at line 0, column "prop". ' . + 'Value "cba" has 1 words, but must have at least 2 words.', + \strip_tags((string)$rule->validate('cba')), + ); + isSame( + '"min_word_count" at line 0, column "prop". ' . + 'Value "cba 123, 123123" has 1 words, but must have at least 2 words.', + \strip_tags((string)$rule->validate('cba 123, 123123')), + ); + } + + public function testMaxWordCount(): void + { + $rule = new MaxWordCount('prop', 0); + isSame(null, $rule->validate('')); + + $rule = new MaxWordCount('prop', 2); + isSame(null, $rule->validate('asd, asdasd')); + isSame(null, $rule->validate('asd, 1232')); + isSame(null, $rule->validate('asd, 1232 113234324 342 . ..')); + isSame( + '"max_word_count" at line 0, column "prop". ' . + 'Value "asd, asdasd asd 1232 asdas" has 4 words, but must have no more than 2 words.', + \strip_tags((string)$rule->validate('asd, asdasd asd 1232 asdas')), + ); + } + + public function testIsAlias(): void + { + $rule = new IsAlias('prop', true); + isSame(null, $rule->validate('')); + isSame(null, $rule->validate('123')); + + $rule = new IsAlias('prop', true); + isSame( + '"is_alias" at line 0, column "prop". ' . + 'Value "Qwerty, asd 123" is not a valid alias. Expected "qwerty-asd-123".', + \strip_tags((string)$rule->validate('Qwerty, asd 123')), + ); + + $rule = new IsAlias('prop', false); + isSame(null, $rule->validate('Qwerty, asd 123')); + } } diff --git a/tests/Blueprint/ValidateCsvTest.php b/tests/Blueprint/ValidateCsvTest.php index a7bc0ec8..780aad04 100644 --- a/tests/Blueprint/ValidateCsvTest.php +++ b/tests/Blueprint/ValidateCsvTest.php @@ -64,23 +64,18 @@ public function testValidateOneFileNegativeTable(): void Found CSV files: 1 (1/1) Invalid file: ./tests/fixtures/demo.csv - +------+------------------+------------------+--- demo.csv -------------------------------------------------+ - | Line | id:Column | Rule | Message | - +------+------------------+------------------+--------------------------------------------------------------+ - | 0 | | filename_pattern | Filename "./tests/fixtures/demo.csv" does not match pattern: | - | | | | "/demo-[12].csv$/i" | - | 5 | 2:Float | max | Value "74605.944" is greater than "74605" | - | 5 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | - | | | | "green", "Blue"] | - | 6 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | - | 6 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | - | | | | "1955-05-15T00:00:00.000+00:00" | - | 8 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | - | | | | "1955-05-15T00:00:00.000+00:00" | - | 9 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | - | | | | "2009-01-01T00:00:00.000+00:00" | - | 11 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | - +------+------------------+------------------+--- demo.csv -------------------------------------------------+ + +------+------------------+------------------+------------- demo.csv -----------------------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------------+------------------+----------------------------------------------------------------------------------+ + | 0 | | filename_pattern | Filename "./tests/fixtures/demo.csv" does not match pattern: "/demo-[12].csv$/i" | + | 5 | 2:Float | max | Value "74605.944" is greater than "74605" | + | 5 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"] | + | 6 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | + | 6 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | + | 8 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | + | 9 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date "2009-01-01T00:00:00.000+00:00" | + | 11 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | + +------+------------------+------------------+------------- demo.csv -----------------------------------------------------------+ Found 8 issues in CSV file. @@ -107,35 +102,30 @@ public function testValidateManyFileNegativeTable(): void Found CSV files: 3 (1/3) Invalid file: ./tests/fixtures/batch/demo-1.csv - +------+------------------+--------------+ demo-1.csv ------------------------------------------+ - | Line | id:Column | Rule | Message | - +------+------------------+--------------+------------------------------------------------------+ - | 3 | 2:Float | max | Value "74605.944" is greater than "74605" | - | 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", | - | | | | "green", "Blue"] | - +------+------------------+--------------+ demo-1.csv ------------------------------------------+ + +------+------------------+--------------+--------- demo-1.csv --------------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------------+--------------+-----------------------------------------------------------------------+ + | 3 | 2:Float | max | Value "74605.944" is greater than "74605" | + | 3 | 4:Favorite color | allow_values | Value "blue" is not allowed. Allowed values: ["red", "green", "Blue"] | + +------+------------------+--------------+--------- demo-1.csv --------------------------------------------------+ (2/3) Invalid file: ./tests/fixtures/batch/demo-2.csv - +------+------------+------------+----- demo-2.csv ---------------------------------------+ - | Line | id:Column | Rule | Message | - +------+------------+------------+--------------------------------------------------------+ - | 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | - | 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | - | | | | "1955-05-15T00:00:00.000+00:00" | - | 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date | - | | | | "1955-05-15T00:00:00.000+00:00" | - | 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date | - | | | | "2009-01-01T00:00:00.000+00:00" | - | 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | - +------+------------+------------+----- demo-2.csv ---------------------------------------+ + +------+------------+------------+------------------ demo-2.csv ----------------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+------------+------------+----------------------------------------------------------------------------------+ + | 2 | 0:Name | min_length | Value "Carl" (length: 4) is too short. Min length is 5 | + | 2 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | + | 4 | 3:Birthday | min_date | Value "1955-05-14" is less than the minimum date "1955-05-15T00:00:00.000+00:00" | + | 5 | 3:Birthday | max_date | Value "2010-07-20" is more than the maximum date "2009-01-01T00:00:00.000+00:00" | + | 7 | 0:Name | min_length | Value "Lois" (length: 4) is too short. Min length is 5 | + +------+------------+------------+------------------ demo-2.csv ----------------------------------------------------+ (3/3) Invalid file: ./tests/fixtures/batch/sub/demo-3.csv - +------+-----------+------------------+---- demo-3.csv -------------------------------------------+ - | Line | id:Column | Rule | Message | - +------+-----------+------------------+-----------------------------------------------------------+ - | 0 | | filename_pattern | Filename "./tests/fixtures/batch/sub/demo-3.csv" does not | - | | | | match pattern: "/demo-[12].csv$/i" | - +------+-----------+------------------+---- demo-3.csv -------------------------------------------+ + +------+-----------+------------------+---------------------- demo-3.csv ------------------------------------------------------------+ + | Line | id:Column | Rule | Message | + +------+-----------+------------------+----------------------------------------------------------------------------------------------+ + | 0 | | filename_pattern | Filename "./tests/fixtures/batch/sub/demo-3.csv" does not match pattern: "/demo-[12].csv$/i" | + +------+-----------+------------------+---------------------- demo-3.csv ------------------------------------------------------------+ Found 8 issues in 3 out of 3 CSV files. diff --git a/tests/Blueprint/ValidatorTest.php b/tests/Blueprint/ValidatorTest.php index 4c6a0ffa..e57f125e 100644 --- a/tests/Blueprint/ValidatorTest.php +++ b/tests/Blueprint/ValidatorTest.php @@ -43,7 +43,7 @@ protected function setUp(): void public function testUndefinedRule(): void { $this->expectExceptionMessage( - 'Rule "undefined_rule" not found. Expected class: "JBZoo\CsvBlueprint\Validators\Rules\UndefinedRule"', + 'Rule "undefined_rule" not found. Expected class: "\JBZoo\CsvBlueprint\Rules\UndefinedRule"', ); $csv = new CsvFile(self::CSV_COMPLEX, $this->getRule('seq', 'undefined_rule', true)); $csv->validate();