Skip to content

Commit

Permalink
Add new logical StructureType (#765)
Browse files Browse the repository at this point in the history
  • Loading branch information
stloyd authored Nov 8, 2023
1 parent ebf07cb commit 455dc66
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);

namespace Flow\ETL\PHP\Type\Logical\Structure;

use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\PHP\Type\Type;

final class StructureElement
{
public function __construct(private readonly string $name, private readonly Type $type)
{
if ('' === $name) {
throw InvalidArgumentException::because('Structure element name cannot be empty');
}
}

public function isEqual(mixed $value) : bool
{
return $this->type->isEqual($value);
}

public function isValid(mixed $value) : bool
{
return $this->type->isValid($value);
}

public function name() : string
{
return $this->name;
}

public function toString() : string
{
return $this->name . ': ' . $this->type->toString();
}

public function type() : Type
{
return $this->type;
}
}
98 changes: 98 additions & 0 deletions src/core/etl/src/Flow/ETL/PHP/Type/Logical/StructureType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php declare(strict_types=1);

namespace Flow\ETL\PHP\Type\Logical;

use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\PHP\Type\Logical\Structure\StructureElement;
use Flow\ETL\PHP\Type\Type;
use Flow\Serializer\Serializable;

/**
* @implements Serializable<array{elements: array<StructureElement>}>
*/
final class StructureType implements LogicalType, Serializable
{
/**
* @var array<StructureElement>
*/
private readonly array $elements;

public function __construct(StructureElement ...$elements)
{
if (0 === \count($elements)) {
throw InvalidArgumentException::because('Structure must receive at least one element.');
}

if (\count($elements) !== \count(\array_unique(\array_map(fn (StructureElement $element) => $element->name(), $elements)))) {
throw InvalidArgumentException::because('All structure element names must be unique');
}

$this->elements = $elements;
}

public function __serialize() : array
{
return ['elements' => $this->elements];
}

public function __unserialize(array $data) : void
{
$this->elements = $data['elements'];
}

public function elements() : array
{
return $this->elements;
}

public function isEqual(Type $type) : bool
{
if (!$type instanceof self) {
return false;
}

foreach ($this->elements as $internalElement) {
foreach ($type->elements() as $element) {
if (!$internalElement->isEqual($element->type())) {
return false;
}
}
}

return true;
}

public function isValid(mixed $value) : bool
{
if (!\is_array($value)) {
return false;
}

if (\array_is_list($value)) {
return false;
}

foreach ($value as $item) {
foreach ($this->elements as $element) {
if ($element->isValid($item)) {
continue 2;
}
}

return false;
}

return true;
}

public function toString() : string
{
$content = [];

foreach ($this->elements as $element) {
$content[] = $element->toString();
}

return 'structure{' . \implode(', ', $content) . '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php declare(strict_types=1);

namespace Flow\ETL\Tests\Unit\PHP\Type\Logical;

use Flow\ETL\Exception\InvalidArgumentException;
use Flow\ETL\PHP\Type\Logical\List\ListElement;
use Flow\ETL\PHP\Type\Logical\ListType;
use Flow\ETL\PHP\Type\Logical\Map\MapKey;
use Flow\ETL\PHP\Type\Logical\Map\MapValue;
use Flow\ETL\PHP\Type\Logical\MapType;
use Flow\ETL\PHP\Type\Logical\Structure\StructureElement;
use Flow\ETL\PHP\Type\Logical\StructureType;
use Flow\ETL\PHP\Type\Native\ScalarType;
use PHPUnit\Framework\TestCase;

final class StructureTypeTest extends TestCase
{
public function test_elements() : void
{
$this->assertEquals(
[$map = new StructureElement('map', new MapType(MapKey::string(), MapValue::float()))],
(new StructureType($map))->elements()
);
}

public function test_equals() : void
{
$this->assertTrue(
(new StructureType(new StructureElement('map', new MapType(MapKey::string(), MapValue::float()))))
->isEqual(new StructureType(new StructureElement('map', new MapType(MapKey::string(), MapValue::float()))))
);
$this->assertFalse(
(new StructureType(new StructureElement('string', ScalarType::string), new StructureElement('bool', ScalarType::boolean)))
->isEqual(new ListType(ListElement::integer()))
);
$this->assertFalse(
(new StructureType(new StructureElement('string', ScalarType::string), new StructureElement('bool', ScalarType::boolean)))
->isEqual(new StructureType(new StructureElement('bool', ScalarType::boolean), new StructureElement('string', ScalarType::string)))
);
}

public function test_structure_element_name_cannot_be_empty() : void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Structure element name cannot be empty');

new StructureElement('', ScalarType::string);
}

public function test_structure_elements_must_have_unique_names() : void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('All structure element names must be unique');

(new StructureType(
new StructureElement('test', ScalarType::string),
new StructureElement('test', ScalarType::string)
));
}

public function test_to_string() : void
{
$struct = new StructureType(
new StructureElement('string', ScalarType::string),
new StructureElement('float', ScalarType::float),
new StructureElement('map', new MapType(MapKey::string(), MapValue::list(new ListType(ListElement::object(\DateTimeInterface::class)))))
);

$this->assertSame(
'structure{string: string, float: float, map: map<string, list<object<DateTimeInterface>>>}',
$struct->toString()
);
}

public function test_valid() : void
{
$this->assertTrue(
(new StructureType(new StructureElement('string', ScalarType::string)))->isValid(['one' => 'two'])
);
$this->assertTrue(
(
new StructureType(
new StructureElement(
'map',
new MapType(
MapKey::integer(),
MapValue::map(new MapType(MapKey::string(), MapValue::list(new ListType(ListElement::integer()))))
)
),
new StructureElement('string', ScalarType::string),
new StructureElement('float', ScalarType::float)
)
)->isValid(['a' => [0 => ['one' => [1, 2]], 1 => ['two' => [3, 4]]], 'b' => 'c', 'd' => 1.5])
);
$this->assertFalse(
(new StructureType(new StructureElement('int', ScalarType::integer)))->isValid([1, 2])
);
}
}

0 comments on commit 455dc66

Please sign in to comment.