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
+ ];
}
}