feat!: refactor tree node API

The class `\CuyZ\Valinor\Mapper\Tree\Node` has been refactored to remove
access to unwanted methods that were not supposed to be part of the
public API. Below are a list of all changes:

- New methods `$node->sourceFilled()` and `$node->sourceValue()` allow
  accessing the source value.

- The method `$node->value()` has been renamed to `$node->mappedValue()`
  and will throw an exception if the node is not value.

- The method `$node->type()` now returns a string.

- The methods `$message->name()`, `$message->path()`, `$message->type()`
  and `$message->value()` have been deprecated in favor of the new
  method `$message->node()`.

- The message parameter `{original_value}` has been deprecated in favor
  of `{source_value}`.
This commit is contained in:
Romain Canon 2022-07-10 18:17:16 +02:00
parent 316d91910d
commit d3b1dcb64e
39 changed files with 767 additions and 460 deletions

View File

@ -12,7 +12,7 @@ on the original message.
| `{node_name}` | name of the node to which the message is bound |
| `{node_path}` | path of the node to which the message is bound |
| `{node_type}` | type of the node to which the message is bound |
| `{original_value}` | the source value that was given to the node |
| `{source_value}` | the source value that was given to the node |
| `{original_message}` | the original message before being customized |
Usage:
@ -46,9 +46,9 @@ try {
} catch (\CuyZ\Valinor\Mapper\MappingError $error) {
$message = $error->node()->messages()[0];
if (is_numeric($message->value())) {
if (is_numeric($message->node()->mappedValue())) {
$message = $message->withBody(
'Invalid amount {original_value, number, currency}'
'Invalid amount {source_value, number, currency}'
);
}
@ -133,7 +133,7 @@ In any case, the content can contain placeholders as described
'Some message content' => 'New content / previous: {original_message}',
// Will match if the given message is an instance of `SomeError`
SomeError::class => 'New content / value: {original_value}',
SomeError::class => 'New content / value: {source_value}',
// A callback can be used to get access to the message instance
OtherError::class => function (NodeMessage $message): string {

View File

@ -6,12 +6,9 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidTraversableKey;
use CuyZ\Valinor\Mapper\Tree\Exception\SourceMustBeIterable;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\Types\ArrayType;
use CuyZ\Valinor\Type\Types\IterableType;
use CuyZ\Valinor\Type\Types\NonEmptyArrayType;
@ -28,7 +25,7 @@ final class ArrayNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
$value = $shell->hasValue() ? $shell->value() : null;
@ -36,7 +33,7 @@ final class ArrayNodeBuilder implements NodeBuilder
assert($type instanceof ArrayType || $type instanceof NonEmptyArrayType || $type instanceof IterableType);
if (null === $value && $this->flexible) {
return Node::branch($shell, [], []);
return TreeNode::branch($shell, [], []);
}
if (! is_array($value)) {
@ -46,11 +43,11 @@ final class ArrayNodeBuilder implements NodeBuilder
$children = $this->children($type, $shell, $rootBuilder);
$array = $this->buildArray($children);
return Node::branch($shell, $array, $children);
return TreeNode::branch($shell, $array, $children);
}
/**
* @return array<Node>
* @return array<TreeNode>
*/
private function children(CompositeTraversableType $type, Shell $shell, RootNodeBuilder $rootBuilder): array
{
@ -74,7 +71,7 @@ final class ArrayNodeBuilder implements NodeBuilder
}
/**
* @param array<Node> $children
* @param array<TreeNode> $children
* @return mixed[]|null
*/
private function buildArray(array $children): ?array

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\NoCasterForType;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
/** @internal */
@ -22,7 +21,7 @@ final class CasterNodeBuilder implements NodeBuilder
$this->builders = $builders;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
/** @internal */
@ -17,13 +16,13 @@ final class CasterProxyNodeBuilder implements NodeBuilder
$this->delegate = $delegate;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
if ($shell->hasValue()) {
$value = $shell->value();
if ($shell->type()->accepts($value)) {
return Node::leaf($shell, $value);
return TreeNode::leaf($shell, $value);
}
}

View File

@ -10,7 +10,6 @@ use CuyZ\Valinor\Mapper\Object\FilledArguments;
use CuyZ\Valinor\Mapper\Object\FilteredObjectBuilder;
use CuyZ\Valinor\Mapper\Object\ObjectBuilder;
use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedArrayKeysForClass;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\Types\ClassType;
@ -41,7 +40,7 @@ final class ClassNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$classTypes = $this->classTypes($shell->type());
@ -70,7 +69,7 @@ final class ClassNodeBuilder implements NodeBuilder
$object = $this->buildObject($builder, $children);
$node = Node::branch($shell, $object, $children);
$node = TreeNode::branch($shell, $object, $children);
if (! $this->flexible) {
$node = $this->checkForUnexpectedKeys($arguments, $node);
@ -109,7 +108,7 @@ final class ClassNodeBuilder implements NodeBuilder
}
/**
* @param Node[] $children
* @param TreeNode[] $children
*/
private function buildObject(ObjectBuilder $builder, array $children): ?object
{
@ -126,7 +125,7 @@ final class ClassNodeBuilder implements NodeBuilder
return $builder->build($arguments);
}
private function checkForUnexpectedKeys(FilledArguments $arguments, Node $node): Node
private function checkForUnexpectedKeys(FilledArguments $arguments, TreeNode $node): TreeNode
{
$superfluousKeys = $arguments->superfluousKeys();

View File

@ -6,7 +6,6 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use BackedEnum;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidEnumValue;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Types\EnumType;
use Stringable;
@ -28,7 +27,7 @@ final class EnumNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
$value = $shell->value();
@ -37,7 +36,7 @@ final class EnumNodeBuilder implements NodeBuilder
foreach ($type->className()::cases() as $case) {
if ($this->valueMatchesEnumCase($value, $case)) {
return Node::leaf($shell, $case);
return TreeNode::leaf($shell, $case);
}
}

View File

@ -7,7 +7,6 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Message\ErrorMessage;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use Throwable;
@ -28,7 +27,7 @@ final class ErrorCatcherNodeBuilder implements NodeBuilder
$this->exceptionFilter = $exceptionFilter;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
try {
return $this->delegate->build($shell, $rootBuilder);
@ -37,7 +36,7 @@ final class ErrorCatcherNodeBuilder implements NodeBuilder
$exception = ($this->exceptionFilter)($exception->previous());
}
return Node::error($shell, $exception);
return TreeNode::error($shell, $exception);
}
}
}

View File

@ -10,7 +10,6 @@ use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Object\FilledArguments;
use CuyZ\Valinor\Mapper\Tree\Exception\ObjectImplementationCallbackError;
use CuyZ\Valinor\Mapper\Tree\Message\UserlandError;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Types\ClassType;
use CuyZ\Valinor\Type\Types\InterfaceType;
@ -44,7 +43,7 @@ final class InterfaceNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
@ -63,7 +62,7 @@ final class InterfaceNodeBuilder implements NodeBuilder
foreach ($children as $child) {
if (! $child->isValid()) {
return Node::branch($shell, null, $children);
return TreeNode::branch($shell, null, $children);
}
$values[] = $child->value();
@ -82,7 +81,7 @@ final class InterfaceNodeBuilder implements NodeBuilder
}
/**
* @return Node[]
* @return TreeNode[]
*/
private function children(Shell $shell, FilledArguments $arguments, RootNodeBuilder $rootBuilder): array
{

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use function is_array;
@ -21,7 +20,7 @@ final class IterableNodeBuilder implements NodeBuilder
$this->delegate = $delegate;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
if ($shell->hasValue()) {
$value = $shell->value();

View File

@ -6,7 +6,6 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidListKey;
use CuyZ\Valinor\Mapper\Tree\Exception\SourceMustBeIterable;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\CompositeTraversableType;
use CuyZ\Valinor\Type\Types\ListType;
@ -25,7 +24,7 @@ final class ListNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
$value = $shell->hasValue() ? $shell->value() : null;
@ -33,7 +32,7 @@ final class ListNodeBuilder implements NodeBuilder
assert($type instanceof ListType || $type instanceof NonEmptyListType);
if (null === $value && $this->flexible) {
return Node::branch($shell, [], []);
return TreeNode::branch($shell, [], []);
}
if (! is_array($value)) {
@ -43,11 +42,11 @@ final class ListNodeBuilder implements NodeBuilder
$children = $this->children($type, $shell, $rootBuilder);
$array = $this->buildArray($children);
return Node::branch($shell, $array, $children);
return TreeNode::branch($shell, $array, $children);
}
/**
* @return array<Node>
* @return array<TreeNode>
*/
private function children(CompositeTraversableType $type, Shell $shell, RootNodeBuilder $rootBuilder): array
{
@ -64,7 +63,7 @@ final class ListNodeBuilder implements NodeBuilder
$children[$expected] = $rootBuilder->build($child->withValue($value));
} else {
$child = $shell->child((string)$key, $subType);
$children[$key] = Node::error($child, new InvalidListKey($key, $expected));
$children[$key] = TreeNode::error($child, new InvalidListKey($key, $expected));
}
$expected++;
@ -74,7 +73,7 @@ final class ListNodeBuilder implements NodeBuilder
}
/**
* @param array<Node> $children
* @param array<TreeNode> $children
* @return mixed[]|null
*/
private function buildArray(array $children): ?array

View File

@ -2,11 +2,10 @@
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
/** @internal */
interface NodeBuilder
{
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node;
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode;
}

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
/** @internal */
@ -17,7 +16,7 @@ final class RootNodeBuilder
$this->root = $root;
}
public function build(Shell $shell): Node
public function build(Shell $shell): TreeNode
{
return $this->root->build($shell, $this);
}

View File

@ -6,7 +6,6 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\CannotCastToScalarValue;
use CuyZ\Valinor\Mapper\Tree\Exception\ValueNotAcceptedByScalarType;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\ScalarType;
@ -22,7 +21,7 @@ final class ScalarNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
$value = $shell->hasValue() ? $shell->value() : null;
@ -37,6 +36,6 @@ final class ScalarNodeBuilder implements NodeBuilder
throw new CannotCastToScalarValue($value, $type);
}
return Node::leaf($shell, $type->cast($value));
return TreeNode::leaf($shell, $type->cast($value));
}
}

View File

@ -7,7 +7,6 @@ namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\ShapedArrayElementMissing;
use CuyZ\Valinor\Mapper\Tree\Exception\SourceMustBeIterable;
use CuyZ\Valinor\Mapper\Tree\Exception\UnexpectedShapedArrayKeys;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Types\ShapedArrayType;
@ -27,7 +26,7 @@ final class ShapedArrayNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();
$value = $shell->hasValue() ? $shell->value() : null;
@ -41,11 +40,11 @@ final class ShapedArrayNodeBuilder implements NodeBuilder
$children = $this->children($type, $shell, $rootBuilder);
$array = $this->buildArray($children);
return Node::branch($shell, $array, $children);
return TreeNode::branch($shell, $array, $children);
}
/**
* @return array<Node>
* @return array<TreeNode>
*/
private function children(ShapedArrayType $type, Shell $shell, RootNodeBuilder $rootBuilder): array
{
@ -62,7 +61,7 @@ final class ShapedArrayNodeBuilder implements NodeBuilder
if (! array_key_exists($key, $value)) {
if (! $element->isOptional()) {
$children[$key] = Node::error($child, new ShapedArrayElementMissing($element));
$children[$key] = TreeNode::error($child, new ShapedArrayElementMissing($element));
}
continue;
@ -82,7 +81,7 @@ final class ShapedArrayNodeBuilder implements NodeBuilder
}
/**
* @param array<Node> $children
* @param array<TreeNode> $children
* @return mixed[]|null
*/
private function buildArray(array $children): ?array

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Mapper\Tree\Visitor\ShellVisitor;
@ -22,7 +21,7 @@ final class ShellVisitorNodeBuilder implements NodeBuilder
$this->visitors = $visitors;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
foreach ($this->visitors as $visitor) {
$shell = $visitor->visit($shell);

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\MissingNodeValue;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Utility\TypeHelper;
@ -22,7 +21,7 @@ final class StrictNodeBuilder implements NodeBuilder
$this->flexible = $flexible;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
if (! $this->flexible) {
TypeHelper::checkPermissiveType($shell->type());
@ -30,7 +29,7 @@ final class StrictNodeBuilder implements NodeBuilder
if (! $shell->hasValue()) {
if ($this->flexible && $shell->type()->accepts(null)) {
return Node::leaf($shell, null);
return TreeNode::leaf($shell, null);
}
throw new MissingNodeValue($shell->type());

View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Exception\DuplicatedNodeChild;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeValue;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use Throwable;
use function assert;
/** @internal */
final class TreeNode
{
private Shell $shell;
/** @var mixed */
private $value;
/** @var array<self> */
private array $children = [];
/** @var array<Message> */
private array $messages = [];
private bool $valid = true;
/**
* @param mixed $value
*/
private function __construct(Shell $shell, $value)
{
$this->shell = $shell;
$this->value = $value;
}
/**
* @param mixed $value
*/
public static function leaf(Shell $shell, $value): self
{
$instance = new self($shell, $value);
$instance->check();
return $instance;
}
/**
* @param mixed $value
* @param array<self> $children
*/
public static function branch(Shell $shell, $value, array $children): self
{
$instance = new self($shell, $value);
foreach ($children as $child) {
$name = $child->name();
if (isset($instance->children[$name])) {
throw new DuplicatedNodeChild($name);
}
$instance->children[$name] = $child;
}
$instance->check();
return $instance;
}
/**
* @param Throwable&Message $message
*/
public static function error(Shell $shell, Throwable $message): self
{
return (new self($shell, null))->withMessage($message);
}
public function name(): string
{
return $this->shell->name();
}
public function isValid(): bool
{
return $this->valid;
}
/**
* @param mixed $value
*/
public function withValue($value): self
{
$clone = clone $this;
$clone->value = $value;
$clone->check();
return $clone;
}
/**
* @return mixed
*/
public function value()
{
assert($this->valid, "Trying to get value of an invalid node at path `{$this->shell->path()}`.");
return $this->value;
}
public function withMessage(Message $message): self
{
$clone = clone $this;
$clone->messages[] = $message;
$clone->valid = $clone->valid && ! $message instanceof Throwable;
return $clone;
}
/**
* @return array<self>
*/
public function children(): array
{
return $this->children;
}
public function node(): Node
{
return $this->buildNode($this);
}
private function check(): void
{
foreach ($this->children as $child) {
if (! $child->valid) {
$this->valid = false;
return;
}
}
if ($this->valid && ! $this->shell->type()->accepts($this->value)) {
throw new InvalidNodeValue($this->value, $this->shell->type());
}
}
private function buildNode(self $self): Node
{
return new Node(
$self->shell->isRoot(),
$self->shell->name(),
$self->shell->path(),
(string)$self->shell->type(),
$this->shell->hasValue(),
$this->shell->hasValue() ? $this->shell->value() : null,
$self->valid ? $self->value : null,
$self->messages,
array_map(
fn (self $child) => $self->buildNode($child),
$self->children
)
);
}
}

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Resolver\Union\UnionNarrower;
use CuyZ\Valinor\Type\Types\UnionType;
@ -22,7 +21,7 @@ final class UnionNodeBuilder implements NodeBuilder
$this->unionNarrower = $unionNarrower;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$type = $shell->type();

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Builder;
use CuyZ\Valinor\Definition\FunctionsContainer;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Shell;
/** @internal */
@ -21,7 +20,7 @@ final class ValueAlteringNodeBuilder implements NodeBuilder
$this->functions = $functions;
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
$node = $this->delegate->build($shell, $rootBuilder);

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Exception;
use CuyZ\Valinor\Mapper\Tree\Node;
use LogicException;
/** @internal */
final class CannotGetInvalidNodeValue extends LogicException
{
public function __construct(Node $node)
{
parent::__construct(
"Trying to get value of an invalid node at path `{$node->path()}`.",
1630680246
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Exception;
use RuntimeException;
/** @internal */
final class InvalidNodeHasNoMappedValue extends RuntimeException
{
public function __construct(string $path)
{
parent::__construct(
"Cannot get mapped value for invalid node at path `$path`; use method `\$node->isValid()`.",
1657466305
);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Exception;
use RuntimeException;
/** @internal */
final class SourceValueWasNotFilled extends RuntimeException
{
public function __construct(string $path)
{
parent::__construct(
"Source was not filled at path `$path`; use method `\$node->sourceFilled()`.",
1657466107
);
}
}

View File

@ -37,7 +37,7 @@ use function is_callable;
* 'Some message content' => 'New content / previous: {original_message}',
*
* // Will match if the given message is an instance of `SomeError`
* SomeError::class => 'New content / value: {original_value}',
* SomeError::class => 'New content / value: {source_value}',
*
* // A callback can be used to get access to the message instance
* OtherError::class => function (NodeMessage $message): string {

View File

@ -5,7 +5,6 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Message\Formatter;
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
use CuyZ\Valinor\Utility\TypeHelper;
use Throwable;
/**
@ -44,9 +43,9 @@ final class PlaceHolderMessageFormatter implements MessageFormatter
$body = sprintf($message->body(), ...$this->values ?: [
$message->code(),
$originalMessage instanceof Throwable ? $originalMessage->getMessage() : $originalMessage->__toString(),
TypeHelper::dump($message->type()),
$message->name(),
$message->path(),
"`{$message->node()->type()}`",
$message->node()->name(),
$message->node()->path(),
]);
return $message->withBody($body);

View File

@ -4,18 +4,15 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree\Message;
use CuyZ\Valinor\Definition\Attributes;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Utility\String\StringFormatter;
use CuyZ\Valinor\Utility\TypeHelper;
use CuyZ\Valinor\Utility\ValueDumper;
use Throwable;
/** @api */
final class NodeMessage implements Message, HasCode
{
private Shell $shell;
private Node $node;
private Message $message;
@ -23,9 +20,9 @@ final class NodeMessage implements Message, HasCode
private string $locale = StringFormatter::DEFAULT_LOCALE;
public function __construct(Shell $shell, Message $message)
public function __construct(Node $node, Message $message)
{
$this->shell = $shell;
$this->node = $node;
$this->message = $message;
if ($this->message instanceof TranslatableMessage) {
@ -37,6 +34,11 @@ final class NodeMessage implements Message, HasCode
}
}
public function node(): Node
{
return $this->node;
}
public function withLocale(string $locale): self
{
$clone = clone $this;
@ -63,32 +65,38 @@ final class NodeMessage implements Message, HasCode
return $this->body;
}
/**
* @deprecated use `$message->node()->name()` instead
*/
public function name(): string
{
return $this->shell->name();
}
public function path(): string
{
return $this->shell->path();
}
public function type(): Type
{
return $this->shell->type();
}
public function attributes(): Attributes
{
return $this->shell->attributes();
return $this->node->name();
}
/**
* @deprecated use `$message->node()->path()` instead
*/
public function path(): string
{
return $this->node->path();
}
/**
* @deprecated use `$message->node()->type()` instead
*/
public function type(): string
{
return $this->node->type();
}
/**
* @deprecated use `$message->node()->mappedValue()` instead
*
* @return mixed
*/
public function value()
{
return $this->shell->value();
return $this->node->mappedValue();
}
public function originalMessage(): Message
@ -126,10 +134,11 @@ final class NodeMessage implements Message, HasCode
{
$parameters = [
'message_code' => $this->code(),
'node_name' => $this->shell->name(),
'node_path' => $this->shell->path(),
'node_type' => TypeHelper::dump($this->shell->type()),
'original_value' => ValueDumper::dump($this->shell->hasValue() ? $this->shell->value() : '*missing*'),
'node_name' => $this->node->name(),
'node_path' => $this->node->path(),
'node_type' => "`{$this->node->type()}`",
'source_value' => $sourceValue = $this->node->sourceFilled() ? ValueDumper::dump($this->node->sourceValue()) : '*missing*',
'original_value' => $sourceValue, // @deprecated
'original_message' => $this->message instanceof Throwable ? $this->message->getMessage() : $this->message->__toString(),
];

View File

@ -4,148 +4,134 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Mapper\Tree;
use CuyZ\Valinor\Definition\Attributes;
use CuyZ\Valinor\Mapper\Tree\Exception\CannotGetInvalidNodeValue;
use CuyZ\Valinor\Mapper\Tree\Exception\DuplicatedNodeChild;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeValue;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeHasNoMappedValue;
use CuyZ\Valinor\Mapper\Tree\Exception\SourceValueWasNotFilled;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
use CuyZ\Valinor\Type\Type;
use Throwable;
/** @api */
final class Node
{
private Shell $shell;
private bool $isRoot;
private string $name;
private string $path;
private string $type;
private bool $sourceFilled;
/** @var mixed */
private $value;
private $sourceValue;
/** @var array<Node> */
private array $children = [];
private bool $isValid = true;
/** @var mixed */
private $mappedValue;
/** @var array<NodeMessage> */
private array $messages = [];
private bool $valid = true;
/** @var array<self> */
private array $children;
/**
* @param mixed $value
* @param mixed $sourceValue
* @param mixed $mappedValue
* @param array<Message> $messages
* @param array<self> $children
*/
private function __construct(Shell $shell, $value)
{
$this->shell = $shell;
$this->value = $value;
}
public function __construct(
bool $isRoot,
string $name,
string $path,
string $type,
bool $sourceFilled,
$sourceValue,
$mappedValue,
array $messages,
array $children
) {
$this->isRoot = $isRoot;
$this->name = $name;
$this->path = $path;
$this->type = $type;
$this->sourceFilled = $sourceFilled;
$this->sourceValue = $sourceValue;
$this->mappedValue = $mappedValue;
$this->children = $children;
/**
* @param mixed $value
*/
public static function leaf(Shell $shell, $value): self
{
$instance = new self($shell, $value);
$instance->check();
foreach ($messages as $message) {
$message = new NodeMessage($this, $message);
return $instance;
}
/**
* @param mixed $value
* @param array<Node> $children
*/
public static function branch(Shell $shell, $value, array $children): self
{
$instance = new self($shell, $value);
foreach ($children as $child) {
$name = $child->name();
if (isset($instance->children[$name])) {
throw new DuplicatedNodeChild($name);
}
$instance->children[$name] = $child;
$this->messages[] = $message;
$this->isValid = $this->isValid && ! $message->isError();
}
$instance->check();
return $instance;
}
/**
* @param Throwable&Message $message
*/
public static function error(Shell $shell, Throwable $message): self
{
return (new self($shell, null))->withMessage($message);
}
public function name(): string
{
return $this->shell->name();
}
public function isRoot(): bool
{
return $this->shell->isRoot();
return $this->isRoot;
}
public function name(): string
{
return $this->name;
}
public function path(): string
{
return $this->shell->path();
return $this->path;
}
public function type(): Type
public function type(): string
{
return $this->shell->type();
return $this->type;
}
public function attributes(): Attributes
public function sourceFilled(): bool
{
return $this->shell->attributes();
}
/**
* @param mixed $value
*/
public function withValue($value): self
{
$clone = clone $this;
$clone->value = $value;
$clone->check();
return $clone;
return $this->sourceFilled;
}
/**
* @return mixed
*/
public function value()
public function sourceValue()
{
if (! $this->valid) {
throw new CannotGetInvalidNodeValue($this);
if (! $this->sourceFilled) {
throw new SourceValueWasNotFilled($this->path);
}
return $this->value;
return $this->sourceValue;
}
public function isValid(): bool
{
return $this->isValid;
}
/**
* @return array<Node>
* @return mixed
*/
public function children(): array
public function mappedValue()
{
return $this->children;
if (! $this->isValid) {
throw new InvalidNodeHasNoMappedValue($this->path);
}
return $this->mappedValue;
}
public function withMessage(Message $message): self
/**
* @deprecated use `$node->mappedValue()` instead
*
* @return mixed
*/
public function value()
{
$message = new NodeMessage($this->shell, $message);
$clone = clone $this;
$clone->messages[] = $message;
$clone->valid = $clone->valid && ! $message->isError();
return $clone;
return $this->mappedValue();
}
/**
@ -156,23 +142,11 @@ final class Node
return $this->messages;
}
public function isValid(): bool
/**
* @return array<self>
*/
public function children(): array
{
return $this->valid;
}
private function check(): void
{
foreach ($this->children as $child) {
if (! $child->valid) {
$this->valid = false;
return;
}
}
if ($this->valid && ! $this->shell->type()->accepts($this->value)) {
throw new InvalidNodeValue($this->value, $this->shell->type());
}
return $this->children;
}
}

View File

@ -6,7 +6,7 @@ namespace CuyZ\Valinor\Mapper;
use CuyZ\Valinor\Mapper\Exception\InvalidMappingTypeSignature;
use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Builder\TreeNode;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Parser\TypeParser;
@ -29,7 +29,7 @@ final class TreeMapperContainer implements TreeMapper
$node = $this->node($signature, $source);
if (! $node->isValid()) {
throw new MappingError($node);
throw new MappingError($node->node());
}
return $node->value();
@ -38,7 +38,7 @@ final class TreeMapperContainer implements TreeMapper
/**
* @param mixed $source
*/
private function node(string $signature, $source): Node
private function node(string $signature, $source): TreeNode
{
try {
$type = $this->typeParser->parse($signature);

View File

@ -6,16 +6,16 @@ namespace CuyZ\Valinor\Tests\Fake\Mapper\Tree\Builder;
use CuyZ\Valinor\Mapper\Tree\Builder\NodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Builder\RootNodeBuilder;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\Builder\TreeNode;
use CuyZ\Valinor\Mapper\Tree\Shell;
final class FakeNodeBuilder implements NodeBuilder
{
/** @var null|callable(Shell): Node */
/** @var null|callable(Shell): TreeNode */
private $callback;
/**
* @param null|callable(Shell): Node $callback
* @param null|callable(Shell): TreeNode $callback
*/
public function __construct(callable $callback = null)
{
@ -24,12 +24,12 @@ final class FakeNodeBuilder implements NodeBuilder
}
}
public function build(Shell $shell, RootNodeBuilder $rootBuilder): Node
public function build(Shell $shell, RootNodeBuilder $rootBuilder): TreeNode
{
if (isset($this->callback)) {
return ($this->callback)($shell);
}
return Node::leaf($shell, $shell->value());
return TreeNode::leaf($shell, $shell->value());
}
}

View File

@ -2,20 +2,21 @@
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fake\Mapper;
namespace CuyZ\Valinor\Tests\Fake\Mapper\Tree\Builder;
use CuyZ\Valinor\Definition\Attributes;
use CuyZ\Valinor\Mapper\Tree\Builder\TreeNode;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Tests\Fake\Definition\FakeAttributes;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeShell;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Type\Type;
use Throwable;
final class FakeNode
final class FakeTreeNode
{
public static function any(): Node
public static function any(): TreeNode
{
return self::leaf(FakeType::permissive(), []);
}
@ -23,18 +24,18 @@ final class FakeNode
/**
* @param mixed $value
*/
public static function leaf(Type $type, $value): Node
public static function leaf(Type $type, $value): TreeNode
{
$shell = FakeShell::new($type, $value);
return Node::leaf($shell, $value);
return TreeNode::leaf($shell, $value);
}
/**
* @param array<array{name?: string, type?: Type, value?: mixed, attributes?: Attributes, message?: Message}> $children
* @param mixed $value
*/
public static function branch(array $children, Type $type = null, $value = null): Node
public static function branch(array $children, Type $type = null, $value = null): TreeNode
{
$shell = FakeShell::new($type ?? FakeType::permissive(), $value);
$nodes = [];
@ -47,7 +48,7 @@ final class FakeNode
$child['attributes'] ?? new FakeAttributes(),
)->withValue($childValue);
$node = Node::leaf($childShell, $childValue);
$node = TreeNode::leaf($childShell, $childValue);
if (isset($child['message'])) {
$node = $node->withMessage($child['message']);
@ -56,16 +57,16 @@ final class FakeNode
$nodes[] = $node;
}
return Node::branch($shell, $value, $nodes);
return TreeNode::branch($shell, $value, $nodes);
}
/**
* @param Throwable&Message $error
*/
public static function error(Throwable $error = null): Node
public static function error(Throwable $error = null): TreeNode
{
$shell = FakeShell::new(FakeType::permissive(), []);
return Node::error($shell, $error ?? new FakeErrorMessage());
return TreeNode::error($shell, $error ?? new FakeErrorMessage());
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Fake\Mapper\Tree;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Node;
final class FakeNode
{
public static function any(): Node
{
return self::build([], []);
}
/**
* @param array<Node> $children
*/
public static function branch(array $children): Node
{
return self::build([], $children);
}
public static function withMessage(Message $message): Node
{
return self::build([$message], []);
}
/**
* @param array<Message> $messages
* @param array<Node> $children
*/
private static function build(array $messages, array $children): Node
{
return new Node(
true,
'nodeName',
'some.node.path',
'string',
true,
'some source value',
'some value',
$messages,
$children,
);
}
}

View File

@ -6,17 +6,25 @@ namespace CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeShell;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
final class FakeNodeMessage
{
public static function any(): NodeMessage
{
return self::with(new FakeMessage());
return self::build(new FakeMessage());
}
public static function with(Message $message): NodeMessage
public static function withMessage(Message $message): NodeMessage
{
return new NodeMessage(FakeShell::any(), $message);
return self::build($message);
}
private static function build(Message $message): NodeMessage
{
return new NodeMessage(
FakeNode::any(),
$message
);
}
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
use PHPUnit\Framework\TestCase;
final class MappingErrorTest extends TestCase

View File

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Builder;
use AssertionError;
use CuyZ\Valinor\Mapper\Tree\Builder\TreeNode;
use CuyZ\Valinor\Mapper\Tree\Exception\DuplicatedNodeChild;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeValue;
use CuyZ\Valinor\Mapper\Tree\Shell;
use CuyZ\Valinor\Tests\Fake\Definition\FakeAttributes;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Builder\FakeTreeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use PHPUnit\Framework\TestCase;
use stdClass;
final class TreeNodeTest extends TestCase
{
public function test_node_leaf_values_can_be_retrieved(): void
{
$type = FakeType::permissive();
$shell = Shell::root($type, 'some source value');
$node = TreeNode::leaf($shell, 'some value')->node();
self::assertTrue($node->isRoot());
self::assertSame('', $node->name());
self::assertSame('', $node->path());
self::assertSame((string)$type, $node->type());
self::assertTrue($node->sourceFilled());
self::assertSame('some source value', $node->sourceValue());
self::assertTrue($node->isValid());
self::assertSame('some value', $node->mappedValue());
}
public function test_node_leaf_with_incorrect_value_throws_exception(): void
{
$type = new FakeType();
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 'foo' does not match type `$type`.");
FakeTreeNode::leaf($type, 'foo');
}
public function test_node_branch_values_can_be_retrieved(): void
{
$typeChildA = FakeType::permissive();
$typeChildB = FakeType::permissive();
$attributesChildA = new FakeAttributes();
$attributesChildB = new FakeAttributes();
$children = FakeTreeNode::branch([
'foo' => ['type' => $typeChildA, 'value' => 'foo', 'attributes' => $attributesChildA],
'bar' => ['type' => $typeChildB, 'value' => 'bar', 'attributes' => $attributesChildB],
])->children();
self::assertSame('foo', $children['foo']->node()->name());
self::assertSame('foo', $children['foo']->node()->path());
self::assertFalse($children['foo']->node()->isRoot());
self::assertSame('bar', $children['bar']->node()->name());
self::assertSame('bar', $children['bar']->node()->path());
self::assertFalse($children['bar']->node()->isRoot());
}
public function test_node_branch_with_duplicated_child_name_throws_exception(): void
{
$this->expectException(DuplicatedNodeChild::class);
$this->expectExceptionCode(1634045114);
$this->expectExceptionMessage('The child `foo` is duplicated in the branch.');
FakeTreeNode::branch([
['name' => 'foo'],
['name' => 'foo'],
]);
}
public function test_node_branch_with_incorrect_value_throws_exception(): void
{
$type = new FakeType();
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 'foo' does not match type `$type`.");
FakeTreeNode::branch([], $type, 'foo');
}
public function test_node_error_values_can_be_retrieved(): void
{
$message = new FakeErrorMessage();
$node = FakeTreeNode::error($message);
self::assertFalse($node->isValid());
self::assertSame('some error message', (string)$node->node()->messages()[0]);
}
public function test_get_value_from_invalid_node_throws_exception(): void
{
$node = FakeTreeNode::error();
$this->expectException(AssertionError::class);
$node->value();
}
public function test_branch_node_with_invalid_child_is_invalid(): void
{
$node = FakeTreeNode::branch([
'foo' => [],
'bar' => ['message' => new FakeErrorMessage()],
]);
self::assertFalse($node->isValid());
}
public function test_node_with_value_returns_node_with_value(): void
{
$nodeA = FakeTreeNode::any();
$nodeB = $nodeA->withValue('bar');
self::assertNotSame($nodeA, $nodeB);
self::assertSame('bar', $nodeB->value());
self::assertTrue($nodeB->isValid());
}
public function test_node_with_invalid_value_returns_invalid_node(): void
{
$type = FakeType::accepting('foo');
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 1337 does not match type `$type`.");
FakeTreeNode::leaf($type, 'foo')->withValue(1337);
}
public function test_node_with_invalid_value_for_object_type_returns_invalid_node(): void
{
$object = new stdClass();
$type = FakeObjectType::accepting($object);
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage('Invalid value 1337.');
FakeTreeNode::leaf($type, $object)->withValue(1337);
}
public function test_node_with_messages_returns_node_with_messages(): void
{
$messageA = new FakeMessage('some message A');
$messageB = new FakeMessage('some message B');
$nodeA = FakeTreeNode::any();
$nodeB = $nodeA->withMessage($messageA)->withMessage($messageB);
self::assertNotSame($nodeA, $nodeB);
self::assertTrue($nodeB->isValid());
self::assertSame('some message A', (string)$nodeB->node()->messages()[0]);
self::assertSame('some message B', (string)$nodeB->node()->messages()[1]);
}
public function test_node_with_error_message_returns_invalid_node(): void
{
$message = new FakeMessage();
$errorMessage = new FakeErrorMessage();
$nodeA = FakeTreeNode::any();
$nodeB = $nodeA->withMessage($message)->withMessage($errorMessage);
self::assertNotSame($nodeA, $nodeB);
self::assertFalse($nodeB->isValid());
self::assertSame('some message', (string)$nodeB->node()->messages()[0]);
self::assertSame('some error message', (string)$nodeB->node()->messages()[1]);
}
}

View File

@ -5,32 +5,26 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Message\Formatter;
use CuyZ\Valinor\Mapper\Tree\Message\Formatter\PlaceHolderMessageFormatter;
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeShell;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeNodeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\Formatter\FakeMessageFormatter;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use PHPUnit\Framework\TestCase;
final class PlaceHolderMessageFormatterTest extends TestCase
{
public function test_format_message_replaces_placeholders_with_default_values(): void
{
$type = FakeType::permissive();
$shell = FakeShell::any()->child('foo', $type)->withValue('some value');
$message = new NodeMessage($shell, new FakeMessage('some message'));
$message = FakeNodeMessage::withMessage(new FakeMessage('some message'));
$message = (new FakeMessageFormatter('%1$s / %2$s / %3$s / %4$s / %5$s'))->format($message);
$message = (new PlaceHolderMessageFormatter())->format($message);
self::assertSame("some_code / some message / `$type` / foo / foo", (string)$message);
self::assertSame("some_code / some message / `string` / nodeName / some.node.path", (string)$message);
}
public function test_format_message_replaces_correct_original_value_if_throwable(): void
public function test_format_message_replaces_correct_source_value_if_throwable(): void
{
$message = FakeNodeMessage::with(new FakeErrorMessage('some error message'));
$message = FakeNodeMessage::withMessage(new FakeErrorMessage('some error message'));
$message = (new FakeMessageFormatter('original: %2$s'))->format($message);
$message = (new PlaceHolderMessageFormatter())->format($message);
@ -41,7 +35,7 @@ final class PlaceHolderMessageFormatterTest extends TestCase
{
$formatter = new PlaceHolderMessageFormatter('foo', 'bar');
$message = FakeNodeMessage::with(new FakeMessage('%1$s / %2$s'));
$message = FakeNodeMessage::withMessage(new FakeMessage('%1$s / %2$s'));
$message = $formatter->format($message);
self::assertSame('foo / bar', (string)$message);

View File

@ -20,7 +20,7 @@ final class TranslationMessageFormatterTest extends TestCase
],
]);
$message = FakeNodeMessage::with(new FakeMessage('some key'));
$message = FakeNodeMessage::withMessage(new FakeMessage('some key'));
$message = $formatter->format($message);
self::assertSame('some message', (string)$message);
@ -38,7 +38,7 @@ final class TranslationMessageFormatterTest extends TestCase
'some other message'
);
$message = FakeNodeMessage::with(new FakeMessage('some key'));
$message = FakeNodeMessage::withMessage(new FakeMessage('some key'));
$message = $formatter->format($message);
self::assertSame('some other message', (string)$message);
@ -57,7 +57,7 @@ final class TranslationMessageFormatterTest extends TestCase
],
]);
$message = FakeNodeMessage::with(new FakeMessage('some key'));
$message = FakeNodeMessage::withMessage(new FakeMessage('some key'));
$message = $formatter->format($message);
self::assertSame('some other message', (string)$message);
@ -79,7 +79,7 @@ final class TranslationMessageFormatterTest extends TestCase
],
]);
$message = FakeNodeMessage::with(new FakeMessage('some other key'));
$message = FakeNodeMessage::withMessage(new FakeMessage('some other key'));
$message = $formatter->format($message);
self::assertSame('some other message', (string)$message);
@ -94,7 +94,7 @@ final class TranslationMessageFormatterTest extends TestCase
);
$originalMessage = new FakeTranslatableMessage('Value {value} is not accepted.', ['value' => 'foo']);
$message = FakeNodeMessage::with($originalMessage);
$message = FakeNodeMessage::withMessage($originalMessage);
$message = $formatter->format($message);
self::assertSame('Value foo is not accepted!', (string)$message);

View File

@ -5,7 +5,8 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Message;
use CuyZ\Valinor\Mapper\Tree\Message\MessagesFlattener;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeNode;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
use PHPUnit\Framework\TestCase;
@ -18,10 +19,20 @@ final class MessagesFlattenerTest extends TestCase
$errorA = new FakeErrorMessage('some error message A');
$errorB = new FakeErrorMessage('some error message B');
$node = FakeNode::branch([
'foo' => ['message' => $messageA],
'bar' => ['message' => $errorA],
])->withMessage($errorB);
$node = new Node(
true,
'nodeName',
'some.node.path',
'string',
true,
'some source value',
'some value',
[$errorB],
[
'foo' => FakeNode::withMessage($messageA),
'bar' => FakeNode::withMessage($errorA),
]
);
$messages = [...(new MessagesFlattener($node))->errors()];

View File

@ -6,14 +6,11 @@ namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree\Message;
use CuyZ\Valinor\Mapper\Tree\Message\Message;
use CuyZ\Valinor\Mapper\Tree\Message\NodeMessage;
use CuyZ\Valinor\Tests\Fake\Definition\FakeAttributes;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeShell;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeNodeMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeTranslatableMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\Formatter\FakeMessageFormatter;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use PHPUnit\Framework\TestCase;
final class NodeMessageTest extends TestCase
@ -21,24 +18,27 @@ final class NodeMessageTest extends TestCase
public function test_node_properties_can_be_accessed(): void
{
$originalMessage = new FakeMessage();
$type = FakeType::permissive();
$attributes = new FakeAttributes();
$shell = FakeShell::any()->child('foo', $type, $attributes)->withValue('some value');
$message = new NodeMessage($shell, $originalMessage);
$message = new NodeMessage(FakeNode::any(), $originalMessage);
self::assertSame('foo', $message->name());
self::assertSame('foo', $message->path());
self::assertSame('nodeName', $message->node()->name());
self::assertSame('nodeName', $message->name());
self::assertSame('some.node.path', $message->node()->path());
self::assertSame('some.node.path', $message->path());
self::assertSame('string', $message->node()->type());
self::assertSame('string', $message->type());
self::assertSame('some source value', $message->node()->sourceValue());
self::assertSame('some value', $message->node()->mappedValue());
self::assertSame('some value', $message->node()->value());
self::assertSame('some value', $message->value());
self::assertSame($type, $message->type());
self::assertSame($attributes, $message->attributes());
self::assertSame($originalMessage, $message->originalMessage());
self::assertFalse($message->isError());
}
public function test_message_is_error_if_original_message_is_throwable(): void
{
$originalMessage = new FakeErrorMessage();
$message = new NodeMessage(FakeShell::any(), $originalMessage);
$message = FakeNodeMessage::withMessage($originalMessage);
self::assertTrue($message->isError());
self::assertSame('1652883436', $message->code());
@ -48,34 +48,23 @@ final class NodeMessageTest extends TestCase
public function test_parameters_are_replaced_in_body(): void
{
$originalMessage = new FakeTranslatableMessage('some original message', ['some_parameter' => 'some parameter value']);
$type = FakeType::permissive();
$shell = FakeShell::any()->child('foo', $type)->withValue('some value');
$message = new NodeMessage($shell, $originalMessage);
$message = $message->withBody('{message_code} / {node_name} / {node_path} / {node_type} / {original_value} / {original_message} / {some_parameter}');
$message = new NodeMessage(FakeNode::any(), $originalMessage);
$message = $message->withBody('{message_code} / {node_name} / {node_path} / {node_type} / {original_value} / {source_value} / {original_message} / {some_parameter}');
self::assertSame("1652902453 / foo / foo / `$type` / 'some value' / some original message (toString) / some parameter value", (string)$message);
self::assertSame("1652902453 / nodeName / some.node.path / `string` / 'some source value' / 'some source value' / some original message (toString) / some parameter value", (string)$message);
}
public function test_replaces_correct_original_message_if_throwable(): void
{
$message = new NodeMessage(FakeShell::any(), new FakeErrorMessage('some error message'));
$originalMessage = new FakeErrorMessage('some error message');
$message = FakeNodeMessage::withMessage($originalMessage);
$message = $message->withBody('original: {original_message}');
self::assertSame('original: some error message', (string)$message);
}
public function test_format_message_uses_formatter_to_replace_content(): void
{
$originalMessage = new FakeMessage('some message');
$message = new NodeMessage(FakeShell::any(), $originalMessage);
$formattedMessage = (new FakeMessageFormatter())->format($message);
self::assertNotSame($message, $formattedMessage);
self::assertSame('formatted: some message', (string)$formattedMessage);
}
public function test_custom_body_returns_clone(): void
{
$messageA = FakeNodeMessage::any();
@ -96,7 +85,7 @@ final class NodeMessageTest extends TestCase
{
$originalMessage = new FakeTranslatableMessage('un message: {value, spellout}', ['value' => '42']);
$message = new NodeMessage(FakeShell::any(), $originalMessage);
$message = FakeNodeMessage::withMessage($originalMessage);
$message = $message->withLocale('fr');
self::assertSame('un message: quarante-deux', (string)$message);
@ -111,7 +100,7 @@ final class NodeMessageTest extends TestCase
}
};
$message = new NodeMessage(FakeShell::any(), $originalMessage);
$message = FakeNodeMessage::withMessage($originalMessage);
self::assertSame('unknown', $message->code());
}

View File

@ -4,177 +4,99 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree;
use CuyZ\Valinor\Mapper\Tree\Exception\CannotGetInvalidNodeValue;
use CuyZ\Valinor\Mapper\Tree\Exception\DuplicatedNodeChild;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeValue;
use CuyZ\Valinor\Tests\Fake\Definition\FakeAttributes;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeNode;
use CuyZ\Valinor\Mapper\Tree\Exception\InvalidNodeHasNoMappedValue;
use CuyZ\Valinor\Mapper\Tree\Exception\SourceValueWasNotFilled;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeErrorMessage;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\Message\FakeMessage;
use CuyZ\Valinor\Tests\Fake\Type\FakeObjectType;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use PHPUnit\Framework\TestCase;
use stdClass;
final class NodeTest extends TestCase
{
public function test_node_leaf_values_can_be_retrieved(): void
public function test_properties_can_be_accessed(): void
{
$type = FakeType::permissive();
$value = 'some node value';
$message = new FakeMessage('some message');
$child = FakeNode::any();
$node = FakeNode::leaf($type, $value);
$node = new Node(
true,
'nodeName',
'some.node.path',
'string',
true,
'some source value',
'some value',
[$message],
[$child]
);
self::assertSame($type, $node->type());
self::assertSame($value, $node->value());
self::assertTrue($node->isRoot());
self::assertTrue($node->isValid());
self::assertSame(true, $node->isRoot());
self::assertSame('nodeName', $node->name());
self::assertSame('some.node.path', $node->path());
self::assertSame('string', $node->type());
self::assertSame('some source value', $node->sourceValue());
self::assertSame('some value', $node->value());
self::assertSame('some value', $node->mappedValue());
self::assertSame(true, $node->isValid());
self::assertSame('some message', (string)$node->messages()[0]);
self::assertSame([$child], $node->children());
}
public function test_node_leaf_with_incorrect_value_throws_exception(): void
{
$type = new FakeType();
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 'foo' does not match type `$type`.");
FakeNode::leaf($type, 'foo');
}
public function test_node_branch_values_can_be_retrieved(): void
{
$typeChildA = FakeType::permissive();
$typeChildB = FakeType::permissive();
$attributesChildA = new FakeAttributes();
$attributesChildB = new FakeAttributes();
$children = FakeNode::branch([
'foo' => ['type' => $typeChildA, 'value' => 'foo', 'attributes' => $attributesChildA],
'bar' => ['type' => $typeChildB, 'value' => 'bar', 'attributes' => $attributesChildB],
])->children();
self::assertSame('foo', $children['foo']->name());
self::assertSame('foo', $children['foo']->path());
self::assertFalse($children['foo']->isRoot());
self::assertSame($attributesChildA, $children['foo']->attributes());
self::assertSame('bar', $children['bar']->name());
self::assertSame('bar', $children['bar']->path());
self::assertFalse($children['bar']->isRoot());
self::assertSame($attributesChildB, $children['bar']->attributes());
}
public function test_node_branch_with_duplicated_child_name_throws_exception(): void
{
$this->expectException(DuplicatedNodeChild::class);
$this->expectExceptionCode(1634045114);
$this->expectExceptionMessage('The child `foo` is duplicated in the branch.');
FakeNode::branch([
['name' => 'foo'],
['name' => 'foo'],
]);
}
public function test_node_branch_with_incorrect_value_throws_exception(): void
{
$type = new FakeType();
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 'foo' does not match type `$type`.");
FakeNode::branch([], $type, 'foo');
}
public function test_node_error_values_can_be_retrieved(): void
public function test_error_message_makes_node_not_valid(): void
{
$message = new FakeErrorMessage();
$node = FakeNode::error($message);
self::assertFalse($node->isValid());
self::assertSame('some error message', (string)$node->messages()[0]);
$node = new Node(
true,
'nodeName',
'some.node.path',
'string',
true,
'some source value',
'some value',
[$message],
[]
);
self::assertSame(false, $node->isValid());
}
public function test_get_value_from_invalid_node_throws_exception(): void
public function test_get_source_value_not_filled_throws_exception(): void
{
$node = FakeNode::error();
$this->expectException(SourceValueWasNotFilled::class);
$this->expectExceptionCode(1657466107);
$this->expectExceptionMessage('Source was not filled at path `some.node.path`; use method `$node->sourceFilled()`.');
$this->expectException(CannotGetInvalidNodeValue::class);
$this->expectExceptionCode(1630680246);
$this->expectExceptionMessage('Trying to get value of an invalid node at path ``.');
$node->value();
(new Node(
true,
'nodeName',
'some.node.path',
'string',
false,
null,
'some value',
[],
[]
))->sourceValue();
}
public function test_branch_node_with_invalid_child_is_invalid(): void
public function test_get_mapped_value_from_invalid_node_throws_exception(): void
{
$node = FakeNode::branch([
'foo' => [],
'bar' => ['message' => new FakeErrorMessage()],
]);
$this->expectException(InvalidNodeHasNoMappedValue::class);
$this->expectExceptionCode(1657466305);
$this->expectExceptionMessage('Cannot get mapped value for invalid node at path `some.node.path`; use method `$node->isValid()`.');
self::assertFalse($node->isValid());
}
public function test_node_with_value_returns_node_with_value(): void
{
$nodeA = FakeNode::any();
$nodeB = $nodeA->withValue('bar');
self::assertNotSame($nodeA, $nodeB);
self::assertSame('bar', $nodeB->value());
self::assertTrue($nodeB->isValid());
}
public function test_node_with_invalid_value_returns_invalid_node(): void
{
$type = FakeType::accepting('foo');
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage("Value 1337 does not match type `$type`.");
FakeNode::leaf($type, 'foo')->withValue(1337);
}
public function test_node_with_invalid_value_for_object_type_returns_invalid_node(): void
{
$object = new stdClass();
$type = FakeObjectType::accepting($object);
$this->expectException(InvalidNodeValue::class);
$this->expectExceptionCode(1630678334);
$this->expectExceptionMessage('Invalid value 1337.');
FakeNode::leaf($type, $object)->withValue(1337);
}
public function test_node_with_messages_returns_node_with_messages(): void
{
$messageA = new FakeMessage('some message A');
$messageB = new FakeMessage('some message B');
$nodeA = FakeNode::any();
$nodeB = $nodeA->withMessage($messageA)->withMessage($messageB);
self::assertNotSame($nodeA, $nodeB);
self::assertTrue($nodeB->isValid());
self::assertSame('some message A', (string)$nodeB->messages()[0]);
self::assertSame('some message B', (string)$nodeB->messages()[1]);
}
public function test_node_with_error_message_returns_invalid_node(): void
{
$message = new FakeMessage();
$errorMessage = new FakeErrorMessage();
$nodeA = FakeNode::any();
$nodeB = $nodeA->withMessage($message)->withMessage($errorMessage);
self::assertNotSame($nodeA, $nodeB);
self::assertFalse($nodeB->isValid());
self::assertSame('some message', (string)$nodeB->messages()[0]);
self::assertSame('some error message', (string)$nodeB->messages()[1]);
(new Node(
true,
'nodeName',
'some.node.path',
'string',
true,
'some source value',
null,
[new FakeErrorMessage()],
[]
))->mappedValue();
}
}

View File

@ -6,24 +6,26 @@ namespace CuyZ\Valinor\Tests\Unit\Mapper\Tree;
use CuyZ\Valinor\Mapper\Tree\Node;
use CuyZ\Valinor\Mapper\Tree\NodeTraverser;
use CuyZ\Valinor\Tests\Fake\Mapper\FakeNode;
use CuyZ\Valinor\Tests\Fake\Mapper\Tree\FakeNode;
use PHPUnit\Framework\TestCase;
final class NodeTraverserTest extends TestCase
{
public function test_nodes_are_visited(): void
{
$node = FakeNode::branch([
'foo' => [],
'bar' => [],
]);
$children = [
'foo' => FakeNode::any(),
'bar' => FakeNode::any(),
];
$node = FakeNode::branch($children);
$visited = [...(new NodeTraverser(
fn (Node $node) => $node
))->traverse($node)];
self::assertContains($node, $visited);
self::assertContains($node->children()['foo'], $visited);
self::assertContains($node->children()['bar'], $visited);
self::assertContains($children['foo'], $visited);
self::assertContains($children['bar'], $visited);
}
}