diff --git a/app/Support/Search/QueryParser/FieldNode.php b/app/Support/Search/QueryParser/FieldNode.php index 4fd847cd5d..5005d5fe57 100644 --- a/app/Support/Search/QueryParser/FieldNode.php +++ b/app/Support/Search/QueryParser/FieldNode.php @@ -28,9 +28,4 @@ class FieldNode extends Node { return $this->value; } - - public function __toString(): string - { - return ($this->prohibited ? '-' : '') . $this->operator . ':' . $this->value; - } } diff --git a/app/Support/Search/QueryParser/Node.php b/app/Support/Search/QueryParser/Node.php index 7dced2e759..3050bd82c7 100644 --- a/app/Support/Search/QueryParser/Node.php +++ b/app/Support/Search/QueryParser/Node.php @@ -9,8 +9,6 @@ namespace FireflyIII\Support\Search\QueryParser; */ abstract class Node { - abstract public function __toString(): string; - protected bool $prohibited; public function isProhibited(): bool diff --git a/app/Support/Search/QueryParser/NodeGroup.php b/app/Support/Search/QueryParser/NodeGroup.php index 967c4d89d4..8cf801dbbc 100644 --- a/app/Support/Search/QueryParser/NodeGroup.php +++ b/app/Support/Search/QueryParser/NodeGroup.php @@ -31,10 +31,4 @@ class NodeGroup extends Node { return $this->nodes; } - - - public function __toString(): string - { - return ($this->prohibited ? '-' : '') . '[' . implode(' ', array_map(fn($node) => (string)$node, $this->nodes)) . ']'; - } } diff --git a/app/Support/Search/QueryParser/StringNode.php b/app/Support/Search/QueryParser/StringNode.php index 387862da30..6164cc1de7 100644 --- a/app/Support/Search/QueryParser/StringNode.php +++ b/app/Support/Search/QueryParser/StringNode.php @@ -21,9 +21,4 @@ class StringNode extends Node { return $this->value; } - - public function __toString(): string - { - return ($this->prohibited ? '-' : '') . $this->value; - } } diff --git a/tests/unit/Support/Search/QueryParser/AbstractQueryParserInterfaceParseQueryTest.php b/tests/unit/Support/Search/QueryParser/AbstractQueryParserInterfaceParseQueryTest.php index 1a1d668a7b..f324a8661a 100644 --- a/tests/unit/Support/Search/QueryParser/AbstractQueryParserInterfaceParseQueryTest.php +++ b/tests/unit/Support/Search/QueryParser/AbstractQueryParserInterfaceParseQueryTest.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace Tests\unit\Support\Search\QueryParser; -use FireflyIII\Support\Search\QueryParser\Field; +use FireflyIII\Support\Search\QueryParser\FieldNode; use FireflyIII\Support\Search\QueryParser\QueryParserInterface; -use FireflyIII\Support\Search\QueryParser\Word; -use FireflyIII\Support\Search\QueryParser\Subquery; +use FireflyIII\Support\Search\QueryParser\StringNode; +use FireflyIII\Support\Search\QueryParser\NodeGroup; use FireflyIII\Support\Search\QueryParser\Node; use Tests\integration\TestCase; @@ -20,164 +20,164 @@ abstract class AbstractQueryParserInterfaceParseQueryTest extends TestCase return [ 'empty query' => [ 'query' => '', - 'expected' => [] + 'expected' => new NodeGroup([]) ], 'simple word' => [ 'query' => 'groceries', - 'expected' => [new Word('groceries')] + 'expected' => new NodeGroup([new StringNode('groceries')]) ], 'prohibited word' => [ 'query' => '-groceries', - 'expected' => [new Word('groceries', true)] + 'expected' => new NodeGroup([new StringNode('groceries', true)]) ], 'prohibited field' => [ 'query' => '-amount:100', - 'expected' => [new Field('amount', '100', true)] + 'expected' => new NodeGroup([new FieldNode('amount', '100', true)]) ], 'quoted word' => [ 'query' => '"test phrase"', - 'expected' => [new Word('test phrase')] + 'expected' => new NodeGroup([new StringNode('test phrase')]) ], 'prohibited quoted word' => [ 'query' => '-"test phrase"', - 'expected' => [new Word('test phrase', true)] + 'expected' => new NodeGroup([new StringNode('test phrase', true)]) ], 'multiple words' => [ 'query' => 'groceries shopping market', - 'expected' => [ - new Word('groceries'), - new Word('shopping'), - new Word('market') - ] + 'expected' => new NodeGroup([ + new StringNode('groceries'), + new StringNode('shopping'), + new StringNode('market') + ]) ], 'field operator' => [ 'query' => 'amount:100', - 'expected' => [new Field('amount', '100')] + 'expected' => new NodeGroup([new FieldNode('amount', '100')]) ], 'quoted field value with single space' => [ 'query' => 'description:"test phrase"', - 'expected' => [new Field('description', 'test phrase')] + 'expected' => new NodeGroup([new FieldNode('description', 'test phrase')]) ], 'multiple fields' => [ 'query' => 'amount:100 category:food', - 'expected' => [ - new Field('amount', '100'), - new Field('category', 'food') - ] + 'expected' => new NodeGroup([ + new FieldNode('amount', '100'), + new FieldNode('category', 'food') + ]) ], 'simple subquery' => [ 'query' => '(amount:100 category:food)', - 'expected' => [ - new Subquery([ - new Field('amount', '100'), - new Field('category', 'food') + 'expected' => new NodeGroup([ + new NodeGroup([ + new FieldNode('amount', '100'), + new FieldNode('category', 'food') ]) - ] + ]) ], 'prohibited subquery' => [ 'query' => '-(amount:100 category:food)', - 'expected' => [ - new Subquery([ - new Field('amount', '100'), - new Field('category', 'food') + 'expected' => new NodeGroup([ + new NodeGroup([ + new FieldNode('amount', '100'), + new FieldNode('category', 'food') ], true) - ] + ]) ], 'nested subquery' => [ 'query' => '(amount:100 (description:"test" category:food))', - 'expected' => [ - new Subquery([ - new Field('amount', '100'), - new Subquery([ - new Field('description', 'test'), - new Field('category', 'food') + 'expected' => new NodeGroup([ + new NodeGroup([ + new FieldNode('amount', '100'), + new NodeGroup([ + new FieldNode('description', 'test'), + new FieldNode('category', 'food') ]) ]) - ] + ]) ], 'mixed words and operators' => [ 'query' => 'groceries amount:50 shopping', - 'expected' => [ - new Word('groceries'), - new Field('amount', '50'), - new Word('shopping') - ] + 'expected' => new NodeGroup([ + new StringNode('groceries'), + new FieldNode('amount', '50'), + new StringNode('shopping') + ]) ], 'subquery after field value' => [ 'query' => 'amount:100 (description:"market" category:food)', - 'expected' => [ - new Field('amount', '100'), - new Subquery([ - new Field('description', 'market'), - new Field('category', 'food') + 'expected' => new NodeGroup([ + new FieldNode('amount', '100'), + new NodeGroup([ + new FieldNode('description', 'market'), + new FieldNode('category', 'food') ]) - ] + ]) ], 'word followed by subquery' => [ 'query' => 'groceries (amount:100 description_contains:"test")', - 'expected' => [ - new Word('groceries'), - new Subquery([ - new Field('amount', '100'), - new Field('description_contains', 'test') + 'expected' => new NodeGroup([ + new StringNode('groceries'), + new NodeGroup([ + new FieldNode('amount', '100'), + new FieldNode('description_contains', 'test') ]) - ] + ]) ], 'nested subquery with prohibited field' => [ 'query' => '(amount:100 (description_contains:"test payment" -has_attachments:true))', - 'expected' => [ - new Subquery([ - new Field('amount', '100'), - new Subquery([ - new Field('description_contains', 'test payment'), - new Field('has_attachments', 'true', true) + 'expected' => new NodeGroup([ + new NodeGroup([ + new FieldNode('amount', '100'), + new NodeGroup([ + new FieldNode('description_contains', 'test payment'), + new FieldNode('has_attachments', 'true', true) ]) ]) - ] + ]) ], 'complex nested subqueries' => [ 'query' => 'shopping (amount:50 market (-category:food word description:"test phrase" (has_notes:true)))', - 'expected' => [ - new Word('shopping'), - new Subquery([ - new Field('amount', '50'), - new Word('market'), - new Subquery([ - new Field('category', 'food', true), - new Word('word'), - new Field('description', 'test phrase'), - new Subquery([ - new Field('has_notes', 'true') + 'expected' => new NodeGroup([ + new StringNode('shopping'), + new NodeGroup([ + new FieldNode('amount', '50'), + new StringNode('market'), + new NodeGroup([ + new FieldNode('category', 'food', true), + new StringNode('word'), + new FieldNode('description', 'test phrase'), + new NodeGroup([ + new FieldNode('has_notes', 'true') ]) ]) ]) - ] + ]) ], 'word with multiple spaces' => [ 'query' => '"multiple spaces"', - 'expected' => [new Word('multiple spaces')] + 'expected' => new NodeGroup([new StringNode('multiple spaces')]) ], 'field with multiple spaces in value' => [ 'query' => 'description:"multiple spaces here"', - 'expected' => [new Field('description', 'multiple spaces here')] + 'expected' => new NodeGroup([new FieldNode('description', 'multiple spaces here')]) ], 'unmatched right parenthesis in word' => [ 'query' => 'test)word', - 'expected' => [new Word('test)word')] + 'expected' => new NodeGroup([new StringNode('test)word')]) ], 'unmatched right parenthesis in field' => [ 'query' => 'description:test)phrase', - 'expected' => [new Field('description', 'test)phrase')] + 'expected' => new NodeGroup([new FieldNode('description', 'test)phrase')]) ], 'subquery followed by word' => [ 'query' => '(amount:100 category:food) shopping', - 'expected' => [ - new Subquery([ - new Field('amount', '100'), - new Field('category', 'food') + 'expected' => new NodeGroup([ + new NodeGroup([ + new FieldNode('amount', '100'), + new FieldNode('category', 'food') ]), - new Word('shopping') - ] + new StringNode('shopping') + ]) ] ]; } @@ -185,96 +185,13 @@ abstract class AbstractQueryParserInterfaceParseQueryTest extends TestCase /** * @dataProvider queryDataProvider * @param string $query The query string to parse - * @param array $expected The expected parse result + * @param Node $expected The expected parse result */ - public function testQueryParsing(string $query, array $expected): void + public function testQueryParsing(string $query, Node $expected): void { - $result = $this->createParser()->parse($query); + $actual = $this->createParser()->parse($query); - $this->assertNodesMatch($expected, $result); - } + $this->assertEquals($expected, $actual); - private function assertNodesMatch(array $expected, array $actual): void - { - $message = sprintf( - "Expected: %s\nActual: %s", - $this->formatNodes($expected), - $this->formatNodes($actual) - ); - - $this->assertCount(count($expected), $actual, $message); - - foreach ($expected as $index => $expectedNode) { - $actualNode = $actual[$index]; - $this->assertNodeMatches($expectedNode, $actualNode); - } - } - - private function assertNodeMatches(Node $expected, Node $actual): void - { - $message = $this->formatAssertMessage($expected, $actual); - - $this->assertInstanceOf(get_class($expected), $actual, $message); - $this->assertEquals($expected->isProhibited(), $actual->isProhibited(), $message); - - match (get_class($expected)) { - Word::class => $this->assertWordMatches($expected, $actual), - Field::class => $this->assertFieldMatches($expected, $actual), - Subquery::class => $this->assertSubqueryMatches($expected, $actual), - default => throw new \InvalidArgumentException(sprintf( - 'Unexpected node type: %s', - get_class($expected) - )) - }; - } - - - private function assertWordMatches(Word $expected, Word $actual): void - { - $message = $this->formatAssertMessage($expected, $actual); // Using your implementation - $this->assertEquals($expected->getValue(), $actual->getValue(), $message); - } - - private function assertFieldMatches(Field $expected, Field $actual): void - { - $message = sprintf( - "\nExpected field: %s\nActual field: %s", - $this->formatNode($expected), - $this->formatNode($actual) - ); - $this->assertEquals($expected->getOperator(), $actual->getOperator(), $message); - $this->assertEquals($expected->getValue(), $actual->getValue(), $message); - } - - private function assertSubqueryMatches(Subquery $expected, Subquery $actual): void - { - $this->assertNodesMatch($expected->getNodes(), $actual->getNodes()); - } - - private function formatAssertMessage(Node $expected, Node $actual): string - { - return sprintf( - "\nExpected: %s\nActual: %s", - $this->formatNode($expected), - $this->formatNode($actual) - ); - } - - private function formatNode(Node $node): string - { - return sprintf( - '%s(%s)', - basename(str_replace('\\', '/', get_class($node))), - $node->__toString() - ); - } - - - private function formatNodes(array $nodes): string - { - return '[' . implode(', ', array_map( - fn(Node $node) => $this->formatNode($node), - $nodes - )) . ']'; } }