Skip to content

Commit

Permalink
Merge pull request #70 from Crell/list-coerce
Browse files Browse the repository at this point in the history
Allow weak mode to apply to values in an array
  • Loading branch information
Crell authored Sep 6, 2024
2 parents 055e12a + cdd244e commit 228cea4
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip
### Added
- Null values may now be excluded when serializing. See the `omitNullFields` and `omitIfNull` flags in the README.
- We now require AttributeUtils 1.2, which lets us use closures rather than method name strings for subAttribute callbacks. (Internal improvement.)
- When `strict` is false on a sequence or dictionary, numeric strings will get cast to an int or float as appropriate. Previously the list values were processed in strict mode regardless of what the field was set to.

### Deprecated
- Nothing
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,8 @@ This key only applies on deserialization. If set to `true`, a type mismatch in

For sequence fields, `strict` set to `true` will reject a non-sequence value. (It must pass an `array_is_list()` check.) If `strict` is `false`, any array-ish value will be accepted but passed through `array_values()` to discard any keys and reindex it.

Additionally, in non-`strict` mode, numeric strings in the incoming array will be cast to ints or floats as appropriate in both sequence fields and dictionary fields. In `strict` mode, numeric strings will still be rejected.

The exact handling of this setting may vary slightly depending on the incoming format, as some formats handle their own types differently. (For instance, everything is a string in XML.)

### `requireValue` (bool, default false)
Expand Down
6 changes: 6 additions & 0 deletions src/Formatter/ArrayBasedDeformatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ public function deserializeSequence(mixed $decoded, Field $field, Deserializer $
// @phpstan-ignore-next-line
$class = $field?->typeField?->arrayType ?? '';
if ($class instanceof ValueType) {
if (!$field->strict) {
$data = $class->coerce($data);
}
if ($class->assert($data)) {
return $data;
} else {
Expand Down Expand Up @@ -179,6 +182,9 @@ public function deserializeDictionary(mixed $decoded, Field $field, Deserializer
// @phpstan-ignore-next-line
$class = $field?->typeField?->arrayType ?? '';
if ($class instanceof ValueType) {
if (!$field->strict) {
$data = $class->coerce($data);
}
if ($class->assert($data)) {
return $data;
} else {
Expand Down
15 changes: 15 additions & 0 deletions src/ValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Crell\Serde;

use function Crell\fp\all;
use function Crell\fp\amap;

enum ValueType
{
Expand All @@ -24,4 +25,18 @@ public function assert(array $values): bool
self::Array => all(is_array(...))($values),
};
}

/**
* @param array<mixed> $values
* @return array<mixed>
*/
public function coerce(array $values): array
{
return match ($this) {
self::String => $values,
self::Int => amap(intval(...))($values),
self::Float => amap(floatval(...))($values),
self::Array => $values,
};
}
}
5 changes: 5 additions & 0 deletions tests/ArrayFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ public function setUp(): void
'strict' => ['A', 'B'],
'nonstrict' => ['a' => 'A', 'b' => 'B'],
];

$this->weakModeLists = [
'seq' => [1, '2', 3],
'dict' => ['a' => 1, 'b' => '2'],
];
}

protected function arrayify(mixed $serialized): array
Expand Down
5 changes: 5 additions & 0 deletions tests/JsonFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public function setUp(): void
'strict' => ['A', 'B'],
'nonstrict' => ['a' => 'A', 'b' => 'B'],
]);

$this->weakModeLists = json_encode([
'seq' => [1, '2', 3],
'dict' => ['a' => 1, 'b' => '2'],
]);
}

protected function arrayify(mixed $serialized): array
Expand Down
21 changes: 21 additions & 0 deletions tests/Records/WeakLists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Crell\Serde\Records;

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

class WeakLists
{
public function __construct(
#[Field(strict: false), SequenceField(ValueType::Int)]
public array $seq,
#[Field(strict: false), DictionaryField(arrayType: ValueType::Int, keyType: KeyType::String)]
public array $dict,
) {}
}
26 changes: 25 additions & 1 deletion tests/SerdeTestCases.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
use Crell\Serde\Records\ValueObjects\JobEntryFlattenedPrefixed;
use Crell\Serde\Records\ValueObjects\Person;
use Crell\Serde\Records\Visibility;
use Crell\Serde\Records\WeakLists;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Test;
Expand Down Expand Up @@ -159,6 +160,13 @@ abstract class SerdeTestCases extends TestCase
*/
protected mixed $dictsInSequenceShouldFail;

/**
* Data to deserialize that contains numeric-string lists, which should still coerce into an integer list safely.
*
* @see lists_in_weak_mode_coerce_elements())
*/
protected mixed $weakModeLists;

/**
* Data to deserialize that should pass, because the strict is valid and non-strict gets coerced to a list.
* @see non_sequence_arrays_in_weak_mode_are_coerced
Expand Down Expand Up @@ -1281,7 +1289,7 @@ public function nullable_null_properties_are_allowed(): void
self::assertEquals($data, $result);
}

#[Test]
#[Test, Group('flattening')]
public function nullable_properties_flattened(): void
{
$s = new SerdeCommon(formatters: $this->formatters);
Expand All @@ -1298,6 +1306,22 @@ public function nullable_properties_flattened(): void
self::assertEquals($data, $result);
}

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

/** @var WeakLists $result */
$result = $s->deserialize($this->weakModeLists, $this->format, WeakLists::class);

self::assertIsArray($result->seq);
self::assertIsArray($result->dict);

foreach ([...$result->seq, ...$result->dict] as $val) {
self::assertIsInt($val);
}
}

#[Test]
public function non_sequence_arrays_are_normalized_to_sequences(): void
{
Expand Down
5 changes: 5 additions & 0 deletions tests/YamlFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public function setUp(): void
'strict' => ['A', 'B'],
'nonstrict' => ['a' => 'A', 'b' => 'B'],
]);

$this->weakModeLists = YAML::dump([
'seq' => [1, '2', 3],
'dict' => ['a' => 1, 'b' => '2'],
]);
}

protected function arrayify(mixed $serialized): array
Expand Down

0 comments on commit 228cea4

Please sign in to comment.