From 4a12b5ae1609cd6fc9282e5ab88f81a6d25ab7ef Mon Sep 17 00:00:00 2001 From: Ilario Pierbattista <987038+ilario-pierbattista@users.noreply.github.com> Date: Tue, 6 Apr 2021 09:12:59 +0200 Subject: [PATCH] Undefined values are decoded with the specified default value (#15) * Implementation of the codec laws as phpunit assertions --- src/Codecs.php | 29 ++++++++- src/Decoders.php | 26 ++++++++ .../LiteralRefiner.php | 2 +- .../LiteralType.php | 2 +- src/Internal/IdentityEncoder.php | 20 ++++++ src/Internal/Primitives/UndefinedDecoder.php | 46 +++++++++++++ .../examples/DecodePartialPropertiesTest.php | 62 ++++++++++++++++++ .../type-assertions/RefineTypeAssertions.php | 2 +- tests/unit/BaseTestCase.php | 65 +++++++++++++++++++ tests/unit/GeneratorUtils.php | 20 ++++++ .../Internal/Combinators/LiteralTypeTest.php | 45 +++++++++++++ .../unit/Internal/Primitives/BoolTypeTest.php | 29 ++------- .../Internal/Primitives/FloatTypeTest.php | 26 ++++++++ .../unit/Internal/Primitives/IntTypeTest.php | 26 ++++++++ .../unit/Internal/Primitives/NullTypeTest.php | 29 +++++++++ .../Internal/Primitives/StringTypeTest.php | 26 ++++++++ .../Primitives/UndefinedDecoderTest.php | 30 +++++++++ 17 files changed, 458 insertions(+), 27 deletions(-) create mode 100644 src/Decoders.php rename src/Internal/{Primitives => Combinators}/LiteralRefiner.php (91%) rename src/Internal/{Primitives => Combinators}/LiteralType.php (95%) create mode 100644 src/Internal/IdentityEncoder.php create mode 100644 src/Internal/Primitives/UndefinedDecoder.php create mode 100644 tests/examples/DecodePartialPropertiesTest.php create mode 100644 tests/unit/GeneratorUtils.php create mode 100644 tests/unit/Internal/Combinators/LiteralTypeTest.php create mode 100644 tests/unit/Internal/Primitives/FloatTypeTest.php create mode 100644 tests/unit/Internal/Primitives/IntTypeTest.php create mode 100644 tests/unit/Internal/Primitives/NullTypeTest.php create mode 100644 tests/unit/Internal/Primitives/StringTypeTest.php create mode 100644 tests/unit/Internal/Primitives/UndefinedDecoderTest.php diff --git a/src/Codecs.php b/src/Codecs.php index e11b86f..1d6c6c5 100644 --- a/src/Codecs.php +++ b/src/Codecs.php @@ -8,13 +8,15 @@ use Facile\PhpCodec\Internal\Arrays\MapType; use Facile\PhpCodec\Internal\Combinators\ClassFromArray; use Facile\PhpCodec\Internal\Combinators\ComposeCodec; +use Facile\PhpCodec\Internal\Combinators\LiteralType; use Facile\PhpCodec\Internal\Combinators\UnionCodec; +use Facile\PhpCodec\Internal\IdentityEncoder; use Facile\PhpCodec\Internal\Primitives\BoolType; use Facile\PhpCodec\Internal\Primitives\FloatType; use Facile\PhpCodec\Internal\Primitives\IntType; -use Facile\PhpCodec\Internal\Primitives\LiteralType; use Facile\PhpCodec\Internal\Primitives\NullType; use Facile\PhpCodec\Internal\Primitives\StringType; +use Facile\PhpCodec\Internal\Primitives\UndefinedDecoder; use Facile\PhpCodec\Internal\Useful\DateTimeFromIsoStringType; use Facile\PhpCodec\Internal\Useful\IntFromStringType; use Facile\PhpCodec\Internal\Useful\RegexType; @@ -189,4 +191,29 @@ public static function regex(string $regex): Codec { return new RegexType($regex); } + + /** + * @template I + * @template T + * + * @param Decoder $decoder + * + * @return Codec + */ + public static function fromDecoder(Decoder $decoder): Codec + { + return new ConcreteCodec($decoder, new IdentityEncoder()); + } + + /** + * @template U + * + * @param U | null $default + * + * @return Codec + */ + public static function undefined($default = null): Codec + { + return self::fromDecoder(new UndefinedDecoder($default)); + } } diff --git a/src/Decoders.php b/src/Decoders.php new file mode 100644 index 0000000..9eb7aa5 --- /dev/null +++ b/src/Decoders.php @@ -0,0 +1,26 @@ + + */ + public static function undefined($default = null): Decoder + { + return new UndefinedDecoder($default); + } +} diff --git a/src/Internal/Primitives/LiteralRefiner.php b/src/Internal/Combinators/LiteralRefiner.php similarity index 91% rename from src/Internal/Primitives/LiteralRefiner.php rename to src/Internal/Combinators/LiteralRefiner.php index e2808dc..ad7f1c5 100644 --- a/src/Internal/Primitives/LiteralRefiner.php +++ b/src/Internal/Combinators/LiteralRefiner.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Facile\PhpCodec\Internal\Primitives; +namespace Facile\PhpCodec\Internal\Combinators; use Facile\PhpCodec\Refiner; diff --git a/src/Internal/Primitives/LiteralType.php b/src/Internal/Combinators/LiteralType.php similarity index 95% rename from src/Internal/Primitives/LiteralType.php rename to src/Internal/Combinators/LiteralType.php index d0d0762..1a4f3ba 100644 --- a/src/Internal/Primitives/LiteralType.php +++ b/src/Internal/Combinators/LiteralType.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Facile\PhpCodec\Internal\Primitives; +namespace Facile\PhpCodec\Internal\Combinators; use Facile\PhpCodec\Internal\Encode; use Facile\PhpCodec\Internal\Type; diff --git a/src/Internal/IdentityEncoder.php b/src/Internal/IdentityEncoder.php new file mode 100644 index 0000000..7164232 --- /dev/null +++ b/src/Internal/IdentityEncoder.php @@ -0,0 +1,20 @@ + + */ +final class IdentityEncoder implements Encoder +{ + public function encode($a) + { + return $a; + } +} diff --git a/src/Internal/Primitives/UndefinedDecoder.php b/src/Internal/Primitives/UndefinedDecoder.php new file mode 100644 index 0000000..7697d3f --- /dev/null +++ b/src/Internal/Primitives/UndefinedDecoder.php @@ -0,0 +1,46 @@ + + */ +class UndefinedDecoder implements Decoder +{ + /** @var U */ + private $default; + + /** + * @param U $default + */ + public function __construct($default) + { + $this->default = $default; + } + + public function validate($i, Context $context): Validation + { + return $i instanceof Undefined + ? Validation::success($this->default) + : Validation::failure($i, $context); + } + + public function decode($i): Validation + { + return standardDecode($this, $i); + } + + public function getName(): string + { + return 'undefined'; + } +} diff --git a/tests/examples/DecodePartialPropertiesTest.php b/tests/examples/DecodePartialPropertiesTest.php new file mode 100644 index 0000000..ee0404f --- /dev/null +++ b/tests/examples/DecodePartialPropertiesTest.php @@ -0,0 +1,62 @@ + Codecs::string(), + 'bar' => Codecs::union(Codecs::int(), Codecs::undefined(-1)), + ], + function (string $foo, int $bar): DecodePartialPropertiesTest\A { + return new DecodePartialPropertiesTest\A($foo, $bar); + }, + DecodePartialPropertiesTest\A::class + ); + + self::asserSuccessInstanceOf( + DecodePartialPropertiesTest\A::class, + $c->decode(['foo' => 'str']), + function (DecodePartialPropertiesTest\A $a): void { + self::assertSame('str', $a->getFoo()); + self::assertSame(-1, $a->getBar()); + } + ); + } +} + +namespace Examples\Facile\PhpCodec\DecodePartialPropertiesTest; + +class A +{ + /** @var string */ + private $foo; + /** @var int */ + private $bar; + + public function __construct( + string $foo, + int $bar + ) { + $this->foo = $foo; + $this->bar = $bar; + } + + public function getFoo(): string + { + return $this->foo; + } + + public function getBar(): int + { + return $this->bar; + } +} diff --git a/tests/type-assertions/RefineTypeAssertions.php b/tests/type-assertions/RefineTypeAssertions.php index 17be92f..455dcff 100644 --- a/tests/type-assertions/RefineTypeAssertions.php +++ b/tests/type-assertions/RefineTypeAssertions.php @@ -5,8 +5,8 @@ namespace TypeAssertions\Facile\PhpCodec; use Facile\PhpCodec\Internal\Arrays\MapRefiner; +use Facile\PhpCodec\Internal\Combinators\LiteralRefiner; use Facile\PhpCodec\Internal\Primitives\InstanceOfRefiner; -use Facile\PhpCodec\Internal\Primitives\LiteralRefiner; class RefineTypeAssertions extends TypeAssertion { diff --git a/tests/unit/BaseTestCase.php b/tests/unit/BaseTestCase.php index e3d2fcc..303df72 100644 --- a/tests/unit/BaseTestCase.php +++ b/tests/unit/BaseTestCase.php @@ -4,6 +4,7 @@ namespace Tests\Facile\PhpCodec; +use Facile\PhpCodec\Codec; use Facile\PhpCodec\PathReporter; use Facile\PhpCodec\Validation\Validation; use Facile\PhpCodec\Validation\ValidationSuccess; @@ -90,4 +91,68 @@ public static function asserSuccessAnd( /** @var ValidationSuccess $v */ return $thenDo($v->getValue()); } + + /** + * @template I + * @template A + * @template O + * + * @param Codec $codec + * + * @return \Closure(I, A): void + */ + public static function codecLaws( + Codec $codec + ): \Closure { + return function ($input, $a) use ($codec): void { + self::assertCodecLaw1($codec, $input); + self::assertCodecLaw2($codec, $a); + }; + } + + /** + * @template I + * @template A + * @template O + * + * @param Codec $codec + * @param I $input + */ + public static function assertCodecLaw1( + Codec $codec, + $input + ): void { + self::assertEquals( + $input, + Validation::fold( + function () use ($input) { + return $input; + }, + function ($a) use ($codec) { + return $codec->encode($a); + }, + $codec->decode($input) + ) + ); + } + + /** + * @template A + * @template I + * @template O + * + * @param Codec $codec + * @param A $a + */ + public static function assertCodecLaw2( + Codec $codec, + $a + ): void { + self::asserSuccessAnd( + $codec->decode($codec->encode($a)), + function ($r) use ($a): void { + self::assertEquals($a, $r); + } + ); + } } diff --git a/tests/unit/GeneratorUtils.php b/tests/unit/GeneratorUtils.php new file mode 100644 index 0000000..8112b3d --- /dev/null +++ b/tests/unit/GeneratorUtils.php @@ -0,0 +1,20 @@ +forAll( + g\bind( + g\oneOf( + g\int(), + g\string(), + g\bool() + ), + function ($literal): g { + return g\tuple( + g\constant(new LiteralType($literal)), + g\oneOf( + GeneratorUtils::scalar(), + g\constant($literal) + ), + g\constant($literal) + ); + } + ) + ) + ->then(destructureIn(function (Codec $codec, $u, $a): void { + self::codecLaws($codec)($u, $a); + })); + } +} diff --git a/tests/unit/Internal/Primitives/BoolTypeTest.php b/tests/unit/Internal/Primitives/BoolTypeTest.php index bd27ede..45f86a6 100644 --- a/tests/unit/Internal/Primitives/BoolTypeTest.php +++ b/tests/unit/Internal/Primitives/BoolTypeTest.php @@ -6,38 +6,21 @@ use Eris\Generator as g; use Eris\TestTrait; -use Facile\PhpCodec\Internal\Primitives\BoolType; -use Facile\PhpCodec\Validation\ValidationFailures; +use Facile\PhpCodec\Codecs; use Tests\Facile\PhpCodec\BaseTestCase; +use Tests\Facile\PhpCodec\GeneratorUtils; class BoolTypeTest extends BaseTestCase { use TestTrait; - public function testValidate(): void + public function testLaws(): void { - $type = new BoolType(); - - self::asserSuccessSameTo(true, $type->decode(true)); - self::asserSuccessSameTo(false, $type->decode(false)); - $this ->forAll( - g\oneOf(g\string(), g\int(), g\float(), g\date(), g\constant(null)) + GeneratorUtils::scalar(), + g\bool() ) - ->then(function ($x) use ($type): void { - self::assertInstanceOf( - ValidationFailures::class, - $type->decode($x) - ); - }); - } - - public function testEncode(): void - { - $type = new BoolType(); - - self::assertTrue($type->encode(true)); - self::assertFalse($type->encode(false)); + ->then(self::codecLaws(Codecs::bool())); } } diff --git a/tests/unit/Internal/Primitives/FloatTypeTest.php b/tests/unit/Internal/Primitives/FloatTypeTest.php new file mode 100644 index 0000000..d9de750 --- /dev/null +++ b/tests/unit/Internal/Primitives/FloatTypeTest.php @@ -0,0 +1,26 @@ +forAll( + GeneratorUtils::scalar(), + g\float() + ) + ->then(self::codecLaws(Codecs::float())); + } +} diff --git a/tests/unit/Internal/Primitives/IntTypeTest.php b/tests/unit/Internal/Primitives/IntTypeTest.php new file mode 100644 index 0000000..7f25ad7 --- /dev/null +++ b/tests/unit/Internal/Primitives/IntTypeTest.php @@ -0,0 +1,26 @@ +forAll( + GeneratorUtils::scalar(), + g\int() + ) + ->then(self::codecLaws(Codecs::int())); + } +} diff --git a/tests/unit/Internal/Primitives/NullTypeTest.php b/tests/unit/Internal/Primitives/NullTypeTest.php new file mode 100644 index 0000000..dbe32ee --- /dev/null +++ b/tests/unit/Internal/Primitives/NullTypeTest.php @@ -0,0 +1,29 @@ +forAll( + g\oneOf( + GeneratorUtils::scalar(), + g\constant(null) + ), + g\constant(null) + ) + ->then(self::codecLaws(Codecs::null())); + } +} diff --git a/tests/unit/Internal/Primitives/StringTypeTest.php b/tests/unit/Internal/Primitives/StringTypeTest.php new file mode 100644 index 0000000..d4d9bf3 --- /dev/null +++ b/tests/unit/Internal/Primitives/StringTypeTest.php @@ -0,0 +1,26 @@ +forAll( + GeneratorUtils::scalar(), + g\string() + ) + ->then(self::codecLaws(Codecs::string())); + } +} diff --git a/tests/unit/Internal/Primitives/UndefinedDecoderTest.php b/tests/unit/Internal/Primitives/UndefinedDecoderTest.php new file mode 100644 index 0000000..c38b710 --- /dev/null +++ b/tests/unit/Internal/Primitives/UndefinedDecoderTest.php @@ -0,0 +1,30 @@ +forAll(GeneratorUtils::scalar()) + ->then(function ($default): void { + self::asserSuccessAnd( + Codecs::undefined($default)->decode(new Undefined()), + function ($x) use ($default): void { + self::assertSame($default, $x); + } + ); + }); + } +}