Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions spec/Node/SimpleNodeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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": []
}');
}
}
6 changes: 4 additions & 2 deletions spec/NodeTraverser/NodeTraverserSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 23 additions & 2 deletions src/Node/AbstractNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Node/DuplicateNodeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace chrisjenkinson\StructuredDocumentParser\Node;

use LogicException;

class DuplicateNodeException extends LogicException
{
public function __construct(string $name)
{
parent::__construct(sprintf('A node named "%s" already exists', $name));
}
}
6 changes: 5 additions & 1 deletion src/Node/NodeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace chrisjenkinson\StructuredDocumentParser\Node;

interface NodeInterface
use JsonSerializable;

interface NodeInterface extends JsonSerializable
{
public function __toString(): string;

Expand Down Expand Up @@ -35,5 +37,7 @@ public function getNodes(): array;

public function addNode(NodeInterface $node): void;

public function replaceNode(NodeInterface $node): void;

public function removeNode(NodeInterface $node);
}
15 changes: 15 additions & 0 deletions src/Node/NodeNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace chrisjenkinson\StructuredDocumentParser\Node;

use RuntimeException;

class NodeNotFoundException extends RuntimeException
{
public function __construct(string $name)
{
parent::__construct(sprintf('No such node "%s"', $name));
}
}
7 changes: 5 additions & 2 deletions src/NodeTraverser/NodeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,13 @@ private function runTraverseNodeOnSubNodes(NodeInterface $node, array $children)
return;
}

if (get_class($newChild) !== get_class($child)) {
$node->removeNode($child);
if ($newChild->getName() === $child->getName()) {
$node->replaceNode($newChild);

return;
}

$node->removeNode($child);
$node->addNode($newChild);
}, $children);
}
Expand Down
Loading