diff --git a/spec/Node/SimpleNodeSpec.php b/spec/Node/SimpleNodeSpec.php index 0b5357b..28dcd67 100644 --- a/spec/Node/SimpleNodeSpec.php +++ b/spec/Node/SimpleNodeSpec.php @@ -4,7 +4,10 @@ namespace spec\chrisjenkinson\StructuredDocumentParser\Node; +use chrisjenkinson\StructuredDocumentParser\Node\DuplicateNodeException; use chrisjenkinson\StructuredDocumentParser\Node\NodeInterface; +use chrisjenkinson\StructuredDocumentParser\Node\NodeNotFoundException; +use chrisjenkinson\StructuredDocumentParser\Node\SimpleNode; use PhpSpec\ObjectBehavior; use RuntimeException; @@ -40,24 +43,90 @@ public function it_has_nodes(NodeInterface $node): void public function it_throws_exception_if_no_such_child_exists(): void { - $this->shouldThrow(RuntimeException::class)->duringGetNode('nonexistent'); + $this->shouldThrow(NodeNotFoundException::class)->duringGetNode('nonexistent'); } - public function it_exports_a_tree_as_a_string(NodeInterface $node): void + public function it_throws_when_adding_a_node_whose_name_is_already_taken(NodeInterface $node, NodeInterface $other): void { $node->getName()->willReturn('ChildNode'); - $node->__toString()->willReturn(''); + $other->getName()->willReturn('ChildNode'); - $this->setAttribute('something', 'result'); $this->addNode($node); + $this->shouldThrow(DuplicateNodeException::class)->duringAddNode($other); + $this->getNode('ChildNode')->shouldReturn($node); + } + + public function it_replaces_a_node_with_the_same_name(NodeInterface $node, NodeInterface $replacement): void + { + $node->getName()->willReturn('ChildNode'); + $replacement->getName()->willReturn('ChildNode'); + + $this->addNode($node); + $this->replaceNode($replacement); + + $this->getNodes()->shouldReturn(['ChildNode' => $replacement]); + } + + public function it_throws_when_replacing_a_node_that_does_not_exist(NodeInterface $node): void + { + $node->getName()->willReturn('ChildNode'); + + $this->shouldThrow(NodeNotFoundException::class)->duringReplaceNode($node); + } + + public function it_exports_a_tree_as_a_string(): void + { + $child = new SimpleNode(); + $grandchild = new SimpleNode(); + $listItem = new SimpleNode(); + + $grandchild->setAttribute('depth', 2); + $listItem->setAttribute('item', 'first'); + $child->addNode($grandchild); + $child->setAttribute('items', [$listItem]); + + $this->setAttribute('something', 'result'); + $this->addNode($child); + $this->__toString()->shouldReturn('{ "attributes": { "something": "result" }, "nodes": { - "ChildNode": {} + "SimpleNode": { + "attributes": { + "items": [ + { + "attributes": { + "item": "first" + }, + "nodes": [] + } + ] + }, + "nodes": { + "SimpleNode": { + "attributes": { + "depth": 2 + }, + "nodes": [] + } + } + } + } +}'); } + + public function it_replaces_invalid_utf8_when_exporting_as_a_string(): void + { + $this->setAttribute('bad', "a\xffb"); + + $this->__toString()->shouldReturn('{ + "attributes": { + "bad": "a\ufffdb" + }, + "nodes": [] }'); } } diff --git a/spec/NodeTraverser/NodeTraverserSpec.php b/spec/NodeTraverser/NodeTraverserSpec.php index 6033219..59daa6d 100644 --- a/spec/NodeTraverser/NodeTraverserSpec.php +++ b/spec/NodeTraverser/NodeTraverserSpec.php @@ -28,15 +28,17 @@ public function it_can_traverse_a_node(NodeInterface $node, NodeVisitorInterface $nodeVisitor->afterTraverse($node)->shouldHaveBeenCalled(); } - public function it_can_add_a_child(NodeInterface $node, NodeInterface $child, NodeVisitorInterface $nodeVisitor, NodeVisitorInterface $nodeVisitor2): void + public function it_puts_a_traversed_child_back_in_place(NodeInterface $node, NodeInterface $child, NodeVisitorInterface $nodeVisitor, NodeVisitorInterface $nodeVisitor2): void { $node->getNodes()->willReturn([$child]); $node->getAttributes()->willReturn([]); $child->getNodes()->willReturn([]); $child->getAttributes()->willReturn([]); + $child->getName()->willReturn('ChildNode'); - $node->addNode($child)->shouldBeCalled(); + $node->replaceNode($child)->shouldBeCalled(); + $node->addNode($child)->shouldNotBeCalled(); $this->addVisitor($nodeVisitor); $this->addVisitor($nodeVisitor2); diff --git a/src/Node/AbstractNode.php b/src/Node/AbstractNode.php index 8558774..e3f70f6 100644 --- a/src/Node/AbstractNode.php +++ b/src/Node/AbstractNode.php @@ -21,7 +21,15 @@ abstract class AbstractNode implements NodeInterface public function __toString(): string { - return json_encode(['attributes' => $this->attributes, 'nodes' => $this->nodes], JSON_PRETTY_PRINT); + return json_encode($this, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); + } + + /** + * @return array{attributes: array, nodes: NodeInterface[]} + */ + public function jsonSerialize(): array + { + return ['attributes' => $this->attributes, 'nodes' => $this->nodes]; } /** @@ -49,7 +57,7 @@ public function getNode(string $key): NodeInterface return $this->nodes[$key]; } - throw new RuntimeException(sprintf('No such node "%s"', $key)); + throw new NodeNotFoundException($key); } /** @@ -62,6 +70,19 @@ public function getNodes(): array public function addNode(NodeInterface $node): void { + if ($this->hasNode($node->getName())) { + throw new DuplicateNodeException($node->getName()); + } + + $this->nodes[$node->getName()] = $node; + } + + public function replaceNode(NodeInterface $node): void + { + if (!$this->hasNode($node->getName())) { + throw new NodeNotFoundException($node->getName()); + } + $this->nodes[$node->getName()] = $node; } diff --git a/src/Node/DuplicateNodeException.php b/src/Node/DuplicateNodeException.php new file mode 100644 index 0000000..e783bb7 --- /dev/null +++ b/src/Node/DuplicateNodeException.php @@ -0,0 +1,15 @@ +removeNode($child); + if ($newChild->getName() === $child->getName()) { + $node->replaceNode($newChild); + + return; } + $node->removeNode($child); $node->addNode($newChild); }, $children); }