data-transfer-object/tests/Values/ValuesTest.php
2026-03-07 11:27:12 -03:00

273 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Values;
use Carbon\CarbonPeriod;
use Icefox\Data\Attributes\CastWith;
use Icefox\Data\Factories\ValueFactory;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
readonly class BasicPrimitives
{
public function __construct(
public string $text,
public int $number,
public bool $flag,
public ?array $items,
public float $floating = 4.7,
) {}
}
test('basic creation works', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => true,
'items' => ['a', 2, false],
'floating' => 32.6,
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(true);
expect($object->items)->toBe(['a', 2, false]);
expect($object->floating)->toBe(32.6);
});
test('uses default values', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => true,
'items' => ['a', 2, false],
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(true);
expect($object->items)->toBe(['a', 2, false]);
expect($object->floating)->toBe(4.7);
});
test('uses default when null and not nullable', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => true,
'items' => ['a', 2, false],
'floating' => null,
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(true);
expect($object->items)->toBe(['a', 2, false]);
expect($object->floating)->toBe(4.7);
});
test('accepts null when nullable', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => true,
'items' => null,
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(true);
expect($object->items)->toBe(null);
expect($object->floating)->toBe(4.7);
});
test('accepts missing as null when nullable', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => true,
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(true);
expect($object->items)->toBe(null);
expect($object->floating)->toBe(4.7);
});
readonly class NestedLeaf
{
public function __construct(public bool $flag) {}
}
readonly class RootWithNestedLeaf
{
public function __construct(public int $value, public NestedLeaf $leaf) {}
}
test('creates nested object', function () {
$root = ValueFactory::make(RootWithNestedLeaf::class, [
'value' => 42,
'leaf' => [
'flag' => true,
],
]);
expect($root->value)->toBe(42);
expect($root->leaf->flag)->toBe(true);
});
readonly class CollectionItem
{
public function __construct(public int $value) {}
}
readonly class CollectionRoot
{
/**
* @param Collection<int, CollectionItem> $items
*/
public function __construct(public string $text, public Collection $items) {}
}
test('creates collection object', function () {
$root = ValueFactory::make(CollectionRoot::class, [
'text' => 'abc',
'items' => [
[ 'value' => 1 ],
[ 'value' => 2 ],
[ 'value' => 4 ],
[ 'value' => 8 ],
],
]);
expect($root->text)->toBe('abc');
expect($root->items)->toBeInstanceOf(Collection::class);
expect($root->items->count())->toBe(4);
expect($root->items[0]->value)->toBe(1);
expect($root->items[1]->value)->toBe(2);
expect($root->items[2]->value)->toBe(4);
expect($root->items[3]->value)->toBe(8);
});
readonly class DoubleCast
{
public static function cast(int $data): int
{
return $data * 2;
}
}
readonly class WithExplicitCast
{
public function __construct(
#[CastWith(DoubleCast::class)]
public int $value,
) {}
}
readonly class WithNestedCast
{
/**
* @param array<int,WithExplicitCast> $items
*/
public function __construct(public array $items) {}
}
test('with explicit cast', function () {
$object = ValueFactory::make(WithExplicitCast::class, ['value' => 32]);
expect($object->value)->toBe(64);
});
test('with nested cast', function () {
$object = ValueFactory::make(WithNestedCast::class, ['items' => [ ['value' => 2], ['value' => 3], ['value' => 5]]]);
expect($object->items[0]->value)->toBe(4);
expect($object->items[1]->value)->toBe(6);
expect($object->items[2]->value)->toBe(10);
});
readonly class CarbonPeriodCast
{
/**
* @param array<int,mixed> $data
*/
public static function cast(array $data): CarbonPeriod
{
return new CarbonPeriod(Carbon::parse($data['start']), Carbon::parse($data['end']));
}
}
readonly class WithObjectCast
{
public function __construct(
#[CastWith(CarbonPeriodCast::class)]
public CarbonPeriod $period,
) {}
}
test('with object cast', function () {
$object = ValueFactory::make(WithObjectCast::class, ['period' => ['start' => '1980-10-01', 'end' => '1990-06-01']]);
expect($object->period)->toBeInstanceOf(CarbonPeriod::class);
expect($object->period->start->format('Y-m-d'))->toBe('1980-10-01');
expect($object->period->end->format('Y-m-d'))->toBe('1990-06-01');
});
enum Status: string
{
case ACTIVE = 'active';
case INACTIVE = 'inactive';
case PENDING = 'pending';
}
readonly class TaskDTOWithEnum
{
public function __construct(
public string $title,
public Status $status,
) {}
}
readonly class TaskDTOWithNullableEnum
{
public function __construct(
public string $title,
public ?Status $status,
) {}
}
test('backed enum properly cast from validated data', function () {
$object = ValueFactory::make(TaskDTOWithEnum::class, [
'title' => 'Task 1',
'status' => 'active',
]);
expect($object->title)->toBe('Task 1')
->and($object->status)->toBeInstanceOf(Status::class)
->and($object->status)->toBe(Status::ACTIVE);
});
test('nullable backed enum with null', function () {
$object = ValueFactory::make(TaskDTOWithNullableEnum::class, [
'title' => 'Task 1',
'status' => null,
]);
expect($object->title)->toBe('Task 1')
->and($object->status)->toBeNull();
});
test('nullable backed enum with valid value', function () {
$object = ValueFactory::make(TaskDTOWithNullableEnum::class, [
'title' => 'Task 1',
'status' => 'pending',
]);
expect($object->title)->toBe('Task 1')
->and($object->status)->toBeInstanceOf(Status::class)
->and($object->status)->toBe(Status::PENDING);
});