From 4ba45a7740700b9d7b6df2280e62155bd8f04345 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 14:49:00 +0100 Subject: [PATCH 1/6] Added type Caster --- src/core/etl/src/Flow/ETL/DSL/functions.php | 5 ++ .../Flow/ETL/Exception/CastingException.php | 15 ++++ src/core/etl/src/Flow/ETL/PHP/Type/Caster.php | 66 +++++++++++++++ .../PHP/Type/Caster/ArrayCastingHandler.php | 38 +++++++++ .../PHP/Type/Caster/BooleanCastingHandler.php | 37 +++++++++ .../ETL/PHP/Type/Caster/CastingContext.php | 20 +++++ .../ETL/PHP/Type/Caster/CastingHandler.php | 14 ++++ .../Type/Caster/DateTimeCastingHandler.php | 53 ++++++++++++ .../PHP/Type/Caster/EnumCastingHandler.php | 33 ++++++++ .../PHP/Type/Caster/FloatCastingHandler.php | 38 +++++++++ .../PHP/Type/Caster/IntegerCastingHandler.php | 38 +++++++++ .../PHP/Type/Caster/JsonCastingHandler.php | 35 ++++++++ .../PHP/Type/Caster/ListCastingHandler.php | 34 ++++++++ .../ETL/PHP/Type/Caster/MapCastingHandler.php | 34 ++++++++ .../PHP/Type/Caster/NullCastingHandler.php | 21 +++++ .../PHP/Type/Caster/ObjectCastingHandler.php | 34 ++++++++ .../PHP/Type/Caster/StringCastingHandler.php | 55 +++++++++++++ .../Type/Caster/StructureCastingHandler.php | 34 ++++++++ .../PHP/Type/Caster/UuidCastingHandler.php | 35 ++++++++ .../ETL/PHP/Type/Caster/XMLCastingHandler.php | 37 +++++++++ .../ETL/Row/Factory/NativeEntryFactory.php | 52 ++++++------ .../Tests/Integration/PHP/Type/CasterTest.php | 82 +++++++++++++++++++ .../Type/Caster/ArrayCastingHandlerTest.php | 52 ++++++++++++ .../Type/Caster/BooleanCastingHandlerTest.php | 39 +++++++++ .../Caster/DateTimeCastingHandlerTest.php | 29 +++++++ .../Type/Caster/EnumCastingHandlerTest.php | 30 +++++++ .../PHP/Type/Caster/Fixtures/ColorsEnum.php | 12 +++ .../Type/Caster/FloatCastingHandlerTest.php | 30 +++++++ .../Type/Caster/IntegerCastingHandlerTest.php | 30 +++++++ .../Type/Caster/JsonCastingHandlerTest.php | 53 ++++++++++++ .../Type/Caster/ObjectCastingHandlerTest.php | 24 ++++++ .../Type/Caster/StringCastingHandlerTest.php | 45 ++++++++++ .../Type/Caster/UuidCastingHandlerTest.php | 37 +++++++++ .../PHP/Type/Caster/XMLCastingHandlerTest.php | 29 +++++++ .../Row/Factory/NativeEntryFactoryTest.php | 16 +--- 35 files changed, 1196 insertions(+), 40 deletions(-) create mode 100644 src/core/etl/src/Flow/ETL/Exception/CastingException.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/DateTimeCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/ListCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/MapCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/NullCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Integration/PHP/Type/CasterTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/Fixtures/ColorsEnum.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/FloatCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php diff --git a/src/core/etl/src/Flow/ETL/DSL/functions.php b/src/core/etl/src/Flow/ETL/DSL/functions.php index bc635997a..185007ac4 100644 --- a/src/core/etl/src/Flow/ETL/DSL/functions.php +++ b/src/core/etl/src/Flow/ETL/DSL/functions.php @@ -420,6 +420,11 @@ function type_int(bool $nullable = false) : ScalarType return ScalarType::integer($nullable); } +function type_integer(bool $nullable = false) : ScalarType +{ + return ScalarType::integer($nullable); +} + function type_string(bool $nullable = false) : ScalarType { return ScalarType::string($nullable); diff --git a/src/core/etl/src/Flow/ETL/Exception/CastingException.php b/src/core/etl/src/Flow/ETL/Exception/CastingException.php new file mode 100644 index 000000000..f3acc3170 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/Exception/CastingException.php @@ -0,0 +1,15 @@ +toString())); + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php new file mode 100644 index 000000000..7f3f32206 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php @@ -0,0 +1,66 @@ + $handlers + */ + public function __construct(private readonly array $handlers) + { + } + + public static function default() : self + { + return new self([ + new StringCastingHandler(), + new IntegerCastingHandler(), + new BooleanCastingHandler(), + new FloatCastingHandler(), + new XMLCastingHandler(), + new UuidCastingHandler(), + new ObjectCastingHandler(), + new DateTimeCastingHandler(), + new JsonCastingHandler(), + new ArrayCastingHandler(), + new ListCastingHandler(), + new MapCastingHandler(), + new StructureCastingHandler(), + new NullCastingHandler(), + new EnumCastingHandler(), + ]); + } + + public function to(Type $type) : CastingContext + { + foreach ($this->handlers as $handler) { + if ($handler->supports($type)) { + return new CastingContext($handler, $type); + } + } + + throw new RuntimeException("There is no casting handler for \"{$type->toString()}\" type"); + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php new file mode 100644 index 000000000..d2a7e00f1 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php @@ -0,0 +1,38 @@ +isBoolean(); + } + + public function value(mixed $value, Type $type) : mixed + { + if (\is_string($value)) { + if (\in_array(\mb_strtolower($value), ['true', '1', 'yes', 'on'], true)) { + return true; + } + + if (\in_array(\mb_strtolower($value), ['false', '0', 'no', 'off'], true)) { + return false; + } + } + + try { + return (bool) $value; + /* @phpstan-ignore-next-line */ + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php new file mode 100644 index 000000000..3adacf1df --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php @@ -0,0 +1,20 @@ +handler->value($value, $this->type); + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php new file mode 100644 index 000000000..97d2d5b03 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php @@ -0,0 +1,14 @@ +add($value); + + } + } catch (\Throwable $e) { + throw new CastingException($value, type_datetime()); + } + + throw new CastingException($value, $type); + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php new file mode 100644 index 000000000..a2e00dc9a --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php @@ -0,0 +1,33 @@ +class; + + if (\is_a($enumClass, \BackedEnum::class, true)) { + return $enumClass::from($value); + } + + throw new CastingException($value, $type); + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php new file mode 100644 index 000000000..f233b7fbe --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php @@ -0,0 +1,38 @@ +isFloat(); + } + + public function value(mixed $value, Type $type) : mixed + { + if ($value instanceof \DateTimeImmutable) { + return (float) $value->format('Uu'); + } + + if ($value instanceof \DateInterval) { + $reference = new \DateTimeImmutable(); + $endTime = $reference->add($value); + + return (float) ($endTime->format('Uu')) - (float) ($reference->format('Uu')); + } + + try { + return (float) $value; + /* @phpstan-ignore-next-line */ + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php new file mode 100644 index 000000000..5ccabe115 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php @@ -0,0 +1,38 @@ +isInteger(); + } + + public function value(mixed $value, Type $type) : mixed + { + if ($value instanceof \DateTimeImmutable) { + return (int) $value->format('Uu'); + } + + if ($value instanceof \DateInterval) { + $reference = new \DateTimeImmutable(); + $endTime = $reference->add($value); + + return (int) ($endTime->format('Uu')) - (int) ($reference->format('Uu')); + } + + try { + return (int) $value; + /* @phpstan-ignore-next-line */ + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php new file mode 100644 index 000000000..101a73007 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php @@ -0,0 +1,35 @@ +class) { + throw new CastingException($value, type_object($type->class)); + } + + return $object; + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php new file mode 100644 index 000000000..994ae3421 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php @@ -0,0 +1,55 @@ +isString(); + } + + public function value(mixed $value, Type $type) : mixed + { + if ($value === null) { + return null; + } + + if (\is_string($value)) { + return $value; + } + + if (\is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (\is_array($value)) { + return \json_encode($value, JSON_THROW_ON_ERROR); + } + + if ($value instanceof \DateTimeInterface) { + return $value->format(\DateTimeInterface::RFC3339); + } + + if ($value instanceof \Stringable) { + return (string) $value; + } + + if ($value instanceof \DOMDocument) { + return $value->saveXML() ?: null; + } + + try { + return (string) $value; + /* @phpstan-ignore-next-line */ + } catch (\Throwable $e) { + throw new CastingException($value, $type); + } + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php new file mode 100644 index 000000000..6bf54fd08 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php @@ -0,0 +1,34 @@ +toRfc4122()); + } + + throw new CastingException($value, $type); + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php new file mode 100644 index 000000000..3e8366f03 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php @@ -0,0 +1,37 @@ +loadXML($value)) { + throw new CastingException($value, type_xml()); + } + + return $doc; + } + + if ($value instanceof \DOMDocument) { + return $value; + } + + throw new CastingException($value, $type); + } +} diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php index d625b9232..1380bbc9f 100644 --- a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php +++ b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php @@ -23,6 +23,7 @@ use function Flow\ETL\DSL\xml_node_entry; use Flow\ETL\Exception\InvalidArgumentException; use Flow\ETL\Exception\RuntimeException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\DateTimeType; use Flow\ETL\PHP\Type\Logical\JsonType; use Flow\ETL\PHP\Type\Logical\ListType; @@ -161,74 +162,69 @@ public function create(string $entryName, mixed $value, ?Schema $schema = null) private function fromDefinition(Schema\Definition $definition, mixed $value) : Entry { - if ($definition->isNullable() && null === $value) { - return null_entry($definition->entry()->name()); + if ($definition->isNullable()) { + if (null === $value) { + return null_entry($definition->entry()->name()); + } + + throw new InvalidArgumentException("Entry \"{$definition->entry()}\" is not nullable, but null value was given"); } + $caster = Caster::default(); + try { if ($definition->type() instanceof ScalarType) { return match ($definition->type()->type()) { - ScalarType::STRING => str_entry($definition->entry()->name(), $value), - ScalarType::INTEGER => int_entry($definition->entry()->name(), $value), - ScalarType::FLOAT => float_entry($definition->entry()->name(), $value), - ScalarType::BOOLEAN => bool_entry($definition->entry()->name(), $value), + ScalarType::STRING => str_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), + ScalarType::INTEGER => int_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), + ScalarType::FLOAT => float_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), + ScalarType::BOOLEAN => bool_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), default => throw new InvalidArgumentException("Can't convert value into entry \"{$definition->entry()}\""), }; } if ($definition->type() instanceof XMLType) { - return xml_entry($definition->entry()->name(), $value); + return xml_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof UuidType) { - return uuid_entry($definition->entry()->name(), $value); + return uuid_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof ObjectType) { - return obj_entry($definition->entry()->name(), $value); + return obj_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof DateTimeType) { - return datetime_entry($definition->entry()->name(), $value); + return datetime_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof EnumType) { - /** @var class-string<\UnitEnum> $enumClass */ - $enumClass = $definition->type()->class; - /** @var array<\UnitEnum> $cases */ - $cases = $definition->type()->class::cases(); - - foreach ($cases as $case) { - if ($case->name === $value) { - return enum_entry($definition->entry()->name(), $case); - } - } - - throw new InvalidArgumentException("Value \"{$value}\" can't be converted to " . $enumClass . ' enum'); + return enum_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof JsonType) { try { - return json_object_entry($definition->entry()->name(), $value); + return json_object_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } catch (InvalidArgumentException) { - return json_entry($definition->entry()->name(), $value); + return json_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } } if ($definition->type() instanceof ArrayType) { - return array_entry($definition->entry()->name(), $value); + return array_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); } if ($definition->type() instanceof MapType) { - return map_entry($definition->entry()->name(), $value, $definition->type()); + return map_entry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); } if ($definition->type() instanceof StructureType) { - return struct_entry($definition->entry()->name(), $value, $definition->type()); + return struct_entry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); } if ($definition->type() instanceof ListType) { - return new Entry\ListEntry($definition->entry()->name(), $value, $definition->type()); + return new Entry\ListEntry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); } } catch (InvalidArgumentException|\TypeError $e) { throw new InvalidArgumentException("Field \"{$definition->entry()}\" conversion exception. {$e->getMessage()}", previous: $e); diff --git a/src/core/etl/tests/Flow/ETL/Tests/Integration/PHP/Type/CasterTest.php b/src/core/etl/tests/Flow/ETL/Tests/Integration/PHP/Type/CasterTest.php new file mode 100644 index 000000000..20a490cdc --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Integration/PHP/Type/CasterTest.php @@ -0,0 +1,82 @@ +assertSame( + '{"items":{"item":1}}', + (Caster::default())->to(type_json())->value(['items' => ['item' => 1]]) + ); + } + + public function test_casting_string_to_datetime() : void + { + $this->assertSame( + '2021-01-01 00:00:00.000000', + (Caster::default())->to(type_datetime())->value('2021-01-01 00:00:00 UTC')->format('Y-m-d H:i:s.u') + ); + } + + public function test_casting_string_to_uuid() : void + { + $this->assertEquals( + new Uuid('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), + (Caster::default())->to(type_uuid())->value('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e') + ); + } + + public function test_casting_string_to_xml() : void + { + $this->assertSame( + '' . "\n" . '1' . "\n", + (Caster::default())->to(type_xml())->value('1')->saveXML() + ); + } + + public function test_casting_to_boolean() : void + { + $this->assertTrue( + (Caster::default())->to(type_boolean())->value('true') + ); + } + + public function test_casting_to_integer() : void + { + $this->assertSame( + 1, + (Caster::default())->to(type_integer())->value('1') + ); + } + + public function test_casting_to_string() : void + { + $this->assertSame( + '1', + (Caster::default())->to(type_string())->value(1) + ); + } + + public function test_casting_values_to_null() : void + { + $this->assertNull( + (Caster::default())->to(type_null())->value('qweqwqw') + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php new file mode 100644 index 000000000..b949d82d5 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php @@ -0,0 +1,52 @@ +assertEquals( + [true], + (new ArrayCastingHandler())->value(true, type_array()) + ); + } + + public function test_casting_datetime_to_array() : void + { + $this->assertEquals( + ['date' => '2021-01-01 00:00:00.000000', 'timezone_type' => 3, 'timezone' => 'UTC'], + (new ArrayCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_array()) + ); + } + + public function test_casting_float_to_array() : void + { + $this->assertEquals( + [1.1], + (new ArrayCastingHandler())->value(1.1, type_array()) + ); + } + + public function test_casting_integer_to_array() : void + { + $this->assertEquals( + [1], + (new ArrayCastingHandler())->value(1, type_array()) + ); + } + + public function test_casting_string_to_array() : void + { + $this->assertSame( + ['items' => ['item' => 1]], + (new ArrayCastingHandler())->value('{"items":{"item":1}}', type_array()) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php new file mode 100644 index 000000000..36ddaf5cb --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php @@ -0,0 +1,39 @@ + ['string', true]; + yield 'string true' => ['true', true]; + yield 'string 1' => ['1', true]; + yield 'string yes' => ['yes', true]; + yield 'string on' => ['on', true]; + yield 'string false' => ['false', false]; + yield 'string 0' => ['0', false]; + yield 'string no' => ['no', false]; + yield 'string off' => ['off', false]; + yield 'int' => [1, true]; + yield 'float' => [1.1, true]; + yield 'bool' => [true, true]; + yield 'array' => [[1, 2, 3], true]; + yield 'DateTimeInterface' => [new \DateTimeImmutable('2021-01-01 00:00:00'), true]; + yield 'DateInterval' => [new \DateInterval('P1D'), true]; + yield 'DOMDocument' => [new \DOMDocument(), true]; + } + + #[DataProvider('boolean_castable_data_provider')] + public function test_casting_different_data_types_to_integer(mixed $value, bool $expected) : void + { + $this->assertSame($expected, (new BooleanCastingHandler())->value($value, type_boolean())); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php new file mode 100644 index 000000000..6927633cd --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php @@ -0,0 +1,29 @@ + ['2021-01-01 00:00:00', new \DateTimeImmutable('2021-01-01 00:00:00')]; + yield 'int' => [1609459200, new \DateTimeImmutable('2021-01-01 00:00:00')]; + yield 'float' => [1609459200.0, new \DateTimeImmutable('2021-01-01 00:00:00')]; + yield 'bool' => [true, new \DateTimeImmutable('1970-01-01 00:00:01')]; + yield 'DateTimeInterface' => [new \DateTimeImmutable('2021-01-01 00:00:00'), new \DateTimeImmutable('2021-01-01 00:00:00')]; + yield 'DateInterval' => [new \DateInterval('P1D'), new \DateTimeImmutable('1970-01-02 00:00:00')]; + } + + #[DataProvider('datetime_castable_data_provider')] + public function test_casting_different_data_types_to_datetime(mixed $value, \DateTimeImmutable $expected) : void + { + $this->assertEquals($expected, (new DateTimeCastingHandler())->value($value, type_datetime())); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php new file mode 100644 index 000000000..0427eb3ce --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php @@ -0,0 +1,30 @@ +expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "integer" into "enum" type'); + + (new EnumCastingHandler())->value(1, type_enum(ColorsEnum::class)); + } + + public function test_casting_string_to_enum() : void + { + $this->assertEquals( + ColorsEnum::RED, + (new EnumCastingHandler())->value('red', type_enum(ColorsEnum::class)) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/Fixtures/ColorsEnum.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/Fixtures/ColorsEnum.php new file mode 100644 index 000000000..18a99a718 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/Fixtures/ColorsEnum.php @@ -0,0 +1,12 @@ + ['string', 0.0]; + yield 'int' => [1, 1.0]; + yield 'float' => [1.1, 1.1]; + yield 'bool' => [true, 1.0]; + yield 'array' => [[1, 2, 3], 1.0]; + yield 'DateTimeInterface' => [new \DateTimeImmutable('2021-01-01 00:00:00'), 1609459200000000.0]; + yield 'DateInterval' => [new \DateInterval('P1D'), 86400000000.0]; + } + + #[DataProvider('float_castable_data_provider')] + public function test_casting_different_data_types_to_float(mixed $value, float $expected) : void + { + $this->assertSame($expected, (new FloatCastingHandler())->value($value, type_float())); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php new file mode 100644 index 000000000..acf808a26 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php @@ -0,0 +1,30 @@ + ['string', 0]; + yield 'int' => [1, 1]; + yield 'float' => [1.1, 1]; + yield 'bool' => [true, 1]; + yield 'array' => [[1, 2, 3], 1]; + yield 'DateTimeInterface' => [new \DateTimeImmutable('2021-01-01 00:00:00'), 1609459200000000]; + yield 'DateInterval' => [new \DateInterval('P1D'), 86400000000]; + } + + #[DataProvider('integer_castable_data_provider')] + public function test_casting_different_data_types_to_integer(mixed $value, int $expected) : void + { + $this->assertSame($expected, (new IntegerCastingHandler())->value($value, type_integer())); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php new file mode 100644 index 000000000..37ecbfe1b --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php @@ -0,0 +1,53 @@ +assertSame( + '{"items":{"item":1}}', + (new JsonCastingHandler())->value(['items' => ['item' => 1]], type_json()) + ); + } + + public function test_casting_datetime_to_json() : void + { + $this->assertSame( + '{"date":"2021-01-01 00:00:00.000000","timezone_type":3,"timezone":"UTC"}', + (new JsonCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_json()) + ); + } + + public function test_casting_integer_to_json() : void + { + $this->expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "integer" into "json" type'); + + (new JsonCastingHandler())->value(1, type_json()); + } + + public function test_casting_json_string_to_json() : void + { + $this->assertSame( + '{"items":{"item":1}}', + (new JsonCastingHandler())->value('{"items":{"item":1}}', type_json()) + ); + } + + public function test_casting_non_json_string_to_json() : void + { + $this->expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "string" into "json" type'); + + (new JsonCastingHandler())->value('string', type_json()); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php new file mode 100644 index 000000000..740f61eeb --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php @@ -0,0 +1,24 @@ +assertEquals( + (object) ['foo' => 'bar'], + (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class)) + ); + $this->assertInstanceOf( + \stdClass::class, + (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class)) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php new file mode 100644 index 000000000..18bd297b3 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php @@ -0,0 +1,45 @@ + ['string', 'string']; + yield 'int' => [1, '1']; + yield 'float' => [1.1, '1.1']; + yield 'bool' => [true, 'true']; + yield 'array' => [[1, 2, 3], '[1,2,3]']; + yield 'DateTimeInterface' => [new \DateTimeImmutable('2021-01-01 00:00:00'), '2021-01-01T00:00:00+00:00']; + yield 'Stringable' => [new class() implements \Stringable { + public function __toString() : string + { + return 'stringable'; + } + }, 'stringable']; + yield 'DOMDocument' => [new \DOMDocument(), '']; + } + + #[DataProvider('string_castable_data_provider')] + public function test_casting_different_data_types_to_string(mixed $value, string $expected) : void + { + $this->assertSame($expected, \trim((new StringCastingHandler())->value($value, type_string()))); + } + + public function test_casting_object_to_string() : void + { + $this->expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "object" into "string" type'); + + (new StringCastingHandler())->value(new class() {}, type_string()); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php new file mode 100644 index 000000000..d55c74f33 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php @@ -0,0 +1,37 @@ +expectException(\Flow\ETL\Exception\CastingException::class); + $this->expectExceptionMessage('Can\'t cast "integer" into "uuid" type'); + + (new UuidCastingHandler())->value(1, type_uuid()); + } + + public function test_casting_ramsey_uuid_to_uuid() : void + { + $this->assertEquals( + new Uuid('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), + (new UuidCastingHandler())->value(\Ramsey\Uuid\Uuid::fromString('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), type_uuid()) + ); + } + + public function test_casting_string_to_uuid() : void + { + $this->assertEquals( + new Uuid('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), + (new UuidCastingHandler())->value('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e', type_uuid()) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php new file mode 100644 index 000000000..c550298ba --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php @@ -0,0 +1,29 @@ +expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "integer" into "xml" type'); + + (new XMLCastingHandler())->value(1, type_xml())->saveXML(); + } + + public function test_casting_string_to_xml() : void + { + $this->assertSame( + '' . "\n" . '1' . "\n", + (new XMLCastingHandler())->value('1', type_xml())->saveXML() + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php index 38972fe73..e15362047 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php @@ -24,6 +24,7 @@ use function Flow\ETL\DSL\type_string; use function Flow\ETL\DSL\uuid_entry; use function Flow\ETL\DSL\xml_entry; +use Flow\ETL\Exception\CastingException; use Flow\ETL\Exception\InvalidArgumentException; use Flow\ETL\PHP\Type\Logical\List\ListElement; use Flow\ETL\PHP\Type\Logical\ListType; @@ -103,15 +104,6 @@ public function test_boolean_with_schema() : void ); } - public function test_conversion_to_different_type_with_schema() : void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Field \"e\" conversion exception. Flow\ETL\DSL\str_entry(): Argument #2 (\$value) must be of type string, int given, called in"); - - (new NativeEntryFactory()) - ->create('e', 1, new Schema(Schema\Definition::string('e'))); - } - public function test_datetime() : void { $this->assertEquals( @@ -152,14 +144,14 @@ public function test_enum_from_string_with_schema() : void $this->assertEquals( enum_entry('e', BackedIntEnum::one), (new NativeEntryFactory()) - ->create('e', 'one', new Schema(Schema\Definition::enum('e', BackedIntEnum::class))) + ->create('e', 1, new Schema(Schema\Definition::enum('e', BackedIntEnum::class))) ); } public function test_enum_invalid_value_with_schema() : void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Value \"invalid\" can't be converted to " . BackedIntEnum::class . ' enum'); + $this->expectException(CastingException::class); + $this->expectExceptionMessage("Can't cast \"string\" into \"enum\" type"); (new NativeEntryFactory()) ->create('e', 'invalid', new Schema(Schema\Definition::enum('e', BackedIntEnum::class))); From e5500653964da2bacef7c2b35ba81e426015455d Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 15:08:56 +0100 Subject: [PATCH 2/6] use Type in Scalar Cast function --- src/core/etl/src/Flow/ETL/DSL/functions.php | 8 +- src/core/etl/src/Flow/ETL/Function/Cast.php | 117 ++++++------------ .../Flow/ETL/Function/ScalarFunctionChain.php | 3 +- .../PHP/Type/Caster/ArrayCastingHandler.php | 7 +- .../Type/Caster/XML}/XMLConverter.php | 2 +- .../ETL/Tests/Benchmark/TypeDetectorBench.php | 4 +- .../Type/Caster/ArrayCastingHandlerTest.php | 11 ++ 7 files changed, 67 insertions(+), 85 deletions(-) rename src/core/etl/src/Flow/ETL/{Function/Cast => PHP/Type/Caster/XML}/XMLConverter.php (98%) diff --git a/src/core/etl/src/Flow/ETL/DSL/functions.php b/src/core/etl/src/Flow/ETL/DSL/functions.php index 185007ac4..9acfedce3 100644 --- a/src/core/etl/src/Flow/ETL/DSL/functions.php +++ b/src/core/etl/src/Flow/ETL/DSL/functions.php @@ -110,6 +110,7 @@ use Flow\ETL\PHP\Type\Native\ResourceType; use Flow\ETL\PHP\Type\Native\ScalarType; use Flow\ETL\PHP\Type\Type; +use Flow\ETL\PHP\Type\TypeDetector; use Flow\ETL\Pipeline; use Flow\ETL\Row; use Flow\ETL\Row\EntryFactory; @@ -666,7 +667,7 @@ function hash(ScalarFunction $function, string $algorithm = 'xxh128', bool $bina return new Hash($function, $algorithm, $binary, $options); } -function cast(ScalarFunction $function, string $type) : Cast +function cast(ScalarFunction $function, string|Type $type) : Cast { return new Cast($function, $type); } @@ -1113,3 +1114,8 @@ function append() : SaveMode { return SaveMode::Append; } + +function get_type(mixed $value) : Type +{ + return (new TypeDetector())->detectType($value); +} diff --git a/src/core/etl/src/Flow/ETL/Function/Cast.php b/src/core/etl/src/Flow/ETL/Function/Cast.php index 5195c8fee..e2966b3ea 100644 --- a/src/core/etl/src/Flow/ETL/Function/Cast.php +++ b/src/core/etl/src/Flow/ETL/Function/Cast.php @@ -4,14 +4,26 @@ namespace Flow\ETL\Function; +use function Flow\ETL\DSL\type_array; +use function Flow\ETL\DSL\type_boolean; +use function Flow\ETL\DSL\type_datetime; +use function Flow\ETL\DSL\type_float; +use function Flow\ETL\DSL\type_integer; +use function Flow\ETL\DSL\type_json; +use function Flow\ETL\DSL\type_object; +use function Flow\ETL\DSL\type_string; +use function Flow\ETL\DSL\type_xml; +use Flow\ETL\Exception\CastingException; use Flow\ETL\Exception\InvalidArgumentException; +use Flow\ETL\PHP\Type\Caster; +use Flow\ETL\PHP\Type\Type; use Flow\ETL\Row; final class Cast extends ScalarFunctionChain { public function __construct( private readonly ScalarFunction $ref, - private readonly string $type + private readonly string|Type $type ) { } @@ -27,88 +39,35 @@ public function eval(Row $row) : mixed return null; } - return match (\mb_strtolower($this->type)) { - 'datetime' => match (\gettype($value)) { - 'string' => new \DateTimeImmutable($value), - 'integer' => \DateTimeImmutable::createFromFormat('U', (string) $value), - default => null, - }, - 'date' => match (\gettype($value)) { - 'string' => (new \DateTimeImmutable($value))->setTime(0, 0, 0, 0), - 'integer' => \DateTimeImmutable::createFromFormat('U', (string) $value), - default => null, - }, - 'int', 'integer' => (int) $value, - 'float', 'double', 'real' => (float) $value, - 'string' => $this->toString($value), - 'bool', 'boolean' => (bool) $value, - 'array' => $this->toArray($value), - 'object' => (object) $value, - 'json' => \json_encode($value, JSON_THROW_ON_ERROR), - 'json_pretty' => \json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), - 'xml' => $this->toXML($value), - default => null - }; - } - - private function toArray(mixed $data) : array - { - if ($data instanceof \DOMDocument) { - return (new Cast\XMLConverter())->toArray($data); - } - - return (array) $data; - } - - private function toString(mixed $value) : ?string - { - if ($value === null) { - return null; - } - - if (\is_string($value)) { - return $value; - } - - if (\is_bool($value)) { - return $value ? 'true' : 'false'; - } + $caster = Caster::default(); - if (\is_array($value)) { - return \json_encode($value, JSON_THROW_ON_ERROR); - } - - if ($value instanceof \DateTimeInterface) { - return $value->format(\DateTimeInterface::RFC3339); - } - - if ($value instanceof \Stringable) { - return (string) $value; - } - - if ($value instanceof \DOMDocument) { - return $value->saveXML() ?: null; - } + $type = $this->type; - return (string) $value; - } - - private function toXML(mixed $value) : null|\DOMDocument - { - if (\is_string($value)) { - $doc = new \DOMDocument(); - - if (!@$doc->load($value)) { - return null; - } - - return $doc; + if ($type instanceof Type) { + return $caster->to($type)->value($value); } - if ($value instanceof \DOMDocument) { - return $value; + try { + return match (\mb_strtolower($type)) { + 'datetime' => $caster->to(type_datetime())->value($value), + 'date' => match (\gettype($value)) { + 'string' => (new \DateTimeImmutable($value))->setTime(0, 0, 0, 0), + 'integer' => \DateTimeImmutable::createFromFormat('U', (string) $value), + default => null, + }, + 'int', 'integer' => $caster->to(type_integer())->value($value), + 'float', 'double', 'real' => $caster->to(type_float())->value($value), + 'string' => $caster->to(type_string())->value($value), + 'bool', 'boolean' => $caster->to(type_boolean())->value($value), + 'array' => $caster->to(type_array())->value($value), + 'object' => $caster->to(type_object(\stdClass::class))->value($value), + 'json' => $caster->to(type_json())->value($value), + 'json_pretty' => \json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT), + 'xml' => $caster->to(type_xml())->value($value), + default => null + }; + } catch (CastingException $e) { + return null; } - - return null; } } diff --git a/src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php b/src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php index f7a9850ab..66b593b00 100644 --- a/src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php +++ b/src/core/etl/src/Flow/ETL/Function/ScalarFunctionChain.php @@ -10,6 +10,7 @@ use Flow\ETL\Function\ArrayExpand\ArrayExpand; use Flow\ETL\Function\ArraySort\Sort; use Flow\ETL\Function\Between\Boundary; +use Flow\ETL\PHP\Type\Type; use Flow\ETL\Row\Entry; abstract class ScalarFunctionChain implements ScalarFunction @@ -59,7 +60,7 @@ public function capitalize() : self return new Capitalize($this); } - public function cast(string $type) : self + public function cast(string|Type $type) : self { return new Cast($this, $type); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php index d2a7e00f1..96c5f2c10 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster\XML\XMLConverter; use Flow\ETL\PHP\Type\Native\ArrayType; use Flow\ETL\PHP\Type\Type; @@ -18,7 +19,7 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type) : mixed { try { - if (\is_string($value)) { + if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); } @@ -26,6 +27,10 @@ public function value(mixed $value, Type $type) : mixed return $value; } + if ($value instanceof \DOMDocument) { + return (new XMLConverter())->toArray($value); + } + if (\is_object($value)) { return \json_decode(\json_encode($value, JSON_THROW_ON_ERROR), true, 512, \JSON_THROW_ON_ERROR); } diff --git a/src/core/etl/src/Flow/ETL/Function/Cast/XMLConverter.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XML/XMLConverter.php similarity index 98% rename from src/core/etl/src/Flow/ETL/Function/Cast/XMLConverter.php rename to src/core/etl/src/Flow/ETL/PHP/Type/Caster/XML/XMLConverter.php index 7345e124d..7f0456fb7 100644 --- a/src/core/etl/src/Flow/ETL/Function/Cast/XMLConverter.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XML/XMLConverter.php @@ -1,6 +1,6 @@ detectType($params['data']); + get_type($params['data']); } public function provideRows() : \Generator diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php index b949d82d5..25aa1b698 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php @@ -49,4 +49,15 @@ public function test_casting_string_to_array() : void (new ArrayCastingHandler())->value('{"items":{"item":1}}', type_array()) ); } + + public function test_casting_xml_document_to_array() : void + { + $xml = new \DOMDocument(); + $xml->loadXML($xmlString = 'bar'); + + $this->assertSame( + ['root' => ['foo' => ['@attributes' => ['baz' => 'buz'], '@value' => 'bar']]], + (new ArrayCastingHandler())->value($xml, type_array()) + ); + } } From a713024fed4d037e6f9e0cf7b41c999d90f20740 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 15:18:55 +0100 Subject: [PATCH 3/6] Unified and reorganized casting/type detection logic --- src/core/etl/src/Flow/ETL/DataFrame.php | 4 +- .../etl/src/Flow/ETL/PHP/Type/AutoCaster.php | 59 +++++++++++++++++++ .../ETL/PHP/Type/Caster/CastingContext.php | 1 - .../Type/Caster}/StringTypeChecker.php | 4 +- .../Flow/ETL/PHP/Type/Logical/JsonType.php | 2 +- .../ETL/Row/Factory/NativeEntryFactory.php | 1 + .../ETL/Transformer/AutoCastTransformer.php | 55 +++-------------- .../Type/Caster}/StringTypeCheckerTest.php | 8 ++- .../Transformer/AutoCastTransformerTest.php | 4 +- 9 files changed, 84 insertions(+), 54 deletions(-) create mode 100644 src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php rename src/core/etl/src/Flow/ETL/{Row/Factory => PHP/Type/Caster}/StringTypeChecker.php (97%) rename src/core/etl/tests/Flow/ETL/Tests/Unit/{Row/Factory => PHP/Type/Caster}/StringTypeCheckerTest.php (91%) diff --git a/src/core/etl/src/Flow/ETL/DataFrame.php b/src/core/etl/src/Flow/ETL/DataFrame.php index 7f2db4484..97998824f 100644 --- a/src/core/etl/src/Flow/ETL/DataFrame.php +++ b/src/core/etl/src/Flow/ETL/DataFrame.php @@ -21,6 +21,8 @@ use Flow\ETL\Loader\SchemaValidationLoader; use Flow\ETL\Loader\StreamLoader\Output; use Flow\ETL\Partition\ScalarFunctionFilter; +use Flow\ETL\PHP\Type\AutoCaster; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\Pipeline\BatchingPipeline; use Flow\ETL\Pipeline\CachingPipeline; use Flow\ETL\Pipeline\CollectingPipeline; @@ -151,7 +153,7 @@ public function appendSafe(bool $appendSafe = true) : self public function autoCast() : self { - $this->pipeline->add(new AutoCastTransformer()); + $this->pipeline->add(new AutoCastTransformer(new AutoCaster(Caster::default()))); return $this; } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php b/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php new file mode 100644 index 000000000..ddaa92eb9 --- /dev/null +++ b/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php @@ -0,0 +1,59 @@ +isNull()) { + return null; + } + + if ($typeChecker->isInteger()) { + return $this->caster->to(type_integer())->value($value); + } + + if ($typeChecker->isFloat()) { + return $this->caster->to(type_float())->value($value); + } + + if ($typeChecker->isBoolean()) { + return $this->caster->to(type_boolean())->value($value); + } + + if ($typeChecker->isJson()) { + return $this->caster->to(type_json())->value($value); + } + + if ($typeChecker->isUuid()) { + return $this->caster->to(type_uuid())->value($value); + } + + if ($typeChecker->isDateTime()) { + return $this->caster->to(type_datetime())->value($value); + } + + return $value; + } +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php index 3adacf1df..ac44df577 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php @@ -10,7 +10,6 @@ final class CastingContext { public function __construct(private readonly CastingHandler $handler, private readonly Type $type) { - } public function value(mixed $value) : mixed diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/StringTypeChecker.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php similarity index 97% rename from src/core/etl/src/Flow/ETL/Row/Factory/StringTypeChecker.php rename to src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php index 9e3c1543b..5bf2d9185 100644 --- a/src/core/etl/src/Flow/ETL/Row/Factory/StringTypeChecker.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Flow\ETL\Row\Factory; +namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Row\Entry\Type\Uuid; @@ -21,7 +21,7 @@ public function isBoolean() : bool return false; } - return \in_array(\strtolower($this->string), ['true', 'false'], true); + return \in_array(\strtolower($this->string), ['true', 'false', 'yes', 'no', 'on', 'off'], true); } public function isDateTime() : bool diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php b/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php index a422998a7..0a2ffdb63 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php @@ -5,9 +5,9 @@ namespace Flow\ETL\PHP\Type\Logical; use Flow\ETL\Exception\InvalidArgumentException; +use Flow\ETL\PHP\Type\Caster\StringTypeChecker; use Flow\ETL\PHP\Type\Native\NullType; use Flow\ETL\PHP\Type\Type; -use Flow\ETL\Row\Factory\StringTypeChecker; final class JsonType implements LogicalType { diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php index 1380bbc9f..debed3a13 100644 --- a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php +++ b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php @@ -24,6 +24,7 @@ use Flow\ETL\Exception\InvalidArgumentException; use Flow\ETL\Exception\RuntimeException; use Flow\ETL\PHP\Type\Caster; +use Flow\ETL\PHP\Type\Caster\StringTypeChecker; use Flow\ETL\PHP\Type\Logical\DateTimeType; use Flow\ETL\PHP\Type\Logical\JsonType; use Flow\ETL\PHP\Type\Logical\ListType; diff --git a/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php b/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php index 10d8d7fcd..8aa6001f0 100644 --- a/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php +++ b/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php @@ -4,14 +4,8 @@ namespace Flow\ETL\Transformer; -use function Flow\ETL\DSL\bool_entry; -use function Flow\ETL\DSL\datetime_entry; -use function Flow\ETL\DSL\float_entry; -use function Flow\ETL\DSL\int_entry; -use function Flow\ETL\DSL\json_entry; -use function Flow\ETL\DSL\null_entry; -use function Flow\ETL\DSL\uuid_entry; use Flow\ETL\FlowContext; +use Flow\ETL\PHP\Type\AutoCaster; use Flow\ETL\Row; use Flow\ETL\Row\Entry; use Flow\ETL\Row\Entry\StringEntry; @@ -20,50 +14,19 @@ final class AutoCastTransformer implements Transformer { - public function autoCast(Entry $entry) : Entry + public function __construct(private readonly AutoCaster $caster) { - if (!$entry instanceof StringEntry) { - return $entry; - } - - $typeChecker = new Row\Factory\StringTypeChecker($entry->value()); - - if ($typeChecker->isNull()) { - return null_entry($entry->name()); - } - - if ($typeChecker->isInteger()) { - return int_entry($entry->name(), (int) $entry->value()); - } - - if ($typeChecker->isFloat()) { - return float_entry($entry->name(), (float) $entry->value()); - } - - if ($typeChecker->isBoolean()) { - return bool_entry($entry->name(), (bool) $entry->value()); - } - - if ($typeChecker->isJson()) { - return json_entry($entry->name(), $entry->value()); - } - - if ($typeChecker->isUuid()) { - return uuid_entry($entry->name(), $entry->value()); - } - - if ($typeChecker->isDateTime()) { - return datetime_entry($entry->name(), $entry->value()); - } - - return $entry; } public function transform(Rows $rows, FlowContext $context) : Rows { - return $rows->map(function (Row $row) { - return $row->map(function (Entry $entry) { - return $this->autoCast($entry); + return $rows->map(function (Row $row) use ($context) { + return $row->map(function (Entry $entry) use ($context) { + if (!$entry instanceof StringEntry) { + return $entry; + } + + return $context->entryFactory()->create($entry->name(), $this->caster->cast($entry->value())); }); }); } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/StringTypeCheckerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php similarity index 91% rename from src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/StringTypeCheckerTest.php rename to src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php index 0f02050e1..aba03d71c 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/StringTypeCheckerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Flow\ETL\Tests\Unit\Row\Factory; +namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; -use Flow\ETL\Row\Factory\StringTypeChecker; +use Flow\ETL\PHP\Type\Caster\StringTypeChecker; use PHPUnit\Framework\TestCase; final class StringTypeCheckerTest extends TestCase @@ -13,6 +13,10 @@ public function test_detecting_boolean() : void { $this->assertTrue((new StringTypeChecker('true'))->isBoolean()); $this->assertTrue((new StringTypeChecker('false'))->isBoolean()); + $this->assertTrue((new StringTypeChecker('yes'))->isBoolean()); + $this->assertTrue((new StringTypeChecker('no'))->isBoolean()); + $this->assertTrue((new StringTypeChecker('on'))->isBoolean()); + $this->assertTrue((new StringTypeChecker('off'))->isBoolean()); $this->assertFalse((new StringTypeChecker('0'))->isBoolean()); $this->assertFalse((new StringTypeChecker('not bool'))->isBoolean()); } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/AutoCastTransformerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/AutoCastTransformerTest.php index 1830c52da..56b8e5b84 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/AutoCastTransformerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Transformer/AutoCastTransformerTest.php @@ -6,6 +6,8 @@ use function Flow\ETL\DSL\array_to_rows; use function Flow\ETL\DSL\flow_context; +use Flow\ETL\PHP\Type\AutoCaster; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\Transformer\AutoCastTransformer; use PHPUnit\Framework\TestCase; @@ -13,7 +15,7 @@ final class AutoCastTransformerTest extends TestCase { public function test_transforming_row() : void { - $transformer = new AutoCastTransformer(); + $transformer = new AutoCastTransformer(new AutoCaster(Caster::default())); $rows = array_to_rows([ [ From 40b210c79763e7455af976dc488a6c8a28d19c76 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 16:03:13 +0100 Subject: [PATCH 4/6] Allow to pass schema into CSV/Json extractors --- .../src/Flow/ETL/Adapter/CSV/CSVExtractor.php | 6 +- .../src/Flow/ETL/Adapter/CSV/functions.php | 6 +- .../Tests/Integration/CSVExtractorTest.php | 56 +++++++++++++++++++ .../JSON/JSONMachine/JsonExtractor.php | 4 +- .../src/Flow/ETL/Adapter/JSON/functions.php | 4 ++ .../JSONMachine/JsonExtractorTest.php | 44 +++++++++++++++ src/core/etl/src/Flow/ETL/DSL/functions.php | 20 +++++-- src/core/etl/src/Flow/ETL/PHP/Type/Caster.php | 44 ++++++++++----- .../ETL/Row/Factory/NativeEntryFactory.php | 47 ++++++++-------- .../src/Flow/ETL/Row/Schema/Definition.php | 12 +++- 10 files changed, 195 insertions(+), 48 deletions(-) diff --git a/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/CSVExtractor.php b/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/CSVExtractor.php index acbdd5903..a9cab672a 100644 --- a/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/CSVExtractor.php +++ b/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/CSVExtractor.php @@ -15,6 +15,7 @@ use Flow\ETL\Filesystem\Path; use Flow\ETL\Filesystem\Stream\Mode; use Flow\ETL\FlowContext; +use Flow\ETL\Row\Schema; final class CSVExtractor implements Extractor, FileExtractor, LimitableExtractor, PartitionsExtractor { @@ -31,7 +32,8 @@ public function __construct( private readonly string|null $separator = null, private readonly string|null $enclosure = null, private readonly string|null $escape = null, - private readonly int $charactersReadInLine = 1000 + private readonly int $charactersReadInLine = 1000, + private readonly Schema|null $schema = null ) { $this->resetLimit(); } @@ -98,7 +100,7 @@ public function extract(FlowContext $context) : \Generator $row['_input_file_uri'] = $stream->path()->uri(); } - $signal = yield array_to_rows($row, $context->entryFactory(), $path->partitions()); + $signal = yield array_to_rows($row, $context->entryFactory(), $path->partitions(), $this->schema); $this->countRow(); if ($signal === Signal::STOP || $this->reachedLimit()) { diff --git a/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/functions.php b/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/functions.php index 7d0f05cc1..5900b0721 100644 --- a/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/functions.php +++ b/src/adapter/etl-adapter-csv/src/Flow/ETL/Adapter/CSV/functions.php @@ -10,6 +10,7 @@ use Flow\ETL\Extractor; use Flow\ETL\Filesystem\Path; use Flow\ETL\Loader; +use Flow\ETL\Row\Schema; /** * @param int<0, max> $characters_read_in_line @@ -21,7 +22,8 @@ function from_csv( string|null $delimiter = null, string|null $enclosure = null, string|null $escape = null, - int $characters_read_in_line = 1000 + int $characters_read_in_line = 1000, + Schema|null $schema = null ) : Extractor { if (\is_array($path)) { $extractors = []; @@ -35,6 +37,7 @@ function from_csv( $enclosure, $escape, $characters_read_in_line, + $schema ); } @@ -49,6 +52,7 @@ function from_csv( $enclosure, $escape, $characters_read_in_line, + $schema ); } diff --git a/src/adapter/etl-adapter-csv/tests/Flow/ETL/Adapter/CSV/Tests/Integration/CSVExtractorTest.php b/src/adapter/etl-adapter-csv/tests/Flow/ETL/Adapter/CSV/Tests/Integration/CSVExtractorTest.php index 58adaf78e..69f766c30 100644 --- a/src/adapter/etl-adapter-csv/tests/Flow/ETL/Adapter/CSV/Tests/Integration/CSVExtractorTest.php +++ b/src/adapter/etl-adapter-csv/tests/Flow/ETL/Adapter/CSV/Tests/Integration/CSVExtractorTest.php @@ -8,6 +8,7 @@ use function Flow\ETL\Adapter\CSV\to_csv; use function Flow\ETL\DSL\df; use function Flow\ETL\DSL\from_array; +use function Flow\ETL\DSL\print_schema; use function Flow\ETL\DSL\ref; use Flow\ETL\Adapter\CSV\CSVExtractor; use Flow\ETL\Config; @@ -143,6 +144,61 @@ public function test_extracting_csv_files_with_header() : void $this->assertSame(998, $rows->count()); } + public function test_extracting_csv_files_with_schema() : void + { + $path = __DIR__ . '/../Fixtures/annual-enterprise-survey-2019-financial-year-provisional-csv.csv'; + + $rows = df() + ->read( + from_csv($path, schema: $schema = df() + ->read(from_csv($path)) + ->autoCast() + ->schema()) + ) + ->fetch(); + + foreach ($rows as $row) { + $this->assertSame( + [ + 'Year', + 'Industry_aggregation_NZSIOC', + 'Industry_code_NZSIOC', + 'Industry_name_NZSIOC', + 'Units', + 'Variable_code', + 'Variable_name', + 'Variable_category', + 'Value', + 'Industry_code_ANZSIC06', + + ], + \array_keys($row->toArray()) + ); + } + + $this->assertSame(998, $rows->count()); + $this->assertEquals($schema, $rows->schema()); + + $this->assertSame( + <<<'SCHEMA' +schema +|-- Year: integer +|-- Industry_aggregation_NZSIOC: string +|-- Industry_code_NZSIOC: string +|-- Industry_name_NZSIOC: string +|-- Units: string +|-- Variable_code: string +|-- Variable_name: string +|-- Variable_category: string +|-- Value: string +|-- Industry_code_ANZSIC06: string + +SCHEMA, + print_schema($rows->schema()) + ); + + } + public function test_extracting_csv_files_without_header() : void { $extractor = from_csv( diff --git a/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/JSONMachine/JsonExtractor.php b/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/JSONMachine/JsonExtractor.php index ae701b072..335e52db7 100644 --- a/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/JSONMachine/JsonExtractor.php +++ b/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/JSONMachine/JsonExtractor.php @@ -15,6 +15,7 @@ use Flow\ETL\Filesystem\Path; use Flow\ETL\Filesystem\Stream\Mode; use Flow\ETL\FlowContext; +use Flow\ETL\Row\Schema; use JsonMachine\Items; use JsonMachine\JsonDecoder\ExtJsonDecoder; @@ -26,6 +27,7 @@ final class JsonExtractor implements Extractor, FileExtractor, LimitableExtracto public function __construct( private readonly Path $path, private readonly ?string $pointer = null, + private readonly Schema|null $schema = null, ) { $this->resetLimit(); } @@ -46,7 +48,7 @@ public function extract(FlowContext $context) : \Generator $row['_input_file_uri'] = $filePath->uri(); } - $signal = yield array_to_rows($row, $context->entryFactory(), $filePath->partitions()); + $signal = yield array_to_rows($row, $context->entryFactory(), $filePath->partitions(), $this->schema); $this->countRow(); if ($signal === Signal::STOP || $this->reachedLimit()) { diff --git a/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/functions.php b/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/functions.php index c4509ee8a..9574e2a1e 100644 --- a/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/functions.php +++ b/src/adapter/etl-adapter-json/src/Flow/ETL/Adapter/JSON/functions.php @@ -9,6 +9,7 @@ use Flow\ETL\Extractor; use Flow\ETL\Filesystem\Path; use Flow\ETL\Loader; +use Flow\ETL\Row\Schema; /** * @param array|Path|string $path - string is internally turned into stream @@ -19,6 +20,7 @@ function from_json( string|Path|array $path, ?string $pointer = null, + Schema|null $schema = null, ) : Extractor { if (\is_array($path)) { $extractors = []; @@ -27,6 +29,7 @@ function from_json( $extractors[] = new JsonExtractor( \is_string($file) ? Path::realpath($file) : $file, $pointer, + $schema ); } @@ -36,6 +39,7 @@ function from_json( return new JsonExtractor( \is_string($path) ? Path::realpath($path) : $path, $pointer, + $schema ); } diff --git a/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php b/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php index ebb20e533..9c001eff2 100644 --- a/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php +++ b/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php @@ -6,7 +6,9 @@ use function Flow\ETL\Adapter\JSON\from_json; use function Flow\ETL\Adapter\JSON\to_json; +use function Flow\ETL\DSL\df; use function Flow\ETL\DSL\from_array; +use function Flow\ETL\DSL\print_schema; use Flow\ETL\Adapter\JSON\JSONMachine\JsonExtractor; use Flow\ETL\Config; use Flow\ETL\Extractor\Signal; @@ -65,6 +67,48 @@ public function test_extracting_json_from_local_file_stream_using_pointer() : vo $this->assertSame(247, $rows->count()); } + public function test_extracting_json_from_local_file_stream_with_schema() : void + { + $rows = df() + ->read(from_json( + __DIR__ . '/../../Fixtures/timezones.json', + schema: $schema = df() + ->read(from_json(__DIR__ . '/../../Fixtures/timezones.json')) + ->autoCast() + ->schema() + )) + ->fetch(); + + foreach ($rows as $row) { + $this->assertSame( + [ + 'timezones', + 'latlng', + 'name', + 'country_code', + 'capital', + ], + \array_keys($row->toArray()) + ); + } + + $this->assertSame(247, $rows->count()); + $this->assertEquals($schema, $rows->schema()); + $this->assertSame( + <<<'SCHEMA' +schema +|-- timezones: list +|-- latlng: array +|-- name: string +|-- country_code: string +|-- capital: ?string + +SCHEMA + , + print_schema($schema) + ); + } + public function test_extracting_json_from_local_file_string_uri() : void { $extractor = new JsonExtractor(Path::realpath(__DIR__ . '/../../Fixtures/timezones.json')); diff --git a/src/core/etl/src/Flow/ETL/DSL/functions.php b/src/core/etl/src/Flow/ETL/DSL/functions.php index 9acfedce3..538719b3e 100644 --- a/src/core/etl/src/Flow/ETL/DSL/functions.php +++ b/src/core/etl/src/Flow/ETL/DSL/functions.php @@ -868,7 +868,7 @@ function number_format(ScalarFunction $function, ?ScalarFunction $decimals = nul * @param array>|array $data * @param array|\Flow\ETL\Partitions $partitions */ -function array_to_rows(array $data, EntryFactory $entryFactory = new NativeEntryFactory(), array|\Flow\ETL\Partitions $partitions = []) : Rows +function array_to_rows(array $data, EntryFactory $entryFactory = new NativeEntryFactory(), array|\Flow\ETL\Partitions $partitions = [], ?Schema $schema = null) : Rows { $partitions = \is_array($partitions) ? new \Flow\ETL\Partitions(...$partitions) : $partitions; @@ -888,12 +888,12 @@ function array_to_rows(array $data, EntryFactory $entryFactory = new NativeEntry foreach ($data as $key => $value) { $name = \is_int($key) ? 'e' . \str_pad((string) $key, 2, '0', STR_PAD_LEFT) : $key; - $entries[$name] = $entryFactory->create($name, $value); + $entries[$name] = $entryFactory->create($name, $value, $schema); } foreach ($partitions as $partition) { if (!\array_key_exists($partition->name, $entries)) { - $entries[$partition->name] = $entryFactory->create($partition->name, $partition->value); + $entries[$partition->name] = $entryFactory->create($partition->name, $partition->value, $schema); } } @@ -907,12 +907,12 @@ function array_to_rows(array $data, EntryFactory $entryFactory = new NativeEntry foreach ($row as $column => $value) { $name = \is_int($column) ? 'e' . \str_pad((string) $column, 2, '0', STR_PAD_LEFT) : $column; - $entries[$name] = $entryFactory->create(\is_int($column) ? 'e' . \str_pad((string) $column, 2, '0', STR_PAD_LEFT) : $column, $value); + $entries[$name] = $entryFactory->create(\is_int($column) ? 'e' . \str_pad((string) $column, 2, '0', STR_PAD_LEFT) : $column, $value, $schema); } foreach ($partitions as $partition) { if (!\array_key_exists($partition->name, $entries)) { - $entries[$partition->name] = $entryFactory->create($partition->name, $partition->value); + $entries[$partition->name] = $entryFactory->create($partition->name, $partition->value, $schema); } } @@ -1119,3 +1119,13 @@ function get_type(mixed $value) : Type { return (new TypeDetector())->detectType($value); } + +function print_schema(Schema $schema, ?SchemaFormatter $formatter = null) : string +{ + return ($formatter ?? new ASCIISchemaFormatter())->format($schema); +} + +function print_rows(Rows $rows, int|bool $truncate = false, ?Formatter $formatter = null) : string +{ + return ($formatter ?? new Formatter\AsciiTableFormatter())->format($rows, $truncate); +} diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php index 7f3f32206..f916811ff 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php @@ -4,6 +4,16 @@ namespace Flow\ETL\PHP\Type; +use function Flow\ETL\DSL\type_array; +use function Flow\ETL\DSL\type_boolean; +use function Flow\ETL\DSL\type_datetime; +use function Flow\ETL\DSL\type_float; +use function Flow\ETL\DSL\type_integer; +use function Flow\ETL\DSL\type_json; +use function Flow\ETL\DSL\type_null; +use function Flow\ETL\DSL\type_string; +use function Flow\ETL\DSL\type_uuid; +use function Flow\ETL\DSL\type_xml; use Flow\ETL\Exception\RuntimeException; use Flow\ETL\PHP\Type\Caster\ArrayCastingHandler; use Flow\ETL\PHP\Type\Caster\BooleanCastingHandler; @@ -35,26 +45,30 @@ public function __construct(private readonly array $handlers) public static function default() : self { return new self([ - new StringCastingHandler(), - new IntegerCastingHandler(), - new BooleanCastingHandler(), - new FloatCastingHandler(), - new XMLCastingHandler(), - new UuidCastingHandler(), - new ObjectCastingHandler(), - new DateTimeCastingHandler(), - new JsonCastingHandler(), - new ArrayCastingHandler(), - new ListCastingHandler(), - new MapCastingHandler(), - new StructureCastingHandler(), - new NullCastingHandler(), - new EnumCastingHandler(), + type_string()->toString() => new StringCastingHandler(), + type_integer()->toString() => new IntegerCastingHandler(), + type_boolean()->toString() => new BooleanCastingHandler(), + type_float()->toString() => new FloatCastingHandler(), + type_xml()->toString() => new XMLCastingHandler(), + type_uuid()->toString() => new UuidCastingHandler(), + 'object' => new ObjectCastingHandler(), + type_datetime()->toString() => new DateTimeCastingHandler(), + type_json()->toString() => new JsonCastingHandler(), + type_array()->toString() => new ArrayCastingHandler(), + 'list' => new ListCastingHandler(), + 'map', new MapCastingHandler(), + 'structure', new StructureCastingHandler(), + type_null()->toString() => new NullCastingHandler(), + 'enum' => new EnumCastingHandler(), ]); } public function to(Type $type) : CastingContext { + if (\array_key_exists($type->toString(), $this->handlers)) { + return new CastingContext($this->handlers[$type->toString()], $type); + } + foreach ($this->handlers as $handler) { if ($handler->supports($type)) { return new CastingContext($handler, $type); diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php index debed3a13..202c25a5d 100644 --- a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php +++ b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php @@ -46,6 +46,13 @@ final class NativeEntryFactory implements EntryFactory { + private readonly Caster $caster; + + public function __construct() + { + $this->caster = Caster::default(); + } + /** * @throws InvalidArgumentException * @throws RuntimeException @@ -163,69 +170,63 @@ public function create(string $entryName, mixed $value, ?Schema $schema = null) private function fromDefinition(Schema\Definition $definition, mixed $value) : Entry { - if ($definition->isNullable()) { - if (null === $value) { - return null_entry($definition->entry()->name()); - } - - throw new InvalidArgumentException("Entry \"{$definition->entry()}\" is not nullable, but null value was given"); + if ($definition->isNullable() && null === $value) { + return null_entry($definition->entry()->name()); } - $caster = Caster::default(); - try { if ($definition->type() instanceof ScalarType) { return match ($definition->type()->type()) { - ScalarType::STRING => str_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), - ScalarType::INTEGER => int_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), - ScalarType::FLOAT => float_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), - ScalarType::BOOLEAN => bool_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)), + ScalarType::STRING => str_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)), + ScalarType::INTEGER => int_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)), + ScalarType::FLOAT => float_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)), + ScalarType::BOOLEAN => bool_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)), default => throw new InvalidArgumentException("Can't convert value into entry \"{$definition->entry()}\""), }; } if ($definition->type() instanceof XMLType) { - return xml_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return xml_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof UuidType) { - return uuid_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return uuid_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof ObjectType) { - return obj_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return obj_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof DateTimeType) { - return datetime_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return datetime_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof EnumType) { - return enum_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return enum_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof JsonType) { try { - return json_object_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return json_object_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } catch (InvalidArgumentException) { - return json_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return json_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } } if ($definition->type() instanceof ArrayType) { - return array_entry($definition->entry()->name(), $caster->to($definition->type())->value($value)); + return array_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value)); } if ($definition->type() instanceof MapType) { - return map_entry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); + return map_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value), $definition->type()); } if ($definition->type() instanceof StructureType) { - return struct_entry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); + return struct_entry($definition->entry()->name(), $this->caster->to($definition->type())->value($value), $definition->type()); } if ($definition->type() instanceof ListType) { - return new Entry\ListEntry($definition->entry()->name(), $caster->to($definition->type())->value($value), $definition->type()); + return new Entry\ListEntry($definition->entry()->name(), $this->caster->to($definition->type())->value($value), $definition->type()); } } catch (InvalidArgumentException|\TypeError $e) { throw new InvalidArgumentException("Field \"{$definition->entry()}\" conversion exception. {$e->getMessage()}", previous: $e); diff --git a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php index 6ddf008bc..fea7a3675 100644 --- a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php +++ b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php @@ -312,7 +312,17 @@ public function merge(self $definition) : self ); } - throw new RuntimeException(\sprintf('Cannot merge definitions for entries, "%s" and "%s"', $this->ref->name(), $definition->ref->name())); + if (\in_array(ArrayEntry::class, $entryClasses, true)) { + return new self( + $this->ref, + ArrayEntry::class, + type_array(false, $this->isNullable() || $definition->isNullable()), + $constraint, + $this->metadata->merge($definition->metadata) + ); + } + + throw new RuntimeException(\sprintf('Cannot merge definitions for entries, "%s (%s)" and "%s (%s)"', $this->ref->name(), $this->type->toString(), $definition->ref->name(), $definition->type->toString())); } public function metadata() : Metadata From f2d5af429ada5b3faf57ccec216dbca8a17d4670 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 22:14:54 +0100 Subject: [PATCH 5/6] Casting Lists/Maps/Structures --- .../JSONMachine/JsonExtractorTest.php | 2 +- src/core/etl/src/Flow/ETL/DSL/functions.php | 15 +++ .../Flow/ETL/Exception/CastingException.php | 8 +- .../ETL/PHP/Type/ArrayContentDetector.php | 16 +-- .../etl/src/Flow/ETL/PHP/Type/AutoCaster.php | 41 ++++++- src/core/etl/src/Flow/ETL/PHP/Type/Caster.php | 8 +- .../PHP/Type/Caster/ArrayCastingHandler.php | 3 +- .../PHP/Type/Caster/BooleanCastingHandler.php | 3 +- .../ETL/PHP/Type/Caster/CastingContext.php | 19 ++- .../ETL/PHP/Type/Caster/CastingHandler.php | 3 +- .../Type/Caster/DateTimeCastingHandler.php | 3 +- .../PHP/Type/Caster/EnumCastingHandler.php | 3 +- .../PHP/Type/Caster/FloatCastingHandler.php | 3 +- .../PHP/Type/Caster/IntegerCastingHandler.php | 3 +- .../PHP/Type/Caster/JsonCastingHandler.php | 3 +- .../PHP/Type/Caster/ListCastingHandler.php | 22 ++-- .../ETL/PHP/Type/Caster/MapCastingHandler.php | 32 +++-- .../PHP/Type/Caster/NullCastingHandler.php | 3 +- .../PHP/Type/Caster/ObjectCastingHandler.php | 3 +- .../PHP/Type/Caster/StringCastingHandler.php | 3 +- .../StringTypeChecker.php | 2 +- .../Type/Caster/StructureCastingHandler.php | 24 +++- .../PHP/Type/Caster/UuidCastingHandler.php | 3 +- .../ETL/PHP/Type/Caster/XMLCastingHandler.php | 3 +- .../Flow/ETL/PHP/Type/Logical/JsonType.php | 2 +- .../ETL/Row/Factory/NativeEntryFactory.php | 2 +- .../src/Flow/ETL/Row/Schema/Definition.php | 16 +++ .../ETL/Transformer/AutoCastTransformer.php | 6 +- .../Tests/Unit/PHP/Type/AutoCasterTest.php | 20 +++ .../Type/Caster/ArrayCastingHandlerTest.php | 13 +- .../Type/Caster/BooleanCastingHandlerTest.php | 3 +- .../Caster/DateTimeCastingHandlerTest.php | 3 +- .../Type/Caster/EnumCastingHandlerTest.php | 5 +- .../Type/Caster/FloatCastingHandlerTest.php | 3 +- .../Type/Caster/IntegerCastingHandlerTest.php | 3 +- .../Type/Caster/JsonCastingHandlerTest.php | 11 +- .../Type/Caster/ListCastingHandlerTest.php | 31 +++++ .../PHP/Type/Caster/MapCastingHandlerTest.php | 55 +++++++++ .../Type/Caster/ObjectCastingHandlerTest.php | 5 +- .../StringTypeCheckerTest.php | 4 +- .../Type/Caster/StringCastingHandlerTest.php | 5 +- .../Caster/StructureCastingHandlerTest.php | 115 ++++++++++++++++++ .../Type/Caster/UuidCastingHandlerTest.php | 10 +- .../PHP/Type/Caster/XMLCastingHandlerTest.php | 5 +- .../Row/Factory/NativeEntryFactoryTest.php | 8 +- .../Tests/Unit/Row/Schema/DefinitionTest.php | 8 ++ 46 files changed, 467 insertions(+), 94 deletions(-) rename src/core/etl/src/Flow/ETL/PHP/Type/Caster/{ => StringCastingHandler}/StringTypeChecker.php (98%) create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/AutoCasterTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ListCastingHandlerTest.php create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/MapCastingHandlerTest.php rename src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/{ => StringCastingHandler}/StringTypeCheckerTest.php (97%) create mode 100644 src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StructureCastingHandlerTest.php diff --git a/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php b/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php index 9c001eff2..4ce4f2af0 100644 --- a/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php +++ b/src/adapter/etl-adapter-json/tests/Flow/ETL/Adapter/JSON/Tests/Integration/JSONMachine/JsonExtractorTest.php @@ -98,7 +98,7 @@ public function test_extracting_json_from_local_file_stream_with_schema() : void <<<'SCHEMA' schema |-- timezones: list -|-- latlng: array +|-- latlng: list |-- name: string |-- country_code: string |-- capital: ?string diff --git a/src/core/etl/src/Flow/ETL/DSL/functions.php b/src/core/etl/src/Flow/ETL/DSL/functions.php index 538719b3e..9775b8f10 100644 --- a/src/core/etl/src/Flow/ETL/DSL/functions.php +++ b/src/core/etl/src/Flow/ETL/DSL/functions.php @@ -358,6 +358,11 @@ function struct_entry(string $name, array $value, StructureType $type) : Row\Ent return new Row\Entry\StructureEntry($name, $value, $type); } +function structure_entry(string $name, array $value, StructureType $type) : Row\Entry\StructureEntry +{ + return new Row\Entry\StructureEntry($name, $value, $type); +} + /** * @param array $elements */ @@ -366,11 +371,21 @@ function struct_type(array $elements, bool $nullable = false) : StructureType return new StructureType($elements, $nullable); } +function structure_type(array $elements, bool $nullable = false) : StructureType +{ + return new StructureType($elements, $nullable); +} + function struct_element(string $name, Type $type) : StructureElement { return new StructureElement($name, $type); } +function structure_element(string $name, Type $type) : StructureElement +{ + return new StructureElement($name, $type); +} + function list_entry(string $name, array $value, ListType $type) : Row\Entry\ListEntry { return new Row\Entry\ListEntry($name, $value, $type); diff --git a/src/core/etl/src/Flow/ETL/Exception/CastingException.php b/src/core/etl/src/Flow/ETL/Exception/CastingException.php index f3acc3170..c4ca90900 100644 --- a/src/core/etl/src/Flow/ETL/Exception/CastingException.php +++ b/src/core/etl/src/Flow/ETL/Exception/CastingException.php @@ -8,8 +8,12 @@ final class CastingException extends RuntimeException { - public function __construct(public readonly mixed $value, public readonly Type $type) + public function __construct(public readonly mixed $value, public readonly Type $type, ?\Throwable $previous = null) { - parent::__construct(\sprintf("Can't cast \"%s\" into \"%s\" type", \gettype($value), $type->toString())); + parent::__construct( + \sprintf("Can't cast \"%s\" into \"%s\" type", \gettype($value), $type->toString()), + 0, + $previous + ); } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/ArrayContentDetector.php b/src/core/etl/src/Flow/ETL/PHP/Type/ArrayContentDetector.php index 1debc23db..e53b5d3c9 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/ArrayContentDetector.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/ArrayContentDetector.php @@ -15,16 +15,16 @@ final class ArrayContentDetector private readonly ?Type $firstValueType; - private readonly int $uniqueKeysCount; + private readonly int $uniqueKeysTypeCount; - private readonly int $uniqueValuesCount; + private readonly int $uniqueValuesTypeCount; public function __construct(Types $uniqueKeysType, Types $uniqueValuesType) { $this->firstKeyType = $uniqueKeysType->first(); $this->firstValueType = $uniqueValuesType->first(); - $this->uniqueKeysCount = $uniqueKeysType->count(); - $this->uniqueValuesCount = $uniqueValuesType->without(type_array(true), type_null())->count(); + $this->uniqueKeysTypeCount = $uniqueKeysType->count(); + $this->uniqueValuesTypeCount = $uniqueValuesType->without(type_array(true), type_null())->count(); } public function firstKeyType() : ?ScalarType @@ -43,12 +43,12 @@ public function firstValueType() : ?Type public function isList() : bool { - return 1 === $this->uniqueValuesCount && $this->firstKeyType()?->isInteger(); + return 1 === $this->uniqueValuesTypeCount && $this->firstKeyType()?->isInteger(); } public function isMap() : bool { - if (1 === $this->uniqueValuesCount && 1 === $this->uniqueKeysCount) { + if (1 === $this->uniqueValuesTypeCount && 1 === $this->uniqueKeysTypeCount) { return !$this->firstKeyType()?->isInteger(); } @@ -61,8 +61,8 @@ public function isStructure() : bool return false; } - return 0 !== $this->uniqueValuesCount - && 1 === $this->uniqueKeysCount + return 0 !== $this->uniqueValuesTypeCount + && 1 === $this->uniqueKeysTypeCount && $this->firstKeyType()?->isString(); } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php b/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php index ddaa92eb9..99f1abdf4 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/AutoCaster.php @@ -4,13 +4,14 @@ namespace Flow\ETL\PHP\Type; +use function Flow\ETL\DSL\get_type; use function Flow\ETL\DSL\type_boolean; use function Flow\ETL\DSL\type_datetime; use function Flow\ETL\DSL\type_float; use function Flow\ETL\DSL\type_integer; use function Flow\ETL\DSL\type_json; use function Flow\ETL\DSL\type_uuid; -use Flow\ETL\PHP\Type\Caster\StringTypeChecker; +use Flow\ETL\PHP\Type\Caster\StringCastingHandler\StringTypeChecker; final class AutoCaster { @@ -20,10 +21,44 @@ public function __construct(private readonly Caster $caster) public function cast(mixed $value) : mixed { - if (!\is_string($value)) { - return $value; + if (\is_string($value)) { + return $this->castToString($value); } + if (\is_array($value)) { + return $this->castArray($value); + } + + return $value; + } + + private function castArray(array $value) : array + { + $keyTypes = []; + $valueTypes = []; + + foreach ($value as $key => $item) { + $keyType = get_type($key); + $valueType = get_type($item); + $keyTypes[$keyType->toString()] = $keyType; + $valueTypes[$valueType->toString()] = $valueType; + } + + if (isset($valueTypes['integer'], $valueTypes['float']) && \count($valueTypes) === 2) { + $castedArray = []; + + foreach ($value as $key => $item) { + $castedArray[$key] = $this->caster->to(type_float())->value($item); + } + + return $castedArray; + } + + return $value; + } + + private function castToString(string $value) : mixed + { $typeChecker = new StringTypeChecker($value); if ($typeChecker->isNull()) { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php index f916811ff..c1670226b 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster.php @@ -56,8 +56,8 @@ public static function default() : self type_json()->toString() => new JsonCastingHandler(), type_array()->toString() => new ArrayCastingHandler(), 'list' => new ListCastingHandler(), - 'map', new MapCastingHandler(), - 'structure', new StructureCastingHandler(), + 'map' => new MapCastingHandler(), + 'structure' => new StructureCastingHandler(), type_null()->toString() => new NullCastingHandler(), 'enum' => new EnumCastingHandler(), ]); @@ -66,12 +66,12 @@ public static function default() : self public function to(Type $type) : CastingContext { if (\array_key_exists($type->toString(), $this->handlers)) { - return new CastingContext($this->handlers[$type->toString()], $type); + return new CastingContext($this->handlers[$type->toString()], $type, $this); } foreach ($this->handlers as $handler) { if ($handler->supports($type)) { - return new CastingContext($handler, $type); + return new CastingContext($handler, $type, $this); } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php index 96c5f2c10..8046cac08 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\XML\XMLConverter; use Flow\ETL\PHP\Type\Native\ArrayType; use Flow\ETL\PHP\Type\Type; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof ArrayType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { try { if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php index 7039adda6..8517fa010 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\ScalarType; use Flow\ETL\PHP\Type\Type; @@ -15,7 +16,7 @@ public function supports(Type $type) : bool return $type instanceof ScalarType && $type->isBoolean(); } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if (\is_string($value)) { if (\in_array(\mb_strtolower($value), ['true', '1', 'yes', 'on'], true)) { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php index ac44df577..295512098 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingContext.php @@ -4,16 +4,29 @@ namespace Flow\ETL\PHP\Type\Caster; +use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Type; final class CastingContext { - public function __construct(private readonly CastingHandler $handler, private readonly Type $type) - { + public function __construct( + private readonly CastingHandler $handler, + private readonly Type $type, + private readonly Caster $caster + ) { } public function value(mixed $value) : mixed { - return $this->handler->value($value, $this->type); + if ($value === null && $this->type->nullable()) { + return null; + } + + if ($value === null && !$this->type->nullable()) { + throw new CastingException($value, $this->type); + } + + return $this->handler->value($value, $this->type, $this->caster); } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php index 97d2d5b03..0bee5a76d 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/CastingHandler.php @@ -4,11 +4,12 @@ namespace Flow\ETL\PHP\Type\Caster; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Type; interface CastingHandler { public function supports(Type $type) : bool; - public function value(mixed $value, Type $type) : mixed; + public function value(mixed $value, Type $type, Caster $caster) : mixed; } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/DateTimeCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/DateTimeCastingHandler.php index 0af7f850c..5263af97a 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/DateTimeCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/DateTimeCastingHandler.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_datetime; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\DateTimeType; use Flow\ETL\PHP\Type\Type; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof DateTimeType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if ($value instanceof \DateTimeImmutable) { return $value; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php index a2e00dc9a..01164dd47 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\EnumType; use Flow\ETL\PHP\Type\Type; @@ -15,7 +16,7 @@ public function supports(Type $type) : bool return $type instanceof EnumType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { try { /** @var EnumType $type */ diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php index f233b7fbe..c3a2fbebd 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\ScalarType; use Flow\ETL\PHP\Type\Type; @@ -15,7 +16,7 @@ public function supports(Type $type) : bool return $type instanceof ScalarType && $type->isFloat(); } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if ($value instanceof \DateTimeImmutable) { return (float) $value->format('Uu'); diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php index 5ccabe115..bec3778aa 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\ScalarType; use Flow\ETL\PHP\Type\Type; @@ -15,7 +16,7 @@ public function supports(Type $type) : bool return $type instanceof ScalarType && $type->isInteger(); } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if ($value instanceof \DateTimeImmutable) { return (int) $value->format('Uu'); diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php index 101a73007..4c1a2bf1d 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/JsonCastingHandler.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_json; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\JsonType; use Flow\ETL\PHP\Type\Type; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof JsonType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { try { if (\is_string($value)) { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ListCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ListCastingHandler.php index 784df1622..e6a049ec6 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ListCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ListCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\ListType; use Flow\ETL\PHP\Type\Type; @@ -15,18 +16,25 @@ public function supports(Type $type) : bool return $type instanceof ListType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { - if (\is_array($value)) { - return $value; - } - + /** @var ListType $type */ try { - if (\is_string($value)) { + if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); } - return (array) $value; + if (!\is_array($value)) { + return [$caster->to($type->element()->type())->value($value)]; + } + + $castedList = []; + + foreach ($value as $key => $item) { + $castedList[$key] = $caster->to($type->element()->type())->value($item); + } + + return $castedList; } catch (\Throwable $e) { throw new CastingException($value, $type); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/MapCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/MapCastingHandler.php index 384972a82..ce57f70b3 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/MapCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/MapCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\MapType; use Flow\ETL\PHP\Type\Type; @@ -15,20 +16,35 @@ public function supports(Type $type) : bool return $type instanceof MapType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { - if (\is_array($value)) { - return $value; - } - + /** @var MapType $type */ try { - if (\is_string($value)) { + if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); } - return (array) $value; + if (!\is_array($value)) { + return [ + $caster->to($type->key()->type())->value(0) => $caster->to($type->value()->type())->value($value), + ]; + } + + $castedMap = []; + + foreach ($value as $key => $item) { + $castedKey = $caster->to($type->key()->type())->value($key); + + if (\array_key_exists($castedKey, $castedMap)) { + throw new CastingException($value, $type); + } + + $castedMap[$caster->to($type->key()->type())->value($key)] = $caster->to($type->value()->type())->value($item); + } + + return $castedMap; } catch (\Throwable $e) { - throw new CastingException($value, $type); + throw new CastingException($value, $type, $e); } } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/NullCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/NullCastingHandler.php index 67e1779af..a4e6f8b98 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/NullCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/NullCastingHandler.php @@ -4,6 +4,7 @@ namespace Flow\ETL\PHP\Type\Caster; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\NullType; use Flow\ETL\PHP\Type\Type; @@ -14,7 +15,7 @@ public function supports(Type $type) : bool return $type instanceof NullType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { return null; } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php index 24dc358d5..bc75439dd 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_object; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\ObjectType; use Flow\ETL\PHP\Type\Type; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof ObjectType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { /** @var ObjectType $type */ try { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php index 994ae3421..2b16ee7f5 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Native\ScalarType; use Flow\ETL\PHP\Type\Type; @@ -15,7 +16,7 @@ public function supports(Type $type) : bool return $type instanceof ScalarType && $type->isString(); } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if ($value === null) { return null; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler/StringTypeChecker.php similarity index 98% rename from src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php rename to src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler/StringTypeChecker.php index 5bf2d9185..f1e00a6eb 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringTypeChecker.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler/StringTypeChecker.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Flow\ETL\PHP\Type\Caster; +namespace Flow\ETL\PHP\Type\Caster\StringCastingHandler; use Flow\ETL\Row\Entry\Type\Uuid; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php index 6bf54fd08..eb08140f9 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\StructureType; use Flow\ETL\PHP\Type\Type; @@ -15,20 +16,31 @@ public function supports(Type $type) : bool return $type instanceof StructureType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { - if (\is_array($value)) { - return $value; + if ($value === null && !$type->nullable()) { + throw new CastingException($value, $type); } + /** @var StructureType $type */ try { - if (\is_string($value)) { + if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); } - return (array) $value; + $castedStructure = []; + + foreach ($type->elements() as $element) { + $elementName = $element->name(); + + $castedStructure[$elementName] = (\is_array($value) && \array_key_exists($elementName, $value)) + ? $caster->to($element->type())->value($value[$elementName]) + : $caster->to($element->type())->value(null); + } + + return $castedStructure; } catch (\Throwable $e) { - throw new CastingException($value, $type); + throw new CastingException($value, $type, $e); } } } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php index 2e231dd3f..853f035c5 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php @@ -5,6 +5,7 @@ namespace Flow\ETL\PHP\Type\Caster; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\UuidType; use Flow\ETL\PHP\Type\Type; use Flow\ETL\Row\Entry\Type\Uuid; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof UuidType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if (\is_string($value)) { return new Uuid($value); diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php index 3e8366f03..b8ca7d3bb 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_xml; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Logical\XMLType; use Flow\ETL\PHP\Type\Type; @@ -16,7 +17,7 @@ public function supports(Type $type) : bool return $type instanceof XMLType; } - public function value(mixed $value, Type $type) : mixed + public function value(mixed $value, Type $type, Caster $caster) : mixed { if (\is_string($value)) { $doc = new \DOMDocument(); diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php b/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php index 0a2ffdb63..de3d56ae3 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Logical/JsonType.php @@ -5,7 +5,7 @@ namespace Flow\ETL\PHP\Type\Logical; use Flow\ETL\Exception\InvalidArgumentException; -use Flow\ETL\PHP\Type\Caster\StringTypeChecker; +use Flow\ETL\PHP\Type\Caster\StringCastingHandler\StringTypeChecker; use Flow\ETL\PHP\Type\Native\NullType; use Flow\ETL\PHP\Type\Type; diff --git a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php index 202c25a5d..7e288d56e 100644 --- a/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php +++ b/src/core/etl/src/Flow/ETL/Row/Factory/NativeEntryFactory.php @@ -24,7 +24,7 @@ use Flow\ETL\Exception\InvalidArgumentException; use Flow\ETL\Exception\RuntimeException; use Flow\ETL\PHP\Type\Caster; -use Flow\ETL\PHP\Type\Caster\StringTypeChecker; +use Flow\ETL\PHP\Type\Caster\StringCastingHandler\StringTypeChecker; use Flow\ETL\PHP\Type\Logical\DateTimeType; use Flow\ETL\PHP\Type\Logical\JsonType; use Flow\ETL\PHP\Type\Logical\ListType; diff --git a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php index fea7a3675..c1fb7b349 100644 --- a/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php +++ b/src/core/etl/src/Flow/ETL/Row/Schema/Definition.php @@ -11,6 +11,7 @@ use function Flow\ETL\DSL\type_float; use function Flow\ETL\DSL\type_int; use function Flow\ETL\DSL\type_json; +use function Flow\ETL\DSL\type_list; use function Flow\ETL\DSL\type_null; use function Flow\ETL\DSL\type_string; use function Flow\ETL\DSL\type_uuid; @@ -254,6 +255,21 @@ public function merge(self $definition) : self $constraint = $this->constraint; } + if ($this->type instanceof ListType && $definition->type instanceof ListType && !$this->type->isEqual($definition->type)) { + $thisTypeString = $this->type->element()->toString(); + $definitionTypeString = $definition->type->element()->toString(); + + if (\in_array($thisTypeString, ['integer', 'float', '?integer', '?float'], true) && \in_array($definitionTypeString, ['integer', 'float', '?integer', '?float'], true)) { + return new self( + $this->ref, + $this->entryClass, + type_list(type_float($this->type->element()->type()->nullable() || $definition->type->element()->type()->nullable())), + $constraint, + $this->metadata->merge($definition->metadata) + ); + } + } + if ($this->entryClass === $definition->entryClass && \in_array($this->entryClass, [ListEntry::class, MapEntry::class, StructureEntry::class], true)) { if (!$this->type->isEqual($definition->type)) { return new self( diff --git a/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php b/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php index 8aa6001f0..202271545 100644 --- a/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php +++ b/src/core/etl/src/Flow/ETL/Transformer/AutoCastTransformer.php @@ -22,9 +22,9 @@ public function transform(Rows $rows, FlowContext $context) : Rows { return $rows->map(function (Row $row) use ($context) { return $row->map(function (Entry $entry) use ($context) { - if (!$entry instanceof StringEntry) { - return $entry; - } + // if (!$entry instanceof StringEntry) { + // return $entry; + // } return $context->entryFactory()->create($entry->name(), $this->caster->cast($entry->value())); }); diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/AutoCasterTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/AutoCasterTest.php new file mode 100644 index 000000000..b7de528b6 --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/AutoCasterTest.php @@ -0,0 +1,20 @@ +assertSame( + [1.0, 2.0, 3.0], + (new AutoCaster(Caster::default()))->cast([1, 2, 3.0]) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php index 25aa1b698..c4c6f205a 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ArrayCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_array; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\ArrayCastingHandler; use PHPUnit\Framework\TestCase; @@ -14,7 +15,7 @@ public function test_casting_boolean_to_array() : void { $this->assertEquals( [true], - (new ArrayCastingHandler())->value(true, type_array()) + (new ArrayCastingHandler())->value(true, type_array(), Caster::default()) ); } @@ -22,7 +23,7 @@ public function test_casting_datetime_to_array() : void { $this->assertEquals( ['date' => '2021-01-01 00:00:00.000000', 'timezone_type' => 3, 'timezone' => 'UTC'], - (new ArrayCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_array()) + (new ArrayCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_array(), Caster::default()) ); } @@ -30,7 +31,7 @@ public function test_casting_float_to_array() : void { $this->assertEquals( [1.1], - (new ArrayCastingHandler())->value(1.1, type_array()) + (new ArrayCastingHandler())->value(1.1, type_array(), Caster::default()) ); } @@ -38,7 +39,7 @@ public function test_casting_integer_to_array() : void { $this->assertEquals( [1], - (new ArrayCastingHandler())->value(1, type_array()) + (new ArrayCastingHandler())->value(1, type_array(), Caster::default()) ); } @@ -46,7 +47,7 @@ public function test_casting_string_to_array() : void { $this->assertSame( ['items' => ['item' => 1]], - (new ArrayCastingHandler())->value('{"items":{"item":1}}', type_array()) + (new ArrayCastingHandler())->value('{"items":{"item":1}}', type_array(), Caster::default()) ); } @@ -57,7 +58,7 @@ public function test_casting_xml_document_to_array() : void $this->assertSame( ['root' => ['foo' => ['@attributes' => ['baz' => 'buz'], '@value' => 'bar']]], - (new ArrayCastingHandler())->value($xml, type_array()) + (new ArrayCastingHandler())->value($xml, type_array(), Caster::default()) ); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php index 36ddaf5cb..b333fbe41 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/BooleanCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_boolean; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\BooleanCastingHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -34,6 +35,6 @@ public static function boolean_castable_data_provider() : \Generator #[DataProvider('boolean_castable_data_provider')] public function test_casting_different_data_types_to_integer(mixed $value, bool $expected) : void { - $this->assertSame($expected, (new BooleanCastingHandler())->value($value, type_boolean())); + $this->assertSame($expected, (new BooleanCastingHandler())->value($value, type_boolean(), Caster::default())); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php index 6927633cd..b2ce75276 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/DateTimeCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_datetime; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\DateTimeCastingHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -24,6 +25,6 @@ public static function datetime_castable_data_provider() : \Generator #[DataProvider('datetime_castable_data_provider')] public function test_casting_different_data_types_to_datetime(mixed $value, \DateTimeImmutable $expected) : void { - $this->assertEquals($expected, (new DateTimeCastingHandler())->value($value, type_datetime())); + $this->assertEquals($expected, (new DateTimeCastingHandler())->value($value, type_datetime(), Caster::default())); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php index 0427eb3ce..fccb309b1 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/EnumCastingHandlerTest.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_enum; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\EnumCastingHandler; use Flow\ETL\Tests\Unit\PHP\Type\Caster\Fixtures\ColorsEnum; use PHPUnit\Framework\TestCase; @@ -17,14 +18,14 @@ public function test_casting_integer_to_enum() : void $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "integer" into "enum" type'); - (new EnumCastingHandler())->value(1, type_enum(ColorsEnum::class)); + (new EnumCastingHandler())->value(1, type_enum(ColorsEnum::class), Caster::default()); } public function test_casting_string_to_enum() : void { $this->assertEquals( ColorsEnum::RED, - (new EnumCastingHandler())->value('red', type_enum(ColorsEnum::class)) + (new EnumCastingHandler())->value('red', type_enum(ColorsEnum::class), Caster::default()) ); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/FloatCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/FloatCastingHandlerTest.php index 135e50431..776b5324e 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/FloatCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/FloatCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_float; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\FloatCastingHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -25,6 +26,6 @@ public static function float_castable_data_provider() : \Generator #[DataProvider('float_castable_data_provider')] public function test_casting_different_data_types_to_float(mixed $value, float $expected) : void { - $this->assertSame($expected, (new FloatCastingHandler())->value($value, type_float())); + $this->assertSame($expected, (new FloatCastingHandler())->value($value, type_float(), Caster::default())); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php index acf808a26..ac1560d43 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/IntegerCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_integer; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\IntegerCastingHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -25,6 +26,6 @@ public static function integer_castable_data_provider() : \Generator #[DataProvider('integer_castable_data_provider')] public function test_casting_different_data_types_to_integer(mixed $value, int $expected) : void { - $this->assertSame($expected, (new IntegerCastingHandler())->value($value, type_integer())); + $this->assertSame($expected, (new IntegerCastingHandler())->value($value, type_integer(), Caster::default())); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php index 37ecbfe1b..ca02ed1e2 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/JsonCastingHandlerTest.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_json; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\JsonCastingHandler; use PHPUnit\Framework\TestCase; @@ -15,7 +16,7 @@ public function test_casting_array_to_json() : void { $this->assertSame( '{"items":{"item":1}}', - (new JsonCastingHandler())->value(['items' => ['item' => 1]], type_json()) + (new JsonCastingHandler())->value(['items' => ['item' => 1]], type_json(), Caster::default()) ); } @@ -23,7 +24,7 @@ public function test_casting_datetime_to_json() : void { $this->assertSame( '{"date":"2021-01-01 00:00:00.000000","timezone_type":3,"timezone":"UTC"}', - (new JsonCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_json()) + (new JsonCastingHandler())->value(new \DateTimeImmutable('2021-01-01 00:00:00 UTC'), type_json(), Caster::default()) ); } @@ -32,14 +33,14 @@ public function test_casting_integer_to_json() : void $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "integer" into "json" type'); - (new JsonCastingHandler())->value(1, type_json()); + (new JsonCastingHandler())->value(1, type_json(), Caster::default()); } public function test_casting_json_string_to_json() : void { $this->assertSame( '{"items":{"item":1}}', - (new JsonCastingHandler())->value('{"items":{"item":1}}', type_json()) + (new JsonCastingHandler())->value('{"items":{"item":1}}', type_json(), Caster::default()) ); } @@ -48,6 +49,6 @@ public function test_casting_non_json_string_to_json() : void $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "string" into "json" type'); - (new JsonCastingHandler())->value('string', type_json()); + (new JsonCastingHandler())->value('string', type_json(), Caster::default()); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ListCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ListCastingHandlerTest.php new file mode 100644 index 000000000..e8a296a5a --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ListCastingHandlerTest.php @@ -0,0 +1,31 @@ +assertSame( + [1.0, 2.0, 3.0], + (new ListCastingHandler())->value([1, 2, 3], type_list(type_float()), Caster::default()) + ); + } + + public function test_casting_string_to_list_of_ints() : void + { + $this->assertSame( + [1], + (new ListCastingHandler())->value(['1'], type_list(type_int()), Caster::default()) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/MapCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/MapCastingHandlerTest.php new file mode 100644 index 000000000..3537d4e8c --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/MapCastingHandlerTest.php @@ -0,0 +1,55 @@ +assertSame( + [ + 'a' => 1.0, + 'b' => 2.0, + 'c' => 3.0, + ], + (new MapCastingHandler())->value(['a' => 1, 'b' => 2, 'c' => 3], type_map(type_string(), type_float()), Caster::default()) + ); + } + + public function test_casting_map_of_string_to_ints_into_map_of_int_to_float() : void + { + $this->expectException(CastingException::class); + $this->expectExceptionMessage('Can\'t cast "array" into "map"'); + + $this->assertSame( + [ + 'a' => 1.0, + 'b' => 2.0, + 'c' => 3.0, + ], + (new MapCastingHandler())->value(['a' => 1, 'b' => 2, 'c' => 3], type_map(type_int(), type_float()), Caster::default()) + ); + } + + public function test_casting_scalar_to_map() : void + { + $this->assertSame( + [ + '0' => 2, + ], + (new MapCastingHandler())->value('2', type_map(type_string(), type_integer()), Caster::default()) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php index 740f61eeb..343140b70 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/ObjectCastingHandlerTest.php @@ -5,6 +5,7 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_object; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\ObjectCastingHandler; use PHPUnit\Framework\TestCase; @@ -14,11 +15,11 @@ public function test_casting_string_to_object() : void { $this->assertEquals( (object) ['foo' => 'bar'], - (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class)) + (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class), Caster::default()) ); $this->assertInstanceOf( \stdClass::class, - (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class)) + (new ObjectCastingHandler())->value((object) ['foo' => 'bar'], type_object(\stdClass::class), Caster::default()) ); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandler/StringTypeCheckerTest.php similarity index 97% rename from src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php rename to src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandler/StringTypeCheckerTest.php index aba03d71c..dd690feb2 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringTypeCheckerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandler/StringTypeCheckerTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; +namespace Flow\ETL\Tests\Unit\PHP\Type\Caster\StringCastingHandler; -use Flow\ETL\PHP\Type\Caster\StringTypeChecker; +use Flow\ETL\PHP\Type\Caster\StringCastingHandler\StringTypeChecker; use PHPUnit\Framework\TestCase; final class StringTypeCheckerTest extends TestCase diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php index 18bd297b3..efe5a2287 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StringCastingHandlerTest.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_string; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\StringCastingHandler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -32,7 +33,7 @@ public function __toString() : string #[DataProvider('string_castable_data_provider')] public function test_casting_different_data_types_to_string(mixed $value, string $expected) : void { - $this->assertSame($expected, \trim((new StringCastingHandler())->value($value, type_string()))); + $this->assertSame($expected, \trim((new StringCastingHandler())->value($value, type_string(), Caster::default()))); } public function test_casting_object_to_string() : void @@ -40,6 +41,6 @@ public function test_casting_object_to_string() : void $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "object" into "string" type'); - (new StringCastingHandler())->value(new class() {}, type_string()); + (new StringCastingHandler())->value(new class() {}, type_string(), Caster::default()); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StructureCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StructureCastingHandlerTest.php new file mode 100644 index 000000000..e9728d63b --- /dev/null +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/StructureCastingHandlerTest.php @@ -0,0 +1,115 @@ +assertSame( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + 'address' => [ + 'street' => 'Polna', + 'city' => 'Warsaw', + ], + ], + (new StructureCastingHandler())->value( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + 'address' => [ + 'street' => 'Polna', + 'city' => 'Warsaw', + ], + ], + struct_type([ + structure_element('name', type_string()), + structure_element('age', type_integer()), + structure_element( + 'address', + structure_type([ + structure_element('street', type_string()), + structure_element('city', type_string()), + ]) + ), + ]), + Caster::default() + ) + ); + } + + public function test_casting_structure_with_empty_not_nullable_fields() : void + { + $this->assertSame( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + 'address' => [ + 'street' => null, + 'city' => null, + ], + ], + (new StructureCastingHandler())->value( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + 'address' => [], + ], + struct_type([ + structure_element('name', type_string()), + structure_element('age', type_integer()), + structure_element( + 'address', + structure_type([ + structure_element('street', type_string(true)), + structure_element('city', type_string(true)), + ]) + ), + ]), + Caster::default() + ) + ); + } + + public function test_casting_structure_with_missing_nullable_fields() : void + { + $this->assertSame( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + 'address' => null, + ], + (new StructureCastingHandler())->value( + [ + 'name' => 'Norbert Orzechowicz', + 'age' => 30, + ], + struct_type([ + structure_element('name', type_string()), + structure_element('age', type_integer()), + structure_element( + 'address', + structure_type([ + structure_element('street', type_string()), + structure_element('city', type_string()), + ], true) + ), + ], true), + Caster::default() + ) + ); + } +} diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php index d55c74f33..bff84cb81 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/UuidCastingHandlerTest.php @@ -5,6 +5,8 @@ namespace Flow\ETL\Tests\Unit\PHP\Type\Caster; use function Flow\ETL\DSL\type_uuid; +use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\UuidCastingHandler; use Flow\ETL\Row\Entry\Type\Uuid; use PHPUnit\Framework\TestCase; @@ -13,17 +15,17 @@ final class UuidCastingHandlerTest extends TestCase { public function test_casting_integer_to_uuid() : void { - $this->expectException(\Flow\ETL\Exception\CastingException::class); + $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "integer" into "uuid" type'); - (new UuidCastingHandler())->value(1, type_uuid()); + (new UuidCastingHandler())->value(1, type_uuid(), Caster::default()); } public function test_casting_ramsey_uuid_to_uuid() : void { $this->assertEquals( new Uuid('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), - (new UuidCastingHandler())->value(\Ramsey\Uuid\Uuid::fromString('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), type_uuid()) + (new UuidCastingHandler())->value(\Ramsey\Uuid\Uuid::fromString('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), type_uuid(), Caster::default()) ); } @@ -31,7 +33,7 @@ public function test_casting_string_to_uuid() : void { $this->assertEquals( new Uuid('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e'), - (new UuidCastingHandler())->value('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e', type_uuid()) + (new UuidCastingHandler())->value('6c2f6e0e-8d8e-4e9e-8f0e-5a2d9c1c4f6e', type_uuid(), Caster::default()) ); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php index c550298ba..8a19b8f48 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/PHP/Type/Caster/XMLCastingHandlerTest.php @@ -6,6 +6,7 @@ use function Flow\ETL\DSL\type_xml; use Flow\ETL\Exception\CastingException; +use Flow\ETL\PHP\Type\Caster; use Flow\ETL\PHP\Type\Caster\XMLCastingHandler; use PHPUnit\Framework\TestCase; @@ -16,14 +17,14 @@ public function test_casting_integer_to_xml() : void $this->expectException(CastingException::class); $this->expectExceptionMessage('Can\'t cast "integer" into "xml" type'); - (new XMLCastingHandler())->value(1, type_xml())->saveXML(); + (new XMLCastingHandler())->value(1, type_xml(), Caster::default())->saveXML(); } public function test_casting_string_to_xml() : void { $this->assertSame( '' . "\n" . '1' . "\n", - (new XMLCastingHandler())->value('1', type_xml())->saveXML() + (new XMLCastingHandler())->value('1', type_xml(), Caster::default())->saveXML() ); } } diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php index e15362047..6a103f097 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Factory/NativeEntryFactoryTest.php @@ -247,10 +247,10 @@ public function test_list_int_with_schema() : void public function test_list_int_with_schema_but_string_list() : void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Field "e" conversion exception. Expected list got different types: list'); - - (new NativeEntryFactory())->create('e', ['1', '2', '3'], new Schema(Schema\Definition::list('e', new ListType(ListElement::integer())))); + $this->assertEquals( + list_entry('e', ['false', 'true', 'true'], type_list(type_string())), + (new NativeEntryFactory())->create('e', [false, true, true], new Schema(Schema\Definition::list('e', new ListType(ListElement::string())))) + ); } public function test_list_of_datetime_with_schema() : void diff --git a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/DefinitionTest.php b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/DefinitionTest.php index aaa285be4..43ba6c415 100644 --- a/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/DefinitionTest.php +++ b/src/core/etl/tests/Flow/ETL/Tests/Unit/Row/Schema/DefinitionTest.php @@ -179,6 +179,14 @@ public function test_merging_different_entries() : void Definition::integer('int')->merge(Definition::string('string')); } + public function test_merging_list_of_ints_and_floats() : void + { + $this->assertEquals( + Definition::list('list', type_list(type_float())), + Definition::list('list', type_list(type_int()))->merge(Definition::list('list', type_list(type_float()))) + ); + } + public function test_merging_numeric_types() : void { $this->assertEquals( From d4f6211b109459b0cb334b1ae35b59884f3a8c42 Mon Sep 17 00:00:00 2001 From: Norbert Orzechowicz Date: Sun, 21 Jan 2024 22:26:30 +0100 Subject: [PATCH 6/6] Microoptimization --- .../PHP/Type/Caster/ArrayCastingHandler.php | 8 +++---- .../PHP/Type/Caster/BooleanCastingHandler.php | 4 ++++ .../PHP/Type/Caster/EnumCastingHandler.php | 5 +++++ .../PHP/Type/Caster/FloatCastingHandler.php | 4 ++++ .../PHP/Type/Caster/IntegerCastingHandler.php | 4 ++++ .../PHP/Type/Caster/ObjectCastingHandler.php | 4 ++++ .../PHP/Type/Caster/StringCastingHandler.php | 4 ---- .../Type/Caster/StructureCastingHandler.php | 4 ---- .../PHP/Type/Caster/UuidCastingHandler.php | 4 ++++ .../ETL/PHP/Type/Caster/XMLCastingHandler.php | 21 +++++++++++++++---- 10 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php index 8046cac08..7ac885ced 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ArrayCastingHandler.php @@ -20,14 +20,14 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { try { - if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { - return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); - } - if (\is_array($value)) { return $value; } + if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { + return \json_decode($value, true, 512, \JSON_THROW_ON_ERROR); + } + if ($value instanceof \DOMDocument) { return (new XMLConverter())->toArray($value); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php index 8517fa010..3af29cbfe 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/BooleanCastingHandler.php @@ -18,6 +18,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if (\is_bool($value)) { + return $value; + } + if (\is_string($value)) { if (\in_array(\mb_strtolower($value), ['true', '1', 'yes', 'on'], true)) { return true; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php index 01164dd47..44cc721dc 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/EnumCastingHandler.php @@ -18,6 +18,11 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + /** @var EnumType $type */ + if ($value instanceof $type->class) { + return $value; + } + try { /** @var EnumType $type */ $enumClass = $type->class; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php index c3a2fbebd..48edae6a1 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/FloatCastingHandler.php @@ -18,6 +18,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if (\is_float($value)) { + return $value; + } + if ($value instanceof \DateTimeImmutable) { return (float) $value->format('Uu'); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php index bec3778aa..59e79ac36 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/IntegerCastingHandler.php @@ -18,6 +18,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if (\is_int($value)) { + return $value; + } + if ($value instanceof \DateTimeImmutable) { return (int) $value->format('Uu'); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php index bc75439dd..57d0c3490 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/ObjectCastingHandler.php @@ -19,6 +19,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if (\is_object($value)) { + return $value; + } + /** @var ObjectType $type */ try { $object = (object) $value; diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php index 2b16ee7f5..190017b39 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StringCastingHandler.php @@ -18,10 +18,6 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { - if ($value === null) { - return null; - } - if (\is_string($value)) { return $value; } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php index eb08140f9..0b4c69d1c 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/StructureCastingHandler.php @@ -18,10 +18,6 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { - if ($value === null && !$type->nullable()) { - throw new CastingException($value, $type); - } - /** @var StructureType $type */ try { if (\is_string($value) && (\str_starts_with($value, '{') || \str_starts_with($value, '['))) { diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php index 853f035c5..32fb9c635 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/UuidCastingHandler.php @@ -19,6 +19,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if ($value instanceof Uuid) { + return $value; + } + if (\is_string($value)) { return new Uuid($value); } diff --git a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php index b8ca7d3bb..ad2afe3c4 100644 --- a/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php +++ b/src/core/etl/src/Flow/ETL/PHP/Type/Caster/XMLCastingHandler.php @@ -4,6 +4,7 @@ namespace Flow\ETL\PHP\Type\Caster; +use function Flow\ETL\DSL\type_string; use function Flow\ETL\DSL\type_xml; use Flow\ETL\Exception\CastingException; use Flow\ETL\PHP\Type\Caster; @@ -19,6 +20,10 @@ public function supports(Type $type) : bool public function value(mixed $value, Type $type, Caster $caster) : mixed { + if ($value instanceof \DOMDocument) { + return $value; + } + if (\is_string($value)) { $doc = new \DOMDocument(); @@ -29,10 +34,18 @@ public function value(mixed $value, Type $type, Caster $caster) : mixed return $doc; } - if ($value instanceof \DOMDocument) { - return $value; - } + try { + $stringValue = $caster->to(type_string())->value($value); - throw new CastingException($value, $type); + $doc = new \DOMDocument(); + + if (!@$doc->loadXML($stringValue)) { + throw new CastingException($stringValue, type_xml()); + } + + return $doc; + } catch (CastingException $e) { + throw new CastingException($value, type_xml(), $e); + } } }