diff --git a/psalm.xml b/psalm.xml index 5a7bd22c..95604c82 100644 --- a/psalm.xml +++ b/psalm.xml @@ -32,6 +32,11 @@ + + + + + diff --git a/src/Xml/Dom/Loader/xml_file_loader.php b/src/Xml/Dom/Loader/xml_file_loader.php index 28e00adf..06d560ed 100644 --- a/src/Xml/Dom/Loader/xml_file_loader.php +++ b/src/Xml/Dom/Loader/xml_file_loader.php @@ -18,7 +18,7 @@ function xml_file_loader(string $file, int $options = 0): Closure load( static function () use ($document, $file, $options): bool { Assert::fileExists($file); - return (bool) $document->load($file, $options); + return $document->load($file, $options); } ); }; diff --git a/src/Xml/Dom/Loader/xml_string_loader.php b/src/Xml/Dom/Loader/xml_string_loader.php index 1dfa2fc6..333a3354 100644 --- a/src/Xml/Dom/Loader/xml_string_loader.php +++ b/src/Xml/Dom/Loader/xml_string_loader.php @@ -15,6 +15,6 @@ function xml_string_loader(string $xml, int $options = 0): Closure { return static function (DOMDocument $document) use ($xml, $options): void { - load(static fn (): bool => (bool) $document->loadXML($xml, $options)); + load(static fn (): bool => $document->loadXML($xml, $options)); }; } diff --git a/src/Xml/Reader/Matcher/any.php b/src/Xml/Reader/Matcher/any.php new file mode 100644 index 00000000..f88aac76 --- /dev/null +++ b/src/Xml/Reader/Matcher/any.php @@ -0,0 +1,25 @@ + $matchers + * + * @return \Closure(NodeSequence): bool + */ +function any(callable ... $matchers): Closure +{ + return static fn (NodeSequence $sequence): bool => Iter\any( + $matchers, + /** + * @param callable(NodeSequence): bool $matcher + */ + static fn (callable $matcher): bool => $matcher($sequence) + ); +} diff --git a/src/Xml/Reader/Matcher/attribute_local_name.php b/src/Xml/Reader/Matcher/attribute_local_name.php new file mode 100644 index 00000000..ab1eb3a3 --- /dev/null +++ b/src/Xml/Reader/Matcher/attribute_local_name.php @@ -0,0 +1,23 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName + ); + }; +} diff --git a/src/Xml/Reader/Matcher/attribute_local_value.php b/src/Xml/Reader/Matcher/attribute_local_value.php new file mode 100644 index 00000000..2a038c5c --- /dev/null +++ b/src/Xml/Reader/Matcher/attribute_local_value.php @@ -0,0 +1,23 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName && $attribute->value() === $value + ); + }; +} diff --git a/src/Xml/Reader/Matcher/attribute_name.php b/src/Xml/Reader/Matcher/attribute_name.php new file mode 100644 index 00000000..813a2696 --- /dev/null +++ b/src/Xml/Reader/Matcher/attribute_name.php @@ -0,0 +1,23 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => $attribute->name() === $name + ); + }; +} diff --git a/src/Xml/Reader/Matcher/attribute_value.php b/src/Xml/Reader/Matcher/attribute_value.php new file mode 100644 index 00000000..ef00ba41 --- /dev/null +++ b/src/Xml/Reader/Matcher/attribute_value.php @@ -0,0 +1,23 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => $attribute->name() === $name && $attribute->value() === $value + ); + }; +} diff --git a/src/Xml/Reader/Matcher/document_element.php b/src/Xml/Reader/Matcher/document_element.php new file mode 100644 index 00000000..0b81a8e5 --- /dev/null +++ b/src/Xml/Reader/Matcher/document_element.php @@ -0,0 +1,18 @@ +parent(); + }; +} diff --git a/src/Xml/Reader/Matcher/element_local_name.php b/src/Xml/Reader/Matcher/element_local_name.php new file mode 100644 index 00000000..a72d7bc5 --- /dev/null +++ b/src/Xml/Reader/Matcher/element_local_name.php @@ -0,0 +1,18 @@ +current()->localName() === $localName; + }; +} diff --git a/src/Xml/Reader/Matcher/element_name.php b/src/Xml/Reader/Matcher/element_name.php new file mode 100644 index 00000000..05b345b4 --- /dev/null +++ b/src/Xml/Reader/Matcher/element_name.php @@ -0,0 +1,18 @@ +current()->name() === $name; + }; +} diff --git a/src/Xml/Reader/Matcher/element_position.php b/src/Xml/Reader/Matcher/element_position.php new file mode 100644 index 00000000..6dd0309f --- /dev/null +++ b/src/Xml/Reader/Matcher/element_position.php @@ -0,0 +1,18 @@ +current()->position() === $position; + }; +} diff --git a/src/Xml/Reader/Matcher/namespaced_attribute.php b/src/Xml/Reader/Matcher/namespaced_attribute.php new file mode 100644 index 00000000..ceb3103a --- /dev/null +++ b/src/Xml/Reader/Matcher/namespaced_attribute.php @@ -0,0 +1,23 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => $attribute->localName() === $localName && $attribute->namespace() === $namespace + ); + }; +} diff --git a/src/Xml/Reader/Matcher/namespaced_attribute_value.php b/src/Xml/Reader/Matcher/namespaced_attribute_value.php new file mode 100644 index 00000000..eeab1f43 --- /dev/null +++ b/src/Xml/Reader/Matcher/namespaced_attribute_value.php @@ -0,0 +1,26 @@ +current()->attributes(), + static fn (AttributeNode $attribute): bool => + $attribute->localName() === $localName + && $attribute->namespace() === $namespace + && $attribute->value() === $value + ); + }; +} diff --git a/src/Xml/Reader/Matcher/namespaced_element.php b/src/Xml/Reader/Matcher/namespaced_element.php new file mode 100644 index 00000000..052f1248 --- /dev/null +++ b/src/Xml/Reader/Matcher/namespaced_element.php @@ -0,0 +1,20 @@ +current(); + + return $current->localName() === $localName && $current->namespace() === $namespace; + }; +} diff --git a/src/Xml/Reader/Matcher/node_attribute.php b/src/Xml/Reader/Matcher/node_attribute.php index f6e0cd29..5e556717 100644 --- a/src/Xml/Reader/Matcher/node_attribute.php +++ b/src/Xml/Reader/Matcher/node_attribute.php @@ -10,6 +10,7 @@ use function Psl\Iter\any; /** + * @deprecated Use attribute_value instead! This will be removed in next major version * @return \Closure(NodeSequence): bool */ function node_attribute(string $key, string $value): Closure diff --git a/src/Xml/Reader/Matcher/node_name.php b/src/Xml/Reader/Matcher/node_name.php index ceec497c..645fb010 100644 --- a/src/Xml/Reader/Matcher/node_name.php +++ b/src/Xml/Reader/Matcher/node_name.php @@ -8,6 +8,7 @@ use VeeWee\Xml\Reader\Node\NodeSequence; /** + * @deprecated Use element_name instead! This will be removed in next major version * @return \Closure(NodeSequence): bool */ function node_name(string $name): Closure diff --git a/src/Xml/Reader/Matcher/sequence.php b/src/Xml/Reader/Matcher/sequence.php new file mode 100644 index 00000000..bb2a2557 --- /dev/null +++ b/src/Xml/Reader/Matcher/sequence.php @@ -0,0 +1,41 @@ + $matcherSequence + * + * @return \Closure(NodeSequence): bool + */ +function sequence(callable ... $matcherSequence): Closure +{ + return static function (NodeSequence $sequence) use ($matcherSequence) : bool { + $nodeSequence = $sequence->sequence(); + if (count($matcherSequence) !== count($nodeSequence)) { + return false; + } + + $currentSequence = new NodeSequence(); + foreach ($nodeSequence as $i => $node) { + $currentSequence = $currentSequence->append($node); + $matcher = $matcherSequence[$i]; + if (!$matcher($currentSequence)) { + return false; + } + } + + return true; + }; +} diff --git a/src/bootstrap.php b/src/bootstrap.php index 607c4caf..6cdd0ec6 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -124,8 +124,21 @@ require_once __DIR__.'/Xml/Reader/Loader/xml_file_loader.php'; require_once __DIR__.'/Xml/Reader/Loader/xml_string_loader.php'; require_once __DIR__.'/Xml/Reader/Matcher/all.php'; +require_once __DIR__.'/Xml/Reader/Matcher/any.php'; +require_once __DIR__.'/Xml/Reader/Matcher/attribute_local_name.php'; +require_once __DIR__.'/Xml/Reader/Matcher/attribute_local_value.php'; +require_once __DIR__.'/Xml/Reader/Matcher/attribute_name.php'; +require_once __DIR__.'/Xml/Reader/Matcher/attribute_value.php'; +require_once __DIR__.'/Xml/Reader/Matcher/document_element.php'; +require_once __DIR__.'/Xml/Reader/Matcher/element_local_name.php'; +require_once __DIR__.'/Xml/Reader/Matcher/element_name.php'; +require_once __DIR__.'/Xml/Reader/Matcher/element_position.php'; +require_once __DIR__.'/Xml/Reader/Matcher/namespaced_attribute.php'; +require_once __DIR__.'/Xml/Reader/Matcher/namespaced_attribute_value.php'; +require_once __DIR__.'/Xml/Reader/Matcher/namespaced_element.php'; require_once __DIR__.'/Xml/Reader/Matcher/node_attribute.php'; require_once __DIR__.'/Xml/Reader/Matcher/node_name.php'; +require_once __DIR__.'/Xml/Reader/Matcher/sequence.php'; require_once __DIR__.'/Xml/Writer/Builder/attribute.php'; require_once __DIR__.'/Xml/Writer/Builder/attributes.php'; require_once __DIR__.'/Xml/Writer/Builder/children.php'; diff --git a/tests/Xml/Reader/Matcher/AbstractMatcherTest.php b/tests/Xml/Reader/Matcher/AbstractMatcherTest.php new file mode 100644 index 00000000..17ae66a3 --- /dev/null +++ b/tests/Xml/Reader/Matcher/AbstractMatcherTest.php @@ -0,0 +1,42 @@ + $expected + */ + public function test_real_xml_cases(Closure $matcher, string $xml, array $expected) + { + $reader = Reader::fromXmlString($xml); + $actual = [...$reader->provide($matcher)]; + + static::assertSame($actual, $expected); + } + + /** + * @dataProvider provideMatcherCases + * + * @param \Closure(NodeSequence): bool $matcher + */ + public function test_matcher_cases(Closure $matcher, NodeSequence $sequence, bool $expected) + { + $actual = $matcher($sequence); + + static::assertSame($actual, $expected); + } +} diff --git a/tests/Xml/Reader/Matcher/AllTest.php b/tests/Xml/Reader/Matcher/AllTest.php index 1de4bc77..bdf824ae 100644 --- a/tests/Xml/Reader/Matcher/AllTest.php +++ b/tests/Xml/Reader/Matcher/AllTest.php @@ -4,42 +4,76 @@ namespace VeeWee\Tests\Xml\Reader\Matcher; -use PHPUnit\Framework\TestCase; +use Generator; use VeeWee\Xml\Reader\Node\NodeSequence; use function VeeWee\Xml\Reader\Matcher\all; +use function VeeWee\Xml\Reader\Matcher\element_name; -final class AllTest extends TestCase +final class AllTest extends AbstractMatcherTest { - public function test_it_returns_true_if_all_matchers_agree(): void + public static function provideRealXmlCases(): Generator { - $matcher = all( - static fn () => true, - static fn () => true, - static fn () => true - ); - static::assertTrue($matcher($this->createSequence())); + yield 'all' => [ + all(), + $xml = <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + $xml, + 'Jos', + 'Bos', + 'Mos' + ] + ]; + yield 'users' => [ + all(element_name('user')), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; } - - public function test_it_returns_false_if__not_all_matchers_agree(): void + public static function provideMatcherCases(): Generator { - $matcher = all( - static fn () => true, - static fn () => true, - static fn () => false - ); - static::assertFalse($matcher($this->createSequence())); - } + $sequence = new NodeSequence(); - - public function test_it_returns_true_if_there_are_no_matchers(): void - { - $matcher = all(); - static::assertTrue($matcher($this->createSequence())); - } + yield 'it_returns_true_if_all_matchers_agree' => [ + all( + static fn () => true, + static fn () => true, + static fn () => true + ), + $sequence, + true + ]; - private function createSequence(): NodeSequence - { - return new NodeSequence(); + yield 'it_returns_false_if_not_all_matchers_agree' => [ + all( + static fn () => true, + static fn () => true, + static fn () => false + ), + $sequence, + false + ]; + + yield 'it_returns_true_if_there_are_no_matchers' => [ + all(), + $sequence, + true + ]; } } diff --git a/tests/Xml/Reader/Matcher/AnyTest.php b/tests/Xml/Reader/Matcher/AnyTest.php new file mode 100644 index 00000000..3fb9aff2 --- /dev/null +++ b/tests/Xml/Reader/Matcher/AnyTest.php @@ -0,0 +1,84 @@ + [ + any(), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [] + ]; + yield 'users' => [ + any(element_name('user')), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; + } + + public static function provideMatcherCases(): Generator + { + $sequence = new NodeSequence(); + + yield 'it_returns_true_if_all_matchers_agree' => [ + any( + static fn () => true, + static fn () => true, + static fn () => true + ), + $sequence, + true + ]; + + yield 'it_returns_true_if_any_matchers_agree' => [ + any( + static fn () => false, + static fn () => true, + static fn () => false + ), + $sequence, + true + ]; + + yield 'it_returns_false_if_no_matchers_agree' => [ + any( + static fn () => false, + static fn () => false, + static fn () => false + ), + $sequence, + false + ]; + + yield 'it_returns_false_if_there_are_no_matchers' => [ + any(), + $sequence, + false + ]; + } +} diff --git a/tests/Xml/Reader/Matcher/AttributeValueTest.php b/tests/Xml/Reader/Matcher/AttributeValueTest.php new file mode 100644 index 00000000..faf05234 --- /dev/null +++ b/tests/Xml/Reader/Matcher/AttributeValueTest.php @@ -0,0 +1,73 @@ + [ + attribute_value('country', 'BE'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Mos', + ] + ]; + yield 'namespaced' => [ + attribute_value('u:country', 'BE'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Mos' + ] + ]; + } + + public static function provideMatcherCases(): Generator + { + $sequence = new NodeSequence( + new ElementNode(1, 'item', 'item', '', '', [ + new AttributeNode('locale', 'locale', '', '', 'nl') + ]) + ); + + yield 'it_returns_true_if_attribute_value_matches' => [ + attribute_value('locale', 'nl'), + $sequence, + true + ]; + + yield 'it_returns_false_if_attribute_value_does_not_match' => [ + attribute_value('locale', 'en'), + $sequence, + false + ]; + + yield 'it_returns_false_if_attribute_value_is_not_available' => [ + attribute_value('unkown', 'en'), + $sequence, + false + ]; + } +} diff --git a/tests/Xml/Reader/Matcher/ElementNameTest.php b/tests/Xml/Reader/Matcher/ElementNameTest.php new file mode 100644 index 00000000..a806a16d --- /dev/null +++ b/tests/Xml/Reader/Matcher/ElementNameTest.php @@ -0,0 +1,66 @@ + [ + element_name('user'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; + yield 'namespaced' => [ + element_name('u:user'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; + } + + public static function provideMatcherCases(): Generator + { + $sequence = new NodeSequence( + new ElementNode(1, 'item', 'item', '', '', []) + ); + + yield 'it_returns_true_if_element_name_matches' => [ + element_name('item'), + $sequence, + true + ]; + + yield 'it_returns_false_if_element_name_does_not_match' => [ + element_name('other'), + $sequence, + false + ]; + } +} diff --git a/tests/Xml/Reader/Matcher/NodeAttributeTest.php b/tests/Xml/Reader/Matcher/NodeAttributeTest.php index 02e1d307..63a92fc2 100644 --- a/tests/Xml/Reader/Matcher/NodeAttributeTest.php +++ b/tests/Xml/Reader/Matcher/NodeAttributeTest.php @@ -4,40 +4,73 @@ namespace VeeWee\Tests\Xml\Reader\Matcher; -use PHPUnit\Framework\TestCase; +use Generator; use VeeWee\Xml\Reader\Node\AttributeNode; use VeeWee\Xml\Reader\Node\ElementNode; use VeeWee\Xml\Reader\Node\NodeSequence; use function VeeWee\Xml\Reader\Matcher\node_attribute; -final class NodeAttributeTest extends TestCase +/** + * @deprecated Use attribute_value instead! This will be removed in next major version + */ +final class NodeAttributeTest extends AbstractMatcherTest { - public function test_it_returns_true_if_node_attribute_matches(): void + public static function provideRealXmlCases(): Generator { - $matcher = node_attribute('locale', 'nl'); - static::assertTrue($matcher($this->createSequence())); + yield 'users' => [ + node_attribute('country', 'BE'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Mos', + ] + ]; + yield 'namespaced' => [ + node_attribute('u:country', 'BE'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Mos' + ] + ]; } - - public function test_it_returns_false_if_node_attribute_does_not_match(): void + public static function provideMatcherCases(): Generator { - $matcher = node_attribute('locale', 'en'); - static::assertFalse($matcher($this->createSequence())); - } - - - public function test_it_returns_false_if_node_attribute_is_not_available(): void - { - $matcher = node_attribute('unkown', 'en'); - static::assertFalse($matcher($this->createSequence())); - } - - private function createSequence(): NodeSequence - { - return new NodeSequence( + $sequence = new NodeSequence( new ElementNode(1, 'item', 'item', '', '', [ new AttributeNode('locale', 'locale', '', '', 'nl') ]) ); + + yield 'it_returns_true_if_node_attribute_matches' => [ + node_attribute('locale', 'nl'), + $sequence, + true + ]; + + yield 'it_returns_false_if_node_attribute_does_not_match' => [ + node_attribute('locale', 'en'), + $sequence, + false + ]; + + yield 'it_returns_false_if_node_attribute_is_not_available' => [ + node_attribute('unkown', 'en'), + $sequence, + false + ]; } } diff --git a/tests/Xml/Reader/Matcher/NodeNameTest.php b/tests/Xml/Reader/Matcher/NodeNameTest.php index a21bc94f..f98b69c6 100644 --- a/tests/Xml/Reader/Matcher/NodeNameTest.php +++ b/tests/Xml/Reader/Matcher/NodeNameTest.php @@ -4,30 +4,66 @@ namespace VeeWee\Tests\Xml\Reader\Matcher; -use PHPUnit\Framework\TestCase; +use Generator; use VeeWee\Xml\Reader\Node\ElementNode; use VeeWee\Xml\Reader\Node\NodeSequence; use function VeeWee\Xml\Reader\Matcher\node_name; -final class NodeNameTest extends TestCase +/** + * @deprecated Use element_name instead! This will be removed in next major version + */ +final class NodeNameTest extends AbstractMatcherTest { - public function test_it_returns_true_if_node_name_matches(): void + public static function provideRealXmlCases(): Generator { - $matcher = node_name('item'); - static::assertTrue($matcher($this->createSequence())); + yield 'users' => [ + node_name('user'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; + yield 'namespaced' => [ + node_name('u:user'), + <<<'EOXML' + + Jos + Bos + Mos + + EOXML, + [ + 'Jos', + 'Bos', + 'Mos' + ] + ]; } - - public function test_it_returns_false_if_node_name_does_not_match(): void + public static function provideMatcherCases(): Generator { - $matcher = node_name('other'); - static::assertFalse($matcher($this->createSequence())); - } - - private function createSequence(): NodeSequence - { - return new NodeSequence( + $sequence = new NodeSequence( new ElementNode(1, 'item', 'item', '', '', []) ); + + yield 'it_returns_true_if_element_name_matches' => [ + node_name('item'), + $sequence, + true + ]; + + yield 'it_returns_false_if_element_name_does_not_match' => [ + node_name('other'), + $sequence, + false + ]; } }