diff --git a/README.md b/README.md index b86b1071..b9c15467 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/jbzoo/csv-blueprint?label=Latest)](https://github.com/jbzoo/csv-blueprint/releases) [![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/tags) [![Docker Image Size](https://img.shields.io/docker/image-size/jbzoo/csv-blueprint)](https://hub.docker.com/r/jbzoo/csv-blueprint/tags) -[![Static Badge](https://img.shields.io/badge/Rules-282-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) [![Static Badge](https://img.shields.io/badge/Rules-71-green?label=Cell%20rules&labelColor=blue&color=gray)](src/Rules/Cell) [![Static Badge](https://img.shields.io/badge/Rules-206-green?label=Aggregate%20rules&labelColor=blue&color=gray)](src/Rules/Aggregate) [![Static Badge](https://img.shields.io/badge/Rules-5-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) [![Static Badge](https://img.shields.io/badge/Rules-207-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml) +[![Static Badge](https://img.shields.io/badge/Rules-292-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) [![Static Badge](https://img.shields.io/badge/Rules-81-green?label=Cell%20rules&labelColor=blue&color=gray)](src/Rules/Cell) [![Static Badge](https://img.shields.io/badge/Rules-206-green?label=Aggregate%20rules&labelColor=blue&color=gray)](src/Rules/Aggregate) [![Static Badge](https://img.shields.io/badge/Rules-5-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) [![Static Badge](https://img.shields.io/badge/Rules-199-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml) ## Introduction @@ -193,10 +193,11 @@ columns: word_count_max: 9 # x <= 9 # Contains rules - contains: Hello # Example: "Hello World". - contains_one: [ a, b ] # At least one of the string must be part of the CSV value. - contains_all: [ a, b, c ] # All the strings must be part of a CSV value. + contains: World # Example: "Hello World!". The string must contain "World" in any place. contains_none: [ a, b ] # All the strings must NOT be part of a CSV value. + contains_one: [ a, b ] # Only one of the strings must be part of the CSV value. + contains_any: [ a, b ] # At least one of the string must be part of the CSV value. + contains_all: [ a, b ] # All the strings must be part of a CSV value. starts_with: "prefix " # Example: "prefix Hello World". ends_with: " suffix" # Example: "Hello World suffix". @@ -241,16 +242,25 @@ columns: # Specific formats is_bool: true # Allow only boolean values "true" and "false", case-insensitive. - is_ip4: true # Only IPv4. Example: "127.0.0.1". - is_url: true # Only URL format. Example: "https://example.com/page?query=string#anchor". - is_email: true # Only email format. Example: "user@example.com". - is_domain: true # Only domain name. Example: "example.com". is_uuid: true # Validates whether the input is a valid UUID. It also supports validation of specific versions 1, 3, 4 and 5. is_slug: true # Only slug format. Example: "my-slug-123". It can contain letters, numbers, and dashes. is_currency_code: true # Validates an ISO 4217 currency code like GBP or EUR. Case-sensitive. See: https://en.wikipedia.org/wiki/ISO_4217. is_base64: true # Validate if a string is Base64-encoded. Example: "cmVzcGVjdCE=". is_angle: true # Check if the cell value is a valid angle (0.0 to 360.0). + # Internet + is_ip: true # Both: IPv4 or IPv6. + is_ip_v4: true # Only IPv4. Example: "127.0.0.1". + is_ip_v6: true # Only IPv6. Example: "2001:0db8:85a3:08d3:1319:8a2e:0370:7334". + is_ip_private: true # IPv4 has ranges: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. IPv6 has ranges starting with FD or FC. + is_ip_reserved: true # IPv4 has ranges: 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 and 240.0.0.0/4. IPv6 has ranges: ::1/128, ::/128, ::ffff:0:0/96 and fe80::/10. + ip_v4_range: [ '127.0.0.1-127.0.0.5', '127.0.0.0/21' ] # Check subnet mask or range for IPv4. Address must be in one of the ranges. + is_mac_address: true # The input is a valid MAC address. Example: 00:00:5e:00:53:01 + is_domain: true # Only domain name. Example: "example.com". + is_public_domain_suffix: true # The input is a public ICANN domain suffix. Example: "com", "nom.br", "net" etc. + is_url: true # Only URL format. Example: "https://example.com/page?query=string#anchor". + is_email: true # Only email format. Example: "user@example.com". + # Validates if the given input is a valid JSON. # This is possible if you escape all special characters correctly and use a special CSV format. is_json: true # Example: {"foo":"bar"}. @@ -259,19 +269,21 @@ columns: is_latitude: true # Can be integer or float. Example: 50.123456. is_longitude: true # Can be integer or float. Example: -89.123456. is_geohash: true # Check if the value is a valid geohash. Example: "u4pruydqqvj". - is_cardinal_direction: true # Valid cardinal direction. Available values: "N", "S", "E", "W", "NE", "SE", "NW", "SW", "none", "" + is_cardinal_direction: true # Valid cardinal direction. Available values: ["N", "S", "E", "W", "NE", "SE", "NW", "SW", "none", ""] is_usa_market_name: true # Check if the value is a valid USA market name. Example: "New York, NY". # Validates whether the input is a country code in ISO 3166-1 standard. # Available options: "alpha-2" (Ex: "US"), "alpha-3" (Ex: "USA"), "numeric" (Ex: "840"). # The rule uses data from iso-codes: https://salsa.debian.org/iso-codes-team/iso-codes. - country_code: alpha-2 # Country code in ISO 3166-1 standard. Examples: "US", "USA", "840". + is_country_code: alpha-2 # Country code in ISO 3166-1 standard. Examples: "US", "USA", "840" # Validates whether the input is language code based on ISO 639. # Available options: "alpha-2" (Ex: "en"), "alpha-3" (Ex: "eng"). # See: https://en.wikipedia.org/wiki/ISO_639. - language_code: alpha-2 # Examples: "en", "eng". + is_language_code: alpha-2 # Examples: "en", "eng" + is_file_exists: true # Check if file exists on the filesystem (It's FS IO operation!). + is_dir_exists: true # Check if directory exists on the filesystem (It's FS IO operation!). #################################################################################################################### # Data validation for the entire(!) column using different data aggregation methods. @@ -282,8 +294,8 @@ columns: is_unique: true # All values in the column are unique. # Check if the column is sorted in a specific order. - # - Direction: "asc", "desc". - # - Method: "natural", "regular", "numeric", "string". + # - Direction: ["asc", "desc"]. + # - Method: ["natural", "regular", "numeric", "string"]. # See: https://www.php.net/manual/en/function.sort.php is_sorted: [ asc, natural ] # Expected ascending order, natural sorting. @@ -295,7 +307,7 @@ columns: first_num_less: 8.0 # x < 8.0 first_num_max: 9.0 # x <= 9.0 first: Expected # First value in the column. Will be compared as strings. - first_not: 'Not Expected' # Not allowed as the first value in the column. Will be compared as strings. + first_not: Not expected # Not allowed as the first value in the column. Will be compared as strings. # N-th value in the column. # The rule expects exactly two arguments: the first is the line number (without header), the second is the expected value. @@ -307,7 +319,7 @@ columns: nth_num_less: [ 42, 8.0 ] # x < 8.0 nth_num_max: [ 42, 9.0 ] # x <= 9.0 nth: [ 2, Expected ] # Nth value in the column. Will be compared as strings. - nth_not: [ 2, 'Not expected' ] # Not allowed as the N-th value in the column. Will be compared as strings. + nth_not: [ 2, Not expected ] # Not allowed as the N-th value in the column. Will be compared as strings. # Last number in the column. Expected value is float or integer. last_num_min: 1.0 # x >= 1.0 @@ -317,7 +329,7 @@ columns: last_num_less: 8.0 # x < 8.0 last_num_max: 9.0 # x <= 9.0 last: Expected # Last value in the column. Will be compared as strings. - last_not: 'Not Expected' # Not allowed as the last value in the column. Will be compared as strings. + last_not: Not expected # Not allowed as the last value in the column. Will be compared as strings. # Sum of the numbers in the column. Example: [1, 2, 3] => 6. sum_min: 1.0 # x >= 1.0 @@ -495,18 +507,18 @@ columns: # Linear interpolation between closest ranks method - Second variant, C = 1 P-th percentile (0 <= P <= 100) of a list of N ordered values (sorted from least to greatest). # Similar method used in NumPy and Excel. # See: https://en.wikipedia.org/wiki/Percentile#Second_variant.2C_.7F.27.22.60UNIQ--postMath-00000043-QINU.60.22.27.7F - # Example: `[ 95, 1.234 ]` The 95th percentile in the column must be "1.234" (float). - percentile_min: [ 95, 1.0 ] # x >= 1.0 - percentile_greater: [ 95, 2.0 ] # x > 2.0 - percentile_not: [ 95, 5.0 ] # x != 5.0 - percentile: [ 95, 7.0 ] # x == 7.0 - percentile_less: [ 95, 8.0 ] # x < 8.0 - percentile_max: [ 95, 9.0 ] # x <= 9.0 + # Example: `[ 95.5, 1.234 ]` The 95.5th percentile in the column must be "1.234" (float). + percentile_min: [ 95.0, 1.0 ] # x >= 1.0 + percentile_greater: [ 95.0, 2.0 ] # x > 2.0 + percentile_not: [ 95.0, 5.0 ] # x != 5.0 + percentile: [ 95.0, 7.0 ] # x == 7.0 + percentile_less: [ 95.0, 8.0 ] # x < 8.0 + percentile_max: [ 95.0, 9.0 ] # x <= 9.0 # Quartiles. Three points that divide the data set into four equal groups, each group comprising a quarter of the data. # See: https://en.wikipedia.org/wiki/Quartile - # There are multiple methods for computing quartiles: "exclusive", "inclusive". Exclusive is ussually classic. - # Available types: "0%", "Q1", "Q2", "Q3", "100%", "IQR" (aka Interquartile Range) + # There are multiple methods for computing quartiles: ["exclusive", "inclusive"]. Exclusive is ussually classic. + # Available types: ["0%", "Q1", "Q2", "Q3", "100%", "IQR"] ("IQR" is Interquartile Range) # Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 50.0 quartiles_min: [ 'exclusive', '0%', 1.0 ] # x >= 1.0 quartiles_greater: [ 'inclusive', 'Q1', 2.0 ] # x > 2.0 @@ -753,7 +765,7 @@ Options: Feel free to use glob pattrens. Usage examples: /full/path/file.yml, p/file.yml, p/*.yml, p/**/*.yml, p/**/name-*.json, **/*.php, etc. (multiple values allowed) -r, --report=REPORT Report output format. Available options: - text, table, github, gitlab, teamcity, junit [default: "table"] + ["text", "table", "github", "gitlab", "teamcity", "junit"] [default: "table"] -Q, --quick[=QUICK] Immediately terminate the check at the first error found. Of course it will speed up the check, but you will get only 1 message out of many. If any error is detected, the utility will return a non-zero exit code. diff --git a/schema-examples/full.json b/schema-examples/full.json index 389666c4..2e74c900 100644 --- a/schema-examples/full.json +++ b/schema-examples/full.json @@ -20,87 +20,99 @@ "example" : "Some example", "rules" : { - "not_empty" : true, - "exact_value" : "Some string", - "allow_values" : ["y", "n", ""], - "not_allow_values" : ["invalid"], - - "regex" : "\/^[\\d]{2}$\/", - - "length_min" : 1, - "length_greater" : 2, - "length_not" : 0, - "length" : 7, - "length_less" : 8, - "length_max" : 9, - - "is_trimmed" : true, - "is_lowercase" : true, - "is_uppercase" : true, - "is_capitalize" : true, - - "word_count_min" : 1, - "word_count_greater" : 2, - "word_count_not" : 0, - "word_count" : 7, - "word_count_less" : 8, - "word_count_max" : 9, - - "contains" : "Hello", - "contains_one" : ["a", "b"], - "contains_all" : ["a", "b", "c"], - "contains_none" : ["a", "b"], - "starts_with" : "prefix ", - "ends_with" : " suffix", - - "num_min" : 1, - "num_greater" : 2, - "num_not" : 5, - "num" : 7, - "num_less" : 8, - "num_max" : 9, - "is_int" : true, - "is_float" : true, - - "precision_min" : 1, - "precision_greater" : 2, - "precision_not" : 0, - "precision" : 7, - "precision_less" : 8, - "precision_max" : 9, - - "date_min" : "-100 years", - "date_greater" : "-99 days", - "date_not" : "2006-01-02 15:04:05 -0700 Europe\/Rome", - "date" : "01 Jan 2000", - "date_less" : "now", - "date_max" : "+1 day", - "date_format" : "Y-m-d", - "is_date" : true, - "is_timezone" : true, - "is_timezone_offset" : true, - "is_time" : true, - "is_leap_year" : true, - - "is_bool" : true, - "is_ip4" : true, - "is_url" : true, - "is_email" : true, - "is_domain" : true, - "is_uuid" : true, - "is_slug" : true, - "is_currency_code" : true, - "is_base64" : true, - "is_angle" : true, - "is_json" : true, - "is_latitude" : true, - "is_longitude" : true, - "is_geohash" : true, - "is_cardinal_direction" : true, - "is_usa_market_name" : true, - - "country_code" : "alpha-2", - "language_code" : "alpha-2" + "not_empty" : true, + "exact_value" : "Some string", + "allow_values" : ["y", "n", ""], + "not_allow_values" : ["invalid"], + + "regex" : "\/^[\\d]{2}$\/", + + "length_min" : 1, + "length_greater" : 2, + "length_not" : 0, + "length" : 7, + "length_less" : 8, + "length_max" : 9, + + "is_trimmed" : true, + "is_lowercase" : true, + "is_uppercase" : true, + "is_capitalize" : true, + + "word_count_min" : 1, + "word_count_greater" : 2, + "word_count_not" : 0, + "word_count" : 7, + "word_count_less" : 8, + "word_count_max" : 9, + + "contains" : "World", + "contains_none" : ["a", "b"], + "contains_one" : ["a", "b"], + "contains_any" : ["a", "b"], + "contains_all" : ["a", "b"], + "starts_with" : "prefix ", + "ends_with" : " suffix", + + "num_min" : 1, + "num_greater" : 2, + "num_not" : 5, + "num" : 7, + "num_less" : 8, + "num_max" : 9, + "is_int" : true, + "is_float" : true, + + "precision_min" : 1, + "precision_greater" : 2, + "precision_not" : 0, + "precision" : 7, + "precision_less" : 8, + "precision_max" : 9, + + "date_min" : "-100 years", + "date_greater" : "-99 days", + "date_not" : "2006-01-02 15:04:05 -0700 Europe\/Rome", + "date" : "01 Jan 2000", + "date_less" : "now", + "date_max" : "+1 day", + "date_format" : "Y-m-d", + "is_date" : true, + "is_timezone" : true, + "is_timezone_offset" : true, + "is_time" : true, + "is_leap_year" : true, + + "is_bool" : true, + "is_uuid" : true, + "is_slug" : true, + "is_currency_code" : true, + "is_base64" : true, + "is_angle" : true, + + "is_ip" : true, + "is_ip_v4" : true, + "is_ip_v6" : true, + "is_ip_private" : true, + "is_ip_reserved" : true, + "ip_v4_range" : ["127.0.0.1-127.0.0.5", "127.0.0.0\/21"], + "is_mac_address" : true, + "is_domain" : true, + "is_public_domain_suffix" : true, + "is_url" : true, + "is_email" : true, + + "is_json" : true, + "is_latitude" : true, + "is_longitude" : true, + "is_geohash" : true, + "is_cardinal_direction" : true, + "is_usa_market_name" : true, + + "is_country_code" : "alpha-2", + "is_language_code" : "alpha-2", + "is_file_exists" : true, + "is_dir_exists" : true }, "aggregate_rules" : { "is_unique" : true, @@ -113,7 +125,7 @@ "first_num_less" : 8, "first_num_max" : 9, "first" : "Expected", - "first_not" : "Not Expected", + "first_not" : "Not expected", "nth_num_min" : [42, 1], "nth_num_greater" : [42, 2], @@ -131,7 +143,7 @@ "last_num_less" : 8, "last_num_max" : 9, "last" : "Expected", - "last_not" : "Not Expected", + "last_not" : "Not expected", "sum_min" : 1, "sum_greater" : 2, diff --git a/schema-examples/full.php b/schema-examples/full.php index 57123df4..b9965e34 100644 --- a/schema-examples/full.php +++ b/schema-examples/full.php @@ -67,10 +67,11 @@ 'word_count_less' => 8, 'word_count_max' => 9, - 'contains' => 'Hello', - 'contains_one' => ['a', 'b'], - 'contains_all' => ['a', 'b', 'c'], + 'contains' => 'World', 'contains_none' => ['a', 'b'], + 'contains_one' => ['a', 'b'], + 'contains_any' => ['a', 'b'], + 'contains_all' => ['a', 'b'], 'starts_with' => 'prefix ', 'ends_with' => ' suffix', @@ -103,16 +104,25 @@ 'is_time' => true, 'is_leap_year' => true, - 'is_bool' => true, - 'is_ip4' => true, - 'is_url' => true, - 'is_email' => true, - 'is_domain' => true, - 'is_uuid' => true, - 'is_slug' => true, - 'is_currency_code' => true, - 'is_base64' => true, - 'is_angle' => true, + 'is_bool' => true, + 'is_uuid' => true, + 'is_slug' => true, + 'is_currency_code' => true, + 'is_base64' => true, + 'is_angle' => true, + + 'is_ip' => true, + 'is_ip_v4' => true, + 'is_ip_v6' => true, + 'is_ip_private' => true, + 'is_ip_reserved' => true, + 'ip_v4_range' => ['127.0.0.1-127.0.0.5', '127.0.0.0/21'], + 'is_mac_address' => true, + 'is_domain' => true, + 'is_public_domain_suffix' => true, + 'is_url' => true, + 'is_email' => true, + 'is_json' => true, 'is_latitude' => true, 'is_longitude' => true, @@ -120,8 +130,11 @@ 'is_cardinal_direction' => true, 'is_usa_market_name' => true, - 'country_code' => 'alpha-2', - 'language_code' => 'alpha-2', + 'is_country_code' => 'alpha-2', + 'is_language_code' => 'alpha-2', + + 'is_file_exists' => true, + 'is_dir_exists' => true, ], 'aggregate_rules' => [ @@ -135,7 +148,7 @@ 'first_num_less' => 8.0, 'first_num_max' => 9.0, 'first' => 'Expected', - 'first_not' => 'Not Expected', + 'first_not' => 'Not expected', 'nth_num_min' => [42, 1.0], 'nth_num_greater' => [42, 2.0], @@ -153,7 +166,7 @@ 'last_num_less' => 8.0, 'last_num_max' => 9.0, 'last' => 'Expected', - 'last_not' => 'Not Expected', + 'last_not' => 'Not expected', 'sum_min' => 1.0, 'sum_greater' => 2.0, @@ -295,12 +308,12 @@ 'cubic_mean_less' => 8.0, 'cubic_mean_max' => 9.0, - 'percentile_min' => [95, 1.0], - 'percentile_greater' => [95, 2.0], - 'percentile_not' => [95, 5.0], - 'percentile' => [95, 7.0], - 'percentile_less' => [95, 8.0], - 'percentile_max' => [95, 9.0], + 'percentile_min' => [95.0, 1.0], + 'percentile_greater' => [95.0, 2.0], + 'percentile_not' => [95.0, 5.0], + 'percentile' => [95.0, 7.0], + 'percentile_less' => [95.0, 8.0], + 'percentile_max' => [95.0, 9.0], 'quartiles_min' => ['exclusive', '0%', 1.0], 'quartiles_greater' => ['inclusive', 'Q1', 2.0], diff --git a/schema-examples/full.yml b/schema-examples/full.yml index d605c0cb..4c00d56e 100644 --- a/schema-examples/full.yml +++ b/schema-examples/full.yml @@ -107,10 +107,11 @@ columns: word_count_max: 9 # x <= 9 # Contains rules - contains: Hello # Example: "Hello World". - contains_one: [ a, b ] # At least one of the string must be part of the CSV value. - contains_all: [ a, b, c ] # All the strings must be part of a CSV value. + contains: World # Example: "Hello World!". The string must contain "World" in any place. contains_none: [ a, b ] # All the strings must NOT be part of a CSV value. + contains_one: [ a, b ] # Only one of the strings must be part of the CSV value. + contains_any: [ a, b ] # At least one of the string must be part of the CSV value. + contains_all: [ a, b ] # All the strings must be part of a CSV value. starts_with: "prefix " # Example: "prefix Hello World". ends_with: " suffix" # Example: "Hello World suffix". @@ -155,16 +156,25 @@ columns: # Specific formats is_bool: true # Allow only boolean values "true" and "false", case-insensitive. - is_ip4: true # Only IPv4. Example: "127.0.0.1". - is_url: true # Only URL format. Example: "https://example.com/page?query=string#anchor". - is_email: true # Only email format. Example: "user@example.com". - is_domain: true # Only domain name. Example: "example.com". is_uuid: true # Validates whether the input is a valid UUID. It also supports validation of specific versions 1, 3, 4 and 5. is_slug: true # Only slug format. Example: "my-slug-123". It can contain letters, numbers, and dashes. is_currency_code: true # Validates an ISO 4217 currency code like GBP or EUR. Case-sensitive. See: https://en.wikipedia.org/wiki/ISO_4217. is_base64: true # Validate if a string is Base64-encoded. Example: "cmVzcGVjdCE=". is_angle: true # Check if the cell value is a valid angle (0.0 to 360.0). + # Internet + is_ip: true # Both: IPv4 or IPv6. + is_ip_v4: true # Only IPv4. Example: "127.0.0.1". + is_ip_v6: true # Only IPv6. Example: "2001:0db8:85a3:08d3:1319:8a2e:0370:7334". + is_ip_private: true # IPv4 has ranges: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. IPv6 has ranges starting with FD or FC. + is_ip_reserved: true # IPv4 has ranges: 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 and 240.0.0.0/4. IPv6 has ranges: ::1/128, ::/128, ::ffff:0:0/96 and fe80::/10. + ip_v4_range: [ '127.0.0.1-127.0.0.5', '127.0.0.0/21' ] # Check subnet mask or range for IPv4. Address must be in one of the ranges. + is_mac_address: true # The input is a valid MAC address. Example: 00:00:5e:00:53:01 + is_domain: true # Only domain name. Example: "example.com". + is_public_domain_suffix: true # The input is a public ICANN domain suffix. Example: "com", "nom.br", "net" etc. + is_url: true # Only URL format. Example: "https://example.com/page?query=string#anchor". + is_email: true # Only email format. Example: "user@example.com". + # Validates if the given input is a valid JSON. # This is possible if you escape all special characters correctly and use a special CSV format. is_json: true # Example: {"foo":"bar"}. @@ -173,19 +183,21 @@ columns: is_latitude: true # Can be integer or float. Example: 50.123456. is_longitude: true # Can be integer or float. Example: -89.123456. is_geohash: true # Check if the value is a valid geohash. Example: "u4pruydqqvj". - is_cardinal_direction: true # Valid cardinal direction. Available values: "N", "S", "E", "W", "NE", "SE", "NW", "SW", "none", "" + is_cardinal_direction: true # Valid cardinal direction. Available values: ["N", "S", "E", "W", "NE", "SE", "NW", "SW", "none", ""] is_usa_market_name: true # Check if the value is a valid USA market name. Example: "New York, NY". # Validates whether the input is a country code in ISO 3166-1 standard. # Available options: "alpha-2" (Ex: "US"), "alpha-3" (Ex: "USA"), "numeric" (Ex: "840"). # The rule uses data from iso-codes: https://salsa.debian.org/iso-codes-team/iso-codes. - country_code: alpha-2 # Country code in ISO 3166-1 standard. Examples: "US", "USA", "840". + is_country_code: alpha-2 # Country code in ISO 3166-1 standard. Examples: "US", "USA", "840" # Validates whether the input is language code based on ISO 639. # Available options: "alpha-2" (Ex: "en"), "alpha-3" (Ex: "eng"). # See: https://en.wikipedia.org/wiki/ISO_639. - language_code: alpha-2 # Examples: "en", "eng". + is_language_code: alpha-2 # Examples: "en", "eng" + is_file_exists: true # Check if file exists on the filesystem (It's FS IO operation!). + is_dir_exists: true # Check if directory exists on the filesystem (It's FS IO operation!). #################################################################################################################### # Data validation for the entire(!) column using different data aggregation methods. @@ -196,8 +208,8 @@ columns: is_unique: true # All values in the column are unique. # Check if the column is sorted in a specific order. - # - Direction: "asc", "desc". - # - Method: "natural", "regular", "numeric", "string". + # - Direction: ["asc", "desc"]. + # - Method: ["natural", "regular", "numeric", "string"]. # See: https://www.php.net/manual/en/function.sort.php is_sorted: [ asc, natural ] # Expected ascending order, natural sorting. @@ -209,7 +221,7 @@ columns: first_num_less: 8.0 # x < 8.0 first_num_max: 9.0 # x <= 9.0 first: Expected # First value in the column. Will be compared as strings. - first_not: 'Not Expected' # Not allowed as the first value in the column. Will be compared as strings. + first_not: Not expected # Not allowed as the first value in the column. Will be compared as strings. # N-th value in the column. # The rule expects exactly two arguments: the first is the line number (without header), the second is the expected value. @@ -221,7 +233,7 @@ columns: nth_num_less: [ 42, 8.0 ] # x < 8.0 nth_num_max: [ 42, 9.0 ] # x <= 9.0 nth: [ 2, Expected ] # Nth value in the column. Will be compared as strings. - nth_not: [ 2, 'Not expected' ] # Not allowed as the N-th value in the column. Will be compared as strings. + nth_not: [ 2, Not expected ] # Not allowed as the N-th value in the column. Will be compared as strings. # Last number in the column. Expected value is float or integer. last_num_min: 1.0 # x >= 1.0 @@ -231,7 +243,7 @@ columns: last_num_less: 8.0 # x < 8.0 last_num_max: 9.0 # x <= 9.0 last: Expected # Last value in the column. Will be compared as strings. - last_not: 'Not Expected' # Not allowed as the last value in the column. Will be compared as strings. + last_not: Not expected # Not allowed as the last value in the column. Will be compared as strings. # Sum of the numbers in the column. Example: [1, 2, 3] => 6. sum_min: 1.0 # x >= 1.0 @@ -409,18 +421,18 @@ columns: # Linear interpolation between closest ranks method - Second variant, C = 1 P-th percentile (0 <= P <= 100) of a list of N ordered values (sorted from least to greatest). # Similar method used in NumPy and Excel. # See: https://en.wikipedia.org/wiki/Percentile#Second_variant.2C_.7F.27.22.60UNIQ--postMath-00000043-QINU.60.22.27.7F - # Example: `[ 95, 1.234 ]` The 95th percentile in the column must be "1.234" (float). - percentile_min: [ 95, 1.0 ] # x >= 1.0 - percentile_greater: [ 95, 2.0 ] # x > 2.0 - percentile_not: [ 95, 5.0 ] # x != 5.0 - percentile: [ 95, 7.0 ] # x == 7.0 - percentile_less: [ 95, 8.0 ] # x < 8.0 - percentile_max: [ 95, 9.0 ] # x <= 9.0 + # Example: `[ 95.5, 1.234 ]` The 95.5th percentile in the column must be "1.234" (float). + percentile_min: [ 95.0, 1.0 ] # x >= 1.0 + percentile_greater: [ 95.0, 2.0 ] # x > 2.0 + percentile_not: [ 95.0, 5.0 ] # x != 5.0 + percentile: [ 95.0, 7.0 ] # x == 7.0 + percentile_less: [ 95.0, 8.0 ] # x < 8.0 + percentile_max: [ 95.0, 9.0 ] # x <= 9.0 # Quartiles. Three points that divide the data set into four equal groups, each group comprising a quarter of the data. # See: https://en.wikipedia.org/wiki/Quartile - # There are multiple methods for computing quartiles: "exclusive", "inclusive". Exclusive is ussually classic. - # Available types: "0%", "Q1", "Q2", "Q3", "100%", "IQR" (aka Interquartile Range) + # There are multiple methods for computing quartiles: ["exclusive", "inclusive"]. Exclusive is ussually classic. + # Available types: ["0%", "Q1", "Q2", "Q3", "100%", "IQR"] ("IQR" is Interquartile Range) # Example: `[ inclusive, 'Q3', 42.0 ]` - the Q3 inclusive quartile is 50.0 quartiles_min: [ 'exclusive', '0%', 1.0 ] # x >= 1.0 quartiles_greater: [ 'inclusive', 'Q1', 2.0 ] # x > 2.0 diff --git a/schema-examples/full_clean.yml b/schema-examples/full_clean.yml index 832cf8d3..042f1389 100644 --- a/schema-examples/full_clean.yml +++ b/schema-examples/full_clean.yml @@ -67,15 +67,17 @@ columns: word_count_less: 8 word_count_max: 9 - contains: Hello + contains: World + contains_none: + - a + - b contains_one: - a - b - contains_all: + contains_any: - a - b - - c - contains_none: + contains_all: - a - b starts_with: 'prefix ' @@ -111,23 +113,37 @@ columns: is_leap_year: true is_bool: true - is_ip4: true - is_url: true - is_email: true - is_domain: true is_uuid: true is_slug: true is_currency_code: true is_base64: true is_angle: true + + is_ip: true + is_ip_v4: true + is_ip_v6: true + is_ip_private: true + is_ip_reserved: true + ip_v4_range: + - 127.0.0.1-127.0.0.5 + - 127.0.0.0/21 + is_mac_address: true + is_domain: true + is_public_domain_suffix: true + is_url: true + is_email: true + is_json: true is_latitude: true is_longitude: true is_geohash: true is_cardinal_direction: true is_usa_market_name: true - country_code: alpha-2 - language_code: alpha-2 + is_country_code: alpha-2 + is_language_code: alpha-2 + + is_file_exists: true + is_dir_exists: true aggregate_rules: is_unique: true @@ -142,7 +158,7 @@ columns: first_num_less: 8.0 first_num_max: 9.0 first: Expected - first_not: 'Not Expected' + first_not: Not expected nth_num_min: - 42 @@ -167,7 +183,7 @@ columns: - Expected nth_not: - 2 - - 'Not expected' + - Not expected last_num_min: 1.0 last_num_greater: 2.0 @@ -176,7 +192,7 @@ columns: last_num_less: 8.0 last_num_max: 9.0 last: Expected - last_not: 'Not Expected' + last_not: Not expected sum_min: 1.0 sum_greater: 2.0 @@ -319,22 +335,22 @@ columns: cubic_mean_max: 9.0 percentile_min: - - 95 + - 95.0 - 1.0 percentile_greater: - - 95 + - 95.0 - 2.0 percentile_not: - - 95 + - 95.0 - 5.0 percentile: - - 95 + - 95.0 - 7.0 percentile_less: - - 95 + - 95.0 - 8.0 percentile_max: - - 95 + - 95.0 - 9.0 quartiles_min: diff --git a/src/Commands/ValidateCsv.php b/src/Commands/ValidateCsv.php index 95f6c2c3..dca115a4 100644 --- a/src/Commands/ValidateCsv.php +++ b/src/Commands/ValidateCsv.php @@ -79,7 +79,7 @@ protected function configure(): void 'r', InputOption::VALUE_REQUIRED, "Report output format. Available options:\n" . - '' . \implode(', ', ErrorSuite::getAvaiableRenderFormats()) . '', + Utils::printList(ErrorSuite::getAvaiableRenderFormats(), 'info'), ErrorSuite::REPORT_DEFAULT, ) ->addOption( @@ -148,7 +148,7 @@ private function getSchemaFilepaths(): array $schemaFilenames = \array_values(Utils::findFiles($this->getOptArray('schema'))); if (\count($schemaFilenames) === 0) { - throw new Exception('Schema file(s) not found: ' . \implode('; ', $this->getOptArray('schema'))); + throw new Exception('Schema file(s) not found: ' . Utils::printList($this->getOptArray('schema'))); } return $schemaFilenames; diff --git a/src/Rules/AbstarctRule.php b/src/Rules/AbstarctRule.php index 6173ca2a..8fe248ce 100644 --- a/src/Rules/AbstarctRule.php +++ b/src/Rules/AbstarctRule.php @@ -115,9 +115,9 @@ protected function getOptionAsBool(): bool { // TODO: Replace to warning message if (!\is_bool($this->options)) { - $options = \is_array($this->options) ? \implode(', ', $this->options) : (string)$this->options; + $options = Utils::printList($this->options, 'c'); throw new Exception( - "Invalid option \"{$options}\" for the \"{$this->getRuleCode()}\" rule. " . + "Invalid option {$options} for the \"{$this->getRuleCode()}\" rule. " . 'It should be true|false.', ); } @@ -129,10 +129,9 @@ protected function getOptionAsString(): string { // TODO: Replace to warning message if (\is_array($this->options)) { - $options = \implode(', ', $this->options); - + $options = Utils::printList($this->options, 'c'); throw new Exception( - "Invalid option \"{$options}\" for the \"{$this->getRuleCode()}\" rule. " . + "Invalid option {$options} for the \"{$this->getRuleCode()}\" rule. " . 'It should be int/float/string.', ); } @@ -144,9 +143,9 @@ protected function getOptionAsInt(): int { // TODO: Replace to warning message if ($this->options === '' || !\is_numeric($this->options)) { - $options = \is_array($this->options) ? '[' . \implode(', ', $this->options) . ']' : $this->options; + $options = Utils::printList($this->options, 'c'); throw new Exception( - "Invalid option \"{$options}\" for the \"{$this->getRuleCode()}\" rule. " . + "Invalid option {$options} for the \"{$this->getRuleCode()}\" rule. " . 'It should be integer.', ); } @@ -158,9 +157,9 @@ protected function getOptionAsFloat(): float { // TODO: Replace to warning message if ($this->options === '' || !\is_numeric($this->options)) { - $options = \is_array($this->options) ? '[' . \implode(', ', $this->options) . ']' : $this->options; + $options = Utils::printList($this->options, 'c'); throw new Exception( - "Invalid option \"{$options}\" for the \"{$this->getRuleCode()}\" rule. " . + "Invalid option {$options} for the \"{$this->getRuleCode()}\" rule. " . 'It should be integer/float.', ); } @@ -172,8 +171,9 @@ protected function getOptionAsArray(): array { // TODO: Replace to warning message if (!\is_array($this->options)) { + $options = Utils::printList($this->options, 'c'); throw new Exception( - "Invalid option \"{$this->options}\" for the \"{$this->getRuleCode()}\" rule. " . + "Invalid option {$options} for the \"{$this->getRuleCode()}\" rule. " . 'It should be array of strings.', ); } diff --git a/src/Rules/Aggregate/ComboPercentile.php b/src/Rules/Aggregate/ComboPercentile.php index 7d06a8a9..25140e16 100644 --- a/src/Rules/Aggregate/ComboPercentile.php +++ b/src/Rules/Aggregate/ComboPercentile.php @@ -41,15 +41,15 @@ public function getHelpMeta(): array 'Similar method used in NumPy and Excel.', 'See: https://en.wikipedia.org/wiki/Percentile#' . 'Second_variant.2C_.7F.27.22.60UNIQ--postMath-00000043-QINU.60.22.27.7F', - 'Example: `[ 95, 1.234 ]` The 95th percentile in the column must be "1.234" (float).', + 'Example: `[ 95.5, 1.234 ]` The 95.5th percentile in the column must be "1.234" (float).', ], [ - self::MIN => ['[ 95, 1.0 ]', 'x >= 1.0'], - self::GREATER => ['[ 95, 2.0 ]', 'x > 2.0'], - self::NOT => ['[ 95, 5.0 ]', 'x != 5.0'], - self::EQ => ['[ 95, 7.0 ]', 'x == 7.0'], - self::LESS => ['[ 95, 8.0 ]', 'x < 8.0'], - self::MAX => ['[ 95, 9.0 ]', 'x <= 9.0'], + self::MIN => ['[ 95.0, 1.0 ]', 'x >= 1.0'], + self::GREATER => ['[ 95.0, 2.0 ]', 'x > 2.0'], + self::NOT => ['[ 95.0, 5.0 ]', 'x != 5.0'], + self::EQ => ['[ 95.0, 7.0 ]', 'x == 7.0'], + self::LESS => ['[ 95.0, 8.0 ]', 'x < 8.0'], + self::MAX => ['[ 95.0, 9.0 ]', 'x <= 9.0'], ], ]; } diff --git a/src/Rules/Aggregate/ComboQuartiles.php b/src/Rules/Aggregate/ComboQuartiles.php index e937f433..5824b95c 100644 --- a/src/Rules/Aggregate/ComboQuartiles.php +++ b/src/Rules/Aggregate/ComboQuartiles.php @@ -17,6 +17,7 @@ namespace JBZoo\CsvBlueprint\Rules\Aggregate; use JBZoo\CsvBlueprint\Rules\AbstarctRule; +use JBZoo\CsvBlueprint\Utils; use MathPHP\Statistics\Descriptive; use function JBZoo\Utils\float; @@ -43,9 +44,9 @@ public function getHelpMeta(): array 'each group comprising a quarter of the data.', 'See: https://en.wikipedia.org/wiki/Quartile', // Options - 'There are multiple methods for computing quartiles: "' . \implode('", "', self::METHODS) . '". ' . + 'There are multiple methods for computing quartiles: ' . Utils::printList(self::METHODS) . '. ' . 'Exclusive is ussually classic.', - 'Available types: "' . \implode('", "', self::TYPES) . '" (aka Interquartile Range)', + 'Available types: ' . Utils::printList(self::TYPES) . ' ("IQR" is Interquartile Range)', // Example 'Example: `[ ' . self::METHODS[1] . ", '" . self::TYPES[3] . "', 42.0 ]`" . ' - the ' . self::TYPES[3] . ' ' . self::METHODS[1] . ' quartile is 50.0', @@ -81,13 +82,11 @@ protected function getActualAggregate(array $colValues): ?float private function getType(): string { - $allowedTypes = ['0%', 'Q1', 'Q2', 'Q3', '100%', 'IQR']; - $type = $this->getParams()[self::TYPE]; - if (!\in_array($type, $allowedTypes, true)) { + if (!\in_array($type, self::TYPES, true)) { throw new \RuntimeException( - "Unknown quartile type: \"{$type}\". Allowed: \"" . \implode('", "', $allowedTypes) . '"', + "Unknown quartile type: \"{$type}\". Allowed: " . Utils::printList(self::TYPES, 'green'), ); } @@ -96,13 +95,11 @@ private function getType(): string private function getMethod(): string { - $allowedMethods = ['exclusive', 'inclusive']; - $method = $this->getParams()[self::METHOD]; - if (!\in_array($method, $allowedMethods, true)) { + if (!\in_array($method, self::METHODS, true)) { throw new \RuntimeException( - "Unknown quartile method: \"{$method}\". Allowed: \"" . \implode('", "', $allowedMethods) . '"', + "Unknown quartile method: \"{$method}\". Allowed: " . Utils::printList(self::METHODS, 'green'), ); } @@ -115,7 +112,9 @@ private function getParams(): array if (\count($params) !== self::ARGS) { throw new \RuntimeException( 'The rule expects exactly three params: ' . - 'method (exclusive, inclusive), type (0%, Q1, Q2, Q3, 100%, IQR), expected value (float)', + 'method ' . Utils::printList(self::METHODS) . ', ' . + 'type ' . Utils::printList(self::TYPES) . ', ' . + 'expected value (float)', ); } diff --git a/src/Rules/Aggregate/FirstNot.php b/src/Rules/Aggregate/FirstNot.php index 29dd4098..5fa10d0f 100644 --- a/src/Rules/Aggregate/FirstNot.php +++ b/src/Rules/Aggregate/FirstNot.php @@ -28,7 +28,7 @@ public function getHelpMeta(): array [], [ self::DEFAULT => [ - "'Not Expected'", + 'Not expected', 'Not allowed as the first value in the column. Will be compared as strings.', ], ], diff --git a/src/Rules/Aggregate/IsSorted.php b/src/Rules/Aggregate/IsSorted.php index db2794e4..7ec9fe82 100644 --- a/src/Rules/Aggregate/IsSorted.php +++ b/src/Rules/Aggregate/IsSorted.php @@ -17,6 +17,7 @@ namespace JBZoo\CsvBlueprint\Rules\Aggregate; use JBZoo\CsvBlueprint\Rules\AbstarctRule; +use JBZoo\CsvBlueprint\Utils; final class IsSorted extends AbstractAggregateRule { @@ -39,8 +40,8 @@ public function getHelpMeta(): array return [ [ 'Check if the column is sorted in a specific order.', - ' - Direction: "' . \implode('", "', self::DIRS) . '".', - ' - Method: "' . \implode('", "', \array_keys(self::METHODS)) . '".', + ' - Direction: ' . Utils::printList(self::DIRS) . '.', + ' - Method: ' . Utils::printList(\array_keys(self::METHODS)) . '.', 'See: https://www.php.net/manual/en/function.sort.php', ], [ @@ -84,7 +85,7 @@ private function getDir(): string if (!\in_array($dir, self::DIRS, true)) { throw new \RuntimeException( - "Unknown sort direction: \"{$dir}\". Allowed: \"" . \implode('", "', self::DIRS) . '"', + "Unknown sort direction: \"{$dir}\". Allowed: " . Utils::printList(self::DIRS, 'green'), ); } @@ -97,7 +98,7 @@ private function getMethod(): int if (!\in_array($method, \array_keys(self::METHODS), true)) { throw new \RuntimeException( - "Unknown sort method: \"{$method}\". Allowed: \"" . \implode('", "', \array_keys(self::METHODS)) . '"', + "Unknown sort method: \"{$method}\". Allowed: " . Utils::printList(\array_keys(self::METHODS), 'green'), ); } @@ -110,7 +111,8 @@ private function getParams(): array if (\count($params) !== self::ARGS) { throw new \RuntimeException( 'The rule expects exactly two params: ' . - 'direction ["asc", "desc"] and method ["natural", "regular", "numeric", "string"]', + 'direction ' . Utils::printList(self::DIRS) . ' and ' . + 'method ' . Utils::printList(\array_keys(self::METHODS)), ); } diff --git a/src/Rules/Aggregate/LastNot.php b/src/Rules/Aggregate/LastNot.php index cceaa5e6..bf24552c 100644 --- a/src/Rules/Aggregate/LastNot.php +++ b/src/Rules/Aggregate/LastNot.php @@ -28,7 +28,7 @@ public function getHelpMeta(): array [], [ self::DEFAULT => [ - "'Not Expected'", + 'Not expected', 'Not allowed as the last value in the column. Will be compared as strings.', ], ], diff --git a/src/Rules/Aggregate/NthNot.php b/src/Rules/Aggregate/NthNot.php index 4ee4129f..3e22db24 100644 --- a/src/Rules/Aggregate/NthNot.php +++ b/src/Rules/Aggregate/NthNot.php @@ -32,7 +32,7 @@ public function getHelpMeta(): array [], [ self::DEFAULT => [ - "[ 2, 'Not expected' ]", + '[ 2, Not expected ]', 'Not allowed as the N-th value in the column. Will be compared as strings.', ], ], diff --git a/src/Rules/Cell/AllowValues.php b/src/Rules/Cell/AllowValues.php index 1049799a..633adc7e 100644 --- a/src/Rules/Cell/AllowValues.php +++ b/src/Rules/Cell/AllowValues.php @@ -16,6 +16,8 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; + class AllowValues extends AbstractCellRule { public function getHelpMeta(): array @@ -36,7 +38,7 @@ public function validateRule(string $cellValue): ?string if (!\in_array($cellValue, $allowedValues, true)) { return "Value \"{$cellValue}\" is not allowed. " . - 'Allowed values: ["' . \implode('", "', $allowedValues) . '"]'; + 'Allowed values: ' . Utils::printList($allowedValues, 'green'); } return null; diff --git a/src/Rules/Cell/Contains.php b/src/Rules/Cell/Contains.php index 374c2a96..468dc38c 100644 --- a/src/Rules/Cell/Contains.php +++ b/src/Rules/Cell/Contains.php @@ -22,7 +22,7 @@ public function getHelpMeta(): array { return [ [], - [self::DEFAULT => ['Hello', 'Example: "Hello World".']], + [self::DEFAULT => ['World', 'Example: "Hello World!". The string must contain "World" in any place.']], ]; } diff --git a/src/Rules/Cell/ContainsAll.php b/src/Rules/Cell/ContainsAll.php index aedc1a31..5033a2f6 100644 --- a/src/Rules/Cell/ContainsAll.php +++ b/src/Rules/Cell/ContainsAll.php @@ -16,13 +16,15 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; + final class ContainsAll extends AbstractCellRule { public function getHelpMeta(): array { return [ [], - [self::DEFAULT => ['[ a, b, c ]', 'All the strings must be part of a CSV value.']], + [self::DEFAULT => ['[ a, b ]', 'All the strings must be part of a CSV value.']], ]; } @@ -39,8 +41,8 @@ public function validateRule(string $cellValue): ?string foreach ($inclusions as $inclusion) { if (\strpos($cellValue, $inclusion) === false) { - return "Value \"{$cellValue}\" must contain all of the following:" . - ' "["' . \implode('", "', $inclusions) . '"]"'; + return "Value \"{$cellValue}\" must contain all of the following: " + . Utils::printList($inclusions, 'green'); } } diff --git a/src/Rules/Cell/ContainsAny.php b/src/Rules/Cell/ContainsAny.php new file mode 100644 index 00000000..39a92aae --- /dev/null +++ b/src/Rules/Cell/ContainsAny.php @@ -0,0 +1,51 @@ + ['[ a, b ]', 'At least one of the string must be part of the CSV value.']], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if ($cellValue === '') { + return null; + } + + $inclusions = $this->getOptionAsArray(); + if (\count($inclusions) === 0) { + return 'Rule must contain at least one inclusion value in schema file.'; + } + + foreach ($inclusions as $inclusion) { + if (\strpos($cellValue, $inclusion) !== false) { + return null; + } + } + + return "Value \"{$cellValue}\" must contain at least one of the following: " . + Utils::printList($inclusions, 'green'); + } +} diff --git a/src/Rules/Cell/ContainsNone.php b/src/Rules/Cell/ContainsNone.php index 15316b33..43a50af6 100644 --- a/src/Rules/Cell/ContainsNone.php +++ b/src/Rules/Cell/ContainsNone.php @@ -16,6 +16,8 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; + final class ContainsNone extends AbstractCellRule { public function getHelpMeta(): array @@ -39,8 +41,8 @@ public function validateRule(string $cellValue): ?string foreach ($exclusions as $exclusion) { if (\strpos($cellValue, $exclusion) !== false) { - return "Value \"{$cellValue}\" must not contain any of the following:" . - ' "["' . \implode('", "', $exclusions) . '"]"'; + return "Value \"{$cellValue}\" must not contain any of the following: " . + Utils::printList($exclusions, 'green'); } } diff --git a/src/Rules/Cell/ContainsOne.php b/src/Rules/Cell/ContainsOne.php index 7961133d..41afdc5e 100644 --- a/src/Rules/Cell/ContainsOne.php +++ b/src/Rules/Cell/ContainsOne.php @@ -16,13 +16,15 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; + final class ContainsOne extends AbstractCellRule { public function getHelpMeta(): array { return [ [], - [self::DEFAULT => ['[ a, b ]', 'At least one of the string must be part of the CSV value.']], + [self::DEFAULT => ['[ a, b ]', 'Only one of the strings must be part of the CSV value.']], ]; } @@ -37,13 +39,18 @@ public function validateRule(string $cellValue): ?string return 'Rule must contain at least one inclusion value in schema file.'; } + $found = 0; foreach ($inclusions as $inclusion) { if (\strpos($cellValue, $inclusion) !== false) { - return null; + $found++; } } - return "Value \"{$cellValue}\" must contain at least one of the following:" . - ' "["' . \implode('", "', $inclusions) . '"]"'; + if ($found === 1) { + return null; + } + + return "Value \"{$cellValue}\" must contain exactly one of the following: " . + Utils::printList($inclusions, 'green'); } } diff --git a/src/Rules/Cell/IpV4Range.php b/src/Rules/Cell/IpV4Range.php new file mode 100644 index 00000000..ada1c97d --- /dev/null +++ b/src/Rules/Cell/IpV4Range.php @@ -0,0 +1,54 @@ + [ + "[ '127.0.0.1-127.0.0.5', '127.0.0.0/21' ]", + 'Check subnet mask or range for IPv4. Address must be in one of the ranges.', + ], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + $ranges = $this->getOptionAsArray(); + + if (\count($ranges) === 0) { + return 'IPv4 range is not defined.'; + } + + foreach ($ranges as $range) { + if (Validator::ip($range, \FILTER_FLAG_IPV4)->validate($cellValue)) { + return null; + } + } + + return "Value \"{$cellValue}\" is not included in any of IPv4 the ranges: " . + Utils::printList($ranges, 'green'); + } +} diff --git a/src/Rules/Cell/IsCardinalDirection.php b/src/Rules/Cell/IsCardinalDirection.php index 69f7c18f..620498e4 100644 --- a/src/Rules/Cell/IsCardinalDirection.php +++ b/src/Rules/Cell/IsCardinalDirection.php @@ -16,6 +16,8 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; + final class IsCardinalDirection extends AllowValues { public function getHelpMeta(): array @@ -25,7 +27,7 @@ public function getHelpMeta(): array [ self::DEFAULT => [ 'true', - 'Valid cardinal direction. Available values: "' . \implode('", "', $this->getOptionAsArray()) . '"', + 'Valid cardinal direction. Available values: ' . Utils::printList($this->getOptionAsArray()), ], ], ]; diff --git a/src/Rules/Cell/CountryCode.php b/src/Rules/Cell/IsCountryCode.php similarity index 90% rename from src/Rules/Cell/CountryCode.php rename to src/Rules/Cell/IsCountryCode.php index 2d17cd50..ecad274e 100644 --- a/src/Rules/Cell/CountryCode.php +++ b/src/Rules/Cell/IsCountryCode.php @@ -16,10 +16,11 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; use Respect\Validation\Rules\CountryCode as RespectCountryCode; use Respect\Validation\Validator; -class CountryCode extends AbstractCellRule +class IsCountryCode extends AbstractCellRule { public function getHelpMeta(): array { @@ -51,11 +52,11 @@ public function validateRule(string $cellValue): ?string if (!\in_array($set, $validSets, true)) { return "Unknown country set: \"{$set}\". " . - 'Available options: [' . \implode(', ', $validSets) . ']'; + 'Available options: ' . Utils::printList($validSets, 'green'); } if (!Validator::countryCode($set)->validate($cellValue)) { - return "Value \"{$cellValue}\" is not a valid {$set} country code."; + return "Value \"{$cellValue}\" is not a valid \"{$set}\" country code."; } return null; diff --git a/src/Rules/Cell/IsDirExists.php b/src/Rules/Cell/IsDirExists.php new file mode 100644 index 00000000..cd9ddb6f --- /dev/null +++ b/src/Rules/Cell/IsDirExists.php @@ -0,0 +1,39 @@ + ['true', "Check if directory exists on the filesystem (It's FS IO operation!)."], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (!\file_exists($cellValue)) { + return "Directory \"{$cellValue}\" not found"; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsDomain.php b/src/Rules/Cell/IsDomain.php index a4ede646..3a6d2f4e 100644 --- a/src/Rules/Cell/IsDomain.php +++ b/src/Rules/Cell/IsDomain.php @@ -16,7 +16,7 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; -use JBZoo\CsvBlueprint\Utils; +use Respect\Validation\Validator; final class IsDomain extends AbstractCellRule { @@ -32,9 +32,7 @@ public function getHelpMeta(): array public function validateRule(string $cellValue): ?string { - $domainPattern = '/^(?!-)[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(\.[A-Za-z]{2,})$/'; - - if (Utils::testRegex($domainPattern, $cellValue)) { + if (!Validator::domain()->validate($cellValue)) { return "Value \"{$cellValue}\" is not a valid domain"; } diff --git a/src/Rules/Cell/IsEmail.php b/src/Rules/Cell/IsEmail.php index cb8d919c..ab51e8d4 100644 --- a/src/Rules/Cell/IsEmail.php +++ b/src/Rules/Cell/IsEmail.php @@ -16,6 +16,8 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use Respect\Validation\Validator; + final class IsEmail extends AbstractCellRule { public function getHelpMeta(): array @@ -30,7 +32,7 @@ public function getHelpMeta(): array public function validateRule(string $cellValue): ?string { - if (\filter_var($cellValue, \FILTER_VALIDATE_EMAIL) === false) { + if (!Validator::email()->validate($cellValue)) { return "Value \"{$cellValue}\" is not a valid email"; } diff --git a/src/Rules/Cell/IsFileExists.php b/src/Rules/Cell/IsFileExists.php new file mode 100644 index 00000000..6252106e --- /dev/null +++ b/src/Rules/Cell/IsFileExists.php @@ -0,0 +1,39 @@ + ['true', "Check if file exists on the filesystem (It's FS IO operation!)."], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (!\file_exists($cellValue)) { + return "File \"{$cellValue}\" not found"; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsIp.php b/src/Rules/Cell/IsIp.php new file mode 100644 index 00000000..5965de64 --- /dev/null +++ b/src/Rules/Cell/IsIp.php @@ -0,0 +1,41 @@ + ['true', 'Both: IPv4 or IPv6.'], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (!Validator::ip('*')->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a valid IPv6 or IPv4"; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsIpPrivate.php b/src/Rules/Cell/IsIpPrivate.php new file mode 100644 index 00000000..f998fa21 --- /dev/null +++ b/src/Rules/Cell/IsIpPrivate.php @@ -0,0 +1,45 @@ + [ + 'true', + 'IPv4 has ranges: 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. ' . + 'IPv6 has ranges starting with FD or FC.', + ], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (Validator::ip('*', \FILTER_FLAG_NO_PRIV_RANGE)->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a private IP address."; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsIpReserved.php b/src/Rules/Cell/IsIpReserved.php new file mode 100644 index 00000000..02f80647 --- /dev/null +++ b/src/Rules/Cell/IsIpReserved.php @@ -0,0 +1,45 @@ + [ + 'true', + 'IPv4 has ranges: 0.0.0.0/8, 169.254.0.0/16, 127.0.0.0/8 and 240.0.0.0/4. ' . + 'IPv6 has ranges: ::1/128, ::/128, ::ffff:0:0/96 and fe80::/10.', + ], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (Validator::ip('*', \FILTER_FLAG_NO_RES_RANGE)->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a reserved IP address."; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsIp4.php b/src/Rules/Cell/IsIpV4.php similarity index 81% rename from src/Rules/Cell/IsIp4.php rename to src/Rules/Cell/IsIpV4.php index 2218848a..ef3b05d2 100644 --- a/src/Rules/Cell/IsIp4.php +++ b/src/Rules/Cell/IsIpV4.php @@ -16,22 +16,24 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; -final class IsIp4 extends AbstractCellRule +use Respect\Validation\Validator; + +final class IsIpV4 extends AbstractCellRule { public function getHelpMeta(): array { return [ [], [ - self::DEFAULT => ['true', 'Only IPv4. Example: "127.0.0.1"'], + self::DEFAULT => ['true', 'Only IPv4. Example: "127.0.0.1".'], ], ]; } public function validateRule(string $cellValue): ?string { - if (\filter_var($cellValue, \FILTER_VALIDATE_IP) === false) { - return "Value \"{$cellValue}\" is not a valid IP"; + if (!Validator::ip('*', \FILTER_FLAG_IPV4)->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a valid IPv4"; } return null; diff --git a/src/Rules/Cell/IsIpV6.php b/src/Rules/Cell/IsIpV6.php new file mode 100644 index 00000000..61e3b93a --- /dev/null +++ b/src/Rules/Cell/IsIpV6.php @@ -0,0 +1,41 @@ + ['true', 'Only IPv6. Example: "2001:0db8:85a3:08d3:1319:8a2e:0370:7334".'], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (!Validator::ip('*', \FILTER_FLAG_IPV6)->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a valid IPv6"; + } + + return null; + } +} diff --git a/src/Rules/Cell/LanguageCode.php b/src/Rules/Cell/IsLanguageCode.php similarity index 89% rename from src/Rules/Cell/LanguageCode.php rename to src/Rules/Cell/IsLanguageCode.php index e081b2fd..38d68ec6 100644 --- a/src/Rules/Cell/LanguageCode.php +++ b/src/Rules/Cell/IsLanguageCode.php @@ -16,10 +16,11 @@ namespace JBZoo\CsvBlueprint\Rules\Cell; +use JBZoo\CsvBlueprint\Utils; use Respect\Validation\Rules\LanguageCode as RespectLanguageCode; use Respect\Validation\Validator; -class LanguageCode extends AbstractCellRule +class IsLanguageCode extends AbstractCellRule { public function getHelpMeta(): array { @@ -47,11 +48,11 @@ public function validateRule(string $cellValue): ?string if (!\in_array($set, $validSets, true)) { return "Unknown language set: \"{$set}\". " . - 'Available options: [' . \implode(', ', $validSets) . ']'; + 'Available options: ' . Utils::printList($validSets, 'green'); } if (!Validator::languageCode($set)->validate($cellValue)) { - return "Value \"{$cellValue}\" is not a valid {$set} language code."; + return "Value \"{$cellValue}\" is not a valid \"{$set}\" language code."; } return null; diff --git a/src/Rules/Cell/IsMacAddress.php b/src/Rules/Cell/IsMacAddress.php new file mode 100644 index 00000000..e1d4f091 --- /dev/null +++ b/src/Rules/Cell/IsMacAddress.php @@ -0,0 +1,41 @@ + ['true', 'The input is a valid MAC address. Example: 00:00:5e:00:53:01'], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + if (!Validator::macAddress()->validate($cellValue)) { + return "Value \"{$cellValue}\" is not a valid MAC address."; + } + + return null; + } +} diff --git a/src/Rules/Cell/IsPublicDomainSuffix.php b/src/Rules/Cell/IsPublicDomainSuffix.php new file mode 100644 index 00000000..4d838ab9 --- /dev/null +++ b/src/Rules/Cell/IsPublicDomainSuffix.php @@ -0,0 +1,46 @@ + [ + 'true', + 'The input is a public ICANN domain suffix. Example: "com", "nom.br", "net" etc.', + ], + ], + ]; + } + + public function validateRule(string $cellValue): ?string + { + // @phpstan-ignore-next-line + if (!Validator::oneOf(Validator::tld(), Validator::publicDomainSuffix())->validate($cellValue)) { + return "The value \"{$cellValue}\" is not a valid public domain suffix. " . + 'Example: "com", "nom.br", "net" etc.'; + } + + return null; + } +} diff --git a/src/Rules/DocBuilder.php b/src/Rules/DocBuilder.php index 27f9ba81..9b3dd37c 100644 --- a/src/Rules/DocBuilder.php +++ b/src/Rules/DocBuilder.php @@ -71,6 +71,7 @@ final class DocBuilder private const HELP_LEFT_PAD = 6; private const HELP_DESC_PAD = 40; + private const HELP_DESC_PAD_BIG = 60; private array $topHelp; private array $options; @@ -148,11 +149,11 @@ private static function renderLine(string $ruleCode, array $row, string $mode): : "{$leftPad}{$ruleCode}_{$mode}: {$row[0]}"; if (\strlen($baseKeyVal) > $descPad) { - $descPad = 60; + $descPad = self::HELP_DESC_PAD_BIG; } if (isset($row[1]) && $row[1] !== '') { - return \str_pad($baseKeyVal, $descPad, ' ', \STR_PAD_RIGHT) . "# {$row[1]}"; + return \str_pad($baseKeyVal, $descPad - 1, ' ', \STR_PAD_RIGHT) . " # {$row[1]}"; } return $baseKeyVal; diff --git a/src/Utils.php b/src/Utils.php index 70ad6f94..54aef887 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -29,6 +29,31 @@ final class Utils { public const MAX_DIRECTORY_DEPTH = 10; + public static function printList(null|array|bool|float|int|string $items, string $color = ''): string + { + if (!\is_array($items)) { + $items = [$items]; + } + + if (\count($items) === 0) { + return '[]'; + } + + if (\count($items) === 1) { + $val = \reset($items); + if ($color === '') { + return "\"{$val}\""; + } + return "\"<{$color}>{$val}\""; + } + + if ($color === '') { + return '["' . \implode('", "', $items) . '"]'; + } + + return "[\"<{$color}>" . \implode("\", \"<{$color}>", $items) . "\"]"; + } + public static function debug(int|string $message): void { if (\defined('PROFILE_MODE')) { @@ -207,7 +232,7 @@ public static function matchTypes( 'array' => [], 'boolean' => [], 'double' => ['string', 'integer'], - 'integer' => ['string', 'double'], + 'integer' => [], 'string' => ['double', 'integer'], ]; diff --git a/src/Validators/ErrorSuite.php b/src/Validators/ErrorSuite.php index 97dea338..579a4a12 100644 --- a/src/Validators/ErrorSuite.php +++ b/src/Validators/ErrorSuite.php @@ -137,10 +137,6 @@ private function renderPlainText(): string $result[] = (string)$error; } - // if (\count($result) > 0) { - // \array_unshift($result, $this->csvFilename); - // } - return \implode("\n", $result) . "\n"; } diff --git a/src/Validators/ValidatorCsv.php b/src/Validators/ValidatorCsv.php index 50a6bf23..37ef0dc9 100644 --- a/src/Validators/ValidatorCsv.php +++ b/src/Validators/ValidatorCsv.php @@ -208,7 +208,7 @@ private function validateColumn(bool $quickStop): ErrorSuite if (\count($notFoundColums) > 0) { $error = new Error( 'csv.header', - 'Columns not found in CSV: "' . \implode(', ', $notFoundColums) . '"', + 'Columns not found in CSV: ' . Utils::printList($notFoundColums, 'c'), '', ValidatorColumn::FALLBACK_LINE, ); diff --git a/tests/Commands/ValidateCsvBasicTest.php b/tests/Commands/ValidateCsvBasicTest.php index ef424ba9..e3882fce 100644 --- a/tests/Commands/ValidateCsvBasicTest.php +++ b/tests/Commands/ValidateCsvBasicTest.php @@ -302,7 +302,7 @@ public function testValidateOneCsvNoHeaderNegative(): void public function testSchemaNotFound(): void { - $this->expectExceptionMessage('Schema file(s) not found: invalid_schema_path.yml'); + $this->expectExceptionMessage('Schema file(s) not found: "invalid_schema_path.yml"'); Tools::virtualExecution('validate:csv', [ 'csv' => './tests/fixtures/no-found-file.csv', 'schema' => 'invalid_schema_path.yml', diff --git a/tests/Rules/Aggregate/ComboAverageTest.php b/tests/Rules/Aggregate/ComboAverageTest.php index eed38160..6e131941 100644 --- a/tests/Rules/Aggregate/ComboAverageTest.php +++ b/tests/Rules/Aggregate/ComboAverageTest.php @@ -79,7 +79,7 @@ public function testInvalidOption(): void $rule = $this->create([1, 2], Combo::MAX); isSame( '"ag:average_max" at line 1, column "prop". ' . - 'Invalid option "[1, 2]" for the "ag:average_max" rule. ' . + 'Invalid option ["1", "2"] for the "ag:average_max" rule. ' . 'It should be integer/float.', (string)$rule->validate(['1', '2', '3']), ); diff --git a/tests/Rules/Aggregate/ComboCountEmptyTest.php b/tests/Rules/Aggregate/ComboCountEmptyTest.php index 5f77c036..0ae2ffed 100644 --- a/tests/Rules/Aggregate/ComboCountEmptyTest.php +++ b/tests/Rules/Aggregate/ComboCountEmptyTest.php @@ -85,7 +85,7 @@ public function testInvalidOption(): void $rule = $this->create([1, 2], Combo::MAX); isSame( '"ag:count_empty_max" at line 1, column "prop". ' . - 'Invalid option "[1, 2]" for the "ag:count_empty_max" rule. ' . + 'Invalid option ["1", "2"] for the "ag:count_empty_max" rule. ' . 'It should be integer/float.', (string)$rule->validate(['1', '2', '3']), ); diff --git a/tests/Rules/Aggregate/ComboQuartilesTest.php b/tests/Rules/Aggregate/ComboQuartilesTest.php index f5b2bb21..79351e5a 100644 --- a/tests/Rules/Aggregate/ComboQuartilesTest.php +++ b/tests/Rules/Aggregate/ComboQuartilesTest.php @@ -79,19 +79,21 @@ public function testInvalidOption(): void $rule = $this->create([950.05], Combo::EQ); isSame( 'The rule expects exactly three params: ' . - 'method (exclusive, inclusive), type (0%, Q1, Q2, Q3, 100%, IQR), expected value (float)', + 'method ["exclusive", "inclusive"], ' . + 'type ["0%", "Q1", "Q2", "Q3", "100%", "IQR"], ' . + 'expected value (float)', $rule->test(\range(1, 200)), ); $rule = $this->create(['qwerty', 'IQR', 5], Combo::EQ); isSame( - 'Unknown quartile method: "qwerty". Allowed: "exclusive", "inclusive"', + 'Unknown quartile method: "qwerty". Allowed: ["exclusive", "inclusive"]', $rule->test(\range(1, 200)), ); $rule = $this->create(['inclusive', 'QQQQ', 5], Combo::EQ); isSame( - 'Unknown quartile type: "QQQQ". Allowed: "0%", "Q1", "Q2", "Q3", "100%", "IQR"', + 'Unknown quartile type: "QQQQ". Allowed: ["0%", "Q1", "Q2", "Q3", "100%", "IQR"]', $rule->test(\range(1, 200)), ); } diff --git a/tests/Rules/Aggregate/IsSortedTest.php b/tests/Rules/Aggregate/IsSortedTest.php index a9f98dfe..968b38f2 100644 --- a/tests/Rules/Aggregate/IsSortedTest.php +++ b/tests/Rules/Aggregate/IsSortedTest.php @@ -67,13 +67,13 @@ public function testNegative(): void $rule = $this->create(['QQQ', 'natural']); isSame( - 'Unknown sort direction: "QQQ". Allowed: "asc", "desc"', + 'Unknown sort direction: "QQQ". Allowed: ["asc", "desc"]', $rule->test(['1', '11', '2', '20', '21']), ); $rule = $this->create(['asc', 'QQQQQQQ']); isSame( - 'Unknown sort method: "QQQQQQQ". Allowed: "natural", "regular", "numeric", "string"', + 'Unknown sort method: "QQQQQQQ". Allowed: ["natural", "regular", "numeric", "string"]', $rule->test(['1', '11', '2', '20', '21']), ); diff --git a/tests/Rules/Cell/AllowValuesTest.php b/tests/Rules/Cell/AllowValuesTest.php index 12099de8..236f8f99 100644 --- a/tests/Rules/Cell/AllowValuesTest.php +++ b/tests/Rules/Cell/AllowValuesTest.php @@ -59,7 +59,8 @@ public function testInvalidOption(): void $rule = $this->create('qwe'); isSame( '"allow_values" at line 1, column "prop". ' . - 'Unexpected error: Invalid option "qwe" for the "allow_values" rule. It should be array of strings.', + 'Unexpected error: Invalid option "qwe" for the "allow_values" rule. ' . + 'It should be array of strings.', (string)$rule->validate('true'), ); } diff --git a/tests/Rules/Cell/ComboLengthTest.php b/tests/Rules/Cell/ComboLengthTest.php index e44317e0..b31d480a 100644 --- a/tests/Rules/Cell/ComboLengthTest.php +++ b/tests/Rules/Cell/ComboLengthTest.php @@ -85,10 +85,12 @@ public function testMax(): void public function testInvalidOption(): void { $this->expectException(\JBZoo\CsvBlueprint\Rules\Exception::class); - $this->expectExceptionMessage('Invalid option "qwerty" for the "length_max" rule. It should be integer.'); + $this->expectExceptionMessage( + 'Invalid option "qwerty" for the "length_max" rule. It should be integer.', + ); $rule = $this->create('qwerty', Combo::MAX); - $rule->test('12345'); + $rule->validate('12345'); } public function testInvalidParsing(): void diff --git a/tests/Rules/Cell/ComboPrecisionTest.php b/tests/Rules/Cell/ComboPrecisionTest.php index 09be781e..9e4f40a9 100644 --- a/tests/Rules/Cell/ComboPrecisionTest.php +++ b/tests/Rules/Cell/ComboPrecisionTest.php @@ -81,7 +81,9 @@ public function testNotEqual(): void public function testInvalidOption(): void { - $this->expectExceptionMessage('Invalid option "s.223" for the "precision_not" rule. It should be integer.'); + $this->expectExceptionMessage( + 'Invalid option "s.223" for the "precision_not" rule. It should be integer.', + ); $rule = $this->create('s.223', Combo::NOT); isSame('', $rule->test('5')); } diff --git a/tests/Rules/Cell/ComboTest.php b/tests/Rules/Cell/ComboTest.php index cf12fd6e..18d5363f 100644 --- a/tests/Rules/Cell/ComboTest.php +++ b/tests/Rules/Cell/ComboTest.php @@ -127,7 +127,8 @@ public function testInvalidParsing(): void public function testInvalidOption2(): void { $this->expectExceptionMessage( - 'Invalid option "1, 2, 3" for the "num_not" rule. It should be int/float/string.', + 'Invalid option ["1", "2", "3"] for the "num_not" rule. ' . + 'It should be int/float/string.', ); $rule = $this->create([1, 2, 3], Combo::NOT); diff --git a/tests/Rules/Cell/ComboWordCountTest.php b/tests/Rules/Cell/ComboWordCountTest.php index 119112d8..747ce0ad 100644 --- a/tests/Rules/Cell/ComboWordCountTest.php +++ b/tests/Rules/Cell/ComboWordCountTest.php @@ -21,7 +21,6 @@ use JBZoo\PHPUnit\Rules\TestAbstractCellRuleCombo; use function JBZoo\PHPUnit\isSame; -use function JBZoo\PHPUnit\success; class ComboWordCountTest extends TestAbstractCellRuleCombo { @@ -98,18 +97,4 @@ public function testMax(): void $rule->test('asd, asdasd asd 1232 asdas'), ); } - - public function testInvalidOption(): void - { - $this->expectException(\JBZoo\CsvBlueprint\Rules\Exception::class); - $this->expectExceptionMessage('Invalid option "qwerty" for the "word_count_max" rule. It should be integer.'); - - $rule = $this->create('qwerty', Combo::MAX); - $rule->test('12345'); - } - - public function testInvalidParsing(): void - { - success('No cases for invalid parsing.'); - } } diff --git a/tests/Rules/Cell/ContainsAllTest.php b/tests/Rules/Cell/ContainsAllTest.php index ddf4817c..091eb961 100644 --- a/tests/Rules/Cell/ContainsAllTest.php +++ b/tests/Rules/Cell/ContainsAllTest.php @@ -45,11 +45,11 @@ public function testNegative(): void $rule = $this->create(['a', 'b', 'c']); isSame( - 'Value "ab" must contain all of the following: "["a", "b", "c"]"', + 'Value "ab" must contain all of the following: ["a", "b", "c"]', $rule->test('ab'), ); isSame( - 'Value "ac" must contain all of the following: "["a", "b", "c"]"', + 'Value "ac" must contain all of the following: ["a", "b", "c"]', $rule->test('ac'), ); } diff --git a/tests/Rules/Cell/ContainsAnyTest.php b/tests/Rules/Cell/ContainsAnyTest.php new file mode 100644 index 00000000..1069ca56 --- /dev/null +++ b/tests/Rules/Cell/ContainsAnyTest.php @@ -0,0 +1,54 @@ +create([]); + isSame('', $rule->test('')); + + $rule = $this->create(['a', 'b', 'c']); + isSame('', $rule->test('a')); + isSame('', $rule->test('ab')); + isSame('', $rule->test('abc')); + isSame('', $rule->test('abc ')); + } + + public function testNegative(): void + { + $rule = $this->create([]); + isSame( + 'Rule must contain at least one inclusion value in schema file.', + $rule->test('ac'), + ); + + $rule = $this->create(['a', 'b', 'c']); + isSame( + 'Value "d" must contain at least one of the following: ["a", "b", "c"]', + $rule->test('d'), + ); + } +} diff --git a/tests/Rules/Cell/ContainsNoneTest.php b/tests/Rules/Cell/ContainsNoneTest.php index 825e92fd..6e83d742 100644 --- a/tests/Rules/Cell/ContainsNoneTest.php +++ b/tests/Rules/Cell/ContainsNoneTest.php @@ -45,13 +45,13 @@ public function testNegative(): void $rule = $this->create(['a', 'b', 'c']); isSame( - 'Value "a" must not contain any of the following: "["a", "b", "c"]"', + 'Value "a" must not contain any of the following: ["a", "b", "c"]', $rule->test('a'), ); $rule = $this->create(['a', 'b', 'c']); isSame( - 'Value "ddddb" must not contain any of the following: "["a", "b", "c"]"', + 'Value "ddddb" must not contain any of the following: ["a", "b", "c"]', $rule->test('ddddb'), ); } diff --git a/tests/Rules/Cell/ContainsOneTest.php b/tests/Rules/Cell/ContainsOneTest.php index 21bae4e5..fe055a00 100644 --- a/tests/Rules/Cell/ContainsOneTest.php +++ b/tests/Rules/Cell/ContainsOneTest.php @@ -32,9 +32,8 @@ public function testPositive(): void $rule = $this->create(['a', 'b', 'c']); isSame('', $rule->test('a')); - isSame('', $rule->test('ab')); - isSame('', $rule->test('abc')); - isSame('', $rule->test('abc ')); + isSame('', $rule->test('b')); + isSame('', $rule->test('c')); } public function testNegative(): void @@ -47,8 +46,14 @@ public function testNegative(): void $rule = $this->create(['a', 'b', 'c']); isSame( - 'Value "d" must contain at least one of the following: "["a", "b", "c"]"', + 'Value "d" must contain exactly one of the following: ["a", "b", "c"]', $rule->test('d'), ); + + $rule = $this->create(['a', 'b', 'c']); + isSame( + 'Value "ab" must contain exactly one of the following: ["a", "b", "c"]', + $rule->test('ab'), + ); } } diff --git a/tests/Rules/Cell/IpV4RangeTest.php b/tests/Rules/Cell/IpV4RangeTest.php new file mode 100644 index 00000000..f83f0d6c --- /dev/null +++ b/tests/Rules/Cell/IpV4RangeTest.php @@ -0,0 +1,71 @@ +create(['127.0.0.1-127.0.0.5', '127.0.0.0/21']); + isSame(null, $rule->validate('')); + isSame('', $rule->test('127.0.0.0')); + isSame('', $rule->test('127.0.0.5')); + isSame('', $rule->test('127.0.0.5')); + isSame('', $rule->test('127.0.7.255')); + + $rule = $this->create(['127.0.0.1-127.0.0.5']); + isSame('', $rule->test('127.0.0.1')); + isSame('', $rule->test('127.0.0.2')); + isSame('', $rule->test('127.0.0.5')); + + $rule = $this->create(['127.0.0.0/21']); + isSame('', $rule->test('127.0.1.1')); + isSame('', $rule->test('127.0.7.255')); + } + + public function testNegative(): void + { + $rule = $this->create(['127.0.0.1-127.0.0.5']); + isSame( + 'Value "1.2.3" is not included in any of IPv4 the ranges: "127.0.0.1-127.0.0.5"', + $rule->test('1.2.3'), + ); + isSame( + '"ip_v4_range" at line 1, column "prop". ' . + 'Value "1.2.3" is not included in any of IPv4 the ranges: "127.0.0.1-127.0.0.5".', + (string)$rule->validate('1.2.3'), + ); + isSame( + 'Value "2001:0db8:85a3:08d3:1319:8a2e:0370:7334" is not included in any of IPv4 the ranges: ' . + '"127.0.0.1-127.0.0.5"', + $rule->test('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), + ); + + $rule = $this->create([]); + isSame( + 'IPv4 range is not defined.', + $rule->test('127.0.0.1'), + ); + } +} diff --git a/tests/Rules/Cell/IsBoolTest.php b/tests/Rules/Cell/IsBoolTest.php index f0b6c5e7..4f9d4f43 100644 --- a/tests/Rules/Cell/IsBoolTest.php +++ b/tests/Rules/Cell/IsBoolTest.php @@ -54,12 +54,4 @@ public function testNegative(): void $rule->test(''), ); } - - public function testInvalidOption(): void - { - $this->expectExceptionMessage('Invalid option "qwerty" for the "is_bool" rule. It should be true|false.'); - - $rule = $this->create('qwerty'); - $rule->validate('true'); - } } diff --git a/tests/Rules/Cell/CountryCodeTest.php b/tests/Rules/Cell/IsCountryCodeTest.php similarity index 80% rename from tests/Rules/Cell/CountryCodeTest.php rename to tests/Rules/Cell/IsCountryCodeTest.php index af16b643..38106645 100644 --- a/tests/Rules/Cell/CountryCodeTest.php +++ b/tests/Rules/Cell/IsCountryCodeTest.php @@ -16,15 +16,15 @@ namespace JBZoo\PHPUnit\Rules\Cell; -use JBZoo\CsvBlueprint\Rules\Cell\CountryCode; +use JBZoo\CsvBlueprint\Rules\Cell\IsCountryCode; use JBZoo\PHPUnit\Rules\TestAbstractCellRule; use Respect\Validation\Rules\CountryCode as RespectCountryCode; use function JBZoo\PHPUnit\isSame; -final class CountryCodeTest extends TestAbstractCellRule +final class IsCountryCodeTest extends TestAbstractCellRule { - protected string $ruleClass = CountryCode::class; + protected string $ruleClass = IsCountryCode::class; public function testPositive(): void { @@ -43,19 +43,19 @@ public function testNegative(): void { $rule = $this->create(RespectCountryCode::ALPHA2); isSame( - 'Value "qq" is not a valid alpha-2 country code.', + 'Value "qq" is not a valid "alpha-2" country code.', $rule->test('qq'), ); $rule = $this->create(RespectCountryCode::ALPHA3); isSame( - 'Value "QQQ" is not a valid alpha-3 country code.', + 'Value "QQQ" is not a valid "alpha-3" country code.', $rule->test('QQQ'), ); $rule = $this->create(RespectCountryCode::NUMERIC); isSame( - 'Value "101010101" is not a valid numeric country code.', + 'Value "101010101" is not a valid "numeric" country code.', $rule->test('101010101'), ); } @@ -64,7 +64,7 @@ public function testInvalidOption(): void { $rule = $this->create('qwerty'); isSame( - 'Unknown country set: "qwerty". Available options: [alpha-2, alpha-3, numeric]', + 'Unknown country set: "qwerty". Available options: ["alpha-2", "alpha-3", "numeric"]', $rule->test('US'), ); } diff --git a/tests/Rules/Cell/IsDirExistsTest.php b/tests/Rules/Cell/IsDirExistsTest.php new file mode 100644 index 00000000..1a0b3686 --- /dev/null +++ b/tests/Rules/Cell/IsDirExistsTest.php @@ -0,0 +1,47 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test(__DIR__)); + isSame('', $rule->test(__DIR__ . '/')); + isSame('', $rule->test(__DIR__ . '/../')); + isSame('', $rule->test(__DIR__ . '/../../')); + isSame('', $rule->test(PROJECT_ROOT)); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Directory "qwerty" not found', + $rule->test('qwerty'), + ); + } +} diff --git a/tests/Rules/Cell/IsDomainTest.php b/tests/Rules/Cell/IsDomainTest.php index ed46a552..04e2e0ac 100644 --- a/tests/Rules/Cell/IsDomainTest.php +++ b/tests/Rules/Cell/IsDomainTest.php @@ -34,7 +34,6 @@ public function testPositive(): void isSame('', $rule->test('sub.sub.example.com')); isSame('', $rule->test('sub.sub-example.com')); isSame('', $rule->test('sub-sub-example.com')); - isSame('', $rule->test('sub-sub-example.qwerty')); $rule = $this->create(false); isSame(null, $rule->validate('example')); @@ -47,5 +46,9 @@ public function testNegative(): void 'Value "example" is not a valid domain', $rule->test('example'), ); + isSame( + 'Value "sub-sub-example.qwerty" is not a valid domain', + $rule->test('sub-sub-example.qwerty'), + ); } } diff --git a/tests/Rules/Cell/IsFileExistsTest.php b/tests/Rules/Cell/IsFileExistsTest.php new file mode 100644 index 00000000..ce9406dd --- /dev/null +++ b/tests/Rules/Cell/IsFileExistsTest.php @@ -0,0 +1,45 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test(__FILE__)); + isSame('', $rule->test('README.md')); + isSame('', $rule->test('./README.md')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'File "qwerty" not found', + $rule->test('qwerty'), + ); + } +} diff --git a/tests/Rules/Cell/IsIpPrivateTest.php b/tests/Rules/Cell/IsIpPrivateTest.php new file mode 100644 index 00000000..b123a20d --- /dev/null +++ b/tests/Rules/Cell/IsIpPrivateTest.php @@ -0,0 +1,49 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('10.0.0.1')); + isSame('', $rule->test('fc01:0db8:85a3:08d3:1319:8a2e:0370:7334')); + isSame('', $rule->test('fd01:0db8:85a3:08d3:1319:8a2e:0370:7334')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Value "189.0.0.1" is not a private IP address.', + $rule->test('189.0.0.1'), + ); + isSame( + 'Value "2020:0000:0000:0000:0000:0000:0000:0001" is not a private IP address.', + $rule->test('2020:0000:0000:0000:0000:0000:0000:0001'), + ); + } +} diff --git a/tests/Rules/Cell/IsIpReservedTest.php b/tests/Rules/Cell/IsIpReservedTest.php new file mode 100644 index 00000000..b153ecf4 --- /dev/null +++ b/tests/Rules/Cell/IsIpReservedTest.php @@ -0,0 +1,52 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('0.0.0.0')); + isSame('', $rule->test('127.0.0.1')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Value "45.46.47.48" is not a reserved IP address.', + $rule->test('45.46.47.48'), + ); + isSame( + 'Value "fd00:0000:0000:0000:0000:0000:0000:0001" is not a reserved IP address.', + $rule->test('fd00:0000:0000:0000:0000:0000:0000:0001'), + ); + isSame( + 'Value "fc00:0000:0000:0000:0000:0000:0000:0001" is not a reserved IP address.', + $rule->test('fc00:0000:0000:0000:0000:0000:0000:0001'), + ); + } +} diff --git a/tests/Rules/Cell/IsIpTest.php b/tests/Rules/Cell/IsIpTest.php index cd8598dd..191dd0f3 100644 --- a/tests/Rules/Cell/IsIpTest.php +++ b/tests/Rules/Cell/IsIpTest.php @@ -16,14 +16,14 @@ namespace JBZoo\PHPUnit\Rules\Cell; -use JBZoo\CsvBlueprint\Rules\Cell\IsIp4; +use JBZoo\CsvBlueprint\Rules\Cell\IsIp; use JBZoo\PHPUnit\Rules\TestAbstractCellRule; use function JBZoo\PHPUnit\isSame; final class IsIpTest extends TestAbstractCellRule { - protected string $ruleClass = IsIp4::class; + protected string $ruleClass = IsIp::class; public function testPositive(): void { @@ -31,13 +31,14 @@ public function testPositive(): void isSame(null, $rule->validate('')); isSame('', $rule->test('127.0.0.1')); isSame('', $rule->test('0.0.0.0')); + isSame('', $rule->test('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); } public function testNegative(): void { $rule = $this->create(true); isSame( - 'Value "1.2.3" is not a valid IP', + 'Value "1.2.3" is not a valid IPv6 or IPv4', $rule->test('1.2.3'), ); } diff --git a/tests/Rules/Cell/IsIpV4Test.php b/tests/Rules/Cell/IsIpV4Test.php new file mode 100644 index 00000000..c60c90b3 --- /dev/null +++ b/tests/Rules/Cell/IsIpV4Test.php @@ -0,0 +1,48 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('127.0.0.1')); + isSame('', $rule->test('0.0.0.0')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Value "1.2.3" is not a valid IPv4', + $rule->test('1.2.3'), + ); + isSame( + 'Value "2001:0db8:85a3:08d3:1319:8a2e:0370:7334" is not a valid IPv4', + $rule->test('2001:0db8:85a3:08d3:1319:8a2e:0370:7334'), + ); + } +} diff --git a/tests/Rules/Cell/IsIpV6Test.php b/tests/Rules/Cell/IsIpV6Test.php new file mode 100644 index 00000000..312452e0 --- /dev/null +++ b/tests/Rules/Cell/IsIpV6Test.php @@ -0,0 +1,47 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('2001:0db8:85a3:08d3:1319:8a2e:0370:7334')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Value "1.2.3" is not a valid IPv6', + $rule->test('1.2.3'), + ); + isSame( + 'Value "127.0.0.1" is not a valid IPv6', + $rule->test('127.0.0.1'), + ); + } +} diff --git a/tests/Rules/Cell/LanguageCodeTest.php b/tests/Rules/Cell/IsLanguageCodeTest.php similarity index 82% rename from tests/Rules/Cell/LanguageCodeTest.php rename to tests/Rules/Cell/IsLanguageCodeTest.php index 0578e152..44a43dac 100644 --- a/tests/Rules/Cell/LanguageCodeTest.php +++ b/tests/Rules/Cell/IsLanguageCodeTest.php @@ -16,14 +16,14 @@ namespace JBZoo\PHPUnit\Rules\Cell; -use JBZoo\CsvBlueprint\Rules\Cell\LanguageCode; +use JBZoo\CsvBlueprint\Rules\Cell\IsLanguageCode; use JBZoo\PHPUnit\Rules\TestAbstractCellRule; use function JBZoo\PHPUnit\isSame; -final class LanguageCodeTest extends TestAbstractCellRule +final class IsLanguageCodeTest extends TestAbstractCellRule { - protected string $ruleClass = LanguageCode::class; + protected string $ruleClass = IsLanguageCode::class; public function testPositive(): void { @@ -41,7 +41,7 @@ public function testNegative(): void { $rule = $this->create('alpha-2'); isSame( - 'Value "qq" is not a valid alpha-2 language code.', + 'Value "qq" is not a valid "alpha-2" language code.', $rule->test('qq'), ); } @@ -50,7 +50,7 @@ public function testInvalidOption(): void { $rule = $this->create('qwerty'); isSame( - 'Unknown language set: "qwerty". Available options: [alpha-2, alpha-3]', + 'Unknown language set: "qwerty". Available options: ["alpha-2", "alpha-3"]', $rule->test('US'), ); } diff --git a/tests/Rules/Cell/IsMacAddressTest.php b/tests/Rules/Cell/IsMacAddressTest.php new file mode 100644 index 00000000..09d05345 --- /dev/null +++ b/tests/Rules/Cell/IsMacAddressTest.php @@ -0,0 +1,44 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('00:11:22:33:44:55')); + isSame('', $rule->test('af-AA-22-33-44-55')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'Value "127.0.0.1" is not a valid MAC address.', + $rule->test('127.0.0.1'), + ); + } +} diff --git a/tests/Rules/Cell/IsPublicDomainSuffixTest.php b/tests/Rules/Cell/IsPublicDomainSuffixTest.php new file mode 100644 index 00000000..5621f5d5 --- /dev/null +++ b/tests/Rules/Cell/IsPublicDomainSuffixTest.php @@ -0,0 +1,48 @@ +create(true); + isSame(null, $rule->validate('')); + isSame('', $rule->test('com')); + isSame('', $rule->test('CO.UK')); + } + + public function testNegative(): void + { + $rule = $this->create(true); + isSame( + 'The value "127.0.0.1" is not a valid public domain suffix. Example: "com", "nom.br", "net" etc.', + $rule->test('127.0.0.1'), + ); + isSame( + 'The value "invalid.com" is not a valid public domain suffix. Example: "com", "nom.br", "net" etc.', + $rule->test('invalid.com'), + ); + } +} diff --git a/tests/Rules/Cell/NotAllowValuesTest.php b/tests/Rules/Cell/NotAllowValuesTest.php index abbcb715..39b4bfe3 100644 --- a/tests/Rules/Cell/NotAllowValuesTest.php +++ b/tests/Rules/Cell/NotAllowValuesTest.php @@ -52,7 +52,8 @@ public function testInvalidOption(): void $rule = $this->create('qwe'); isSame( '"not_allow_values" at line 1, column "prop". ' . - 'Unexpected error: Invalid option "qwe" for the "not_allow_values" rule. It should be array of strings.', + 'Unexpected error: Invalid option "qwe" for the "not_allow_values" rule. ' . + 'It should be array of strings.', (string)$rule->validate('true'), ); } diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 64909137..20426e68 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -302,6 +302,8 @@ public function testMatchTypes(): void 'float !== null', 'int !== null', 'null !== string', + 'float !== int', + 'int !== string', ]; $invalidPairs = []; diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index b5338a69..971c3080 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -103,6 +103,18 @@ public function testFindFilesNotFound(): void isSame([], $this->getFileName(Utils::findFiles(['demo.csv']))); } + public function testPrintList(): void + { + isSame('["one", "two", "three"]', Utils::printList(['one', 'two', 'three'])); + isSame('["one", "two", "three"]', Utils::printList(['one', 'two', 'three'], 'c')); + isSame('"one"', Utils::printList(['one'])); + isSame('"one"', Utils::printList('one')); + isSame('"one"', Utils::printList(['one'], 'c')); + isSame('"one"', Utils::printList('one', 'c')); + isSame('[]', Utils::printList([])); + isSame('[]', Utils::printList([], 'c')); + } + public function testColorsTags(): void { $packs = [ diff --git a/tests/schemas/todo.yml b/tests/schemas/todo.yml index 433a1636..74dd06a5 100644 --- a/tests/schemas/todo.yml +++ b/tests/schemas/todo.yml @@ -41,6 +41,7 @@ columns: rules: # https://github.com/Respect/Validation/blob/main/docs/08-list-of-rules-by-category.md is_bool_value: true # https://github.com/Respect/Validation/blob/main/docs/rules/BoolVal.md + is_null: true # see empty_values # Dates age: 35 @@ -61,8 +62,8 @@ columns: # ids is_credit_card: brands[] # https://github.com/Respect/Validation/blob/main/docs/rules/CreditCard.md - is_iban: true - postal_code: country code # https://github.com/Respect/Validation/blob/main/docs/rules/PostalCode.md + is_postal_code: country code # https://github.com/Respect/Validation/blob/main/docs/rules/PostalCode.md + is_phone: true is_bsn: true is_cnh: true is_cnpj: true @@ -79,6 +80,7 @@ columns: is_polish_id_card: true is_portuguese_nif: true is_bic: true + is_iban: true is_card_number: true # Hashes @@ -161,18 +163,6 @@ columns: is_hmac_sha256: true is_hmac_sha512: true - # Web - ip: 220.78.168.0/21 # https://github.com/Respect/Validation/blob/main/docs/rules/Ip.md - is_ip6: true # Check if the value is a valid IPv6 address. Example: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 - ip4_subnets: [ 192.168.1.2/24 ] # If not set, then no subnet check - is_mac_address: true - is_public_domain_suffix: true - # is_email: true # improve - is_tid: true - # is_domain: true # improve - is_video_url: true - is_null: true # see empty_values - # URL url_scheme: https # Can be set of schemes [http, https, ftp] url_host: example.com # Can be regex @@ -190,8 +180,6 @@ columns: # Math is_fibonacci: true is_prime_number: true - is_digit: true - is_digits: true is_even: true is_odd: true is_roman: true @@ -199,13 +187,14 @@ columns: # Strings is_alnum: true + is_hex: true + is_binary: true is_alpha: true is_charset: true is_hex_rgb_color: true no_whitespace: true - is_phone: true + is_punct: true - is_spaces: true is_space: true is_regex: true is_version: true