Skip to content

Commit

Permalink
Add support for mixed typed properties.
Browse files Browse the repository at this point in the history
  • Loading branch information
Crell committed Oct 10, 2022
1 parent 6f2f6ec commit 4970271
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/Attributes/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ protected function deriveTypeCategory(): TypeCategory
\enum_exists($this->phpType) => $this->enumType($this->phpType),
$this->phpType === 'object', \class_exists($this->phpType), \interface_exists($this->phpType) => TypeCategory::Object,
$this->phpType === 'null' => TypeCategory::Null,
$this->phpType === 'mixed' => TypeCategory::Mixed,
default => throw UnsupportedType::create($this->phpType),
};
}
Expand Down
15 changes: 14 additions & 1 deletion src/PropertyHandler/DictionaryExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ public function exportValue(Serializer $serializer, Field $field, mixed $value,
return $serializer->formatter->serializeString($runningValue, $field, $typeField->implode($value));
}

$dict = $this->arrayToDict($value, $field);

return $serializer->formatter->serializeDictionary($runningValue, $field, $dict, $serializer);
}

/**
* @param array<mixed, mixed> $value
*/
protected function arrayToDict(array $value, Field $field): Dict
{
/** @var DictionaryField|null $typeField */
$typeField = $field->typeField;

$dict = new Dict();
foreach ($value as $k => $v) {
// Most $runningValue implementations will be an array.
Expand All @@ -41,7 +54,7 @@ public function exportValue(Serializer $serializer, Field $field, mixed $value,
$dict->items[] = new CollectionItem(field: $f, value: $v);
}

return $serializer->formatter->serializeDictionary($runningValue, $field, $dict, $serializer);
return $dict;
}

public function canExport(Field $field, mixed $value, string $format): bool
Expand Down
107 changes: 107 additions & 0 deletions src/PropertyHandler/MixedExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace Crell\Serde\PropertyHandler;

use Crell\Serde\Attributes\DictionaryField;
use Crell\Serde\Attributes\Field;
use Crell\Serde\CollectionItem;
use Crell\Serde\Deserializer;
use Crell\Serde\Dict;
use Crell\Serde\InvalidArrayKeyType;
use Crell\Serde\KeyType;
use Crell\Serde\Sequence;
use Crell\Serde\Serializer;
use Crell\Serde\TypeCategory;

class MixedExporter implements Importer, Exporter
{
public function exportValue(Serializer $serializer, Field $field, mixed $value, mixed $runningValue): mixed
{
$type = \get_debug_type($value);

if ($type === 'array') {
if (array_is_list($value)) {
return $serializer->formatter->serializeSequence($runningValue, $field, $this->arrayToSequence($value), $serializer);
} else {
$dict = $this->arrayToDict($value, $field);
return $serializer->formatter->serializeDictionary($runningValue, $field, $dict, $serializer);
}
}

return match ($type) {
'int' => $serializer->formatter->serializeInt($runningValue, $field, $value),
'float' => $serializer->formatter->serializeFloat($runningValue, $field, $value),
'bool' => $serializer->formatter->serializeBool($runningValue, $field, $value),
'string' => $serializer->formatter->serializeString($runningValue, $field, $value),
};
}

/**
* @param mixed[] $value
*/
protected function arrayToSequence(array $value): Sequence
{
$seq = new Sequence();
foreach ($value as $k => $v) {
$f = Field::create(serializedName: "$k", phpType: \get_debug_type($v));
$seq->items[] = new CollectionItem(field: $f, value: $v);
}
return $seq;
}

/**
* @param array<mixed, mixed> $value
*/
protected function arrayToDict(array $value, Field $field): Dict
{
/** @var DictionaryField|null $typeField */
$typeField = $field->typeField;

$dict = new Dict();
foreach ($value as $k => $v) {
// Most $runningValue implementations will be an array.
// Arrays in PHP force-cast an integer-string key to
// an integer. That means we cannot guarantee the type
// of the key going out in the Exporter. The Formatter
// will have to do so, if it cares. However, we can still
// detect and reject string-in-int.
if ($typeField?->keyType === KeyType::Int && \get_debug_type($k) === 'string') {
// It's an int field, but the key is a string. That's a no-no.
throw InvalidArrayKeyType::create($field, 'string');
}
$f = Field::create(serializedName: "$k", phpType: \get_debug_type($v));
$dict->items[] = new CollectionItem(field: $f, value: $v);
}

return $dict;
}

public function importValue(Deserializer $deserializer, Field $field, mixed $source): mixed
{
// This is normally a bad idea, as the $source should be opaque. In this
// case, we're guaranteed that the $source is array-based, so we can introspect
// it directly.
return match (\get_debug_type($source[$field->serializedName])) {
'int' => $deserializer->deformatter->deserializeInt($source, $field),
'float' => $deserializer->deformatter->deserializeFloat($source, $field),
'bool' => $deserializer->deformatter->deserializeBool($source, $field),
'string' => $deserializer->deformatter->deserializeString($source, $field),
'array' => $deserializer->deformatter->deserializeDictionary($source, $field, $deserializer),
};
}

public function canExport(Field $field, mixed $value, string $format): bool
{
return $field->typeCategory === TypeCategory::Mixed;
}

public function canImport(Field $field, string $format): bool
{
// We can only import if we know that the $source will be an array so that it
// can be introspected. If it's not, then this class has no way to tell what
// type to tell the Deformatter to read.
return $field->typeCategory === TypeCategory::Mixed && in_array($format, ['json', 'yaml', 'array']);
}
}
13 changes: 11 additions & 2 deletions src/PropertyHandler/SequenceExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,22 @@ public function exportValue(Serializer $serializer, Field $field, mixed $value,
return $serializer->formatter->serializeString($runningValue, $field, $typeField->implode($value));
}

$seq = $this->arrayToSequence($value);

return $serializer->formatter->serializeSequence($runningValue, $field, $seq, $serializer);
}

/**
* @param mixed[] $value
*/
protected function arrayToSequence(array $value): Sequence
{
$seq = new Sequence();
foreach ($value as $k => $v) {
$f = Field::create(serializedName: "$k", phpType: \get_debug_type($v));
$seq->items[] = new CollectionItem(field: $f, value: $v);
}

return $serializer->formatter->serializeSequence($runningValue, $field, $seq, $serializer);
return $seq;
}

public function canExport(Field $field, mixed $value, string $format): bool
Expand Down
2 changes: 2 additions & 0 deletions src/SerdeCommon.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Crell\Serde\PropertyHandler\EnumExporter;
use Crell\Serde\PropertyHandler\Exporter;
use Crell\Serde\PropertyHandler\Importer;
use Crell\Serde\PropertyHandler\MixedExporter;
use Crell\Serde\PropertyHandler\NativeSerializeExporter;
use Crell\Serde\PropertyHandler\NullExporter;
use Crell\Serde\PropertyHandler\ObjectExporter;
Expand Down Expand Up @@ -73,6 +74,7 @@ public function __construct(
$handlers = [
new ScalarExporter(),
new NullExporter(),
new MixedExporter(),
new SequenceExporter(),
new DictionaryExporter(),
new DateTimeExporter(),
Expand Down
1 change: 1 addition & 0 deletions src/TypeCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum TypeCategory
case IntEnum;
case StringEnum;
case Null;
case Mixed;

public function isEnum(): bool
{
Expand Down
10 changes: 10 additions & 0 deletions tests/Records/MixedVal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Crell\Serde\Records;

class MixedVal
{
public function __construct(public mixed $val) {}
}
34 changes: 34 additions & 0 deletions tests/SerdeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Crell\Serde\Records\MappedCollected\ThingB;
use Crell\Serde\Records\MappedCollected\ThingC;
use Crell\Serde\Records\MappedCollected\ThingList;
use Crell\Serde\Records\MixedVal;
use Crell\Serde\Records\MultiCollect\ThingOneA;
use Crell\Serde\Records\MultiCollect\ThingTwoC;
use Crell\Serde\Records\MultiCollect\Wrapper;
Expand Down Expand Up @@ -1229,6 +1230,39 @@ public function array_of_null_serializes_cleanly_validate(mixed $serialized): vo

}

/**
* @test
* @dataProvider mixed_val_property_examples
*/
public function mixed_val_property(mixed $data): void
{
$s = new SerdeCommon(formatters: $this->formatters);

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

$this->mixed_val_property_validate($serialized, $data);

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

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

public function mixed_val_property_examples(): iterable
{
yield 'string' => [new MixedVal('hello')];
yield 'int' => [new MixedVal(5)];
yield 'float' => [new MixedVal(3.14)];
yield 'array' => [new MixedVal(['a', 'b', 'c'])];
// Objects can't work, because they cannot be imported without type data.
// Exporting might. Todo for later.
//yield 'object' => [new Point(3, 4, 5)];
}

public function mixed_val_property_validate(mixed $serialized, mixed $data): void
{

}

/**
* @test
* @dataProvider scopes_examples()
Expand Down

0 comments on commit 4970271

Please sign in to comment.