diff --git a/CHANGELOG.md b/CHANGELOG.md index cd2693d..571ba24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased v2.x] +### Added + +- Support from JSON document (require RediSearch 2.2) + +### Fixed + +- (dev) Code coverage with XDebug 3 + ## [2.0.2] ### Added diff --git a/Makefile b/Makefile index e9185a6..bd42908 100644 --- a/Makefile +++ b/Makefile @@ -23,18 +23,18 @@ test-with-integration: | vendor coverage: | vendor @if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi - $(COMPOSER) exec -v phpunit -- --coverage-text --color + XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --coverage-text --color coverage-with-integration: | vendor @if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi - $(COMPOSER) exec -v phpunit -- --group default,integration --coverage-text --color + XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --group default,integration --coverage-text --color integration-test: | vendor $(COMPOSER) exec -v phpunit -- --group integration integration-coverage: | vendor @if [ -z "`php -v | grep -i 'xdebug'`" ]; then echo "You need to install Xdebug in order to do this action"; exit 1; fi - $(COMPOSER) exec -v phpunit -- --group integration --coverage-text --color + XDEBUG_MODE=coverage $(COMPOSER) exec -v phpunit -- --group integration --coverage-text --color validation: fix-code analyze test-with-integration coverage-with-integration diff --git a/src/IndexBuilder.php b/src/IndexBuilder.php index 6a1dfe5..31215a0 100644 --- a/src/IndexBuilder.php +++ b/src/IndexBuilder.php @@ -34,6 +34,7 @@ use MacFJA\RediSearch\Redis\Command\AddFieldOptionTrait; use MacFJA\RediSearch\Redis\Command\Create; use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption; use RuntimeException; use function strlen; @@ -84,6 +85,10 @@ * @method IndexBuilder withAddedNumericField(string $name, bool $sortable = false, bool $noIndex = false) * @method IndexBuilder withAddedGeoField(string $name, bool $noIndex = false) * @method IndexBuilder withAddedTagField(string $name, ?string $separator = null, bool $sortable = false, bool $noIndex = false) + * @method IndexBuilder withAddedJSONTextField(string $path, string $attribute, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false) + * @method IndexBuilder withAddedJSONNumericField(string $path, string $attribute, bool $sortable = false, bool $noIndex = false) + * @method IndexBuilder withAddedJSONGeoField(string $path, string $attribute, bool $noIndex = false) + * @method IndexBuilder withAddedJSONTagField(string $path, string $attribute, ?string $separator = null, bool $sortable = false, bool $noIndex = false) * * @SuppressWarnings(PHPMD.TooManyFields) */ @@ -185,6 +190,13 @@ public function addField(CreateCommandFieldOption $option): self return $this; } + public function addJSONField(string $path, CreateCommandFieldOption $option): self + { + $this->fields[$option->getFieldName()] = new JSONFieldOption($path, $option); + + return $this; + } + /** * @return mixed|string */ diff --git a/src/Redis/Command/AddFieldOptionTrait.php b/src/Redis/Command/AddFieldOptionTrait.php index 389d6ec..451c1e6 100644 --- a/src/Redis/Command/AddFieldOptionTrait.php +++ b/src/Redis/Command/AddFieldOptionTrait.php @@ -25,6 +25,7 @@ use function get_class; use MacFJA\RediSearch\Redis\Command\CreateCommand\CreateCommandFieldOption; use MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption; use MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption; use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption; use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption; @@ -75,6 +76,54 @@ public function addTagField(string $name, ?string $separator = null, bool $sorta ); } + public function addJSONTextField(string $path, string $attribute, bool $noStem = false, ?float $weight = null, ?string $phonetic = null, bool $sortable = false, bool $noIndex = false): self + { + return $this->addJSONField( + $path, + (new TextFieldOption()) + ->setField($attribute) + ->setNoStem($noStem) + ->setWeight($weight) + ->setPhonetic($phonetic) + ->setSortable($sortable) + ->setNoIndex($noIndex) + ); + } + + public function addJSONNumericField(string $path, string $attribute, bool $sortable = false, bool $noIndex = false): self + { + return $this->addJSONField( + $path, + (new NumericFieldOption()) + ->setField($attribute) + ->setSortable($sortable) + ->setNoIndex($noIndex) + ); + } + + public function addJSONGeoField(string $path, string $attribute, bool $noIndex = false): self + { + return $this->addJSONField( + $path, + (new GeoFieldOption()) + ->setField($attribute) + ->setNoIndex($noIndex) + ); + } + + public function addJSONTagField(string $path, string $attribute, ?string $separator = null, bool $sortable = false, bool $noIndex = false, bool $caseSensitive = false): self + { + return $this->addJSONField( + $path, + (new TagFieldOption()) + ->setField($attribute) + ->setSeparator($separator) + ->setCaseSensitive($caseSensitive) + ->setSortable($sortable) + ->setNoIndex($noIndex) + ); + } + public function addField(CreateCommandFieldOption $option): self { if (!($this instanceof AbstractCommand)) { @@ -85,4 +134,15 @@ public function addField(CreateCommandFieldOption $option): self return $this; } + + public function addJSONField(string $path, CreateCommandFieldOption $option): self + { + if (!($this instanceof AbstractCommand)) { + throw new BadMethodCallException('This method is not callable in '.get_class($this)); + } + + $this->options['fields'][$option->getFieldName()] = new JSONFieldOption($path, $option); + + return $this; + } } diff --git a/src/Redis/Command/Create.php b/src/Redis/Command/Create.php index 5687404..6e840c9 100644 --- a/src/Redis/Command/Create.php +++ b/src/Redis/Command/Create.php @@ -37,7 +37,10 @@ public function __construct(string $rediSearchVersion = self::MIN_IMPLEMENTED_VE { parent::__construct([ 'index' => new NamelessOption(null, '>=2.0.0'), - 'structure' => CV::allowedValues(new NamedOption('ON', null, '>=2.0.0'), ['HASH']), + 'structure' => [ + CV::allowedValues(new NamedOption('ON', null, '>=2.0.0 <2.2.0'), ['HASH']), + CV::allowedValues(new NamedOption('ON', null, '>=2.2.0'), ['HASH', 'JSON']), + ], 'prefixes' => new NotEmptyOption(new NumberedOption('PREFIX', null, '>=2.0.0')), 'filter' => new NamedOption('FILTER', null, '>=2.0.0'), 'default_lang' => $this->getLanguageOptions(), @@ -67,7 +70,9 @@ public function setIndex(string $index): self public function setStructure(string $structureType = 'HASH'): self { - $this->options['structure']->setValue($structureType); + array_walk($this->options['structure'], static function (CV $option) use ($structureType): void { + $option->setValue($structureType); + }); return $this; } diff --git a/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php b/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php new file mode 100644 index 0000000..a41eb67 --- /dev/null +++ b/src/Redis/Command/CreateCommand/CreateCommandJSONFieldOption.php @@ -0,0 +1,27 @@ +=2.2.0'); + $this->path = $path; + $this->decorated = $decorated; + } + + public function isValid(): bool + { + return !empty($this->path) && $this->decorated->isValid(); + } + + public function getOptionData() + { + return array_merge(['path' => $this->path], $this->decorated->getOptionData()); + } + + public function getVersionConstraint(): string + { + return '>=2.2.0'; + } + + public function getFieldName(): string + { + return $this->decorated->getFieldName(); + } + + public function getJSONPath(): string + { + return $this->path; + } + + protected function doRender(?string $version): array + { + return array_merge([$this->path, 'AS'], $this->decorated->render($version)); + } +} diff --git a/tests/IndexBuilderTest.php b/tests/IndexBuilderTest.php index 6587e5c..96a98bb 100644 --- a/tests/IndexBuilderTest.php +++ b/tests/IndexBuilderTest.php @@ -48,6 +48,7 @@ * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption + * @uses \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption * @uses \MacFJA\RediSearch\Redis\Command\Option\DecoratedOptionAwareTrait * @uses \MacFJA\RediSearch\Redis\Command\Option\GroupedOption * @uses \MacFJA\RediSearch\Redis\Command\Option\WithPublicGroupedSetterTrait @@ -252,6 +253,45 @@ public function testAllAdd(): void static::assertEquals($createCommand, $builder->getCommand()); } + public function testAllJsonAdd(): void + { + $createCommand = new Create(); + + $builder = new IndexBuilder(); + + $builder->setIndex('city'); + $createCommand->setIndex('city'); + + $oldBuilder = $builder; + $builder = $builder->addPrefixes('city-', 'c-'); + static::assertNotEquals($createCommand, $builder->getCommand()); + $createCommand->setPrefixes('city-', 'c-'); + static::assertEquals($createCommand, $builder->getCommand()); + static::assertSame($oldBuilder, $builder); + + $builder->addStopWords('hello', 'world'); + $createCommand->setStopWords('hello', 'world'); + + $builder->addJSONField('$.city.name', (new TextFieldOption())->setField('name')); + $createCommand->addJSONTextField('$.city.name', 'name'); + + $builder->addJSONField('$.city.country', (new TextFieldOption())->setField('country')); + $createCommand->addJSONTextField('$.city.country', 'country'); + + $builder->addJSONTextField('$.city.continent', 'continent'); + $createCommand->addJSONTextField('$.city.continent', 'continent'); + + $builder->addJSONNumericField('$.city.population', 'population'); + $createCommand->addJSONNumericField('$.city.population', 'population'); + + $builder->addJSONGeoField('$.city.gps', 'gps'); + $createCommand->addJSONGeoField('$.city.gps', 'gps'); + + $builder->addJSONTagField('$.city.languages', 'languages'); + $createCommand->addJSONTagField('$.city.languages', 'languages'); + static::assertEquals($createCommand, $builder->getCommand()); + } + /** * @medium */ diff --git a/tests/Redis/Command/AddFieldOptionTraitTest.php b/tests/Redis/Command/AddFieldOptionTraitTest.php index 0b5ade7..78187d7 100644 --- a/tests/Redis/Command/AddFieldOptionTraitTest.php +++ b/tests/Redis/Command/AddFieldOptionTraitTest.php @@ -51,6 +51,16 @@ public function testWrongParent(): void $command->addNumericField('foo'); } + public function testWrongParent2(): void + { + $command = new FakeAddFieldOptionTraitClass1(); + + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('This method is not callable in '.FakeAddFieldOptionTraitClass1::class); + + $command->addJSONNumericField('$.foo', 'foo'); + } + public function testValidParent(): void { $command = new FakeAddFieldOptionTraitClass2([]); diff --git a/tests/Redis/Command/AggregateTest.php b/tests/Redis/Command/AggregateTest.php index 23a93ae..afd4192 100644 --- a/tests/Redis/Command/AggregateTest.php +++ b/tests/Redis/Command/AggregateTest.php @@ -60,6 +60,13 @@ public function testGetId(): void static::assertSame('FT.AGGREGATE', $command->getId()); } + public function testGetIndex(): void + { + $command = new Aggregate(); + $command->setIndex('idx'); + static::assertSame('idx', $command->getIndex()); + } + public function testMultiReduce(): void { $group = new GroupByOption(['@text1']); diff --git a/tests/Redis/Command/CreateTest.php b/tests/Redis/Command/CreateTest.php index 00c028d..923ab4b 100644 --- a/tests/Redis/Command/CreateTest.php +++ b/tests/Redis/Command/CreateTest.php @@ -21,8 +21,11 @@ namespace MacFJA\RediSearch\tests\Redis\Command; +use InvalidArgumentException; use MacFJA\RediSearch\Redis\Command\AbstractCommand; use MacFJA\RediSearch\Redis\Command\Create; +use MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption; +use MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption; use MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption; use PHPUnit\Framework\TestCase; @@ -30,9 +33,9 @@ * @covers \MacFJA\RediSearch\Redis\Command\AbstractCommand * @covers \MacFJA\RediSearch\Redis\Command\Create * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\GeoFieldOption + * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\JSONFieldOption * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\NumericFieldOption * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TagFieldOption - * * @covers \MacFJA\RediSearch\Redis\Command\CreateCommand\TextFieldOption * * @uses \MacFJA\RediSearch\Redis\Command\Option\AbstractCommandOption @@ -191,4 +194,67 @@ public function testUnNormalizedForm(): void $textField->setSortable(false); static::assertSame(['idx', 'SCHEMA', 'foo', 'TEXT'], $command->getArguments()); } + + public function testJSONStructure(): void + { + $expected = [ + 'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG', + ]; + $command = new Create('2.2.0'); + $command + ->setIndex('userIdx') + ->setStructure('JSON') + ->addField(new JSONFieldOption('$.user.name', (new TextFieldOption())->setField('name'))) + ->addField(new JSONFieldOption('$.user.tag', (new TagFieldOption())->setField('country'))) + ; + + static::assertSame($expected, $command->getArguments()); + } + + public function testJSONStructureShortHand(): void + { + $expected = [ + 'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG', + ]; + $command = new Create('2.2.0'); + $command + ->setIndex('userIdx') + ->setStructure('JSON') + ->addJSONField('$.user.name', (new TextFieldOption())->setField('name')) + ->addJSONField('$.user.tag', (new TagFieldOption())->setField('country')) + ; + + static::assertSame($expected, $command->getArguments()); + } + + public function testJSONStructureShortHand2(): void + { + $expected = [ + 'userIdx', 'ON', 'JSON', 'SCHEMA', '$.user.name', 'AS', 'name', 'TEXT', '$.user.tag', 'AS', 'country', 'TAG', + ]; + $command = new Create('2.2.0'); + $command + ->setIndex('userIdx') + ->setStructure('JSON') + ->addJSONTextField('$.user.name', 'name') + ->addJSONTagField('$.user.tag', 'country') + ; + + static::assertSame($expected, $command->getArguments()); + } + + public function testJSONStructureWrongVersion(): void + { + $command = new Create('2.0.0'); + $command + ->setIndex('userIdx') + ->setStructure('JSON') + ->addJSONTextField('$.user.name', 'name') + ->addJSONTagField('$.user.tag', 'country') + ; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Missing command option: fields'); + $command->getArguments(); + } } diff --git a/tests/Redis/Command/Option/JSONFieldOptionTest.php b/tests/Redis/Command/Option/JSONFieldOptionTest.php new file mode 100644 index 0000000..90ba780 --- /dev/null +++ b/tests/Redis/Command/Option/JSONFieldOptionTest.php @@ -0,0 +1,108 @@ +isValid()); + static::assertFalse($json->isValid()); + + $option->setField('data'); + $json = new JSONFieldOption('', $option); + static::assertTrue($option->isValid()); + static::assertFalse($json->isValid()); + + $json = new JSONFieldOption('$.data', $option); + static::assertTrue($option->isValid()); + static::assertTrue($json->isValid()); + } + + public function testGetOptionData(): void + { + $option = new TextFieldOption(); + $option->setField('data'); + $json = new JSONFieldOption('$.data', $option); + + $expected = [ + 'path' => '$.data', + ] + $option->getOptionData(); + + static::assertSame($expected, $json->getOptionData()); + } + + public function testGetVersionConstraint(): void + { + $option = new TextFieldOption(); + $json = new JSONFieldOption('$.data', $option); + + static::assertSame('>=2.2.0', $json->getVersionConstraint()); + } + + public function testGetFieldName(): void + { + $option = new TextFieldOption(); + $option->setField('data'); + $json = new JSONFieldOption('$.data', $option); + + static::assertSame('data', $json->getFieldName()); + } + + public function testGetJSONPath(): void + { + $option = new TextFieldOption(); + $json = new JSONFieldOption('$.data', $option); + + static::assertSame('$.data', $json->getJSONPath()); + } + + public function testDoRender(): void + { + $option = new TextFieldOption(); + $option->setField('data'); + $json = new JSONFieldOption('$.data', $option); + + static::assertSame(['$.data', 'AS', 'data', 'TEXT'], $json->render()); + } +}