From ca07291cce5110c93f87108cf6fb0e2e86e1640e Mon Sep 17 00:00:00 2001 From: Denis Smetannikov Date: Mon, 1 Apr 2024 13:44:55 +0400 Subject: [PATCH] Add `required` rule for a column (#119) --- CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 README.md | 60 ++-- schema-examples/full.json | 3 +- schema-examples/full.php | 3 +- schema-examples/full.yml | 25 +- schema-examples/full_clean.yml | 3 +- src/Csv/Column.php | 45 +-- src/Csv/CsvFile.php | 83 +++-- .../{ParseConfig.php => CsvParserConfig.php} | 24 +- src/Rules/Aggregate/Sorted.php | 2 +- src/Schema.php | 36 +- src/Validators/ErrorSuite.php | 10 +- src/Validators/ValidatorCsv.php | 21 +- src/Validators/ValidatorSchema.php | 6 +- tests/Commands/ValidateCsvBasicTest.php | 2 +- tests/ExampleSchemasTest.php | 4 +- tests/ReadmeTest.php | 7 +- tests/SchemaTest.php | 17 +- tests/Validators/CsvValidatorTest.php | 337 ++++++++++++++++-- tests/schemas/todo.yml | 3 +- 20 files changed, 490 insertions(+), 201 deletions(-) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) rename src/Csv/{ParseConfig.php => CsvParserConfig.php} (80%) diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/README.md b/README.md index 13e1a122..f2600145 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ -[![Static Badge](https://img.shields.io/badge/Rules-366-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) +[![Static Badge](https://img.shields.io/badge/Rules-367-green?label=Total%20number%20of%20rules&labelColor=darkgreen&color=gray)](schema-examples/full.yml) [![Static Badge](https://img.shields.io/badge/Rules-153-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-7-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) -[![Static Badge](https://img.shields.io/badge/Rules-32/54/13-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml) +[![Static Badge](https://img.shields.io/badge/Rules-8-green?label=Extra%20checks&labelColor=blue&color=gray)](#extra-checks) +[![Static Badge](https://img.shields.io/badge/Rules-32/54/9-green?label=Plan%20to%20add&labelColor=gray&color=gray)](tests/schemas/todo.yml) A console utility designed for validating CSV files against a strictly defined schema and validation rules outlined @@ -232,7 +232,6 @@ columns: count: 10 ``` - @@ -242,19 +241,33 @@ In the [example Yml file](schema-examples/full.yml) you can find a detailed desc It's also covered by tests, so it's always up-to-date. **Important notes** + * I have deliberately refused typing of columns (like `type: integer`) and replaced them with rules, which can be combined in any sequence and completely at your discretion. This gives you great flexibility when validating CSV files. -* All fields (unless explicitly stated otherwise) are optional, and you can choose not to declare them. Up to you. -* If you specify a wrong rule name, non-existent values (which are not in the example below) or a different variable - type for any of the options, you will get a schema validation error. At your own risk, you can use the `skip-schema` +* All options (unless explicitly stated otherwise) are optional, and you can choose not to declare them. Up to you. +* If you specify a wrong rule name, non-existent values (which are not in the example below) or a different variable + type for any of the options, you will get a schema validation error. At your own risk, you can use the `--skip-schema` option to avoid seeing these errors and use your keys in the schema. - +* All rules except `not_empty` ignored for empty strings (length 0). If the value must be non-empty, + use `not_empty: true` as extra rule. Keep in mind that a space (` `) is also a character. In this case the string + length + will be `1`. If you want to avoid such situations, add the `is_trimmed: true` rule. +* All rules don't depend on each other. They know nothing about each other and cannot influence each other. +* You can use the rules in any combination. Or not use any of them. They are grouped below simply for ease of navigation + and reading. +* If you see the value for the rule is `is_some_rule: true` - that's just an enable flag. In other cases, these are rule + parameters. +* The order of rules execution is the same as in the schema. But in reality it will only change the order of errors in + the report. +* Most of the rules are case-sensitive. Unless otherwise specified. +* As backup plan, you always can use the `regex` rule. But it is much more reliable to use clear combinations of rules. + That way it will be more obvious what went wrong. Below you'll find the full list of rules and a brief commentary and example for context. This part of the readme is also covered by autotests, so these code are always up-to-date. -In any unclear situation, look into it first. +In any unclear situation, look into it first ;) ```yml @@ -265,7 +278,7 @@ In any unclear situation, look into it first. name: CSV Blueprint Schema Example # Name of a CSV file. Not used in the validation process. description: | # Any description of the CSV file. Not used in the validation process. This YAML file provides a detailed description and validation rules for CSV files - to be processed by JBZoo/Csv-Blueprint tool. It includes specifications for file name patterns, + to be processed by CSV Blueprint tool. It includes specifications for file name patterns, CSV formatting options, and extensive validation criteria for individual columns and their values, supporting a wide range of data validation rules from basic type checks to complex regex validations. This example serves as a comprehensive guide for creating robust CSV file validations. @@ -298,26 +311,17 @@ structural_rules: # Here are default values. # This will not affect the validator, but will make it easier for you to navigate. # For convenience, use the first line as a header (if possible). columns: - - name: Column Name (header) # Any custom name of the column in the CSV file (first row). Required if "csv_structure.header" is true. + - name: Column Name (header) # Any custom name of the column in the CSV file (first row). Required if "csv.header" is true. description: Lorem ipsum # Description of the column. Not used in the validation process. example: Some example # Example of the column value. Schema will also check this value on its own. - # Important notes about the validation rules. - # 1. All rules except "not_empty" ignored for empty strings (length 0). - # If the value must be non-empty, use "not_empty" as extra rule! - # 2. All rules don't depend on each other. They are independent. - # They know nothing about each other and cannot influence each other. - # 3. You can use the rules in any combination. Or not use any of them. - # They are grouped below simply for ease of navigation and reading. - # 4. If you see the value for the rule is "true" - that's just an enable flag. - # In other cases, these are rule parameters. - # 5. The order of rules execution is the same as in the schema. But it doesn't matter. - # The result will be the same in any order. - # 6. Most of the rules are case-sensitive. Unless otherwise specified. - # 7. As backup plan, you always can use the "regex" rule. ON YOUR OWN RISK! + # If the column is required. If true, the column must be present in the CSV file. If false, the column can be missing in the CSV file. + # So, if you want to make the column optional, set this value to false, and it will validate the column only if it is present. + # By default, the column is required. It works only if "csv.header" is true and "structural_rules.allow_extra_columns" is false. + required: true #################################################################################################################### - # Data validation for each(!) value in the column. + # Data validation for each(!) value in the column. Please, see notes in README.md # Every rule is optional. rules: # General rules @@ -487,7 +491,7 @@ columns: # Check if the column is sorted in a specific order. # - Direction: ["asc", "desc"]. - # - Method: ["natural", "regular", "numeric", "string"]. + # - Method: ["natural", "regular", "numeric", "string"]. # See: https://www.php.net/manual/en/function.sort.php sorted: [ asc, natural ] # Expected ascending order, natural sorting. @@ -821,7 +825,8 @@ Behind the scenes to what is outlined in the yml above, there are additional che * With `filename_pattern` rule, you can check if the file name matches the pattern. -* Property `name` is not defined in a column. If `csv.header: true`. +* Checks if property `name` is not defined in a column. Only if `csv.header: true`. +* If property `required` is set to `true`, the column must must be present in CSV. Only if `csv.header: true` * Check that each row matches the number of columns. * With `strict_column_order` rule, you can check that the columns are in the correct order. * With `allow_extra_columns` rule, you can check that there are no extra columns in the CSV file. @@ -1284,7 +1289,6 @@ It's random ideas and plans. No promises and deadlines. Feel free to [help me!]( file name. * **Validation** - * `required` flag for the column. * Multi values in one cell. * Custom cell rule as a callback. It's useful when you have a complex rule that can't be described in the schema file. diff --git a/schema-examples/full.json b/schema-examples/full.json index 1c1c2e82..bcdd2ddd 100644 --- a/schema-examples/full.json +++ b/schema-examples/full.json @@ -1,6 +1,6 @@ { "name" : "CSV Blueprint Schema Example", - "description" : "This YAML file provides a detailed description and validation rules for CSV files\nto be processed by JBZoo\/Csv-Blueprint tool. It includes specifications for file name patterns,\nCSV formatting options, and extensive validation criteria for individual columns and their values,\nsupporting a wide range of data validation rules from basic type checks to complex regex validations.\nThis example serves as a comprehensive guide for creating robust CSV file validations.\n", + "description" : "This YAML file provides a detailed description and validation rules for CSV files\nto be processed by CSV Blueprint tool. It includes specifications for file name patterns,\nCSV formatting options, and extensive validation criteria for individual columns and their values,\nsupporting a wide range of data validation rules from basic type checks to complex regex validations.\nThis example serves as a comprehensive guide for creating robust CSV file validations.\n", "filename_pattern" : "\/demo(-\\d+)?\\.csv$\/i", @@ -23,6 +23,7 @@ "name" : "Column Name (header)", "description" : "Lorem ipsum", "example" : "Some example", + "required" : true, "rules" : { "not_empty" : true, diff --git a/schema-examples/full.php b/schema-examples/full.php index acf9dc43..b22340ad 100644 --- a/schema-examples/full.php +++ b/schema-examples/full.php @@ -17,7 +17,7 @@ return [ 'name' => 'CSV Blueprint Schema Example', 'description' => 'This YAML file provides a detailed description and validation rules for CSV files -to be processed by JBZoo/Csv-Blueprint tool. It includes specifications for file name patterns, +to be processed by CSV Blueprint tool. It includes specifications for file name patterns, CSV formatting options, and extensive validation criteria for individual columns and their values, supporting a wide range of data validation rules from basic type checks to complex regex validations. This example serves as a comprehensive guide for creating robust CSV file validations. @@ -44,6 +44,7 @@ 'name' => 'Column Name (header)', 'description' => 'Lorem ipsum', 'example' => 'Some example', + 'required' => true, 'rules' => [ 'not_empty' => true, diff --git a/schema-examples/full.yml b/schema-examples/full.yml index 0d9ff045..1d1caa8b 100644 --- a/schema-examples/full.yml +++ b/schema-examples/full.yml @@ -17,7 +17,7 @@ name: CSV Blueprint Schema Example # Name of a CSV file. Not used in the validation process. description: | # Any description of the CSV file. Not used in the validation process. This YAML file provides a detailed description and validation rules for CSV files - to be processed by JBZoo/Csv-Blueprint tool. It includes specifications for file name patterns, + to be processed by CSV Blueprint tool. It includes specifications for file name patterns, CSV formatting options, and extensive validation criteria for individual columns and their values, supporting a wide range of data validation rules from basic type checks to complex regex validations. This example serves as a comprehensive guide for creating robust CSV file validations. @@ -50,26 +50,17 @@ structural_rules: # Here are default values. # This will not affect the validator, but will make it easier for you to navigate. # For convenience, use the first line as a header (if possible). columns: - - name: Column Name (header) # Any custom name of the column in the CSV file (first row). Required if "csv_structure.header" is true. + - name: Column Name (header) # Any custom name of the column in the CSV file (first row). Required if "csv.header" is true. description: Lorem ipsum # Description of the column. Not used in the validation process. example: Some example # Example of the column value. Schema will also check this value on its own. - # Important notes about the validation rules. - # 1. All rules except "not_empty" ignored for empty strings (length 0). - # If the value must be non-empty, use "not_empty" as extra rule! - # 2. All rules don't depend on each other. They are independent. - # They know nothing about each other and cannot influence each other. - # 3. You can use the rules in any combination. Or not use any of them. - # They are grouped below simply for ease of navigation and reading. - # 4. If you see the value for the rule is "true" - that's just an enable flag. - # In other cases, these are rule parameters. - # 5. The order of rules execution is the same as in the schema. But it doesn't matter. - # The result will be the same in any order. - # 6. Most of the rules are case-sensitive. Unless otherwise specified. - # 7. As backup plan, you always can use the "regex" rule. ON YOUR OWN RISK! + # If the column is required. If true, the column must be present in the CSV file. If false, the column can be missing in the CSV file. + # So, if you want to make the column optional, set this value to false, and it will validate the column only if it is present. + # By default, the column is required. It works only if "csv.header" is true and "structural_rules.allow_extra_columns" is false. + required: true #################################################################################################################### - # Data validation for each(!) value in the column. + # Data validation for each(!) value in the column. Please, see notes in README.md # Every rule is optional. rules: # General rules @@ -239,7 +230,7 @@ columns: # Check if the column is sorted in a specific order. # - Direction: ["asc", "desc"]. - # - Method: ["natural", "regular", "numeric", "string"]. + # - Method: ["natural", "regular", "numeric", "string"]. # See: https://www.php.net/manual/en/function.sort.php sorted: [ asc, natural ] # Expected ascending order, natural sorting. diff --git a/schema-examples/full_clean.yml b/schema-examples/full_clean.yml index 45d2d25d..241d8fda 100644 --- a/schema-examples/full_clean.yml +++ b/schema-examples/full_clean.yml @@ -16,7 +16,7 @@ name: 'CSV Blueprint Schema Example' description: | This YAML file provides a detailed description and validation rules for CSV files - to be processed by JBZoo/Csv-Blueprint tool. It includes specifications for file name patterns, + to be processed by CSV Blueprint tool. It includes specifications for file name patterns, CSV formatting options, and extensive validation criteria for individual columns and their values, supporting a wide range of data validation rules from basic type checks to complex regex validations. This example serves as a comprehensive guide for creating robust CSV file validations. @@ -39,6 +39,7 @@ columns: - name: 'Column Name (header)' description: 'Lorem ipsum' example: 'Some example' + required: true rules: not_empty: true diff --git a/src/Csv/Column.php b/src/Csv/Column.php index f3e361b9..32f6aacd 100644 --- a/src/Csv/Column.php +++ b/src/Csv/Column.php @@ -24,25 +24,22 @@ final class Column { private const FALLBACK_VALUES = [ - 'inherit' => '', 'name' => '', 'description' => '', - 'type' => 'base', // TODO: class - 'required' => false, - 'allow_empty' => false, - 'regex' => null, + 'required' => true, 'rules' => [], 'aggregate_rules' => [], ]; - private int $id; + private ?int $csvOffset = null; + private int $schemaId; private Data $column; private array $rules; private array $aggRules; - public function __construct(int $id, array $config) + public function __construct(int $schemaId, array $config) { - $this->id = $id; + $this->schemaId = $schemaId; $this->column = new Data($config); $this->rules = $this->prepareRuleSet('rules'); $this->aggRules = $this->prepareRuleSet('aggregate_rules'); @@ -53,28 +50,30 @@ public function getName(): string return $this->column->getString('name', self::FALLBACK_VALUES['name']); } - public function getId(): int + public function getCsvOffset(): ?int { - return $this->id; + return $this->csvOffset; } - public function getDescription(): string + public function getSchemaId(): int { - return $this->column->getString('description', self::FALLBACK_VALUES['description']); + return $this->schemaId; } - public function getHumanName(): string + public function getDescription(): string { - return $this->getId() . ':' . \trim($this->getName()); + return $this->column->getString('description', self::FALLBACK_VALUES['description']); } - public function getKey(): string + public function getHumanName(): string { - if ($this->getName() !== '') { - return $this->getName(); + if ($this->csvOffset !== null) { + $prefix = $this->csvOffset; + } else { + $prefix = $this->schemaId; } - return (string)$this->getId(); + return $prefix . ':' . \trim($this->getName()); } public function isRequired(): bool @@ -92,11 +91,6 @@ public function getAggregateRules(): array return $this->aggRules; } - public function getInherit(): string - { - return $this->column->getString('inherit', self::FALLBACK_VALUES['inherit']); - } - public function getValidator(): ValidatorColumn { return new ValidatorColumn($this); @@ -107,9 +101,9 @@ public function validateCell(string $cellValue, int $line = Error::UNDEFINED_LIN return $this->getValidator()->validateCell($cellValue, $line); } - public function setId(int $realIndex): void + public function setCsvOffset(int $csvOffset): void { - $this->id = $realIndex; + $this->csvOffset = $csvOffset; } private function prepareRuleSet(string $schemaKey): array @@ -117,7 +111,6 @@ private function prepareRuleSet(string $schemaKey): array $rules = []; $ruleSetConfig = $this->column->getSelf($schemaKey, [])->getArrayCopy(); - foreach ($ruleSetConfig as $ruleName => $ruleValue) { $rules[$ruleName] = $ruleValue; } diff --git a/src/Csv/CsvFile.php b/src/Csv/CsvFile.php index 24ba0a60..c496c2bb 100644 --- a/src/Csv/CsvFile.php +++ b/src/Csv/CsvFile.php @@ -17,7 +17,9 @@ namespace JBZoo\CsvBlueprint\Csv; use JBZoo\CsvBlueprint\Schema; +use JBZoo\CsvBlueprint\Validators\Error; use JBZoo\CsvBlueprint\Validators\ErrorSuite; +use JBZoo\CsvBlueprint\Validators\ValidatorColumn; use JBZoo\CsvBlueprint\Validators\ValidatorCsv; use League\Csv\Reader as LeagueReader; use League\Csv\Statement; @@ -25,12 +27,12 @@ final class CsvFile { - private string $csvFilename; - private ParseConfig $structure; - private LeagueReader $reader; - private Schema $schema; - private bool $isEmpty; - private ?array $header = null; + private string $csvFilename; + private CsvParserConfig $csvParserConfig; + private LeagueReader $reader; + private Schema $schema; + private bool $isEmpty; + private ?array $header = null; public function __construct(string $csvFilename, null|array|string $csvSchemaFilenameOrArray = null) { @@ -41,7 +43,7 @@ public function __construct(string $csvFilename, null|array|string $csvSchemaFil $this->csvFilename = $csvFilename; $this->isEmpty = \filesize($this->csvFilename) <= 1; $this->schema = new Schema($csvSchemaFilenameOrArray); - $this->structure = $this->schema->getCsvStructure(); + $this->csvParserConfig = $this->schema->getCsvParserConfig(); $this->reader = $this->prepareReader(); } @@ -50,9 +52,9 @@ public function getCsvFilename(): string return $this->csvFilename; } - public function getCsvStructure(): ParseConfig + public function getCsvStructure(): CsvParserConfig { - return $this->structure; + return $this->csvParserConfig; } /** @@ -62,7 +64,7 @@ public function getHeader(): array { if ($this->header === null) { $this->header = []; - if ($this->structure->isHeader() && !$this->isEmpty) { + if ($this->csvParserConfig->isHeader() && !$this->isEmpty) { // TODO: add handler for empty file // League\Csv\SyntaxError : The header record does not exist or is empty at offset: `0 $this->header = $this->getRecordsChunk(0, 1)->first(); @@ -74,9 +76,15 @@ public function getHeader(): array return $this->header; } - public function getRecords(): \Iterator + public function getRecords(?int $offset = null): \Iterator { - return $this->reader->getRecords([]); + if ($offset !== null) { + $records = $this->reader->fetchColumnByOffset($offset); + } else { + $records = $this->reader->getRecords(); + } + + return $records; } public function getRecordsChunk(int $offset = 0, int $limit = -1): TabularDataReader @@ -102,33 +110,68 @@ public function getSchema(): Schema /** * @return Column[] */ - public function getColumnsMappedByHeader(): array + public function getColumnsMappedByHeader(?ErrorSuite $errors = null): array { + $isHeader = $this->schema->getCsvParserConfig()->isHeader(); + $map = []; + $errors ??= new ErrorSuite(); $realHeader = $this->getHeader(); - foreach ($realHeader as $realIndex => $realColumn) { + foreach ($realHeader as $realIndex => $realColumnName) { $realIndex = (int)$realIndex; - $schemaColumn = $this->schema->getColumn($realColumn); + if ($isHeader) { + $schemaColumn = $this->schema->getColumn($realColumnName); + } else { + $schemaColumn = $this->schema->getColumn($realIndex); + } if ($schemaColumn !== null) { - $schemaColumn->setId($realIndex); + $schemaColumn->setCsvOffset($realIndex); $map[$realIndex] = $schemaColumn; } } + if ($this->schema->isAllowExtraColumns()) { + $unusedSchemaColumns = \array_filter( + $this->schema->getColumns(), + static fn ($column) => $column->getCsvOffset() === null, + ); + + foreach ($unusedSchemaColumns as $unusedSchemaColumn) { + if ($unusedSchemaColumn->isRequired()) { + $errors->addError( + new Error( + 'required', + 'Required column not found in CSV', + "Schema Col Id: {$unusedSchemaColumn->getSchemaId()}", + ValidatorColumn::FALLBACK_LINE, + ), + ); + } + } + } + return $map; } + /** + * @return string[] + */ + public function getColumnsMappedByHeaderNamesOnly(?ErrorSuite $errors = null): array + { + return \array_map(static fn (Column $column) => $column->getName(), $this->getColumnsMappedByHeader($errors)); + } + private function prepareReader(): LeagueReader { $reader = LeagueReader::createFromPath($this->csvFilename) - ->setDelimiter($this->structure->getDelimiter()) - ->setEnclosure($this->structure->getEnclosure()) - ->setEscape($this->structure->getQuoteChar()) + ->setDelimiter($this->csvParserConfig->getDelimiter()) + ->setEnclosure($this->csvParserConfig->getEnclosure()) + ->setEscape($this->csvParserConfig->getQuoteChar()) ->setHeaderOffset(null); // It's important to set it to null to optimize memory usage! - if ($this->structure->isBom()) { + if ($this->csvParserConfig->isBom()) { $reader->includeInputBOM(); } else { $reader->skipInputBOM(); diff --git a/src/Csv/ParseConfig.php b/src/Csv/CsvParserConfig.php similarity index 80% rename from src/Csv/ParseConfig.php rename to src/Csv/CsvParserConfig.php index 41ed7e37..f801f921 100644 --- a/src/Csv/ParseConfig.php +++ b/src/Csv/CsvParserConfig.php @@ -18,7 +18,7 @@ use JBZoo\Data\Data; -final class ParseConfig +final class CsvParserConfig { public const ENCODING_UTF8 = 'utf-8'; public const ENCODING_UTF16 = 'utf-16'; @@ -109,37 +109,15 @@ public function isHeader(): bool return $this->structure->getBool('header', self::FALLBACK_VALUES['header']); } - public function isStrictColumnOrder(): bool - { - return $this->structure->findBool( - 'structural_rules.strict_column_order', - self::FALLBACK_VALUES['strict_column_order'], - ); - } - - public function isAllowExtraColumns(): bool - { - return $this->structure->findBool( - 'structural_rules.allow_extra_columns', - self::FALLBACK_VALUES['allow_extra_columns'], - ); - } - public function getArrayCopy(): array { return [ - // System rules - // 'inherit' => $this->getInherit(), // TODO Implement me - // Reading rules 'header' => $this->isHeader(), 'delimiter' => $this->getDelimiter(), 'quote_char' => $this->getQuoteChar(), 'enclosure' => $this->getEnclosure(), 'encoding' => $this->getEncoding(), 'bom' => $this->isBom(), - // Global validation rules - // 'strict_column_order' => $this->isStrictColumnOrder(), // TODO Implement me - // 'other_columns_possible' => $this->isOtherColumnsPossible(), // TODO Implement me ]; } } diff --git a/src/Rules/Aggregate/Sorted.php b/src/Rules/Aggregate/Sorted.php index 66104cbb..c88820aa 100644 --- a/src/Rules/Aggregate/Sorted.php +++ b/src/Rules/Aggregate/Sorted.php @@ -41,7 +41,7 @@ public function getHelpMeta(): array [ 'Check if the column is sorted in a specific order.', ' - Direction: ' . Utils::printList(self::DIRS) . '.', - ' - Method: ' . Utils::printList(\array_keys(self::METHODS)) . '.', + ' - Method: ' . Utils::printList(\array_keys(self::METHODS)) . '.', 'See: https://www.php.net/manual/en/function.sort.php', ], [ diff --git a/src/Schema.php b/src/Schema.php index 17bcf9d2..99bb89bf 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -17,7 +17,7 @@ namespace JBZoo\CsvBlueprint; use JBZoo\CsvBlueprint\Csv\Column; -use JBZoo\CsvBlueprint\Csv\ParseConfig; +use JBZoo\CsvBlueprint\Csv\CsvParserConfig; use JBZoo\CsvBlueprint\Validators\ErrorSuite; use JBZoo\CsvBlueprint\Validators\ValidatorSchema; use JBZoo\Data\AbstractData; @@ -73,9 +73,9 @@ public function getFilename(): ?string return $this->filename; } - public function getCsvStructure(): ParseConfig + public function getCsvParserConfig(): CsvParserConfig { - return new ParseConfig($this->data->getArray('csv')); + return new CsvParserConfig($this->data->getArray('csv')); } /** @@ -89,12 +89,16 @@ public function getColumns(): array public function getColumn(int|string $columNameOrId): ?Column { if (\is_int($columNameOrId)) { - $column = \array_values($this->getColumns())[$columNameOrId] ?? null; - } else { - $column = $this->getColumns()[$columNameOrId] ?? null; + return \array_values($this->getColumns())[$columNameOrId] ?? null; } - return $column; + foreach ($this->getColumns() as $schemaColumn) { + if ($schemaColumn->getName() === $columNameOrId) { + return $schemaColumn; + } + } + + return null; } public function getFilenamePattern(): ?string @@ -133,7 +137,21 @@ public function getData(): AbstractData public function getSchemaHeader(): array { - return \array_keys($this->getColumns()); + $schemaColumns = $this->getColumns(); + return \array_reduce($schemaColumns, static function (array $carry, Column $column) { + $carry[] = $column->getName(); + return $carry; + }, []); + } + + public function isStrictColumnOrder(): bool + { + return $this->data->findBool('structural_rules.strict_column_order', true); + } + + public function isAllowExtraColumns(): bool + { + return $this->data->findBool('structural_rules.allow_extra_columns', false); } /** @@ -146,7 +164,7 @@ private function prepareColumns(): array foreach ($this->data->getArray('columns') as $columnId => $columnPreferences) { $column = new Column((int)$columnId, $columnPreferences); - $result[$column->getKey()] = $column; + $result[$column->getSchemaId()] = $column; } return $result; diff --git a/src/Validators/ErrorSuite.php b/src/Validators/ErrorSuite.php index 579a4a12..beb3caa9 100644 --- a/src/Validators/ErrorSuite.php +++ b/src/Validators/ErrorSuite.php @@ -51,7 +51,7 @@ public function __toString(): string return (string)$this->render(self::REPORT_TEXT); } - public function render(string $mode = self::REPORT_TEXT): ?string + public function render(string $mode = self::REPORT_TEXT, bool $cleanOutput = false): ?string { if ($this->count() === 0) { return null; @@ -71,7 +71,13 @@ public function render(string $mode = self::REPORT_TEXT): ?string ]; if (isset($map[$mode])) { - return $map[$mode](); + $output = $map[$mode](); + + if ($cleanOutput) { + return \trim(\strip_tags($output)); + } + + return $output; } throw new Exception("Unknown error render mode: {$mode}"); diff --git a/src/Validators/ValidatorCsv.php b/src/Validators/ValidatorCsv.php index f12125cd..8c8dbbff 100644 --- a/src/Validators/ValidatorCsv.php +++ b/src/Validators/ValidatorCsv.php @@ -75,7 +75,7 @@ private function validateHeader(bool $quickStop = false): ErrorSuite { $errors = new ErrorSuite(); - if (!$this->schema->getCsvStructure()->isHeader()) { + if (!$this->schema->getCsvParserConfig()->isHeader()) { return $errors; } @@ -97,7 +97,7 @@ private function validateHeader(bool $quickStop = false): ErrorSuite } } - if ($this->schema->getCsvStructure()->isStrictColumnOrder()) { + if ($this->schema->isStrictColumnOrder()) { $realColumns = $this->csv->getHeader(); $schemaColumns = $this->schema->getSchemaHeader(); @@ -127,10 +127,11 @@ private function validateHeader(bool $quickStop = false): ErrorSuite private function validateLines(bool $quickStop = false): ErrorSuite { $errors = new ErrorSuite(); - $mappedColumns = $this->csv->getColumnsMappedByHeader(); - $isHeaderEnabled = $this->schema->getCsvStructure()->isHeader(); + $mappedColumns = $this->csv->getColumnsMappedByHeader($errors); + $isHeaderEnabled = $this->schema->getCsvParserConfig()->isHeader(); foreach ($mappedColumns as $columnIndex => $column) { + $columnIndex = (int)$columnIndex; $messPrefix = "Column \"{$column->getHumanName()}\" -"; // System message prefix. Debug only! $columValues = []; @@ -166,7 +167,7 @@ private function validateLines(bool $quickStop = false): ErrorSuite if ($isRules) { // Time optimization if (!isset($record[$columnIndex])) { - $errors->addError( + $errors->addError( // Something really went wrong. See debug getColumnsMappedByHeader(). new Error( 'csv.column', "Column index:{$columnIndex} not found", @@ -233,11 +234,15 @@ private function validateColumn(bool $quickStop): ErrorSuite { $errors = new ErrorSuite(); - if (!$this->schema->getCsvStructure()->isAllowExtraColumns()) { - if ($this->schema->getCsvStructure()->isHeader()) { + if (!$this->schema->isAllowExtraColumns()) { + if ($this->schema->getCsvParserConfig()->isHeader()) { $realColumns = $this->csv->getHeader(); $schemaColumns = $this->schema->getSchemaHeader(); - $notFoundColums = \array_diff($schemaColumns, $realColumns); + + $notFoundColums = \array_filter( // Filter to exclude duplicate error. See test testCellRuleNoName + \array_diff($schemaColumns, $realColumns), + static fn ($name) => $name !== '', + ); if (\count($notFoundColums) > 0) { $error = new Error( diff --git a/src/Validators/ValidatorSchema.php b/src/Validators/ValidatorSchema.php index 1af8d5e2..595f865f 100644 --- a/src/Validators/ValidatorSchema.php +++ b/src/Validators/ValidatorSchema.php @@ -33,7 +33,7 @@ public function __construct(Schema $schema) { $this->filename = $schema->getFilename(); $this->data = $schema->getData(); - $this->isHeader = $schema->getCsvStructure()->isHeader(); + $this->isHeader = $schema->getCsvParserConfig()->isHeader(); } public function validate(bool $quickStop = false): ErrorSuite @@ -125,14 +125,14 @@ private function validateColumnName(array $actualColumn, string $columnId): ?Err return null; } - private static function validateColumnExample(array $actualColumn, int $columnKey): ?ErrorSuite + private static function validateColumnExample(array $actualColumn, int $schemaColumnId): ?ErrorSuite { $exclude = [ 'Some example', // I.e. this value is taken from full.yml, then it will be invalid in advance. ]; if (isset($actualColumn['example']) && !\in_array($actualColumn['example'], $exclude, true)) { - return (new Column($columnKey, $actualColumn))->validateCell((string)$actualColumn['example']); + return (new Column($schemaColumnId, $actualColumn))->validateCell((string)$actualColumn['example']); } return null; diff --git a/tests/Commands/ValidateCsvBasicTest.php b/tests/Commands/ValidateCsvBasicTest.php index 79ab3151..8afdbf40 100644 --- a/tests/Commands/ValidateCsvBasicTest.php +++ b/tests/Commands/ValidateCsvBasicTest.php @@ -55,8 +55,8 @@ public function testValidateOneCsvPositive(): void TXT; - isSame(0, $exitCode, $actual); isSame($expected, $actual); + isSame(0, $exitCode, $actual); } public function testValidateOneCsvNegative(): void diff --git a/tests/ExampleSchemasTest.php b/tests/ExampleSchemasTest.php index afceaec7..8440e5fe 100644 --- a/tests/ExampleSchemasTest.php +++ b/tests/ExampleSchemasTest.php @@ -87,9 +87,9 @@ public function testCsvStrutureDefaultValues(): void $defaultsInDoc = yml(Tools::SCHEMA_FULL_YML)->findArray('csv'); $schema = new Schema([]); - $schema->getCsvStructure()->getArrayCopy(); + $schema->getCsvParserConfig()->getArrayCopy(); - isSame($defaultsInDoc, $schema->getCsvStructure()->getArrayCopy()); + isSame($defaultsInDoc, $schema->getCsvParserConfig()->getArrayCopy()); } public function testCheckPhpExample(): void diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php index 7eb42ecf..cc1adad2 100644 --- a/tests/ReadmeTest.php +++ b/tests/ReadmeTest.php @@ -25,7 +25,8 @@ final class ReadmeTest extends TestCase { private const EXTRA_RULES = [ '* With `filename_pattern` rule, you can check if the file name matches the pattern.', - '* Property `name` is not defined in a column. If `csv.header: true`.', + '* Checks if property `name` is not defined in a column. Only if `csv.header: true`.', + '* If property `required` is set to `true`, the column must must be present in CSV. Only if `csv.header: true`', '* Check that each row matches the number of columns.', '* With `strict_column_order` rule, you can check that the columns are in the correct order.', '* With `allow_extra_columns` rule, you can check that there are no extra columns in the CSV file.', @@ -85,14 +86,10 @@ public function testBadgeOfRules(): void $planToAdd = \count($todoYml->findArray('columns.0.rules')) . '/' . (\count($todoYml->findArray('columns.0.aggregate_rules')) * 6) . '/' . (\count([ - 'required', 'null_values', 'multiple + separator', - 'strict_column_order', - 'other_columns_possible', 'complex_rules. one example', 'inherit', - 'rule not found', ]) + \count($todoYml->findArray('structural_rules'))); $badge = static function (string $label, int|string $count, string $url, string $color): string { diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php index 3b217040..248ec784 100644 --- a/tests/SchemaTest.php +++ b/tests/SchemaTest.php @@ -49,29 +49,23 @@ public function testScvStruture(): void { $schemaEmpty = new Schema(Tools::SCHEMA_EXAMPLE_EMPTY); isSame([ - // 'inherit' => null, 'header' => true, 'delimiter' => ',', 'quote_char' => '\\', 'enclosure' => '"', 'encoding' => 'utf-8', 'bom' => false, - // 'strict_column_order' => false, - // 'other_columns_possible' => false, - ], $schemaEmpty->getCsvStructure()->getArrayCopy()); + ], $schemaEmpty->getCsvParserConfig()->getArrayCopy()); $schemaFull = new Schema(Tools::SCHEMA_FULL_YML); isSame([ - // 'inherit' => 'alias_1', 'header' => true, 'delimiter' => ',', 'quote_char' => '\\', 'enclosure' => '"', 'encoding' => 'utf-8', 'bom' => false, - // 'strict_column_order' => true, - // 'other_columns_possible' => true, - ], $schemaFull->getCsvStructure()->getArrayCopy()); + ], $schemaFull->getCsvParserConfig()->getArrayCopy()); } public function testColumns(): void @@ -84,7 +78,7 @@ public function testColumns(): void 0 => 'Column Name (header)', 1 => 'another_column', 2 => 'third_column', - ], \array_keys($schemaFull->getColumns())); + ], $schemaFull->getSchemaHeader()); } public function testColumnByNameAndId(): void @@ -132,8 +126,7 @@ public function testGetColumnMinimal(): void isSame('Column Name (header)', $column->getName()); isSame('Lorem ipsum', $column->getDescription()); - isSame('', $column->getInherit()); - isFalse($column->isRequired()); + isTrue($column->isRequired()); isTrue(\is_array($column->getRules())); isNotEmpty($column->getRules()); @@ -150,7 +143,7 @@ public function testGetColumnProps(): void isSame('Column Name (header)', $column->getName()); isSame('Lorem ipsum', $column->getDescription()); - isFalse($column->isRequired()); + isTrue($column->isRequired()); isTrue(\is_array($column->getRules())); isNotEmpty($column->getRules()); diff --git a/tests/Validators/CsvValidatorTest.php b/tests/Validators/CsvValidatorTest.php index f0ea7411..716de91e 100644 --- a/tests/Validators/CsvValidatorTest.php +++ b/tests/Validators/CsvValidatorTest.php @@ -116,12 +116,8 @@ public function testCellRuleNoName(): void { $csv = new CsvFile(Tools::CSV_COMPLEX, Tools::getRule(null, 'not_empty', true)); isSame( - <<<'TXT' - "csv.header" at line 1, column "0:". Property "name" is not defined in schema: "_custom_array_". - "allow_extra_columns" at line 1. Column(s) not found in CSV: "0". - - TXT, - \strip_tags((string)$csv->validate()), + '"csv.header" at line 1, column "0:". Property "name" is not defined in schema: "_custom_array_".', + $csv->validate()->render(cleanOutput: true), ); } @@ -140,8 +136,6 @@ public function testQuickStop(): void public function testErrorToArray(): void { $csv = new CsvFile(Tools::CSV_COMPLEX, Tools::getRule('yn', 'is_email', true)); - // dump($csv); - isSame([ 'ruleCode' => 'is_email', 'message' => 'Value "N" is not a valid email', @@ -184,23 +178,12 @@ public function testHeaderMatchingIfHeaderEnabled(): void isSame(['Name', 'City', 'Float', 'Birthday', 'Favorite color'], $csv->getHeader()); isSame(['Name', 'City', 'Float', 'Favorite color'], $csv->getSchema()->getSchemaHeader()); - $mappedColumns = $csv->getColumnsMappedByHeader(); - isSame('not_set', $mappedColumns[3] ?? 'not_set'); - - isSame([0, 1, 2, 4], \array_keys($mappedColumns)); - - $names = []; - foreach ($mappedColumns as $columnIndex => $column) { - isSame($columnIndex, $column->getId()); - $names[] = [$column->getName(), $column->getHumanName()]; - } - isSame([ - ['Name', '0:Name'], - ['City', '1:City'], - ['Float', '2:Float'], - ['Favorite color', '4:Favorite color'], // 4 is important here - ], $names); + 0 => 'Name', + 1 => 'City', + 2 => 'Float', + 4 => 'Favorite color', // 4 important here + ], $csv->getColumnsMappedByHeaderNamesOnly()); } public function testHeaderMatchingIfHeaderDisabled(): void @@ -218,23 +201,29 @@ public function testHeaderMatchingIfHeaderDisabled(): void isSame([0, 1, 2, 3, 4], $csv->getHeader()); isSame(['Name', 'City', 'Float', 'Favorite color'], $csv->getSchema()->getSchemaHeader()); - $mappedColumns = $csv->getColumnsMappedByHeader(); - isSame('not_set', $mappedColumns[4] ?? 'not_set'); + isSame([ + 0 => 'Name', + 1 => 'City', + 2 => 'Float', + 3 => 'Favorite color', // 3 important here + ], $csv->getColumnsMappedByHeaderNamesOnly()); + } - isSame([0, 1, 2, 3], \array_keys($mappedColumns)); + public function testHeaderMatchingIfHeaderEnabledExtraColumn(): void + { + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'csv' => ['header' => true], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'Optional column'], // Extra column + ], + ]); - $names = []; - foreach ($mappedColumns as $columnIndex => $column) { - isSame($columnIndex, $column->getId()); - $names[] = [$column->getName(), $column->getHumanName()]; - } + isSame(['Name', 'City', 'Float', 'Birthday', 'Favorite color'], $csv->getHeader()); + isSame(['Name', 'Optional column'], $csv->getSchema()->getSchemaHeader()); - isSame([ - ['Name', '0:Name'], - ['City', '1:City'], - ['Float', '2:Float'], - ['Favorite color', '3:Favorite color'], // 3 is important here - ], $names); + isSame([0 => 'Name'], $csv->getColumnsMappedByHeaderNamesOnly()); } public function testStrictColumnOrderValid(): void @@ -319,8 +308,278 @@ public function testStrictColumnOrderInvalid(): void isSame( '"strict_column_order" at line 1. Real columns order doesn\'t match schema. ' . 'Expected: ["Name", "City", "Float", "Birthday", "Favorite color"]. ' . - 'Actual: ["City", "Name", "Float", "Favorite color", "Birthday"].' . "\n", + 'Actual: ["City", "Name", "Float", "Favorite color", "Birthday", "Birthday"].' . "\n", $csv->validate()->render(), ); } + + public function testAllowExtraColumnsHeaderEnabled(): void + { + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ], + ]); + isSame(null, $csv->validate()->render()); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ], + ]); + isSame( + '"required" at line 1, column "Schema Col Id: 5". Required column not found in CSV.', + $csv->validate()->render(cleanOutput: true), + ); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column', 'required' => false], + ], + ]); + isSame(null, $csv->validate()->render(cleanOutput: true)); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => false], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ], + ]); + isSame( + '"allow_extra_columns" at line 1. Column(s) not found in CSV: "Optional column".', + $csv->validate()->render(cleanOutput: true), + ); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => false], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ['name' => 'Optional column 2'], + ], + ]); + isSame( + '"allow_extra_columns" at line 1. Column(s) not found in CSV: ["Optional column", "Optional column 2"].', + $csv->validate()->render(cleanOutput: true), + ); + } + + public function testAllowExtraColumnsHeaderDisabled(): void + { + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ], + ]); + isSame(null, $csv->validate()->render()); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ], + ]); + isSame( + '"required" at line 1, column "Schema Col Id: 5". Required column not found in CSV.', + $csv->validate()->render(cleanOutput: true), + ); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column', 'required' => false], + ], + ]); + isSame(null, $csv->validate()->render(cleanOutput: true)); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'structural_rules' => ['allow_extra_columns' => false], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ], + ]); + isSame( + '"allow_extra_columns" at line 1. Schema number of columns "6" greater than real "5".', + $csv->validate()->render(cleanOutput: true), + ); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'csv' => ['header' => false], + 'structural_rules' => ['allow_extra_columns' => false], + 'columns' => [ + ['name' => 'Name'], + ['name' => 'City'], + ['name' => 'Float'], + ['name' => 'Birthday'], + ['name' => 'Favorite color'], + ['name' => 'Optional column'], + ['name' => 'Optional column 2'], + ], + ]); + isSame( + '"allow_extra_columns" at line 1. Schema number of columns "7" greater than real "5".', + $csv->validate()->render(cleanOutput: true), + ); + } + + public function testRequiredColumnValid(): void + { + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'columns' => [ + ['name' => 'Name', 'required' => true], + ['name' => 'City', 'required' => true], + ['name' => 'Float', 'required' => true], + ['name' => 'Birthday', 'required' => true], + ['name' => 'Favorite color', 'required' => true], + ], + ]); + isSame(null, $csv->validate()->render()); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'columns' => [ + ['name' => 'Name', 'required' => false], + ['name' => 'City', 'required' => false], + ['name' => 'Float', 'required' => false], + ['name' => 'Birthday', 'required' => false], + ['name' => 'Favorite color', 'required' => false], + ], + ]); + isSame(null, $csv->validate()->render()); + + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + + 'columns' => [ + ['name' => 'Name', 'required' => true], + ['name' => 'City', 'required' => true], + ['name' => 'Float', 'required' => true], + ['name' => 'Birthday', 'required' => true], + ['name' => 'Favorite color', 'required' => true], + ['name' => 'Optional column', 'required' => true], + ['name' => 'Optional column 2', 'required' => true], + ], + ]); + isSame( + <<<'TEXT' + "required" at line 1, column "Schema Col Id: 5". Required column not found in CSV. + "required" at line 1, column "Schema Col Id: 6". Required column not found in CSV. + TEXT, + $csv->validate()->render(cleanOutput: true), + ); + } + + public function testRequiredColumnInvalid(): void + { + // Extra. Required. + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + [ + 'name' => 'Optinal column', + 'required' => true, + 'rules' => ['not_empty' => true], + ], + ], + ]); + isSame([0 => 'Name'], $csv->getColumnsMappedByHeaderNamesOnly()); + isSame( + '"required" at line 1, column "Schema Col Id: 1". Required column not found in CSV.', + $csv->validate()->render(cleanOutput: true), + ); + + // Extra. Not required. + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + [ + 'name' => 'Optinal column', + 'required' => false, + 'rules' => ['not_empty' => true], + ], + ], + ]); + isSame([0 => 'Name'], $csv->getColumnsMappedByHeaderNamesOnly()); + isSame(null, $csv->validate()->render(cleanOutput: true)); + + // Real. Required. + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + [ + 'name' => 'Birthday', + 'required' => false, + 'rules' => ['not_empty' => true], + ], + ], + ]); + isSame([0 => 'Name', 3 => 'Birthday'], $csv->getColumnsMappedByHeaderNamesOnly()); + isSame(null, $csv->validate()->render(cleanOutput: true)); + + // Real. Not required. + $csv = new CsvFile(Tools::DEMO_CSV, [ + 'structural_rules' => ['allow_extra_columns' => true], + 'columns' => [ + ['name' => 'Name'], + [ + 'name' => 'Birthday', + 'required' => false, + 'rules' => ['not_empty' => true], + ], + ], + ]); + isSame([0 => 'Name', 3 => 'Birthday'], $csv->getColumnsMappedByHeaderNamesOnly()); + isSame(null, $csv->validate()->render(cleanOutput: true)); + } } diff --git a/tests/schemas/todo.yml b/tests/schemas/todo.yml index cff73e45..74c49e79 100644 --- a/tests/schemas/todo.yml +++ b/tests/schemas/todo.yml @@ -33,8 +33,7 @@ structural_rules: columns: - - required: true # If true, then column must be present in the file - null_values: # (Override csv\empty_values) List of values that will be treated as empty + - null_values: # (Override csv\empty_values) List of values that will be treated as empty - "" - null - none