diff --git a/README.md b/README.md index d417b8c..eb5700f 100644 --- a/README.md +++ b/README.md @@ -96,25 +96,22 @@ You may also utilize other place-holders in validation messages. For example the ##### Available rules -`between:min,max`: -``` -Validates that a cell value is between a :min and :max. The rule exposes the :min and :max placeholder for inline messages -``` -`ascii_only`: -``` -Validates that a cell value does not contain a non-ascii character -``` -`url`: -``` -Validates that a cell value is a valid URL. By valid URL we mean - -(#protocol) -(#basic auth) -(#a domain name or #an IP address or #an IPv6 address) -(#a port(optional)) then -(#a /, nothing, a / with something, a query or a fragment) - -``` +| Rule | Description | +| --- | --- | +| `alpha` | Validates that a cell value contains only lower and upper case alphabets | +| `alpha_num` | Validates that a cell value contains only numbers, lower and upper case alphabets | +| `ascii_only` | Validates that a cell value does not contain a non-ascii character | +| `between:min,max` | Validates that a cell value is between a :min and :max. The rule exposes the :min and :max placeholder for inline messages | +| `in:foo,bar,baz...` | Validates that a cell value is one of the values specified by the rule | +| `integer` | Validates that a cell has an integer value | +| `max:max_val` | Validates that a cell value must be less than or equal to max_val. | +| `max_length:length` | Validates that a cell value must contain less than or equal to length characters. | +| `min:min_val` | Validates that a cell value must be greater than or equal to min_val. | +| `min_length:length` | Validates that a cell value must contain greater than or equal to length characters. | +| `numeric` | Validates that a cell has a numeric value | +| `required` | Validates that a cell cannot be empty | +| `required_if:other_column,other_column_val` | Validates that a cell value must be present if a column specified by other_column has the value as other_column_val | +| `url` | Validates that a cell value is a valid URL. By valid URL we mean
(#protocol)
(#basic auth)
(#a domain name or #an IP address or #an IPv6 address)
(#a port(optional)) then
(#a /, nothing, a / with something, a query or a fragment) | ##### Writing CSV Output Data diff --git a/src/Contracts/ArrayParameterizedRuleInterface.php b/src/Contracts/ArrayParameterizedRuleInterface.php new file mode 100644 index 0000000..e13ac04 --- /dev/null +++ b/src/Contracts/ArrayParameterizedRuleInterface.php @@ -0,0 +1,22 @@ +replaceParameterPlaceholder( + $message, + $rule->allowedParameters(), + $rule->parseParameterValues($parameters) + ); + } + $message = $this->replaceValuePlaceholder($message, $value); $message = $this->replaceErrorLinePlaceholder($message, $lineNumber); diff --git a/src/Rules/Alpha.php b/src/Rules/Alpha.php new file mode 100644 index 0000000..eab95fa --- /dev/null +++ b/src/Rules/Alpha.php @@ -0,0 +1,35 @@ += (int) $min && $size <= (int) $max; + return +$value >= +$min && +$value <= +$max; } /** @@ -33,6 +31,6 @@ public function passes($value, array $parameters): bool */ public function message(): string { - return 'The :attribute value :value is not between :min - :max on line :line.'; + return 'The :attribute value :value must be between :min and :max on line :line.'; } } diff --git a/src/Rules/ClosureValidationRule.php b/src/Rules/ClosureValidationRule.php index f54b9f7..40d2080 100755 --- a/src/Rules/ClosureValidationRule.php +++ b/src/Rules/ClosureValidationRule.php @@ -42,11 +42,11 @@ public function __construct(\Closure $callback) * * @param mixed $value */ - public function passes($value, array $parameters): bool + public function passes($value, array $parameters, array $row): bool { $this->failed = false; - $this->callback->__invoke($value, function ($message) { + $this->callback->__invoke($value, $row, function ($message) { $this->failed = true; $this->message = $message; diff --git a/src/Rules/In.php b/src/Rules/In.php new file mode 100644 index 0000000..5193bfd --- /dev/null +++ b/src/Rules/In.php @@ -0,0 +1,65 @@ += +$min; + } + + /** + * Get the validation error message. + */ + public function message(): string + { + return 'The :attribute value :value may not be less than :min on line :line.'; + } +} diff --git a/src/Rules/MinLength.php b/src/Rules/MinLength.php new file mode 100644 index 0000000..c65d9f6 --- /dev/null +++ b/src/Rules/MinLength.php @@ -0,0 +1,36 @@ += $length; + } + + /** + * Get the validation error message. + */ + public function message(): string + { + return 'The :attribute value :value may not have less than :length characters on line :line.'; + } +} diff --git a/src/Rules/Numeric.php b/src/Rules/Numeric.php new file mode 100644 index 0000000..85db62e --- /dev/null +++ b/src/Rules/Numeric.php @@ -0,0 +1,35 @@ +filePath, 'r'))) { while (false !== ($row = fgetcsv($handle, 0, $this->delimiter))) { ++$this->currentRowLineNumber; + + if ($this->shouldTrim) { + $row = array_map('trim', $row); + } + if (empty($this->headers)) { $this->setHeaders($row); continue; @@ -277,7 +290,7 @@ protected function validateAttribute(string $attribute, $rule): void if ($this->isValidateAble($rule, $parameters)) { $ruleClass = $this->getRuleClass($rule); - if (!$ruleClass->passes($value, $parameters)) { + if (!$ruleClass->passes($value, $parameters, $this->currentRow)) { $this->addFailure( $this->getMessage($attribute, $ruleClass, $rule), $attribute, @@ -357,6 +370,13 @@ protected function passesParameterCheck($rule, array $parameters): bool return $parameterCount === $ruleParameterCount; } + if ($rule instanceof ArrayParameterizedRuleInterface) { + $ruleParameterCount = count($rule->allowedParameters()); + $parameterCount = count($parameters); + + return $parameterCount >= $ruleParameterCount; + } + return true; } @@ -371,7 +391,7 @@ protected function validateUsingCustomRule( array $parameters, ValidationRuleInterface $rule ): void { - if (!$rule->passes($value, $parameters)) { + if (!$rule->passes($value, $parameters, $this->currentRow)) { $this->addFailure($rule->message(), $attribute, $value, $rule, $parameters); } } @@ -407,4 +427,15 @@ protected function getValue(string $attribute) { return $this->currentRow[$attribute]; } + + /** + * Set if cell values should be trimmed + * before validation. + * + * @return void + */ + public function setShouldTrim(bool $shouldTrim = false) + { + $this->shouldTrim = $shouldTrim; + } } diff --git a/tests/data/alpha_num_test.csv b/tests/data/alpha_num_test.csv new file mode 100644 index 0000000..594214f --- /dev/null +++ b/tests/data/alpha_num_test.csv @@ -0,0 +1,2 @@ +name,address,stars,contact,uri + Well Health Hotels ,Inga N. P.O. Box 567,3,,http//:well.org diff --git a/tests/data/in_test.csv b/tests/data/in_test.csv new file mode 100644 index 0000000..d8c7e4f --- /dev/null +++ b/tests/data/in_test.csv @@ -0,0 +1,3 @@ +name,address,stars,contact,uri +Well Health Hotels,Inga N. P.O. Box 567,2,Kasper Zen,http//:well.org +Bad Health Hotels,Inga N. P.O. Box 568,2,,http//:well.org diff --git a/tests/data/integer_test.csv b/tests/data/integer_test.csv new file mode 100644 index 0000000..b80dc05 --- /dev/null +++ b/tests/data/integer_test.csv @@ -0,0 +1,2 @@ +id,name,address,stars,contact,uri +,Well Health Hotels,Inga N. P.O. Box 567,5.5,Kasper Zen,http//:well.org diff --git a/tests/data/min_max_test.csv b/tests/data/min_max_test.csv new file mode 100644 index 0000000..d868f8a --- /dev/null +++ b/tests/data/min_max_test.csv @@ -0,0 +1,2 @@ +name,address,stars,contact,uri +Well Health Hotels,Inga N. P.O. Box 567,3,Kasper Zen,http//:well.org diff --git a/tests/data/numeric_test.csv b/tests/data/numeric_test.csv new file mode 100644 index 0000000..4084d72 --- /dev/null +++ b/tests/data/numeric_test.csv @@ -0,0 +1,2 @@ +id,name,address,stars,contact,uri +,Well Health Hotels,Inga N. P.O. Box 567,A,Kasper Zen,http//:well.org diff --git a/tests/data/required_if_test.csv b/tests/data/required_if_test.csv new file mode 100644 index 0000000..a0e853f --- /dev/null +++ b/tests/data/required_if_test.csv @@ -0,0 +1,3 @@ +name,address,stars,contact,uri +Well Health Hotels,Inga N. P.O. Box 567,3,,http//:well.org +Bad Health Hotels,Inga N. P.O. Box 568,2,,http//:well.org diff --git a/tests/data/required_test.csv b/tests/data/required_test.csv new file mode 100644 index 0000000..9158c03 --- /dev/null +++ b/tests/data/required_test.csv @@ -0,0 +1,2 @@ +name,address,stars,contact,uri +Well Health Hotels,,3,Kasper Zen,http//:well.org diff --git a/tests/src/CsvValidatorTest.php b/tests/src/CsvValidatorTest.php index 3422405..eede04b 100755 --- a/tests/src/CsvValidatorTest.php +++ b/tests/src/CsvValidatorTest.php @@ -36,6 +36,64 @@ public function testInvalidCsvFilePath() ); } + public function testAlphaValidationRule() + { + $file = $this->testAssets . '/alpha_num_test.csv'; + + $validator = new Validator($file, ',', [ + 'name' => ['alpha'], + 'contact' => ['alpha'], + ]); + + $validator->setShouldTrim(true); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The name value Well Health Hotels may only contain letters on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testAlphaNumValidationRule() + { + $file = $this->testAssets . '/alpha_num_test.csv'; + + $validator = new Validator($file, ',', [ + 'address' => ['alpha_num'], + 'contact' => ['alpha_num'], + ]); + + $validator->setShouldTrim(true); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The address value Inga N. P.O. Box 567 may only contain letters and numbers on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + public function testAsciiOnlyValidationRule() { $file = $this->testAssets . '/ascii_test.csv'; @@ -83,17 +141,18 @@ public function testBetweenValidationRule() ); $this->assertContains( - 'The stars value 3 is not between 4 - 10 on line 2.', + 'The stars value 3 must be between 4 and 10 on line 2.', $validator->errors()['data'][0]['errors'] ); } - public function testUrlValidationRule() + public function testInValidationRule() { - $file = $this->testAssets . '/url_test.csv'; + $file = $this->testAssets . '/in_test.csv'; $validator = new Validator($file, ',', [ - 'uri' => ['url'], + 'stars' => ['in:3,5,8,10'], + 'contact' => ['in:Kasper Zen'], ]); $this->assertTrue($validator->fails()); @@ -103,28 +162,252 @@ public function testUrlValidationRule() $validator->errors()['message'] ); - $validationErrors = $validator->errors(); + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The stars value 2 does not exist in 3,5,8,10 on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testIntegerValidationRule() + { + $file = $this->testAssets . '/integer_test.csv'; - for ($csvRow = 0; $csvRow < 3; ++$csvRow) { - $this->assertArrayHasKey( - 'errors', - $validationErrors['data'][$csvRow] - ); - } + $validator = new Validator($file, ',', [ + 'stars' => ['integer'], + 'id' => ['integer'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); $this->assertContains( - 'The uri value http//:well.org is not a valid url on line 2.', - $validationErrors['data'][0]['errors'] + 'The stars value 5.5 must be an integer on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testMaxValidationRule() + { + $file = $this->testAssets . '/min_max_test.csv'; + + $validator = new Validator($file, ',', [ + 'stars' => ['max:1'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] ); $this->assertContains( - 'The uri value is not a valid url on line 3.', - $validationErrors['data'][1]['errors'] + 'The stars value 3 may not be greater than 1 on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testMaxLengthValidationRule() + { + $file = $this->testAssets . '/min_max_test.csv'; + + $validator = new Validator($file, ',', [ + 'name' => ['max_length:15'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] ); $this->assertContains( - 'The uri value is not a valid url on line 4.', - $validationErrors['data'][2]['errors'] + 'The name value Well Health Hotels may not have more than 15 characters on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testMinValidationRule() + { + $file = $this->testAssets . '/min_max_test.csv'; + + $validator = new Validator($file, ',', [ + 'stars' => ['min:4'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The stars value 3 may not be less than 4 on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testMinLengthValidationRule() + { + $file = $this->testAssets . '/min_max_test.csv'; + + $validator = new Validator($file, ',', [ + 'name' => ['min_length:20'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The name value Well Health Hotels may not have less than 20 characters on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testNumericValidationRule() + { + $file = $this->testAssets . '/numeric_test.csv'; + + $validator = new Validator($file, ',', [ + 'stars' => ['numeric'], + 'id' => ['numeric'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The stars value A must be a number on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testRequiredIfValidationRule() + { + $file = $this->testAssets . '/required_if_test.csv'; + + $validator = new Validator($file, ',', [ + 'contact' => ['required_if:address,Inga N. P.O. Box 567'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The contact field is required when address is Inga N. P.O. Box 567 on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testRequiredValidationRule() + { + $file = $this->testAssets . '/required_test.csv'; + + $validator = new Validator($file, ',', [ + 'address' => ['required'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $this->assertArrayHasKey( + 'errors', + $validator->errors()['data'][0] + ); + + $this->assertContains( + 'The address value is required on line 2.', + $validator->errors()['data'][0]['errors'] + ); + } + + public function testUrlValidationRule() + { + $file = $this->testAssets . '/url_test.csv'; + + $validator = new Validator($file, ',', [ + 'uri' => ['url'], + ]); + + $this->assertTrue($validator->fails()); + + $this->assertSame( + $validator::ERROR_MESSAGE, + $validator->errors()['message'] + ); + + $validationErrors = $validator->errors(); + + $this->assertArrayHasKey( + 'errors', + $validationErrors['data'][0] + ); + + $this->assertContains( + 'The uri value http//:well.org is not a valid url on line 2.', + $validationErrors['data'][0]['errors'] ); } @@ -159,7 +442,7 @@ public function testValidatorWithCustomRuleClosure() $file = $this->testAssets . '/url_test.csv'; $validator = new Validator($file, ',', [ - 'uri' => [function ($value, $fail) { + 'uri' => [function ($value, $row, $fail) { if (0 !== strpos($value, 'https://')) { return $fail('The URL passed must be https i.e it must start with https://'); } diff --git a/tests/src/UppercaseRule.php b/tests/src/UppercaseRule.php index 47a20b9..5af9ba9 100644 --- a/tests/src/UppercaseRule.php +++ b/tests/src/UppercaseRule.php @@ -8,9 +8,8 @@ class UppercaseRule implements ValidationRuleInterface { /** * @param mixed $value - * @param $parameters */ - public function passes($value, array $parameters): bool + public function passes($value, array $parameters, array $row): bool { return strtoupper($value) === $value; }