From 304db395bf1b16d1b8c4db3278f38fcac3f020d1 Mon Sep 17 00:00:00 2001 From: Filippo Tessarotto Date: Mon, 16 Sep 2024 19:05:38 +0200 Subject: [PATCH 1/4] misc: change implicitly nullable parameter types --- src/Mapper/Tree/Shell.php | 2 +- src/Type/Types/ClassStringType.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapper/Tree/Shell.php b/src/Mapper/Tree/Shell.php index cec31b4d..150083cd 100644 --- a/src/Mapper/Tree/Shell.php +++ b/src/Mapper/Tree/Shell.php @@ -53,7 +53,7 @@ public static function root( return (new self($settings, $type))->withValue($value); } - public function child(string $name, Type $type, Attributes $attributes = null): self + public function child(string $name, Type $type, ?Attributes $attributes = null): self { $instance = new self($this->settings, $type); $instance->name = $name; diff --git a/src/Type/Types/ClassStringType.php b/src/Type/Types/ClassStringType.php index e1404e55..d715d646 100644 --- a/src/Type/Types/ClassStringType.php +++ b/src/Type/Types/ClassStringType.php @@ -27,7 +27,7 @@ final class ClassStringType implements StringType, CompositeType private string $signature; - public function __construct(ObjectType|UnionType $subType = null) + public function __construct(ObjectType|UnionType|null $subType = null) { if ($subType instanceof UnionType) { foreach ($subType->types() as $type) { From f3e8c1e09c0b947693cd40941d2ae5ab9b31bbd9 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 16 Sep 2024 20:26:41 +0300 Subject: [PATCH 2/4] feat: add support for `JSON_FORCE_OBJECT` option in JSON normalizer ```php (new \CuyZ\Valinor\MapperBuilder()) ->normalizer(Format::json()) ->withOptions(JSON_FORCE_OBJECT) ->normalize(['foo', 'bar']); // {"0":"foo","1":"bar"} ``` --- src/Normalizer/Formatter/JsonFormatter.php | 8 +++++-- src/Normalizer/JsonNormalizer.php | 4 +++- .../Integration/Normalizer/NormalizerTest.php | 21 +++++++++++++++---- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Normalizer/Formatter/JsonFormatter.php b/src/Normalizer/Formatter/JsonFormatter.php index 6e46220d..e7dfe298 100644 --- a/src/Normalizer/Formatter/JsonFormatter.php +++ b/src/Normalizer/Formatter/JsonFormatter.php @@ -17,6 +17,7 @@ use function is_scalar; use function json_encode; +use const JSON_FORCE_OBJECT; use const JSON_THROW_ON_ERROR; /** @internal */ @@ -54,8 +55,11 @@ public function format(mixed $value): void // afterward, this leads to a JSON array being written, while it // should have been an object. This is a trade-off we accept, // considering most generators starting at 0 are actually lists. - $isList = ($value instanceof Generator && $value->key() === 0) - || (is_array($value) && array_is_list($value)); + $isList = ! ($this->jsonEncodingOptions & JSON_FORCE_OBJECT) + && ( + ($value instanceof Generator && $value->key() === 0) + || (is_array($value) && array_is_list($value)) + ); $isFirst = true; diff --git a/src/Normalizer/JsonNormalizer.php b/src/Normalizer/JsonNormalizer.php index 872955ed..3ccb8702 100644 --- a/src/Normalizer/JsonNormalizer.php +++ b/src/Normalizer/JsonNormalizer.php @@ -14,6 +14,7 @@ use function is_resource; use function stream_get_contents; +use const JSON_FORCE_OBJECT; use const JSON_HEX_AMP; use const JSON_HEX_APOS; use const JSON_HEX_QUOT; @@ -34,7 +35,8 @@ */ final class JsonNormalizer implements Normalizer { - private const ACCEPTABLE_JSON_OPTIONS = JSON_HEX_QUOT + private const ACCEPTABLE_JSON_OPTIONS = JSON_FORCE_OBJECT + | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS diff --git a/tests/Integration/Normalizer/NormalizerTest.php b/tests/Integration/Normalizer/NormalizerTest.php index 80bbb59e..6599fd70 100644 --- a/tests/Integration/Normalizer/NormalizerTest.php +++ b/tests/Integration/Normalizer/NormalizerTest.php @@ -30,6 +30,7 @@ use function array_merge; +use const JSON_FORCE_OBJECT; use const JSON_HEX_TAG; use const JSON_THROW_ON_ERROR; @@ -194,6 +195,21 @@ public static function normalize_basic_values_yields_expected_output_data_provid 'expected json' => '{"foo":"foo","bar":"bar"}', ]; + yield 'list' => [ + 'input' => ['foo', 'bar'], + 'expected array' => ['foo', 'bar'], + 'expected json' => '["foo","bar"]', + ]; + + yield 'list kept as object in json' => [ + 'input' => ['foo', 'bar'], + 'expected array' => ['foo', 'bar'], + 'expected json' => '{"0":"foo","1":"bar"}', + [], + [], + JSON_FORCE_OBJECT + ]; + yield 'ArrayObject' => [ 'input' => new ArrayObject(['foo' => 'foo', 'bar' => 'bar']), 'expected array' => [ @@ -1148,16 +1164,13 @@ public function test_json_transformer_will_always_throw_on_error(): void public function test_json_transformer_only_accepts_acceptable_json_options(): void { - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT); - self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PRETTY_PRINT); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT); + $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); } } From b9c6add856ba6b2ee2034deaaee312a99159e885 Mon Sep 17 00:00:00 2001 From: Joas Schilling <213943+nickvergessen@users.noreply.github.com> Date: Mon, 16 Sep 2024 19:45:03 +0200 Subject: [PATCH 3/4] misc: fix typo in property type annotation --- src/Type/Parser/Lexer/TokenizedAnnotation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Parser/Lexer/TokenizedAnnotation.php b/src/Type/Parser/Lexer/TokenizedAnnotation.php index 4e4c403c..0aa15f99 100644 --- a/src/Type/Parser/Lexer/TokenizedAnnotation.php +++ b/src/Type/Parser/Lexer/TokenizedAnnotation.php @@ -13,7 +13,7 @@ final class TokenizedAnnotation public function __construct( /** @var non-empty-string */ private string $name, - /** @var non-empty-list> */ + /** @var non-empty-list */ private array $tokens, ) {} From 8742b273f1bdd6e7037f1398b9d8822508ffda63 Mon Sep 17 00:00:00 2001 From: NanoSector Date: Mon, 16 Sep 2024 20:57:14 +0200 Subject: [PATCH 4/4] fix: handle float type casting properly Regression fix for 0479532fbc96fca35dcbfb4c1f5a9ef63e7625c5 --- src/Mapper/Tree/Shell.php | 24 ++++++++++++------- .../Integration/Mapping/UnionMappingTest.php | 9 +++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/Mapper/Tree/Shell.php b/src/Mapper/Tree/Shell.php index 150083cd..10831820 100644 --- a/src/Mapper/Tree/Shell.php +++ b/src/Mapper/Tree/Shell.php @@ -80,6 +80,7 @@ public function withType(Type $newType): self { $clone = clone $this; $clone->type = $newType; + $clone->value = self::castCompatibleValue($newType, $this->value); return $clone; } @@ -91,17 +92,9 @@ public function type(): Type public function withValue(mixed $value): self { - // When the value is an integer and the type is a float, the value is - // cast to float, to follow the rule of PHP regarding acceptance of an - // integer value in a float type. Note that PHPStan/Psalm analysis - // applies the same rule. - if ($this->type instanceof FloatType && is_int($value)) { - $value = (float)$value; - } - $clone = clone $this; $clone->hasValue = true; - $clone->value = $value; + $clone->value = self::castCompatibleValue($clone->type, $value); return $clone; } @@ -173,4 +166,17 @@ public function path(): string return implode('.', $path); } + + private static function castCompatibleValue(Type $type, mixed $value): mixed + { + // When the value is an integer and the type is a float, the value is + // cast to float, to follow the rule of PHP regarding acceptance of an + // integer value in a float type. Note that PHPStan/Psalm analysis + // applies the same rule. + if ($type instanceof FloatType && is_int($value)) { + return (float)$value; + } + + return $value; + } } diff --git a/tests/Integration/Mapping/UnionMappingTest.php b/tests/Integration/Mapping/UnionMappingTest.php index 8612d507..e840a98c 100644 --- a/tests/Integration/Mapping/UnionMappingTest.php +++ b/tests/Integration/Mapping/UnionMappingTest.php @@ -64,6 +64,15 @@ public static function union_mapping_works_properly_data_provider(): iterable 'assertion' => fn (mixed $result) => self::assertNull($result), ]; + yield 'nullable float with integer value' => [ + 'type' => 'float|null', + 'source' => 42, + 'assertion' => function (mixed $result) { + self::assertIsFloat($result); + self::assertEquals(42.0, $result); + }, + ]; + yield 'string or list of string, with string' => [ 'type' => 'string|list', 'source' => 'foo',