From e674f507bcd31c5d75256f6ce46234ecca75f5f4 Mon Sep 17 00:00:00 2001 From: Pavel Agaletskiy Date: Sun, 21 Jul 2019 22:49:18 +0300 Subject: [PATCH] Initial version --- .gitignore | 4 + .php_cs | 57 +++++ .scrutinizer.yml | 16 ++ .travis.yml | 29 +++ LICENSE | 21 ++ README.md | 62 +++++ composer.json | 33 +++ phpunit.xml.dist | 27 +++ src/Barcode.php | 86 +++++++ src/Constants.php | 16 ++ src/Exception/GS1ParserExceptionInterface.php | 12 + src/Exception/InvalidBarcodeException.php | 40 ++++ src/Parser/Parser.php | 171 ++++++++++++++ src/Parser/ParserConfig.php | 78 +++++++ src/Parser/ParserInterface.php | 12 + src/Validator/ErrorCodes.php | 14 ++ src/Validator/Resolution.php | 44 ++++ src/Validator/Validator.php | 85 +++++++ src/Validator/ValidatorConfig.php | 45 ++++ src/Validator/ValidatorInterface.php | 10 + tests/BarcodeAssert.php | 26 +++ tests/BarcodeTest.php | 39 ++++ tests/Parser/ParserConfigTest.php | 44 ++++ tests/Parser/ParserTest.php | 215 ++++++++++++++++++ tests/Validator/ResolutionTest.php | 35 +++ tests/Validator/ValidatorConfigTest.php | 35 +++ tests/Validator/ValidatorTest.php | 139 +++++++++++ 27 files changed, 1395 insertions(+) create mode 100644 .gitignore create mode 100644 .php_cs create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/Barcode.php create mode 100644 src/Constants.php create mode 100644 src/Exception/GS1ParserExceptionInterface.php create mode 100644 src/Exception/InvalidBarcodeException.php create mode 100644 src/Parser/Parser.php create mode 100644 src/Parser/ParserConfig.php create mode 100644 src/Parser/ParserInterface.php create mode 100644 src/Validator/ErrorCodes.php create mode 100644 src/Validator/Resolution.php create mode 100644 src/Validator/Validator.php create mode 100644 src/Validator/ValidatorConfig.php create mode 100644 src/Validator/ValidatorInterface.php create mode 100644 tests/BarcodeAssert.php create mode 100644 tests/BarcodeTest.php create mode 100644 tests/Parser/ParserConfigTest.php create mode 100644 tests/Parser/ParserTest.php create mode 100644 tests/Validator/ResolutionTest.php create mode 100644 tests/Validator/ValidatorConfigTest.php create mode 100644 tests/Validator/ValidatorTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0aa56b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/.build/ +composer.lock +.phpunit.result.cache \ No newline at end of file diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..2bd9207 --- /dev/null +++ b/.php_cs @@ -0,0 +1,57 @@ +setUsingCache(true) + ->setFinder( + Finder::create() + ->exclude([ + 'vendor', + ]) + ->in(__DIR__) + ) + ->setRules([ + '@PSR2' => true, + '@Symfony' => true, + 'class_definition' => [ + 'multiLineExtendsEachSingleLine' => true, + ], + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => [ + 'align_double_arrow' => false, + 'align_equals' => false, + ], + 'concat_space' => [ + 'spacing' => 'one', + ], + 'general_phpdoc_annotation_remove' => [ + 'author', + 'package', + ], + 'no_multiline_whitespace_before_semicolons' => true, + 'no_null_property_initialization' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct' + ] + ], + 'ordered_imports' => [ + 'sortAlgorithm' => 'alpha' + ], + 'phpdoc_order' => true, + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last' + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_annotation_without_dot' => true, + 'yoda_style' => false + ]); \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..823cdca --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,16 @@ +checks: + php: + code_rating: true + duplication: true + +build: + tests: + override: + - command: vendor/bin/phpunit --coverage-clover=.build/clover.xml + coverage: + file: .build/clover.xml + format: clover + +filter: + excluded_paths: + - tests/* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ff08221 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: php + +php: + - 7.1 + - 7.2 + - 7.3 + - 7.4snapshot + # php8 is not supported yet + #- nightly + +env: + matrix: + - DEPENDENCIES="high" + - DEPENDENCIES="low" + global: + - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-ansi --no-progress --no-suggest" + +matrix: + fast_finish: true + allow_failures: + - php: 7.4snapshot + +install: + - if [[ "$DEPENDENCIES" = 'high' ]]; then travis_retry composer update $DEFAULT_COMPOSER_FLAGS; fi + - if [[ "$DEPENDENCIES" = 'low' ]]; then travis_retry composer update $DEFAULT_COMPOSER_FLAGS --prefer-lowest; fi + +cache: + directories: + - $HOME/.composer/cache \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..33ed1f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Lamoda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a66de96 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +Lamoda GS1 Barcode parser and validator +======================================= + +[![Build Status](https://travis-ci.org/lamoda/gs1-barcode-parser.svg?branch=master)](https://travis-ci.org/lamoda/gs1-barcode-parser) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/?branch=master) +[![Code Coverage](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/?branch=master) +[![Build Status](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/badges/build.png?b=master)](https://scrutinizer-ci.com/g/lamoda/gs1-barcode-parser/build-status/master) + + +## Installation + +### Composer + +```sh +composer require lamoda/gs1-barcode-parser +``` + +## Description + +This library provides parsing of GS1 Barcodes according to +[GS1 General specification](https://www.gs1.org/sites/default/files/docs/barcodes/GS1_General_Specifications.pdf) +and [GS1 DataMatrix Guideline](https://www.gs1.org/docs/barcodes/GS1_DataMatrix_Guideline.pdf). + +Library also provides general purpose validator for barcode's content. + +## Usage + +### Parser +```php +parse($value); + +// $barcode is an object of Barcode class +``` + +### Validator +```php +validate($value); + +if ($resolution->isValid()) { + // ... +} else { + var_dump($resolution->getErrors()); +} + +``` \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5bfe9c0 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "lamoda/gs1-barcode-parser", + "description": "GS1 Barcode parser and validator compatible with official GS1 documentation", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Lamoda developers", + "homepage": "https://tech.lamoda.ru/" + } + ], + "minimum-stability": "stable", + "require": { + "php": "^7.1" + }, + "autoload": { + "psr-4": { + "Lamoda\\GS1Parser\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Lamoda\\GS1Parser\\Tests\\": "tests/" + } + }, + "config": { + "sort-packages": true + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "phpunit/phpunit": "^7.5" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f56dc60 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + + + + + tests + + + + + + src + + src/Exception + + + + \ No newline at end of file diff --git a/src/Barcode.php b/src/Barcode.php new file mode 100644 index 0000000..c3288a3 --- /dev/null +++ b/src/Barcode.php @@ -0,0 +1,86 @@ +content = $content; + $this->type = $type; + $this->ais = $ais; + $this->buffer = $buffer; + $this->fnc1Prefix = $fnc1Prefix; + } + + + public function type(): string + { + return $this->type; + } + + public function ais(): array + { + return $this->ais; + } + + public function buffer(): array + { + return $this->buffer; + } + + public function hasAI(string $code): bool + { + return array_key_exists($code, $this->ais); + } + + public function ai(string $code): string + { + return $this->ais[$code] ?? ''; + } + + public function raw(): string + { + return $this->content; + } + + public function fnc1Prefix(): string + { + return $this->fnc1Prefix; + } + + public function normalized(): string + { + $prefixLength = strlen($this->fnc1Prefix); + return substr($this->content, $prefixLength); + } +} \ No newline at end of file diff --git a/src/Constants.php b/src/Constants.php new file mode 100644 index 0000000..0925ec1 --- /dev/null +++ b/src/Constants.php @@ -0,0 +1,16 @@ + 20, + '01' => 16, + '02' => 16, + '03' => 16, + '04' => 18, + '11' => 8, + '12' => 8, + '13' => 8, + '14' => 8, + '15' => 8, + '16' => 8, + '17' => 8, + '18' => 8, + '19' => 8, + '20' => 4, + '31' => 10, + '32' => 10, + '33' => 10, + '34' => 10, + '35' => 10, + '36' => 10, + '41' => 16, + ]; + private const FIXED_AI_LENGTH = 2; + + /** + * @var ParserConfig + */ + private $config; + + public function __construct(ParserConfig $config) + { + $this->config = $config; + } + + public function parse(string $data): Barcode + { + $data = trim($data); + + if ($data === '') { + throw InvalidBarcodeException::becauseBarcodeIsEmpty(); + } + + [$fnc1Prefix, $codeType] = $this->fetchFNC1Prefix($data); + + if ($fnc1Prefix === null && $this->config->isFnc1SequenceRequired()) { + throw InvalidBarcodeException::becauseFNC1SequenceIsNotFound(); + } + + $codeOffset = strlen((string)$fnc1Prefix); + $dataLength = strlen($data); + + if ($dataLength <= $codeOffset) { + throw InvalidBarcodeException::becauseNoDataPresent(); + } + + $position = $codeOffset; + $foundAIs = []; + $buffer = []; + while ($position < $dataLength) { + [$ai, $length] = $this->fetchFixedAI($data, $position, $dataLength); + $value = null; + + if ($ai !== null) { + if ($position + $length > $dataLength) { + throw InvalidBarcodeException::becauseNotEnoughDataFoAI( + $ai, + $length, + $dataLength - $position + ); + } + + $isKnownAI = in_array($ai, $this->config->getKnownAIs(), true); + + if ($isKnownAI) { + $value = substr($data, $position + self::FIXED_AI_LENGTH, $length - self::FIXED_AI_LENGTH); + } else { + $ai = null; + $value = substr($data, $position, $length); + } + + if (strpos($value, $this->config->getGroupSeparator()) !== false) { + throw InvalidBarcodeException::becauseGroupSeparatorWasNotExpected($value); + } + + $position += $length; + } else { + [$ai, $aiLength] = $this->fetchKnownAI($data, $position); + + $groupSeparatorPosition = strpos($data, $this->config->getGroupSeparator(), $position); + if ($groupSeparatorPosition !== false) { + $length = $groupSeparatorPosition - $position; + } else { + $length = $dataLength - $position; + } + + if ($ai) { + $value = substr($data, $position + $aiLength, $length - $aiLength); + } else { + $value = substr($data, $position, $length); + } + + $position += $length + strlen($this->config->getGroupSeparator()); + } + + if ($ai) { + $foundAIs[$ai] = $value; + } else { + $buffer[] = $value; + } + } + + return new Barcode($data, $codeType, $foundAIs, $buffer, (string)$fnc1Prefix); + } + + private function fetchFNC1Prefix(string $data): array + { + foreach ($this->config->getFnc1PrefixMap() as $prefix => $codeType) { + if (substr_compare($data, $prefix, 0, strlen($prefix), true) === 0) { + return [$prefix, $codeType]; + } + } + + return [null, Barcode::TYPE_UNKNOWN]; + } + + private function fetchFixedAI(string $data, int $position, int $dataLength): array + { + if ($dataLength - $position < self::FIXED_AI_LENGTH) { + return [null, null]; + } + + $ai = substr($data, $position, self::FIXED_AI_LENGTH); + + $length = self::FIXED_LENGTH_AIS[$ai] ?? null; + + if ($length === null) { + return [null, null]; + } + + return [$ai, $length]; + } + + private function fetchKnownAI(string $data, int $position): array + { + foreach ($this->config->getKnownAIs() as $ai) { + $aiLength = strlen($ai); + if (substr_compare($data, $ai, $position, $aiLength, true) === 0) { + return [substr($data, $position, $aiLength), $aiLength]; + } + } + + return [null, null]; + } +} \ No newline at end of file diff --git a/src/Parser/ParserConfig.php b/src/Parser/ParserConfig.php new file mode 100644 index 0000000..0cbe9e4 --- /dev/null +++ b/src/Parser/ParserConfig.php @@ -0,0 +1,78 @@ + Barcode::TYPE_GS1_DATAMATRIX, + Constants::FNC1_GS1_128_SEQUENCE => Barcode::TYPE_GS1_128, + Constants::FNC1_GS1_QRCODE_SEQUENCE => Barcode::TYPE_GS1_QRCODE, + Constants::FNC1_GS1_EAN_SEQUENCE => Barcode::TYPE_EAN, + ]; + + /** + * @var string + */ + private $groupSeparator = Constants::GROUP_SEPARATOR_SYMBOL; + /** + * @var array of strings + */ + private $knownAIs = []; + + public function isFnc1SequenceRequired(): bool + { + return $this->fnc1SequenceRequired; + } + + public function setFnc1SequenceRequired(bool $fnc1SequenceRequired): self + { + $this->fnc1SequenceRequired = $fnc1SequenceRequired; + return $this; + } + + public function getFnc1PrefixMap(): array + { + return $this->fnc1PrefixMap; + } + + public function setFnc1PrefixMap(array $fnc1PrefixMap): self + { + $this->fnc1PrefixMap = $fnc1PrefixMap; + return $this; + } + + public function getGroupSeparator(): string + { + return $this->groupSeparator; + } + + public function setGroupSeparator(string $groupSeparator): self + { + $this->groupSeparator = $groupSeparator; + return $this; + } + + public function getKnownAIs(): array + { + return $this->knownAIs; + } + + public function setKnownAIs(array $knownAIs): self + { + $this->knownAIs = $knownAIs; + return $this; + } +} \ No newline at end of file diff --git a/src/Parser/ParserInterface.php b/src/Parser/ParserInterface.php new file mode 100644 index 0000000..e8e1ef4 --- /dev/null +++ b/src/Parser/ParserInterface.php @@ -0,0 +1,12 @@ +isValid = $isValid; + $this->errors = $errors; + } + + public static function createValid(): self + { + return new static(true, []); + } + + public static function createInvalid(array $errors): self + { + return new static(false, $errors); + } + + public function isValid(): bool + { + return $this->isValid; + } + + public function getErrors(): array + { + return $this->errors; + } + +} \ No newline at end of file diff --git a/src/Validator/Validator.php b/src/Validator/Validator.php new file mode 100644 index 0000000..13b84e2 --- /dev/null +++ b/src/Validator/Validator.php @@ -0,0 +1,85 @@ +parser = $parser; + $this->config = $config; + } + + public function validate($value): Resolution + { + if ($value === null && $this->config->isAllowEmpty()) { + return Resolution::createValid(); + } + + if (!is_string($value)) { + return Resolution::createInvalid([ + ErrorCodes::VALUE_IS_NOT_STRING => 'Value is not a string', + ]); + } + + $trimmedValue = trim($value); + + if ($trimmedValue === '') { + return $this->config->isAllowEmpty() ? + Resolution::createValid() : + Resolution::createInvalid([ + ErrorCodes::VALUE_EMPTY => 'Value is empty', + ]); + } + + try { + $barcode = $this->parser->parse($trimmedValue); + } catch (InvalidBarcodeException $exception) { + return Resolution::createInvalid([ + ErrorCodes::INVALID_VALUE => sprintf( + 'Value is invalid: %s', + $exception->getCode() + ), + ]); + } + + $ais = array_keys($barcode->ais()); + $requiredAis = array_diff($this->config->getRequiredAIs(), $ais); + $forbiddenAIs = array_intersect($this->config->getForbiddenAIs(), $ais); + + if (count($requiredAis) > 0) { + return Resolution::createInvalid([ + ErrorCodes::MISSING_AIS => sprintf( + 'AIs are missing: "%s"', + implode('", "', $requiredAis) + ), + ]); + } + + if (count($forbiddenAIs) > 0) { + return Resolution::createInvalid([ + ErrorCodes::FORBIDDEN_AIS => sprintf( + 'AIs are forbidden: "%s"', + implode('", "', $forbiddenAIs) + ), + ]); + } + + return Resolution::createValid(); + } + +} \ No newline at end of file diff --git a/src/Validator/ValidatorConfig.php b/src/Validator/ValidatorConfig.php new file mode 100644 index 0000000..2a17d10 --- /dev/null +++ b/src/Validator/ValidatorConfig.php @@ -0,0 +1,45 @@ +requiredAIs; + } + + public function setRequiredAIs(array $requiredAIs): self + { + $this->requiredAIs = $requiredAIs; + return $this; + } + + public function getForbiddenAIs(): array + { + return $this->forbiddenAIs; + } + + public function setForbiddenAIs(array $forbiddenAIs): self + { + $this->forbiddenAIs = $forbiddenAIs; + return $this; + } + + public function isAllowEmpty(): bool + { + return $this->allowEmpty; + } + + public function setAllowEmpty(bool $allowEmpty): self + { + $this->allowEmpty = $allowEmpty; + return $this; + } +} \ No newline at end of file diff --git a/src/Validator/ValidatorInterface.php b/src/Validator/ValidatorInterface.php new file mode 100644 index 0000000..5eb602c --- /dev/null +++ b/src/Validator/ValidatorInterface.php @@ -0,0 +1,10 @@ +raw(), $actual->raw(), 'Wrong RAW format'); + Assert::assertEquals($expected->type(), $actual->type(), 'Wrong type'); + Assert::assertEquals($expected->ais(), $actual->ais(), 'Wrong AIs list'); + Assert::assertEquals($expected->buffer(), $actual->buffer(), 'Wrong buffer'); + } +} \ No newline at end of file diff --git a/tests/BarcodeTest.php b/tests/BarcodeTest.php new file mode 100644 index 0000000..1788d34 --- /dev/null +++ b/tests/BarcodeTest.php @@ -0,0 +1,39 @@ + '03453120000011', + ], [ + '17191125', + '10ABCD1234', + ], ']d2'); + + $this->assertEquals(Barcode::TYPE_GS1_DATAMATRIX, $barcode->type()); + $this->assertEquals(']d201034531200000111719112510ABCD1234', $barcode->raw()); + $this->assertEquals('01034531200000111719112510ABCD1234', $barcode->normalized()); + $this->assertEquals(']d2', $barcode->fnc1Prefix()); + $this->assertEquals([ + '01' => '03453120000011', + ], $barcode->ais()); + $this->assertEquals([ + '17191125', + '10ABCD1234', + ], $barcode->buffer()); + $this->assertTrue($barcode->hasAI('01')); + $this->assertFalse($barcode->hasAI('02')); + $this->assertEquals('03453120000011', $barcode->ai('01')); + } +} diff --git a/tests/Parser/ParserConfigTest.php b/tests/Parser/ParserConfigTest.php new file mode 100644 index 0000000..a190168 --- /dev/null +++ b/tests/Parser/ParserConfigTest.php @@ -0,0 +1,44 @@ +assertNotEmpty($config->getGroupSeparator()); + $this->assertNotEmpty($config->getFnc1PrefixMap()); + $this->assertEmpty($config->getKnownAIs()); + $this->assertTrue($config->isFnc1SequenceRequired()); + } + + public function testGettersSetters(): void + { + $config = (new ParserConfig()) + ->setKnownAIs(['10']) + ->setFnc1SequenceRequired(false) + ->setFnc1PrefixMap([ + Constants::FNC1_GS1_EAN_SEQUENCE => Barcode::TYPE_EAN, + ]) + ->setGroupSeparator(Constants::GROUP_SEPARATOR_SYMBOL); + + $this->assertEquals(['10'], $config->getKnownAIs()); + $this->assertEquals([ + Constants::FNC1_GS1_EAN_SEQUENCE => Barcode::TYPE_EAN, + ], $config->getFnc1PrefixMap()); + $this->assertFalse($config->isFnc1SequenceRequired()); + $this->assertEquals(Constants::GROUP_SEPARATOR_SYMBOL, $config->getGroupSeparator()); + } +} diff --git a/tests/Parser/ParserTest.php b/tests/Parser/ParserTest.php new file mode 100644 index 0000000..bfa714f --- /dev/null +++ b/tests/Parser/ParserTest.php @@ -0,0 +1,215 @@ +parse($data); + + self::assertBarcodesAreEqual($expected, $result); + } + + public function dataParsingValidCode(): array + { + $defaultConfig = new ParserConfig(); + + $defaultConfigWithKnownAIs = (new ParserConfig()) + ->setKnownAIs(['01', '17', '10']); + + $configWithNotRequiredFNC1 = (new ParserConfig()) + ->setFnc1SequenceRequired(false); + + $configForMarkingCode = (new ParserConfig()) + ->setFnc1SequenceRequired(false) + ->setKnownAIs([ + '01', + '21', + '240', + '91', + '92' + ]); + + $anotherSeparator = (new ParserConfig()) + ->setGroupSeparator('|'); + + $multiCharSeparator = (new ParserConfig()) + ->setGroupSeparator(''); + + return [ + 'base' => [ + $defaultConfig, + ']d201034531200000111719112510ABCD1234', + new Barcode(']d201034531200000111719112510ABCD1234', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '0103453120000011', + '17191125', + '10ABCD1234', + ], ']d2') + ], + 'base - with known ais' => [ + $defaultConfigWithKnownAIs, + ']d201034531200000111719112510ABCD1234', + new Barcode(']d201034531200000111719112510ABCD1234', Barcode::TYPE_GS1_DATAMATRIX, [ + '01' => '03453120000011', + '17' => '191125', + '10' => 'ABCD1234', + ], [], ']d2') + ], + 'base - very short' => [ + $defaultConfig, + ']d21', + new Barcode(']d21', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '1', + ], ']d2') + ], + 'base - not req FNC1' => [ + $configWithNotRequiredFNC1, + ']d201034531200000111719112510ABCD1234', + new Barcode(']d201034531200000111719112510ABCD1234', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '0103453120000011', + '17191125', + '10ABCD1234' + ], ']d2') + ], + 'base - not req FNC1, FNC1 not present' => [ + $configWithNotRequiredFNC1, + '01034531200000111719112510ABCD1234', + new Barcode('01034531200000111719112510ABCD1234', Barcode::TYPE_UNKNOWN, [], [ + '0103453120000011', + '17191125', + '10ABCD1234' + ], '') + ], + 'switched position #1' => [ + $defaultConfig, + ']d217191125010345312000001110ABCD1234', + new Barcode(']d217191125010345312000001110ABCD1234', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '17191125', + '0103453120000011', + '10ABCD1234' + ], ']d2') + ], + 'switched position #2' => [ + $defaultConfig, + "]d210ABCD1234\u{001d}171911250103453120000011", + new Barcode("]d210ABCD1234\u{001d}171911250103453120000011", Barcode::TYPE_GS1_DATAMATRIX, [], [ + '10ABCD1234', + '17191125', + '0103453120000011', + ], ']d2') + ], + 'another separator' => [ + $anotherSeparator, + ']d2010345312000001110ABCD1234|17191125', + new Barcode(']d2010345312000001110ABCD1234|17191125', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '0103453120000011', + '10ABCD1234', + '17191125', + ], ']d2') + ], + 'multi char separator' => [ + $multiCharSeparator, + ']d2010345312000001110ABCD123417191125', + new Barcode(']d2010345312000001110ABCD123417191125', Barcode::TYPE_GS1_DATAMATRIX, [], [ + '0103453120000011', + '10ABCD1234', + '17191125', + ], ']d2') + ], + 'russian product marking code' => [ + $configWithNotRequiredFNC1, + "010467003301005321gJk6o54AQBJfX\u{001d}2406401\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", + new Barcode("010467003301005321gJk6o54AQBJfX\u{001d}2406401\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", Barcode::TYPE_UNKNOWN, [], [ + '0104670033010053', + '21gJk6o54AQBJfX', + '2406401', + '91ffd0', + '92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==' + ], '') + ], + 'russian product marking code - known ais' => [ + $configForMarkingCode, + "010467003301005321gJk6o54AQBJfX\u{001d}2406401\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", + new Barcode("010467003301005321gJk6o54AQBJfX\u{001d}2406401\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", Barcode::TYPE_UNKNOWN, [ + '01' => '04670033010053', + '21' => 'gJk6o54AQBJfX', + '240' => '6401', + '91' => 'ffd0', + '92' => 'LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==' + ], [], '') + ], + 'russian product marking code - known ais, no tnved' => [ + $configForMarkingCode, + "010467003301005321gJk6o54AQBJfX\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", + new Barcode("010467003301005321gJk6o54AQBJfX\u{001d}91ffd0\u{001d}92LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==", Barcode::TYPE_UNKNOWN, [ + '01' => '04670033010053', + '21' => 'gJk6o54AQBJfX', + '91' => 'ffd0', + '92' => 'LGYcm3FRQrRdNOO+8t0pz78QTyxxBmYKhLXaAS03jKV7oy+DWGy1SeU+BZ8o7B8+hs9LvPdNA7B6NPGjrCm34A==' + ], [], '') + ], + ]; + } + + /** + * @dataProvider dataParsingInvalidBarcode + */ + public function testParsingInvalidBarcode(ParserConfig $config, string $data): void + { + $parser = new Parser($config); + + $this->expectException(InvalidBarcodeException::class); + $parser->parse($data); + } + + /** + * @return array + */ + public function dataParsingInvalidBarcode(): array + { + $default = new ParserConfig(); + + return [ + 'empty' => [ + $default, + '' + ], + 'no fnc1' => [ + $default, + '01034531200000111719112510ABCD1234' + ], + 'no data after fnc1' => [ + $default, + ']d2' + ], + 'invalid data for fixed length ai' => [ + $default, + ']d2010345' + ], + 'group separator inside fixed length data' => [ + $default, + "]d20103453\u{001d}200000111719112510ABCD1234" + ] + ]; + } +} diff --git a/tests/Validator/ResolutionTest.php b/tests/Validator/ResolutionTest.php new file mode 100644 index 0000000..b446afd --- /dev/null +++ b/tests/Validator/ResolutionTest.php @@ -0,0 +1,35 @@ +isValid()); + self::assertEmpty($resolution->getErrors()); + } + + public function testInvalid(): void + { + $resolution = Resolution::createInvalid([ + ErrorCodes::VALUE_IS_NOT_STRING => 'test' + ]); + + self::assertFalse($resolution->isValid()); + self::assertEquals([ + ErrorCodes::VALUE_IS_NOT_STRING => 'test' + ], $resolution->getErrors()); + } +} diff --git a/tests/Validator/ValidatorConfigTest.php b/tests/Validator/ValidatorConfigTest.php new file mode 100644 index 0000000..97d9b65 --- /dev/null +++ b/tests/Validator/ValidatorConfigTest.php @@ -0,0 +1,35 @@ +getRequiredAIs()); + self::assertEmpty($config->getForbiddenAIs()); + self::assertFalse($config->isAllowEmpty()); + } + + public function testGettersSetters(): void + { + $config = (new ValidatorConfig()) + ->setAllowEmpty(true) + ->setRequiredAIs(['10']) + ->setForbiddenAIs(['01']); + + self::assertTrue($config->isAllowEmpty()); + self::assertEquals(['10'], $config->getRequiredAIs()); + self::assertEquals(['01'], $config->getForbiddenAIs()); + } +} diff --git a/tests/Validator/ValidatorTest.php b/tests/Validator/ValidatorTest.php new file mode 100644 index 0000000..ed71927 --- /dev/null +++ b/tests/Validator/ValidatorTest.php @@ -0,0 +1,139 @@ +parser = $this->createMock(ParserInterface::class); + } + + /** + * @dataProvider dataValidate + */ + public function testValidate(ValidatorConfig $config, $value, Resolution $expected): void + { + $barcode = new Barcode(']d201034531200000111719112510ABCD1234', Barcode::TYPE_GS1_DATAMATRIX, [ + '01' => '03453120000011', + ], [ + '17191125', + '10ABCD1234', + ], ']d2'); + + $this->parser->method('parse') + ->with($value) + ->willReturn($barcode); + + $validator = new Validator($this->parser, $config); + + $result = $validator->validate($value); + + $this->assertEquals($expected->isValid(), $result->isValid()); + $this->assertEquals(array_keys($expected->getErrors()), array_keys($result->getErrors())); + } + + public function dataValidate(): array + { + $default = new ValidatorConfig(); + $allowEmpty = (new ValidatorConfig()) + ->setAllowEmpty(true); + $requiredAI = (new ValidatorConfig()) + ->setRequiredAIs(['10']); + $forbiddenAI = (new ValidatorConfig()) + ->setForbiddenAIs(['01']); + + return [ + 'valid' => [ + $default, + ']d201034531200000111719112510ABCD1234', + Resolution::createValid(), + ], + 'valid - allow empty' => [ + $allowEmpty, + '', + Resolution::createValid(), + ], + 'valid - allow empty - null' => [ + $allowEmpty, + null, + Resolution::createValid(), + ], + 'not a string - null' => [ + $default, + null, + Resolution::createInvalid([ + ErrorCodes::VALUE_IS_NOT_STRING => '', + ]), + ], + 'not a string - number' => [ + $default, + 10, + Resolution::createInvalid([ + ErrorCodes::VALUE_IS_NOT_STRING => '', + ]), + ], + 'empty' => [ + $default, + '', + Resolution::createInvalid([ + ErrorCodes::VALUE_EMPTY => '', + ]), + ], + 'missing required ai' => [ + $requiredAI, + ']d201034531200000111719112512ABCD1234', + Resolution::createInvalid([ + ErrorCodes::MISSING_AIS => '', + ]), + ], + 'forbidden ai' => [ + $forbiddenAI, + ']d201034531200000111719112511ABCD1234', + Resolution::createInvalid([ + ErrorCodes::FORBIDDEN_AIS => '', + ]), + ], + ]; + } + + /** + * @dataProvider dataValidate + */ + public function testValidateOnParsingError(): void + { + $config = new ValidatorConfig(); + $value = ']d201034531200000111719112510ABCD1234'; + + $this->parser->method('parse') + ->with($value) + ->willThrowException(new InvalidBarcodeException('test')); + + $validator = new Validator($this->parser, $config); + + $result = $validator->validate($value); + + $this->assertFalse($result->isValid()); + $this->assertEquals([ErrorCodes::INVALID_VALUE], array_keys($result->getErrors())); + } +}