Skip to content

Commit

Permalink
Add support for forcing Sequences and Dicts to be scalar arrays when …
Browse files Browse the repository at this point in the history
…deserializing.
  • Loading branch information
Crell committed Oct 21, 2023
1 parent df58388 commit 90d2223
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 14 deletions.
7 changes: 4 additions & 3 deletions src/Attributes/DictionaryField.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Crell\AttributeUtils\SupportsScopes;
use Crell\Serde\KeyType;
use Crell\Serde\TypeField;
use Crell\Serde\ValueType;
use function Crell\fp\afilter;
use function Crell\fp\amap;
use function Crell\fp\amapWithKeys;
Expand All @@ -20,8 +21,8 @@
class DictionaryField implements TypeField, SupportsScopes
{
/**
* @param string|null $arrayType
* Elements in this array are objects of this type.
* @param string|ValueType|null $arrayType
* Elements in this array are values of this type.
* @param string|null $implodeOn
* Scalar values of this array should be imploded to a string and exploded on deserialization.
* @param string|null $joinOn
Expand All @@ -32,7 +33,7 @@ class DictionaryField implements TypeField, SupportsScopes
* The scopes in which this attribute should apply.
*/
public function __construct(
public readonly ?string $arrayType = null,
public readonly string|ValueType|null $arrayType = null,
public readonly ?KeyType $keyType = null,
public readonly ?string $implodeOn = null,
public readonly ?string $joinOn = null,
Expand Down
7 changes: 4 additions & 3 deletions src/Attributes/SequenceField.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
use Attribute;
use Crell\AttributeUtils\SupportsScopes;
use Crell\Serde\TypeField;
use Crell\Serde\ValueType;

#[Attribute(Attribute::TARGET_PROPERTY)]
class SequenceField implements TypeField, SupportsScopes
{
/**
* @param string|null $arrayType
* Elements in this array are objects of this type.
* @param string|ValueType|null $arrayType
* Elements in this array are values of this type.
* @param string|null $implodeOn
* Scalar values of this array should be imploded to a string and exploded on deserialization.
* @param bool $trim
Expand All @@ -22,7 +23,7 @@ class SequenceField implements TypeField, SupportsScopes
* The scopes in which this attribute should apply.
*/
public function __construct(
public readonly ?string $arrayType = null,
public readonly string|ValueType|null $arrayType = null,
public readonly ?string $implodeOn = null,
public readonly bool $trim = true,
protected readonly array $scopes = [null],
Expand Down
31 changes: 24 additions & 7 deletions src/Formatter/ArrayBasedDeformatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Crell\Serde\SerdeError;
use Crell\Serde\TypeCategory;
use Crell\Serde\TypeMismatch;
use Crell\Serde\ValueType;
use function Crell\fp\first;
use function Crell\fp\pipe;
use function Crell\fp\reduceWithKeys;
Expand Down Expand Up @@ -134,11 +135,18 @@ public function deserializeSequence(mixed $decoded, Field $field, Deserializer $
// arrayType property, it resolves to null anyway, which is exactly what we want.
// @phpstan-ignore-next-line
$class = $field?->typeField?->arrayType ?? '';
if (class_exists($class) || interface_exists($class)) {
if ($class instanceof ValueType) {
if ($class->assert($data)) {
return $data;
} else {
throw TypeMismatch::create($field->serializedName, "array($class->name)", "array(" . \get_debug_type($data[0] . ')'));
}
}
else if (class_exists($class) || interface_exists($class)) {
return $this->upcastArray($data, $deserializer, $class);
} else {
return $this->upcastArray($data, $deserializer);
}

return $this->upcastArray($data, $deserializer);
}

public function deserializeDictionary(mixed $decoded, Field $field, Deserializer $deserializer): array|SerdeError|null
Expand All @@ -156,15 +164,24 @@ public function deserializeDictionary(mixed $decoded, Field $field, Deserializer
throw FormatParseError::create($field, $this->format(), $decoded);
}

$data = $decoded[$field->serializedName];

// This line is fine, because if typeField is somehow not of a type with an
// arrayType property, it resolves to null anyway, which is exactly what we want.
// @phpstan-ignore-next-line
$class = $field?->typeField?->arrayType ?? '';
if (class_exists($class) || interface_exists($class)) {
return $this->upcastArray($decoded[$field->serializedName], $deserializer, $class);
if ($class instanceof ValueType) {
if ($class->assert($data)) {
return $data;
} else {
throw TypeMismatch::create($field->serializedName, "array($class->name)", "array(" . \get_debug_type($data[array_key_first($data)] . ')'));
}
}
else if (class_exists($class) || interface_exists($class)) {
return $this->upcastArray($data, $deserializer, $class);
} else {
return $this->upcastArray($data, $deserializer);
}

return $this->upcastArray($decoded[$field->serializedName], $deserializer);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/Formatter/CsvFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ public function deserializeInitialize(
if (! $typeField instanceof SequenceField) {
throw CsvFormatRequiresExplicitRowType::create($classDef, $rowField);
}
if (!$typeField->arrayType || !class_exists($typeField->arrayType)) {
// The row must be an object, to an array type of a primitive doesn't make sense.
if (!$typeField->arrayType || !is_string($typeField->arrayType) || !class_exists($typeField->arrayType)) {
throw CsvFormatRequiresExplicitRowType::create($classDef, $rowField);
}

Expand Down
27 changes: 27 additions & 0 deletions src/ValueType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);

namespace Crell\Serde;

use function Crell\fp\all;

enum ValueType
{
case String;
case Int;
case Float;
case Array;

/**
* @param array<mixed> $values
*/
public function assert(array $values): bool
{
return match ($this) {
self::String => all(is_string(...))($values),
self::Int => all(is_int(...))($values),
self::Float => all(is_float(...))($values),
self::Array => all(is_array(...))($values),
};
}
}
27 changes: 27 additions & 0 deletions tests/Records/ScalarArrays.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Crell\Serde\Records;

use Crell\Serde\Attributes\DictionaryField;
use Crell\Serde\Attributes\SequenceField;
use Crell\Serde\KeyType;
use Crell\Serde\ValueType;

class ScalarArrays
{
public function __construct(
#[SequenceField(arrayType: ValueType::Int)]
public array $ints,

#[SequenceField(arrayType: ValueType::Float)]
public array $floats,

#[DictionaryField(keyType: KeyType::String, arrayType: ValueType::String)]
public array $stringMap,

#[DictionaryField(keyType: KeyType::String, arrayType: ValueType::Array)]
public array $arrayMap,
) {}
}
55 changes: 55 additions & 0 deletions tests/SerdeTestCases.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
use Crell\Serde\Records\RequiresFieldValuesClass;
use Crell\Serde\Records\RootMap\Type;
use Crell\Serde\Records\RootMap\TypeB;
use Crell\Serde\Records\ScalarArrays;
use Crell\Serde\Records\SequenceOfStrings;
use Crell\Serde\Records\Shapes\Box;
use Crell\Serde\Records\Shapes\Circle;
Expand Down Expand Up @@ -1586,4 +1587,58 @@ public function non_sequence_arrays_in_weak_mode_are_coerced(): void
self::assertEquals('A', $result->nonstrict[0]);
self::assertEquals('B', $result->nonstrict[1]);
}

#[Test]
public function arrays_with_valid_scalar_values(): void
{
$s = new SerdeCommon(formatters: $this->formatters);

$data = new ScalarArrays(
ints: [1, 2, 3],
floats: [3.14, 2.7],
stringMap: ['a' => 'A'],
arrayMap: ['a' => [1, 2, 3]],
);

$serialized = $s->serialize($data, $this->format);

$this->arrays_with_valid_scalar_values_validate($serialized);

$result = $s->deserialize($serialized, from: $this->format, to: $data::class);

self::assertEquals($data, $result);
}

public function arrays_with_valid_scalar_values_validate(mixed $serialized): void
{

}

#[Test]
public function arrays_with_invalid_scalar_values(): void
{
$s = new SerdeCommon(formatters: $this->formatters);

$this->expectException(TypeMismatch::class);

// This should serialize fine, but then refuse to deserialize because
// of the floats in the int section.
$data = new ScalarArrays(
ints: [1.1, 2.2, 3],
floats: [3.14, 2.7],
stringMap: ['a' => 'A'],
arrayMap: ['a' => [1, 2, 3]],
);

$serialized = $s->serialize($data, $this->format);

$this->arrays_with_valid_scalar_values_validate($serialized);

$result = $s->deserialize($serialized, from: $this->format, to: $data::class);
}

public function arrays_with_invalid_scalar_values_validate(mixed $serialized): void
{

}
}

0 comments on commit 90d2223

Please sign in to comment.