Skip to content

Commit

Permalink
feat: allow custom normalizer to target types other than objects
Browse files Browse the repository at this point in the history
  • Loading branch information
romm committed Oct 7, 2023
1 parent 8b1c9ec commit 8cd61a0
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/MapperBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ public function filterExceptions(callable $filter): self
/**
* @todo doc
*
* @param callable(object, callable(): mixed): mixed $callback
* @psalm-param pure-callable $callback
*/
public function registerNormalizer(callable $callback, int $priority = 0): self
{
Expand Down
1 change: 0 additions & 1 deletion src/Normalizer/FunctionsCheckerNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use CuyZ\Valinor\Normalizer\Exception\NormalizerHandlerHasNoParameter;
use CuyZ\Valinor\Normalizer\Exception\NormalizerHandlerHasTooManyParameters;
use CuyZ\Valinor\Type\Types\CallableType;
use RuntimeException;

/** @internal */
final class FunctionsCheckerNormalizer implements Normalizer
Expand Down
95 changes: 47 additions & 48 deletions src/Normalizer/RecursiveNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
use CuyZ\Valinor\Definition\FunctionsContainer;
use CuyZ\Valinor\Normalizer\Exception\CircularReferenceFoundDuringNormalization;
use CuyZ\Valinor\Normalizer\Exception\TypeUnhandledByNormalizer;
use CuyZ\Valinor\Type\Types\NativeClassType;
use DateTimeInterface;
use Generator;
use RuntimeException;
use stdClass;
use UnitEnum;

Expand Down Expand Up @@ -41,88 +39,89 @@ public function normalize(mixed $value): mixed
*/
private function doNormalize(mixed $value, array $references): mixed
{
if ($value === null) {
return null;
}

if (is_scalar($value)) {
return $value;
}

if (is_object($value) && ! $value instanceof Closure && ! $value instanceof Generator) {
if (is_object($value)) {
$id = spl_object_id($value);

if (isset($references[$id])) {
throw new CircularReferenceFoundDuringNormalization($value);
}

$references[$id] = true;

Check warning on line 49 in src/Normalizer/RecursiveNormalizer.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ if (isset($references[$id])) { throw new CircularReferenceFoundDuringNormalization($value); } - $references[$id] = true; + $references[$id] = false; } if ($this->handlers->count() === 0) { $value = $this->defaultNormalizer($value);

return $this->doNormalize($this->normalizeObject($value), $references);
}

if (is_iterable($value)) {
if (! is_array($value)) {
$value = iterator_to_array($value);
}

return array_map(
fn (mixed $value) => $this->doNormalize($value, $references),
$value
if ($this->handlers->count() === 0) {

Check warning on line 52 in src/Normalizer/RecursiveNormalizer.php

View workflow job for this annotation

GitHub Actions / Mutation tests

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ } $references[$id] = true; } - if ($this->handlers->count() === 0) { + if ($this->handlers->count() === -1) { $value = $this->defaultNormalizer($value); } else { $handlers = array_filter([...$this->handlers], fn(FunctionObject $function) => $function->definition()->parameters()->at(0)->type()->accepts($value));
$value = $this->defaultNormalizer($value);
} else {
$handlers = array_filter(
[...$this->handlers],
fn (FunctionObject $function) => $function->definition()->parameters()->at(0)->type()->accepts($value),
);
}

throw new TypeUnhandledByNormalizer($value);
}

private function normalizeObject(object $object): mixed
{
if ($this->handlers->count() === 0) {
return ($this->defaultObjectNormalizer($object))();
$value = $this->nextNormalizer($handlers, $value)();
}

$type = new NativeClassType($object::class);

$handlers = array_filter(
[...$this->handlers],
fn (FunctionObject $function) => $type->matches($function->definition()->parameters()->at(0)->type())
);
if (is_array($value)) {
$value = array_map(
fn (mixed $value) => $this->doNormalize($value, $references),
$value,
);
}

return $this->nextNormalizer($handlers, $object)();
return $value;
}

/**
* @param array<FunctionObject> $handlers
*/
private function nextNormalizer(array $handlers, object $object): callable
private function nextNormalizer(array $handlers, mixed $value): callable
{
if ($handlers === []) {
return $this->defaultObjectNormalizer($object);
return fn () => $this->defaultNormalizer($value);
}

$handler = array_shift($handlers);
$arguments = [
$object,
fn () => $this->nextNormalizer($handlers, $object)(),
$value,
fn () => $this->nextNormalizer($handlers, $value)(),
];

return fn () => ($handler->callback())(...$arguments);
}

private function defaultObjectNormalizer(object $object): callable
private function defaultNormalizer(mixed $value): mixed
{
if ($object instanceof UnitEnum) {
return fn () => $object instanceof BackedEnum ? $object->value : $object->name;
if ($value === null) {
return null;
}

if ($object instanceof DateTimeInterface) {
return fn () => $object->format('Y-m-d\\TH:i:s.uP'); // RFC 3339
if (is_scalar($value)) {
return $value;
}

if ($object::class === stdClass::class) {
return fn () => (array)$object;
if (is_object($value) && ! $value instanceof Closure && ! $value instanceof Generator) {
if ($value instanceof UnitEnum) {
return $value instanceof BackedEnum ? $value->value : $value->name;
}

if ($value instanceof DateTimeInterface) {
return $value->format('Y-m-d\\TH:i:s.uP'); // RFC 3339
}

if ($value::class === stdClass::class) {
return (array)$value;
}

return (fn () => get_object_vars($this))->call($value);
}

return fn () => (fn () => get_object_vars($this))->call($object);
if (is_iterable($value)) {
if (! is_array($value)) {
$value = iterator_to_array($value);
}

return $value;
}

throw new TypeUnhandledByNormalizer($value);
}
}
55 changes: 49 additions & 6 deletions tests/Integration/Normalizer/NormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use stdClass;
use Traversable;

use function array_merge;

final class NormalizerTest extends TestCase
{
/**
Expand Down Expand Up @@ -54,16 +56,51 @@ public function normalize_basic_values_yields_expected_output_data_provider(): i
'expected' => 'foo bar',
];

yield 'string with handler' => [
'input' => 'foo',
'expected' => 'foo!',
'handlers' => [
[fn (string $value) => $value . '!'],
],
];

yield 'integer' => [
'input' => 42,
'expected' => 42,
];

yield 'integer with handler' => [
'input' => 42,
'expected' => 43,
'handlers' => [
[fn (int $value) => $value + 1],
],
];

yield 'integer with negative-int handler' => [
'input' => 42,
'expected' => 42,
'handlers' => [
[
/** @param negative-int $value */
fn (int $value) => $value + 1
],
],
];

yield 'float' => [
'input' => 1337.404,
'expected' => 1337.404,
];

yield 'float with handler' => [
'input' => 1337.404,
'expected' => 1337.405,
'handlers' => [
[fn (float $value) => $value + 0.001],
],
];

yield 'boolean' => [
'input' => true,
'expected' => true,
Expand All @@ -84,6 +121,14 @@ public function normalize_basic_values_yields_expected_output_data_provider(): i
],
];

yield 'array with handler' => [
'input' => ['foo'],
'expected' => ['foo', 'bar'],
'handlers' => [
[fn (array $value) => array_merge($value, ['bar'])],
],
];

yield 'iterable of scalar' => [
'input' => (function (): iterable {
yield 'string' => 'foo';
Expand Down Expand Up @@ -267,14 +312,14 @@ public function getIterator(): Traversable
'value' => 'foo',
'bar' => 'bar',
],
'handlers' => [
[function (object $object, callable $next) {
'handlers' => [[
function (object $object, callable $next) {
$result = $next();
$result['bar'] = 'bar';

return $result;
}],
],
}
]],
];

yield 'object with several prioritized handlers' => [
Expand Down Expand Up @@ -335,7 +380,6 @@ public function test_too_many_params_in_callable_throws_exception(): void
$this->expectExceptionMessageMatches('/Normalizer handler must have at most 2 parameters, 3 given for `.*`\./');

(new MapperBuilder())
// @phpstan-ignore-next-line
->registerNormalizer(fn (stdClass $object, callable $next, int $unexpectedParameter) => 42)
->normalizer()
->normalize(new stdClass());
Expand All @@ -348,7 +392,6 @@ public function test_second_param_is_not_callable_throws_exception(): void
$this->expectExceptionMessageMatches('/Normalizer handler\'s second parameter must be a callable, `int` given for `.*`\./');

(new MapperBuilder())
// @phpstan-ignore-next-line
->registerNormalizer(fn (stdClass $object, int $unexpectedParameterType) => 42)
->normalizer()
->normalize(new stdClass());
Expand Down

0 comments on commit 8cd61a0

Please sign in to comment.