From 0fc8124df21605a77210ceed6189de9fe4906561 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 15 Jan 2024 14:25:08 +0100 Subject: [PATCH] Test cleanup --- src/Concerns/DefaultableData.php | 11 - src/Contracts/DataObject.php | 2 +- src/Contracts/DefaultableData.php | 8 - src/Data.php | 1 - src/DataPipes/DefaultValuesDataPipe.php | 12 +- src/Dto.php | 5 +- src/Resource.php | 3 +- src/Support/DataClass.php | 2 - tests/AppendTest.php | 86 + tests/Casts/DateTimeInterfaceCastTest.php | 17 + tests/CreationTest.php | 909 +++++++++++ tests/DataCollectionTest.php | 193 +-- tests/DataPipelineTest.php | 28 - .../DataPipes/CastPropertiesDataPipeTest.php | 275 ---- tests/DataPipes/DefaultValuesDataPipeTest.php | 36 - tests/DataTest.php | 1426 +---------------- tests/Datasets/DataCollection.php | 11 - tests/Datasets/DataTest.php | 197 --- tests/EmptyTest.php | 54 + ...peTest.php => FillRouteParametersTest.php} | 0 tests/LivewireTest.php | 19 + ...solverTest.php => MagicalCreationTest.php} | 1 + ...ertiesDataPipeTest.php => MappingTest.php} | 121 +- tests/PartialsTest.php | 503 ++++++ tests/PipelineTest.php | 60 + .../{RequestDataTest.php => RequestTest.php} | 30 +- .../PartialsTreeFromRequestResolverTest.php | 262 --- .../VisibleDataFieldsResolverTest.php | 56 +- tests/Support/DataTypeTest.php | 2 - tests/TransformationTest.php | 371 +++++ tests/WithDataTest.php | 61 + tests/WrapTest.php | 233 +++ ...a_paginated_cursor_data_collection__1.json | 58 + ...nsform_a_paginated_data_collection__1.json | 109 ++ 34 files changed, 2656 insertions(+), 2506 deletions(-) delete mode 100644 src/Concerns/DefaultableData.php delete mode 100644 src/Contracts/DefaultableData.php create mode 100644 tests/AppendTest.php create mode 100644 tests/CreationTest.php delete mode 100644 tests/DataPipelineTest.php delete mode 100644 tests/DataPipes/CastPropertiesDataPipeTest.php delete mode 100644 tests/DataPipes/DefaultValuesDataPipeTest.php delete mode 100644 tests/Datasets/DataCollection.php delete mode 100644 tests/Datasets/DataTest.php create mode 100644 tests/EmptyTest.php rename tests/{DataPipes/FillRouteParameterPropertiesPipeTest.php => FillRouteParametersTest.php} (100%) create mode 100644 tests/LivewireTest.php rename tests/{Resolvers/DataFromSomethingResolverTest.php => MagicalCreationTest.php} (99%) rename tests/{DataPipes/MapPropertiesDataPipeTest.php => MappingTest.php} (55%) create mode 100644 tests/PipelineTest.php rename tests/{RequestDataTest.php => RequestTest.php} (82%) delete mode 100644 tests/Resolvers/PartialsTreeFromRequestResolverTest.php create mode 100644 tests/TransformationTest.php create mode 100644 tests/WithDataTest.php create mode 100644 tests/WrapTest.php create mode 100644 tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json create mode 100644 tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json diff --git a/src/Concerns/DefaultableData.php b/src/Concerns/DefaultableData.php deleted file mode 100644 index 3eea8915f..000000000 --- a/src/Concerns/DefaultableData.php +++ /dev/null @@ -1,11 +0,0 @@ -defaultable - ? app()->call([$class->name, 'defaults']) - : []; - $class ->properties ->filter(fn (DataProperty $property) => ! $properties->has($property->name)) - ->each(function (DataProperty $property) use ($dataDefaults, &$properties) { - if (array_key_exists($property->name, $dataDefaults)) { - $properties[$property->name] = $dataDefaults[$property->name]; - - return; - } - + ->each(function (DataProperty $property) use (&$properties) { if ($property->hasDefaultValue) { $properties[$property->name] = $property->defaultValue; diff --git a/src/Dto.php b/src/Dto.php index 396f30fa3..3e566fc59 100644 --- a/src/Dto.php +++ b/src/Dto.php @@ -3,15 +3,12 @@ namespace Spatie\LaravelData; use Spatie\LaravelData\Concerns\BaseData; -use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; -use Spatie\LaravelData\Contracts\DefaultableData as DefaultDataContract; use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; -class Dto implements ValidateableDataContract, BaseDataContract, DefaultDataContract +class Dto implements ValidateableDataContract, BaseDataContract { use ValidateableData; use BaseData; - use DefaultableData; } diff --git a/src/Resource.php b/src/Resource.php index 314a49856..47cfab6d9 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -20,7 +20,7 @@ use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract, DefaultDataContract +class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract { use BaseData; use AppendableData; @@ -30,5 +30,4 @@ class Resource implements BaseDataContract, AppendableDataContract, IncludeableD use WrappableData; use EmptyData; use ContextableData; - use DefaultableData; } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 04caef580..f337bc7cb 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -44,7 +44,6 @@ public function __construct( public readonly bool $responsable, public readonly bool $transformable, public readonly bool $validateable, - public readonly bool $defaultable, public readonly bool $wrappable, public readonly bool $emptyData, public readonly Collection $attributes, @@ -107,7 +106,6 @@ public static function create(ReflectionClass $class): self responsable: $responsable, transformable: $class->implementsInterface(TransformableData::class), validateable: $class->implementsInterface(ValidateableData::class), - defaultable: $class->implementsInterface(DefaultableData::class), wrappable: $class->implementsInterface(WrappableData::class), emptyData: $class->implementsInterface(EmptyData::class), attributes: $attributes, diff --git a/tests/AppendTest.php b/tests/AppendTest.php new file mode 100644 index 000000000..d7e2b277b --- /dev/null +++ b/tests/AppendTest.php @@ -0,0 +1,86 @@ + "{$this->name} from Spatie"]; + } + }; + + expect($data->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'Freek from Spatie', + ]); +}); + +it('can append data via method overwriting with closures', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + + public function with(): array + { + return [ + 'alt_name' => static function (self $data) { + return $data->name.' from Spatie via closure'; + }, + ]; + } + }; + + expect($data->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'Freek from Spatie via closure', + ]); +}); + +it('can append data via method call', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + }; + + $transformed = $data->additional([ + 'company' => 'Spatie', + 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", + ])->toArray(); + + expect($transformed)->toMatchArray([ + 'name' => 'Freek', + 'company' => 'Spatie', + 'alt_name' => 'Freek from Spatie', + ]); +}); + + +test('when using additional method and with method the additional method will be prioritized', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + + public function with(): array + { + return [ + 'alt_name' => static function (self $data) { + return $data->name.' from Spatie via closure'; + }, + ]; + } + }; + + expect($data->additional(['alt_name' => 'I m Freek from additional'])->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'I m Freek from additional', + ]); +}); + diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 4b3e6a606..239abea90 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -3,8 +3,10 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Carbon\CarbonTimeZone; +use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Casts\Uncastable; +use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; @@ -191,3 +193,18 @@ ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); }); + +it('can define multiple date formats to be used', function () { + $data = new class () extends Data { + public function __construct( + #[WithCast(DateTimeInterfaceCast::class, ['Y-m-d\TH:i:sP', 'Y-m-d H:i:s'])] + public ?DateTime $date = null + ) { + } + }; + + expect($data::from(['date' => '2022-05-16T14:37:56+00:00']))->toArray() + ->toMatchArray(['date' => '2022-05-16T14:37:56+00:00']) + ->and($data::from(['date' => '2022-05-16 17:00:00']))->toArray() + ->toMatchArray(['date' => '2022-05-16T17:00:00+00:00']); +}); diff --git a/tests/CreationTest.php b/tests/CreationTest.php new file mode 100644 index 000000000..3fa0cb133 --- /dev/null +++ b/tests/CreationTest.php @@ -0,0 +1,909 @@ + 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => '16-06-1994', + 'defaultCast' => '1994-05-16T12:00:00+01:00', + 'nestedData' => [ + 'string' => 'hello', + ], + 'nestedCollection' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + 'nestedArray' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + ]); + + expect($data)->toBeInstanceOf(ComplicatedData::class) + ->withoutType->toEqual(42) + ->int->toEqual(42) + ->bool->toBeTrue() + ->float->toEqual(3.14) + ->string->toEqual('Hello world') + ->array->toEqual([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() + ->undefinable->toBeInstanceOf(Optional::class) + ->mixed->toEqual(42) + ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) + ->explicitCast->toEqual(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994')) + ->nestedData->toEqual(SimpleData::from('hello')) + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ])); +}); + +it("won't cast a property that is already in the correct type", function () { + $data = ComplicatedData::from([ + 'withoutType' => 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => DateTime::createFromFormat('d-m-Y', '16-06-1994'), + 'defaultCast' => DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00'), + 'nestedData' => SimpleData::from('hello'), + 'nestedCollection' => SimpleData::collect([ + 'never', 'gonna', 'give', 'you', 'up', + ], DataCollection::class), + 'nestedArray' => SimpleData::collect([ + 'never', 'gonna', 'give', 'you', 'up', + ]), + ]); + + expect($data)->toBeInstanceOf(ComplicatedData::class) + ->withoutType->toEqual(42) + ->int->toEqual(42) + ->bool->toBeTrue() + ->float->toEqual(3.14) + ->string->toEqual('Hello world') + ->array->toEqual([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() + ->mixed->toBe(42) + ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) + ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) + ->nestedData->toEqual(SimpleData::from('hello')) + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ])); +}); + +it('allows creating data objects using Lazy', function () { + $data = NestedLazyData::from([ + 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), + ]); + + expect($data->simple) + ->toBeInstanceOf(Lazy::class) + ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); +}); + +it('can set a custom cast', function () { + $dataClass = new class () extends Data { + #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')] + public DateTimeImmutable $date; + }; + + $data = $dataClass::from([ + 'date' => '2022-01-18', + ]); + + expect($data->date) + ->toBeInstanceOf(DateTimeImmutable::class) + ->toEqual(DateTimeImmutable::createFromFormat('Y-m-d', '2022-01-18')); +}); + +it('allows casting of enums', function () { + $data = EnumData::from([ + 'enum' => 'foo', + ]); + + expect($data->enum) + ->toBeInstanceOf(DummyBackedEnum::class) + ->toEqual(DummyBackedEnum::FOO); +}); + +it('can optionally create data', function () { + expect(SimpleData::optional(null))->toBeNull(); + expect(new SimpleData('Hello world'))->toEqual( + SimpleData::optional(['string' => 'Hello world']) + ); +}); + +it('can create a data model without constructor', function () { + expect(SimpleDataWithoutConstructor::fromString('Hello')) + ->toEqual(SimpleDataWithoutConstructor::from('Hello')); + + expect(SimpleDataWithoutConstructor::fromString('Hello')) + ->toEqual(SimpleDataWithoutConstructor::from([ + 'string' => 'Hello', + ])); + + expect( + new DataCollection(SimpleDataWithoutConstructor::class, [ + SimpleDataWithoutConstructor::fromString('Hello'), + SimpleDataWithoutConstructor::fromString('World'), + ]) + ) + ->toEqual(SimpleDataWithoutConstructor::collect(['Hello', 'World'], DataCollection::class)); +}); + +it('can create a data object from a model', function () { + DummyModel::migrate(); + + $model = DummyModel::create([ + 'string' => 'test', + 'boolean' => true, + 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), + 'nullable_date' => null, + ]); + + $dataClass = new class () extends Data { + public string $string; + + public bool $boolean; + + public Carbon $date; + + public ?Carbon $nullable_date; + }; + + $data = $dataClass::from(DummyModel::findOrFail($model->id)); + + expect($data) + ->string->toEqual('test') + ->boolean->toBeTrue() + ->nullable_date->toBeNull() + ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); +}); + +it('can create a data object from a stdClass object', function () { + $object = (object) [ + 'string' => 'test', + 'boolean' => true, + 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), + 'nullable_date' => null, + ]; + + $dataClass = new class () extends Data { + public string $string; + + public bool $boolean; + + public CarbonImmutable $date; + + public ?Carbon $nullable_date; + }; + + $data = $dataClass::from($object); + + expect($data) + ->string->toEqual('test') + ->boolean->toBeTrue() + ->nullable_date->toBeNull() + ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); +}); + +it('has support for readonly properties', function () { + $dataClass = new class ('') extends Data { + public function __construct( + public readonly string $string + ) { + } + }; + + $data = $dataClass::from(['string' => 'Hello world']); + + expect($data)->toBeInstanceOf($dataClass::class) + ->and($data->string)->toEqual('Hello world'); +}); + + +it('has support for intersection types', function () { + $collection = collect(['a', 'b', 'c']); + + $dataClass = new class () extends Data { + public Arrayable & \Countable $intersection; + }; + + $data = $dataClass::from(['intersection' => $collection]); + + expect($data)->toBeInstanceOf($dataClass::class) + ->and($data->intersection)->toEqual($collection); +}); + +it( + 'can construct a data object with both constructor promoted and default properties', + function () { + $dataClass = new class ('') extends Data { + public string $property; + + public function __construct( + public string $promoted_property, + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'A', + 'promoted_property' => 'B', + ]); + + expect($data) + ->property->toEqual('A') + ->promoted_property->toEqual('B'); + } +); + +it('can construct a data object with default values', function () { + $dataClass = new class ('', '') extends Data { + public string $property; + + public string $default_property = 'Hello'; + + public function __construct( + public string $promoted_property, + public string $default_promoted_property = 'Hello Again', + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'Test', + 'promoted_property' => 'Test Again', + ]); + + expect($data) + ->property->toEqual('Test') + ->promoted_property->toEqual('Test Again') + ->default_property->toEqual('Hello') + ->default_promoted_property->toEqual('Hello Again'); +}); + + +it('can construct a data object with default values and overwrite them', function () { + $dataClass = new class ('', '') extends Data { + public string $property; + + public string $default_property = 'Hello'; + + public function __construct( + public string $promoted_property, + public string $default_promoted_property = 'Hello Again', + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'Test', + 'default_property' => 'Test', + 'promoted_property' => 'Test Again', + 'default_promoted_property' => 'Test Again', + ]); + + expect($data) + ->property->toEqual('Test') + ->promoted_property->toEqual('Test Again') + ->default_property->toEqual('Test') + ->default_promoted_property->toEqual('Test Again'); +}); + +it('can manually set values in the constructor', function () { + $dataClass = new class ('', '') extends Data { + public string $member; + + public string $other_member; + + public string $member_with_default = 'default'; + + public string $member_to_set; + + public function __construct( + public string $promoted, + string $non_promoted, + string $non_promoted_with_default = 'default', + public string $promoted_with_with_default = 'default', + ) { + $this->member = "changed_in_constructor: {$non_promoted}"; + $this->other_member = "changed_in_constructor: {$non_promoted_with_default}"; + } + }; + + $data = $dataClass::from([ + 'promoted' => 'A', + 'non_promoted' => 'B', + 'non_promoted_with_default' => 'C', + 'promoted_with_with_default' => 'D', + 'member_to_set' => 'E', + 'member_with_default' => 'F', + ]); + + expect($data->toArray())->toMatchArray([ + 'member' => 'changed_in_constructor: B', + 'other_member' => 'changed_in_constructor: C', + 'member_with_default' => 'F', + 'promoted' => 'A', + 'promoted_with_with_default' => 'D', + 'member_to_set' => 'E', + ]); + + $data = $dataClass::from([ + 'promoted' => 'A', + 'non_promoted' => 'B', + 'member_to_set' => 'E', + ]); + + expect($data->toArray())->toMatchArray([ + 'member' => 'changed_in_constructor: B', + 'other_member' => 'changed_in_constructor: default', + 'member_with_default' => 'default', + 'promoted' => 'A', + 'promoted_with_with_default' => 'default', + 'member_to_set' => 'E', + ]); +}); + +it('can cast data object and collectables using a custom cast', function () { + $dataWithDefaultCastsClass = new class () extends Data { + public SimpleData $nestedData; + + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; + }; + + $dataWithCustomCastsClass = new class () extends Data { + #[WithCast(ConfidentialDataCast::class)] + public SimpleData $nestedData; + + #[WithCast(ConfidentialDataCollectionCast::class)] + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; + }; + + $dataWithDefaultCasts = $dataWithDefaultCastsClass::from([ + 'nestedData' => 'a secret', + 'nestedDataCollection' => ['another secret', 'yet another secret'], + ]); + + $dataWithCustomCasts = $dataWithCustomCastsClass::from([ + 'nestedData' => 'a secret', + 'nestedDataCollection' => ['another secret', 'yet another secret'], + ]); + + expect($dataWithDefaultCasts) + ->nestedData->toEqual(SimpleData::from('a secret')) + ->and($dataWithDefaultCasts) + ->nestedDataCollection->toEqual(SimpleData::collect(['another secret', 'yet another secret'])); + + expect($dataWithCustomCasts) + ->nestedData->toEqual(SimpleData::from('CONFIDENTIAL')) + ->and($dataWithCustomCasts) + ->nestedDataCollection->toEqual(SimpleData::collect(['CONFIDENTIAL', 'CONFIDENTIAL'])); +}); + +it('can create a data object with defaults by calling an empty from', function () { + $dataClass = new class ('', '', '') extends Data { + public function __construct( + public ?string $string, + public Optional|string $optionalString, + public string $stringWithDefault = 'Hi', + ) { + } + }; + + expect(new $dataClass(null, new Optional(), 'Hi')) + ->toEqual($dataClass::from([])); +}); + +it('can cast built-in types with custom casts', function () { + $dataClass = new class ('', '') extends Data { + public function __construct( + public string $without_cast, + #[WithCast(StringToUpperCast::class)] + public string $with_cast + ) { + } + }; + + $data = $dataClass::from([ + 'without_cast' => 'Hello World', + 'with_cast' => 'Hello World', + ]); + + expect($data) + ->without_cast->toEqual('Hello World') + ->with_cast->toEqual('HELLO WORLD'); +}); + +it('can cast data object with a castable property using anonymous class', function () { + $dataWithCastablePropertyClass = new class (new SimpleCastable('')) extends Data { + public function __construct( + #[WithCastable(SimpleCastable::class)] + public SimpleCastable $castableData, + ) { + } + }; + + $dataWithCastableProperty = $dataWithCastablePropertyClass::from(['castableData' => 'HELLO WORLD']); + + expect($dataWithCastableProperty) + ->castableData->toEqual(new SimpleCastable('HELLO WORLD')); +}); + +it('can assign a false value and the process will continue', function () { + $dataClass = new class () extends Data { + public bool $false; + + public bool $true; + }; + + $data = $dataClass::from([ + 'false' => false, + 'true' => true, + ]); + + expect($data) + ->false->toBeFalse() + ->true->toBeTrue(); +}); + +it('can create an partial data object using optional values', function () { + $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { + public function __construct( + public string $string, + public string|Optional $undefinable_string, + #[WithCast(StringToUpperCast::class)] + public string|Optional $undefinable_string_with_cast, + ) { + } + }; + + $partialData = $dataClass::from([ + 'string' => 'Hello World', + ]); + + expect($partialData) + ->string->toEqual('Hello World') + ->undefinable_string->toEqual(Optional::create()) + ->undefinable_string_with_cast->toEqual(Optional::create()); + + $fullData = $dataClass::from([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_cast' => 'Hello World', + ]); + + expect($fullData) + ->string->toEqual('Hello World') + ->undefinable_string->toEqual('Hello World') + ->undefinable_string_with_cast->toEqual('HELLO WORLD'); +}); + +it('can use context in casts based upon the properties of the data object', function () { + $dataClass = new class () extends Data { + public SimpleData $nested; + + public string $string; + + #[WithCast(ContextAwareCast::class)] + public string $casted; + }; + + $data = $dataClass::from([ + 'nested' => 'Hello', + 'string' => 'world', + 'casted' => 'json:', + ]); + + expect($data)->casted + ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); +}); + +it('can magically create a data object', function () { + $dataClass = new class ('', '') extends Data { + public function __construct( + public mixed $propertyA, + public mixed $propertyB, + ) { + } + + public static function fromStringWithDefault(string $a, string $b = 'World') + { + return new self($a, $b); + } + + public static function fromIntsWithDefault(int $a, int $b) + { + return new self($a, $b); + } + + public static function fromSimpleDara(SimpleData $data) + { + return new self($data->string, $data->string); + } + + public static function fromData(Data $data) + { + return new self('data', json_encode($data)); + } + }; + + expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) + ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) + ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); +}); + +it( + 'will throw a custom exception when a data constructor cannot be called due to missing component', + function () { + SimpleData::from([]); + } +)->throws(CannotCreateData::class, 'the constructor requires 1 parameters'); + +it('will take properties from a base class into account when creating a data object', function () { + $dataClass = new class ('') extends SimpleData { + public int $int; + }; + + $data = $dataClass::from(['string' => 'Hi', 'int' => 42]); + + expect($data) + ->string->toBe('Hi') + ->int->toBe(42); +}); + +it('can set a default value for data object which is taken into account when creating the data object', function () { + $dataObject = new class ('', '') extends Data { + #[Min(10)] + public string|Optional $full_name; + + public function __construct( + public string $first_name, + public string $last_name, + ) { + $this->full_name = "{$this->first_name} {$this->last_name}"; + } + }; + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Versieck'); + + expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect(fn () => $dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'too short'])) + ->toThrow(ValidationException::class); +}); + +it('can have a computed value when creating the data object', function () { + $dataObject = new class ('', '') extends Data { + #[Computed] + public string $full_name; + + public function __construct( + public string $first_name, + public string $last_name, + ) { + $this->full_name = "{$this->first_name} {$this->last_name}"; + } + }; + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect(fn () => $dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) + ->toThrow(CannotSetComputedValue::class); +}); + +it('can have a nullable computed value', function () { + $dataObject = new class ('', '') extends Data { + #[Computed] + public ?string $upper_name; + + public function __construct( + public ?string $name, + ) { + $this->upper_name = $name ? strtoupper($name) : null; + } + }; + + expect($dataObject::from(['name' => 'Ruben'])) + ->name->toBe('Ruben') + ->upper_name->toBe('RUBEN'); + + expect($dataObject::from(['name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); + + expect($dataObject::validateAndCreate(['name' => 'Ruben'])) + ->name->toBe('Ruben') + ->upper_name->toBe('RUBEN'); + + expect($dataObject::validateAndCreate(['name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); + + expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => 'RUBEN'])) + ->toThrow(CannotSetComputedValue::class); + + expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); // Case conflicts with DefaultsPipe, ignoring it for now +}); + +it('throws a readable exception message when the constructor fails', function ( + array $data, + string $message, +) { + try { + MultiData::from($data); + } catch (CannotCreateData $e) { + expect($e->getMessage())->toBe($message); + + return; + } + + throw new Exception('We should not reach this point'); +})->with(fn () => [ + yield 'no params' => [[], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 0 given. Parameters missing: first, second.'], + yield 'one param' => [['first' => 'First'], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 1 given. Parameters given: first. Parameters missing: second.'], +]); + +it('a can create a collection of data objects', function () { + $collectionA = new DataCollection(SimpleData::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]); + + $collectionB = SimpleData::collect([ + 'A', + 'B', + ], DataCollection::class); + + expect($collectionB)->toArray() + ->toMatchArray($collectionA->toArray()); +}); + +it('will use magic methods when creating a collection of data objects', function () { + $dataClass = new class ('') extends Data { + public function __construct(public string $otherString) + { + } + + public static function fromSimpleData(SimpleData $simpleData): static + { + return new self($simpleData->string); + } + }; + + $collection = new DataCollection($dataClass::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]); + + expect($collection[0]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('A'); + + expect($collection[1]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('B'); +}); + +it('can return a custom data collection when collecting data', function () { + $class = new class ('') extends Data implements DeprecatedDataContract { + use WithDeprecatedCollectionMethod; + + protected static string $_collectionClass = CustomDataCollection::class; + + public function __construct(public string $string) + { + } + }; + + $collection = $class::collection([ + ['string' => 'A'], + ['string' => 'B'], + ]); + + expect($collection)->toBeInstanceOf(CustomDataCollection::class); +}); + +it('can return a custom paginated data collection when collecting data', function () { + $class = new class ('') extends Data implements DeprecatedDataContract { + use WithDeprecatedCollectionMethod; + + protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; + + public function __construct(public string $string) + { + } + }; + + $collection = $class::collection(new LengthAwarePaginator([['string' => 'A'], ['string' => 'B']], 2, 15)); + + expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); +}); + +it('can magically collect data', function () { + class TestSomeCustomCollection extends Collection + { + } + + $dataClass = new class () extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); + +it('will allow a nested data object to cast properties however it wants', function () { + $model = new DummyModel(['id' => 10]); + + $withoutModelData = NestedModelData::from([ + 'model' => ['id' => 10], + ]); + + expect($withoutModelData) + ->toBeInstanceOf(NestedModelData::class) + ->model->id->toEqual(10); + + /** @var \Spatie\LaravelData\Tests\Fakes\NestedModelData $data */ + $withModelData = NestedModelData::from([ + 'model' => $model, + ]); + + expect($withModelData) + ->toBeInstanceOf(NestedModelData::class) + ->model->id->toEqual(10); +}); + +it('will allow a nested collection object to cast properties however it wants', function () { + $data = NestedModelCollectionData::from([ + 'models' => [['id' => 10], ['id' => 20],], + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); + + $data = NestedModelCollectionData::from([ + 'models' => [new DummyModel(['id' => 10]), new DummyModel(['id' => 20]),], + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); + + $data = NestedModelCollectionData::from([ + 'models' => ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class), + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); +}); diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 2577d2a8a..71ebcfd40 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -20,55 +20,7 @@ use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; -it('can get a paginated data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); - - $paginator = new LengthAwarePaginator( - $items->forPage(1, 15), - 100, - 15 - ); - - $collection = new PaginatedDataCollection(SimpleData::class, $paginator); - - expect($collection)->toBeInstanceOf(PaginatedDataCollection::class); - assertMatchesJsonSnapshot($collection->toJson()); -}); - -it('can get a paginated cursor data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); - - $paginator = new CursorPaginator( - $items, - 15, - ); - - $collection = new CursorPaginatedDataCollection(SimpleData::class, $paginator); - - if (version_compare(app()->version(), '9.0.0', '<=')) { - $this->markTestIncomplete('Laravel 8 uses a different format'); - } - - expect($collection)->toBeInstanceOf(CursorPaginatedDataCollection::class); - assertMatchesJsonSnapshot($collection->toJson()); -}); - -test('a collection can be constructed with data object', function () { - $collectionA = new DataCollection(SimpleData::class, [ - SimpleData::from('A'), - SimpleData::from('B'), - ]); - - $collectionB = SimpleData::collect([ - 'A', - 'B', - ], DataCollection::class); - - expect($collectionB)->toArray() - ->toMatchArray($collectionA->toArray()); -}); - -test('a collection can be filtered', function () { +it('can filter a collection', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->filter(fn (SimpleData $data) => $data->string === 'A')->toArray(); @@ -79,7 +31,7 @@ ->toMatchArray($filtered); }); -test('a collection can be rejected', function () { +it('can reject items within a collection', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->reject(fn (SimpleData $data) => $data->string === 'B')->toArray(); @@ -90,18 +42,8 @@ ->toMatchArray($filtered); }); -test('a collection can be transformed', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); - - expect($filtered)->toMatchArray([ - ['string' => 'Ax'], - ['string' => 'Bx'], - ]); -}); -test('a paginated collection can be transformed', function () { +it('it can put items through a paginated data collection', function () { $collection = new PaginatedDataCollection( SimpleData::class, new LengthAwarePaginator(['A', 'B'], 2, 15), @@ -177,8 +119,7 @@ expect($collection)->toHaveCount(4); }); - -it('can update data properties withing a collection', function () { +it('can update data properties within a collection', function () { LazyData::setAllowedIncludes(null); $collection = new DataCollection(LazyData::class, [ @@ -213,7 +154,7 @@ ]); }); -it('supports lazy collections', function () { +it('can create a data collection from a Lazy Collection', function () { $lazyCollection = new LazyCollection(function () { $items = [ 'Never gonna give you up!', @@ -257,40 +198,6 @@ ); }); -test('a collection can be transformed to JSON', function () { - $collection = (new DataCollection(SimpleData::class, ['A', 'B', 'C'])); - - expect('[{"string":"A"},{"string":"B"},{"string":"C"}]') - ->toEqual($collection->toJson()) - ->toEqual(json_encode($collection)); -}); - -it('will cast data object into the data collection objects', function () { - $dataClass = new class ('') extends Data { - public function __construct(public string $otherString) - { - } - - public static function fromSimpleData(SimpleData $simpleData): static - { - return new self($simpleData->string); - } - }; - - $collection = new DataCollection($dataClass::class, [ - SimpleData::from('A'), - SimpleData::from('B'), - ]); - - expect($collection[0]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('A'); - - expect($collection[1]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('B'); -}); - it('can reset the keys', function () { $collection = new DataCollection(SimpleData::class, [ 1 => SimpleData::from('a'), @@ -305,63 +212,6 @@ public static function fromSimpleData(SimpleData $simpleData): static )->toEqual($collection->values()); }); -it('can use magical creation methods to create a collection', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - expect($collection->toCollection()->all()) - ->toMatchArray([ - SimpleData::from('A'), - SimpleData::from('B'), - ]); -}); - -it('can return a custom data collection when collecting data', function () { - $class = new class ('') extends Data implements DeprecatedDataContract { - use WithDeprecatedCollectionMethod; - - protected static string $_collectionClass = CustomDataCollection::class; - - public function __construct(public string $string) - { - } - }; - - $collection = $class::collection([ - ['string' => 'A'], - ['string' => 'B'], - ]); - - expect($collection)->toBeInstanceOf(CustomDataCollection::class); -}); - -it('can return a custom paginated data collection when collecting data', function () { - $class = new class ('') extends Data implements DeprecatedDataContract { - use WithDeprecatedCollectionMethod; - - protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; - - public function __construct(public string $string) - { - } - }; - - $collection = $class::collection(new LengthAwarePaginator([['string' => 'A'], ['string' => 'B']], 2, 15)); - - expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); -}); - -it( - 'can perform some collection operations', - function (string $operation, array $arguments, array $expected) { - $collection = new DataCollection(SimpleData::class, ['A', 'B', 'C']); - - $changedCollection = $collection->{$operation}(...$arguments); - - expect($changedCollection->toArray()) - ->toEqual($expected); - } -)->with('collection-operations'); - it('can return a sole data object', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); @@ -380,7 +230,7 @@ function (string $operation, array $arguments, array $expected) { ->toEqual($filtered->string); }); -test('a collection can be merged', function () { +it('a collection can be merged', function () { $collectionA = SimpleData::collect(collect(['A', 'B'])); $collectionB = SimpleData::collect(collect(['C', 'D'])); @@ -435,34 +285,3 @@ function (string $operation, array $arguments, array $expected) { expect($collection[1])->toBeInstanceOf(SimpleData::class); }); -it('can magically collect data', function () { - class TestSomeCustomCollection extends Collection - { - } - - $dataClass = new class () extends Data { - public string $string; - - public static function fromString(string $string): self - { - $s = new self(); - - $s->string = $string; - - return $s; - } - - public static function collectArray(array $items): \TestSomeCustomCollection - { - return new \TestSomeCustomCollection($items); - } - }; - - expect($dataClass::collect(['a', 'b', 'c'])) - ->toBeInstanceOf(\TestSomeCustomCollection::class) - ->all()->toEqual([ - $dataClass::from('a'), - $dataClass::from('b'), - $dataClass::from('c'), - ]); -}); diff --git a/tests/DataPipelineTest.php b/tests/DataPipelineTest.php deleted file mode 100644 index c64d1ce82..000000000 --- a/tests/DataPipelineTest.php +++ /dev/null @@ -1,28 +0,0 @@ -through(DefaultValuesDataPipe::class) - ->through(CastPropertiesDataPipe::class) - ->firstThrough(AuthorizedDataPipe::class); - - $reflectionProperty = tap( - new ReflectionProperty(DataPipeline::class, 'pipes'), - static fn (ReflectionProperty $r) => $r->setAccessible(true), - ); - - $pipes = $reflectionProperty->getValue($pipeline); - - expect($pipes) - ->toHaveCount(3) - ->toMatchArray([ - AuthorizedDataPipe::class, - DefaultValuesDataPipe::class, - CastPropertiesDataPipe::class, - ]); -}); diff --git a/tests/DataPipes/CastPropertiesDataPipeTest.php b/tests/DataPipes/CastPropertiesDataPipeTest.php deleted file mode 100644 index 193ddc56c..000000000 --- a/tests/DataPipes/CastPropertiesDataPipeTest.php +++ /dev/null @@ -1,275 +0,0 @@ -pipe = resolve(CastPropertiesDataPipe::class); -}); - -it('maps default types', function () { - $data = ComplicatedData::from([ - 'withoutType' => 42, - 'int' => 42, - 'bool' => true, - 'float' => 3.14, - 'string' => 'Hello world', - 'array' => [1, 1, 2, 3, 5, 8], - 'nullable' => null, - 'mixed' => 42, - 'explicitCast' => '16-06-1994', - 'defaultCast' => '1994-05-16T12:00:00+01:00', - 'nestedData' => [ - 'string' => 'hello', - ], - 'nestedCollection' => [ - ['string' => 'never'], - ['string' => 'gonna'], - ['string' => 'give'], - ['string' => 'you'], - ['string' => 'up'], - ], - 'nestedArray' => [ - ['string' => 'never'], - ['string' => 'gonna'], - ['string' => 'give'], - ['string' => 'you'], - ['string' => 'up'], - ], - ]); - - expect($data)->toBeInstanceOf(ComplicatedData::class) - ->withoutType->toEqual(42) - ->int->toEqual(42) - ->bool->toBeTrue() - ->float->toEqual(3.14) - ->string->toEqual('Hello world') - ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull() - ->undefinable->toBeInstanceOf(Optional::class) - ->mixed->toEqual(42) - ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) - ->explicitCast->toEqual(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994')) - ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ], DataCollection::class)) - ->nestedArray->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ])); -}); - -it("won't cast a property that is already in the correct type", function () { - $data = ComplicatedData::from([ - 'withoutType' => 42, - 'int' => 42, - 'bool' => true, - 'float' => 3.14, - 'string' => 'Hello world', - 'array' => [1, 1, 2, 3, 5, 8], - 'nullable' => null, - 'mixed' => 42, - 'explicitCast' => DateTime::createFromFormat('d-m-Y', '16-06-1994'), - 'defaultCast' => DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00'), - 'nestedData' => SimpleData::from('hello'), - 'nestedCollection' => SimpleData::collect([ - 'never', 'gonna', 'give', 'you', 'up', - ], DataCollection::class), - 'nestedArray' => SimpleData::collect([ - 'never', 'gonna', 'give', 'you', 'up', - ]), - ]); - - expect($data)->toBeInstanceOf(ComplicatedData::class) - ->withoutType->toEqual(42) - ->int->toEqual(42) - ->bool->toBeTrue() - ->float->toEqual(3.14) - ->string->toEqual('Hello world') - ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull() - ->mixed->toBe(42) - ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) - ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) - ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ], DataCollection::class)) - ->nestedArray->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ])); -}); - -it('will allow a nested data object to handle their own types', function () { - $model = new DummyModel(['id' => 10]); - - $withoutModelData = NestedModelData::from([ - 'model' => ['id' => 10], - ]); - - expect($withoutModelData) - ->toBeInstanceOf(NestedModelData::class) - ->model->id->toEqual(10); - - /** @var \Spatie\LaravelData\Tests\Fakes\NestedModelData $data */ - $withModelData = NestedModelData::from([ - 'model' => $model, - ]); - - expect($withModelData) - ->toBeInstanceOf(NestedModelData::class) - ->model->id->toEqual(10); -}); - -it('will allow a nested collection object to handle its own types', function () { - $data = NestedModelCollectionData::from([ - 'models' => [['id' => 10], ['id' => 20],], - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); - - $data = NestedModelCollectionData::from([ - 'models' => [new DummyModel(['id' => 10]), new DummyModel(['id' => 20]),], - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); - - $data = NestedModelCollectionData::from([ - 'models' => ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class), - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); -}); - -it('works nicely with lazy data', function () { - $data = NestedLazyData::from([ - 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), - ]); - - expect($data->simple) - ->toBeInstanceOf(Lazy::class) - ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); -}); - -it('allows casting', function () { - $dataClass = new class () extends Data { - #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')] - public DateTimeImmutable $date; - }; - - $data = $dataClass::from([ - 'date' => '2022-01-18', - ]); - - expect($data->date) - ->toBeInstanceOf(DateTimeImmutable::class) - ->toEqual(DateTimeImmutable::createFromFormat('Y-m-d', '2022-01-18')); -}); - -it('allows casting of enums', function () { - $data = EnumData::from([ - 'enum' => 'foo', - ]); - - expect($data->enum) - ->toBeInstanceOf(DummyBackedEnum::class) - ->toEqual(DummyBackedEnum::FOO); -}); - -it('can manually set values in the constructor', function () { - $dataClass = new class ('', '') extends Data { - public string $member; - - public string $other_member; - - public string $member_with_default = 'default'; - - public string $member_to_set; - - public function __construct( - public string $promoted, - string $non_promoted, - string $non_promoted_with_default = 'default', - public string $promoted_with_with_default = 'default', - ) { - $this->member = "changed_in_constructor: {$non_promoted}"; - $this->other_member = "changed_in_constructor: {$non_promoted_with_default}"; - } - }; - - $data = $dataClass::from([ - 'promoted' => 'A', - 'non_promoted' => 'B', - 'non_promoted_with_default' => 'C', - 'promoted_with_with_default' => 'D', - 'member_to_set' => 'E', - 'member_with_default' => 'F', - ]); - - expect($data->toArray())->toMatchArray([ - 'member' => 'changed_in_constructor: B', - 'other_member' => 'changed_in_constructor: C', - 'member_with_default' => 'F', - 'promoted' => 'A', - 'promoted_with_with_default' => 'D', - 'member_to_set' => 'E', - ]); - - $data = $dataClass::from([ - 'promoted' => 'A', - 'non_promoted' => 'B', - 'member_to_set' => 'E', - ]); - - expect($data->toArray())->toMatchArray([ - 'member' => 'changed_in_constructor: B', - 'other_member' => 'changed_in_constructor: default', - 'member_with_default' => 'default', - 'promoted' => 'A', - 'promoted_with_with_default' => 'default', - 'member_to_set' => 'E', - ]); -}); diff --git a/tests/DataPipes/DefaultValuesDataPipeTest.php b/tests/DataPipes/DefaultValuesDataPipeTest.php deleted file mode 100644 index 2097a1be5..000000000 --- a/tests/DataPipes/DefaultValuesDataPipeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -toEqual($dataClass::from([])); -}); - -it('can create a data object with defined defaults', function () { - $dataClass = new class ('', '', '') extends Data { - public function __construct( - public string $stringWithDefault, - ) { - } - - public static function defaults(): array - { - return [ - 'stringWithDefault' => 'Hi', - ]; - } - }; - - expect(new $dataClass('Hi'))->toEqual($dataClass::from([])); -}); diff --git a/tests/DataTest.php b/tests/DataTest.php index d1794b981..31042b02f 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -19,7 +19,7 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DefaultableData; +use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; @@ -67,1160 +67,7 @@ use function Spatie\Snapshots\assertMatchesSnapshot; -it('can create a resource', function () { - $data = new SimpleData('Ruben'); - - expect($data->toArray())->toMatchArray([ - 'string' => 'Ruben', - ]); -}); - -it('can create a collection of resources', function () { - $collection = SimpleData::collect(collect([ - 'Ruben', - 'Freek', - 'Brent', - ]), DataCollection::class); - - expect($collection->toArray()) - ->toMatchArray([ - ['string' => 'Ruben'], - ['string' => 'Freek'], - ['string' => 'Brent'], - ]); -}); - -it('can get the empty version of a data object', function () { - $dataClass = new class () extends Data { - public string $property; - - public string|Lazy $lazyProperty; - - public array $array; - - public Collection $collection; - - #[DataCollectionOf(SimpleData::class)] - public DataCollection $dataCollection; - - public SimpleData $data; - - public Lazy|SimpleData $lazyData; - - public bool $defaultProperty = true; - }; - - expect($dataClass::empty())->toMatchArray([ - 'property' => null, - 'lazyProperty' => null, - 'array' => [], - 'collection' => [], - 'dataCollection' => [], - 'data' => [ - 'string' => null, - ], - 'lazyData' => [ - 'string' => null, - ], - 'defaultProperty' => true, - ]); -}); - -it('can overwrite properties in an empty version of a data object', function () { - expect(SimpleData::empty())->toMatchArray([ - 'string' => null, - ]); - - expect(SimpleData::empty(['string' => 'Ruben']))->toMatchArray([ - 'string' => 'Ruben', - ]); -}); - -it('will use transformers to convert specific types', function () { - $date = new DateTime('16 may 1994'); - - $data = new class ($date) extends Data { - public function __construct(public DateTime $date) - { - } - }; - - expect($data->toArray())->toMatchArray(['date' => '1994-05-16T00:00:00+00:00']); -}); - -it('can manually specific a transformer', function () { - $date = new DateTime('16 may 1994'); - - $data = new class ($date) extends Data { - public function __construct( - #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] - public $date - ) { - } - }; - - expect($data->toArray())->toMatchArray(['date' => '16-05-1994']); -}); - -test('a transformer will never handle a null value', function () { - $data = new class (null) extends Data { - public function __construct( - #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] - public $date - ) { - } - }; - - expect($data->toArray())->toMatchArray(['date' => null]); -}); - -it('can get the data object without transforming', function () { - $data = new class ( - $dataObject = new SimpleData('Test'), - $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), - Lazy::create(fn () => new SimpleData('Lazy')), - 'Test', - $transformable = new DateTime('16 may 1994') - ) extends Data { - public function __construct( - public SimpleData $data, - #[DataCollectionOf(SimpleData::class)] - public DataCollection $dataCollection, - public Lazy|Data $lazy, - public string $string, - public DateTime $transformable - ) { - } - }; - - expect($data->all())->toMatchArray([ - 'data' => $dataObject, - 'dataCollection' => $dataCollection, - 'string' => 'Test', - 'transformable' => $transformable, - ]); - - expect($data->include('lazy')->all())->toMatchArray([ - 'data' => $dataObject, - 'dataCollection' => $dataCollection, - 'lazy' => (new SimpleData('Lazy')), - 'string' => 'Test', - 'transformable' => $transformable, - ]); -}); - -it('can append data via method overwriting', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return ['alt_name' => "{$this->name} from Spatie"]; - } - }; - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'Freek from Spatie', - ]); -}); - -it('can append data via method overwriting with closures', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return [ - 'alt_name' => static function (self $data) { - return $data->name.' from Spatie via closure'; - }, - ]; - } - }; - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'Freek from Spatie via closure', - ]); -}); - -test('when using additional method and with method additional method gets priority', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return [ - 'alt_name' => static function (self $data) { - return $data->name.' from Spatie via closure'; - }, - ]; - } - }; - - expect($data->additional(['alt_name' => 'I m Freek from additional'])->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'I m Freek from additional', - ]); -}); - -it('can get the data object without mapping properties names', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - - expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) - ->toMatchArray([ - 'camelName' => 'Freek', - ]); -}); - -it('can get the data object without mapping', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - - expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) - ->toMatchArray([ - 'camelName' => 'Freek', - ]); -}); - -it('can get the data object with mapping properties by default', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - expect($data->transform())->toMatchArray([ - 'snake_name' => 'Freek', - ]); -}); - -it('can get the data object with mapping properties names', function () { - $data = new class ('Freek', 'Hello World') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName, - public string $helloCamelName - ) { - } - }; - - expect($data->toArray())->toMatchArray([ - 'snake_name' => 'Freek', - 'helloCamelName' => 'Hello World', - ]); -}); - -it('can append data via method call', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - }; - - $transformed = $data->additional([ - 'company' => 'Spatie', - 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", - ])->toArray(); - - expect($transformed)->toMatchArray([ - 'name' => 'Freek', - 'company' => 'Spatie', - 'alt_name' => 'Freek from Spatie', - ]); -}); - -it('can optionally create data', function () { - expect(SimpleData::optional(null))->toBeNull(); - expect(new SimpleData('Hello world'))->toEqual( - SimpleData::optional(['string' => 'Hello world']) - ); -}); - -it('can create a data model without constructor', function () { - expect(SimpleDataWithoutConstructor::fromString('Hello')) - ->toEqual(SimpleDataWithoutConstructor::from('Hello')); - - expect(SimpleDataWithoutConstructor::fromString('Hello')) - ->toEqual(SimpleDataWithoutConstructor::from([ - 'string' => 'Hello', - ])); - - expect( - new DataCollection(SimpleDataWithoutConstructor::class, [ - SimpleDataWithoutConstructor::fromString('Hello'), - SimpleDataWithoutConstructor::fromString('World'), - ]) - ) - ->toEqual(SimpleDataWithoutConstructor::collect(['Hello', 'World'], DataCollection::class)); -}); - -it('can create a data object from a model', function () { - DummyModel::migrate(); - - $model = DummyModel::create([ - 'string' => 'test', - 'boolean' => true, - 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), - 'nullable_date' => null, - ]); - - $dataClass = new class () extends Data { - public string $string; - - public bool $boolean; - - public Carbon $date; - - public ?Carbon $nullable_date; - }; - - $data = $dataClass::from(DummyModel::findOrFail($model->id)); - - expect($data) - ->string->toEqual('test') - ->boolean->toBeTrue() - ->nullable_date->toBeNull() - ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); -}); - -it('can create a data object from a stdClass object', function () { - $object = (object) [ - 'string' => 'test', - 'boolean' => true, - 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), - 'nullable_date' => null, - ]; - - $dataClass = new class () extends Data { - public string $string; - - public bool $boolean; - - public CarbonImmutable $date; - - public ?Carbon $nullable_date; - }; - - $data = $dataClass::from($object); - - expect($data) - ->string->toEqual('test') - ->boolean->toBeTrue() - ->nullable_date->toBeNull() - ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); -}); - -it('can add the WithData trait to a request', function () { - $formRequest = new class () extends FormRequest { - use WithData; - - public string $dataClass = SimpleData::class; - }; - - $formRequest->replace([ - 'string' => 'Hello World', - ]); - - $data = $formRequest->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('can add the WithData trait to a model', function () { - $model = new class () extends Model { - use WithData; - - protected string $dataClass = SimpleData::class; - }; - - $model->fill([ - 'string' => 'Hello World', - ]); - - $data = $model->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('can define the WithData trait data class by method', function () { - $arrayable = new class () implements Arrayable { - use WithData; - - public function toArray() - { - return [ - 'string' => 'Hello World', - ]; - } - - protected function dataClass(): string - { - return SimpleData::class; - } - }; - - $data = $arrayable->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('has support fro readonly properties', function () { - $dataClass = new class ('') extends Data { - public function __construct( - public readonly string $string - ) { - } - }; - - $data = $dataClass::from(['string' => 'Hello world']); - - expect($data)->toBeInstanceOf($dataClass::class) - ->and($data->string)->toEqual('Hello world'); -}); - -it('has support for intersection types', function () { - $collection = collect(['a', 'b', 'c']); - - $dataClass = new class () extends Data { - public Arrayable & \Countable $intersection; - }; - - $data = $dataClass::from(['intersection' => $collection]); - - expect($data)->toBeInstanceOf($dataClass::class) - ->and($data->intersection)->toEqual($collection); -}); - -it('can transform to JSON', function () { - expect('{"string":"Hello"}') - ->toEqual(SimpleData::from('Hello')->toJson()) - ->toEqual(json_encode(SimpleData::from('Hello'))); -}); - -it( - 'can construct a data object with both constructor promoted and default properties', - function () { - $dataClass = new class ('') extends Data { - public string $property; - - public function __construct( - public string $promoted_property, - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'A', - 'promoted_property' => 'B', - ]); - - expect($data) - ->property->toEqual('A') - ->promoted_property->toEqual('B'); - } -); - -it('can construct a data object with default values', function () { - $dataClass = new class ('', '') extends Data { - public string $property; - - public string $default_property = 'Hello'; - - public function __construct( - public string $promoted_property, - public string $default_promoted_property = 'Hello Again', - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'Test', - 'promoted_property' => 'Test Again', - ]); - - expect($data) - ->property->toEqual('Test') - ->promoted_property->toEqual('Test Again') - ->default_property->toEqual('Hello') - ->default_promoted_property->toEqual('Hello Again'); -}); - -it('can construct a data object with default values and overwrite them', function () { - $dataClass = new class ('', '') extends Data { - public string $property; - - public string $default_property = 'Hello'; - - public function __construct( - public string $promoted_property, - public string $default_promoted_property = 'Hello Again', - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'Test', - 'default_property' => 'Test', - 'promoted_property' => 'Test Again', - 'default_promoted_property' => 'Test Again', - ]); - - expect($data) - ->property->toEqual('Test') - ->promoted_property->toEqual('Test Again') - ->default_property->toEqual('Test') - ->default_promoted_property->toEqual('Test Again'); -}); - -it('can use a custom transformer', function () { - $nestedData = new class (42, 'Hello World') extends Data { - public function __construct( - public int $integer, - public string $string, - ) { - } - }; - - $nestedDataCollection = $nestedData::collect([ - ['integer' => 314, 'string' => 'pi'], - ['integer' => '69', 'string' => 'Laravel after hours'], - ]); - - $dataWithDefaultTransformers = new class ($nestedData, $nestedDataCollection) extends Data { - public function __construct( - public Data $nestedData, - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection, - ) { - } - }; - - $dataWithSpecificTransformers = new class ($nestedData, $nestedDataCollection) extends Data { - public function __construct( - #[WithTransformer(ConfidentialDataTransformer::class)] - public Data $nestedData, - #[ - WithTransformer(ConfidentialDataCollectionTransformer::class), - DataCollectionOf(SimpleData::class) - ] - public array $nestedDataCollection, - ) { - } - }; - - expect($dataWithDefaultTransformers->toArray()) - ->toMatchArray([ - 'nestedData' => ['integer' => 42, 'string' => 'Hello World'], - 'nestedDataCollection' => [ - ['integer' => 314, 'string' => 'pi'], - ['integer' => '69', 'string' => 'Laravel after hours'], - ], - ]); - - expect($dataWithSpecificTransformers->toArray()) - ->toMatchArray([ - 'nestedData' => ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - 'nestedDataCollection' => [ - ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - ], - ]); -}); - -it('can transform built it types with custom transformers', function () { - $data = new class ('Hello World', 'Hello World') extends Data { - public function __construct( - public string $without_transformer, - #[WithTransformer(StringToUpperTransformer::class)] - public string $with_transformer - ) { - } - }; - - expect($data->toArray())->toMatchArray([ - 'without_transformer' => 'Hello World', - 'with_transformer' => 'HELLO WORLD', - ]); -}); - -it('can cast data object and collections using a custom cast', function () { - $dataWithDefaultCastsClass = new class () extends Data { - public SimpleData $nestedData; - - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection; - }; - - $dataWithCustomCastsClass = new class () extends Data { - #[WithCast(ConfidentialDataCast::class)] - public SimpleData $nestedData; - - #[WithCast(ConfidentialDataCollectionCast::class)] - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection; - }; - - $dataWithDefaultCasts = $dataWithDefaultCastsClass::from([ - 'nestedData' => 'a secret', - 'nestedDataCollection' => ['another secret', 'yet another secret'], - ]); - - $dataWithCustomCasts = $dataWithCustomCastsClass::from([ - 'nestedData' => 'a secret', - 'nestedDataCollection' => ['another secret', 'yet another secret'], - ]); - - expect($dataWithDefaultCasts) - ->nestedData->toEqual(SimpleData::from('a secret')) - ->and($dataWithDefaultCasts) - ->nestedDataCollection->toEqual(SimpleData::collect(['another secret', 'yet another secret'])); - - expect($dataWithCustomCasts) - ->nestedData->toEqual(SimpleData::from('CONFIDENTIAL')) - ->and($dataWithCustomCasts) - ->nestedDataCollection->toEqual(SimpleData::collect(['CONFIDENTIAL', 'CONFIDENTIAL'])); -}); - -it('can cast data object with a castable property using anonymous class', function () { - $dataWithCastablePropertyClass = new class (new SimpleCastable('')) extends Data { - public function __construct( - #[WithCastable(SimpleCastable::class)] - public SimpleCastable $castableData, - ) { - } - }; - - $dataWithCastableProperty = $dataWithCastablePropertyClass::from(['castableData' => 'HELLO WORLD']); - - expect($dataWithCastableProperty) - ->castableData->toEqual(new SimpleCastable('HELLO WORLD')); -}); - -it('can cast built-in types with custom casts', function () { - $dataClass = new class ('', '') extends Data { - public function __construct( - public string $without_cast, - #[WithCast(StringToUpperCast::class)] - public string $with_cast - ) { - } - }; - - $data = $dataClass::from([ - 'without_cast' => 'Hello World', - 'with_cast' => 'Hello World', - ]); - - expect($data) - ->without_cast->toEqual('Hello World') - ->with_cast->toEqual('HELLO WORLD'); -}); - -it('continues value assignment after a false boolean', function () { - $dataClass = new class () extends Data { - public bool $false; - - public bool $true; - - public string $string; - - public Carbon $date; - }; - - $data = $dataClass::from([ - 'false' => false, - 'true' => true, - 'string' => 'string', - 'date' => Carbon::create(2020, 05, 16, 12, 00, 00), - ]); - - expect($data) - ->false->toBeFalse() - ->true->toBeTrue() - ->string->toEqual('string') - ->and(Carbon::create(2020, 05, 16, 12, 00, 00)->equalTo($data->date))->toBeTrue(); -}); - -it('can create an partial data object', function () { - $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { - public function __construct( - public string $string, - public string|Optional $undefinable_string, - #[WithCast(StringToUpperCast::class)] - public string|Optional $undefinable_string_with_cast, - ) { - } - }; - - $partialData = $dataClass::from([ - 'string' => 'Hello World', - ]); - - expect($partialData) - ->string->toEqual('Hello World') - ->undefinable_string->toEqual(Optional::create()) - ->undefinable_string_with_cast->toEqual(Optional::create()); - - $fullData = $dataClass::from([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_cast' => 'Hello World', - ]); - - expect($fullData) - ->string->toEqual('Hello World') - ->undefinable_string->toEqual('Hello World') - ->undefinable_string_with_cast->toEqual('HELLO WORLD'); -}); - -it('can transform a partial object', function () { - $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { - public function __construct( - public string $string, - public string|Optional $undefinable_string, - #[WithTransformer(StringToUpperTransformer::class)] - public string|Optional $undefinable_string_with_transformer, - ) { - } - }; - - $partialData = $dataClass::from([ - 'string' => 'Hello World', - ]); - - $fullData = $dataClass::from([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_transformer' => 'Hello World', - ]); - - expect($partialData->toArray())->toMatchArray([ - 'string' => 'Hello World', - ]); - - expect($fullData->toArray())->toMatchArray([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_transformer' => 'HELLO WORLD', - ]); -}); - -it('can map transformed property names', function () { - $data = new SimpleDataWithMappedProperty('hello'); - $dataCollection = SimpleDataWithMappedProperty::collect([ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ]); - - $dataClass = new class ('hello', $data, $data, $dataCollection, $dataCollection) extends Data { - public function __construct( - #[MapOutputName('property')] - public string $string, - public SimpleDataWithMappedProperty $nested, - #[MapOutputName('nested_other')] - public SimpleDataWithMappedProperty $nested_renamed, - #[DataCollectionOf(SimpleDataWithMappedProperty::class)] - public array $nested_collection, - #[ - MapOutputName('nested_other_collection'), - DataCollectionOf(SimpleDataWithMappedProperty::class) - ] - public array $nested_renamed_collection, - ) { - } - }; - - expect($dataClass->toArray())->toMatchArray([ - 'property' => 'hello', - 'nested' => [ - 'description' => 'hello', - ], - 'nested_other' => [ - 'description' => 'hello', - ], - 'nested_collection' => [ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ], - 'nested_other_collection' => [ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ], - ]); -}); - -it('can map transformed properties from a complete class', function () { - $data = DataWithMapper::from([ - 'cased_property' => 'We are the knights who say, ni!', - 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], - 'data_collection_cased_property' => [ - ['string' => 'One that looks nice!'], - ['string' => 'But not too expensive!'], - ], - ]); - - expect($data->toArray())->toMatchArray([ - 'cased_property' => 'We are the knights who say, ni!', - 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], - 'data_collection_cased_property' => [ - ['string' => 'One that looks nice!'], - ['string' => 'But not too expensive!'], - ], - ]); -}); - -it('can use context in casts based upon the properties of the data object', function () { - $dataClass = new class () extends Data { - public SimpleData $nested; - - public string $string; - - #[WithCast(ContextAwareCast::class)] - public string $casted; - }; - - $data = $dataClass::from([ - 'nested' => 'Hello', - 'string' => 'world', - 'casted' => 'json:', - ]); - - expect($data)->casted - ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); -}); - -it('will transform native enums', function () { - $data = EnumData::from([ - 'enum' => DummyBackedEnum::FOO, - ]); - - expect($data->toArray())->toMatchArray([ - 'enum' => 'foo', - ]) - ->and($data->all())->toMatchArray([ - 'enum' => DummyBackedEnum::FOO, - ]); -}); - -it('can magically create a data object', function () { - $dataClass = new class ('', '') extends Data { - public function __construct( - public mixed $propertyA, - public mixed $propertyB, - ) { - } - - public static function fromStringWithDefault(string $a, string $b = 'World') - { - return new self($a, $b); - } - - public static function fromIntsWithDefault(int $a, int $b) - { - return new self($a, $b); - } - - public static function fromSimpleDara(SimpleData $data) - { - return new self($data->string, $data->string); - } - - public static function fromData(Data $data) - { - return new self('data', json_encode($data)); - } - }; - - expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) - ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) - ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); -}); - -it('can wrap data objects by method call', function () { - expect( - SimpleData::from('Hello World') - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true) - )->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class) - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true) - )->toMatchArray([ - 'wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); -}); - -it('can wrap data objects using a global default', function () { - config()->set('data.wrap', 'wrap'); - - expect( - SimpleData::from('Hello World') - ->toResponse(\request())->getData(true) - )->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::from('Hello World') - ->wrap('other-wrap') - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class) - ->toResponse(\request())->getData(true) - ) - ->toMatchArray([ - 'wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); - - expect( - SimpleData::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); - - expect( - (new DataCollection(SimpleData::class, ['Hello', 'World'])) - ->wrap('other-wrap') - ->toResponse(\request()) - ->getData(true) - ) - ->toMatchArray([ - 'other-wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); - - expect( - (new DataCollection(SimpleData::class, ['Hello', 'World'])) - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray([ - ['string' => 'Hello'], - ['string' => 'World'], - ]); -}); - -it('can set a default wrap on a data object', function () { - expect( - SimpleDataWithWrap::from('Hello World') - ->toResponse(\request()) - ->getData(true) - ) - ->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleDataWithWrap::from('Hello World') - ->wrap('other-wrap') - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); - - expect( - SimpleDataWithWrap::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); -}); - -it('wraps additional data', function () { - $dataClass = new class ('Hello World') extends Data { - public function __construct( - public string $string - ) { - } - - public function with(): array - { - return ['with' => 'this']; - } - }; - - $data = $dataClass->additional(['additional' => 'this']) - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true); - - expect($data)->toMatchArray([ - 'wrap' => ['string' => 'Hello World'], - 'additional' => 'this', - 'with' => 'this', - ]); -}); - -it('wraps complex data structures', function () { - $data = new MultiNestedData( - new NestedData(SimpleData::from('Hello')), - [ - new NestedData(SimpleData::from('World')), - ], - ); - - expect( - $data->wrap('wrap')->toResponse(\request())->getData(true) - )->toMatchArray([ - 'wrap' => [ - 'nested' => ['simple' => ['string' => 'Hello']], - 'nestedCollection' => [ - ['simple' => ['string' => 'World']], - ], - ], - ]); -}); - -it('wraps complex data structures with a global', function () { - config()->set('data.wrap', 'wrap'); - - $data = new MultiNestedData( - new NestedData(SimpleData::from('Hello')), - [ - new NestedData(SimpleData::from('World')), - ], - ); - - expect( - $data->wrap('wrap')->toResponse(\request())->getData(true) - )->toMatchArray([ - 'wrap' => [ - 'nested' => ['simple' => ['string' => 'Hello']], - 'nestedCollection' => [ - 'wrap' => [ - ['simple' => ['string' => 'World']], - ], - ], - ], - ]); -}); - -it('only wraps responses', function () { - expect( - SimpleData::from('Hello World')->wrap('wrap') - ) - ->toArray() - ->toMatchArray(['string' => 'Hello World']); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class)->wrap('wrap') - ) - ->toArray() - ->toMatchArray([ - ['string' => 'Hello'], - ['string' => 'World'], - ]); -}); - -it('can use only when transforming', function (array $directive, array $expectedOnly) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->only(...$directive)) - ->toArray() - ->toMatchArray($expectedOnly); -})->with('only-inclusion'); - -it('can use except when transforming', function ( - array $directive, - array $expectedOnly, - array $expectedExcept -) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->except(...$directive)->toArray()) - ->toEqual($expectedExcept); -})->with('only-inclusion'); - -it('can use a trait', function () { +it('also works by using traits and interfaces, skipping the base data class', function () { $data = new class ('') implements DataObject { use ResponsableData; use IncludeableData; @@ -1229,9 +76,8 @@ public function with(): array use WrappableData; use TransformableData; use BaseData; - use \Spatie\LaravelData\Concerns\EmptyData; + use EmptyData; use ContextableData; - use DefaultableData; public function __construct(public string $string) { @@ -1248,112 +94,6 @@ public static function fromString(string $string): static ->and($data::from('Hi'))->toEqual(new $data('Hi')); }); -it('supports conversion from multiple date formats', function () { - $data = new class () extends Data { - public function __construct( - #[WithCast(DateTimeInterfaceCast::class, ['Y-m-d\TH:i:sP', 'Y-m-d H:i:s'])] - public ?DateTime $date = null - ) { - } - }; - - expect($data::from(['date' => '2022-05-16T14:37:56+00:00']))->toArray() - ->toMatchArray(['date' => '2022-05-16T14:37:56+00:00']) - ->and($data::from(['date' => '2022-05-16 17:00:00']))->toArray() - ->toMatchArray(['date' => '2022-05-16T17:00:00+00:00']); -}); - -it( - 'will throw a custom exception when a data constructor cannot be called due to missing component', - function () { - SimpleData::from([]); - } -)->throws(CannotCreateData::class, 'the constructor requires 1 parameters'); - -it('can inherit properties from a base class', function () { - $dataClass = new class ('') extends SimpleData { - public int $int; - }; - - $data = $dataClass::from(['string' => 'Hi', 'int' => 42]); - - expect($data) - ->string->toBe('Hi') - ->int->toBe(42); -}); - -it('can have a circular dependency', function () { - $data = CircData::from([ - 'string' => 'Hello World', - 'ular' => [ - 'string' => 'Hello World', - 'circ' => [ - 'string' => 'Hello World', - ], - ], - ]); - - expect($data)->toEqual( - new CircData('Hello World', new UlarData('Hello World', new CircData('Hello World', null))) - ); - - expect($data->toArray())->toMatchArray([ - 'string' => 'Hello World', - 'ular' => [ - 'string' => 'Hello World', - 'circ' => [ - 'string' => 'Hello World', - 'ular' => null, - ], - ], - ]); -}); - -it('can restructure payload', function () { - $class = new class () extends Data { - public function __construct( - public string|null $name = null, - public string|null $address = null, - ) { - } - - public static function prepareForPipeline(Collection $properties): Collection - { - $properties->put('address', $properties->only(['line_1', 'city', 'state', 'zipcode'])->join(',')); - - return $properties; - } - }; - - $instance = $class::from([ - 'name' => 'Freek', - 'line_1' => '123 Sesame St', - 'city' => 'New York', - 'state' => 'NJ', - 'zipcode' => '10010', - ]); - - expect($instance->toArray())->toMatchArray([ - 'name' => 'Freek', - 'address' => '123 Sesame St,New York,NJ,10010', - ]); -}); - - -it('works with livewire', function () { - $class = new class ('') extends Data { - use WireableData; - - public function __construct( - public string $name, - ) { - } - }; - - $data = $class::fromLivewire(['name' => 'Freek']); - - expect($data)->toEqual(new $class('Freek')); -}); it('can serialize and unserialize a data object', function () { $object = SimpleData::from('Hello world'); @@ -1400,166 +140,6 @@ public function __construct( expect($invaded->_dataContext)->toBeNull(); }); - -it('can set a default value for data object', function () { - $dataObject = new class ('', '') extends Data { - #[Min(10)] - public string|Optional $full_name; - - public function __construct( - public string $first_name, - public string $last_name, - ) { - $this->full_name = "{$this->first_name} {$this->last_name}"; - } - }; - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Versieck'); - - expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect(fn () => $dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'too short'])) - ->toThrow(ValidationException::class); -}); - -it('can have a computed value', function () { - $dataObject = new class ('', '') extends Data { - #[Computed] - public string $full_name; - - public function __construct( - public string $first_name, - public string $last_name, - ) { - $this->full_name = "{$this->first_name} {$this->last_name}"; - } - }; - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect(fn () => $dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) - ->toThrow(CannotSetComputedValue::class); -}); - -it('can have a nullable computed value', function () { - $dataObject = new class ('', '') extends Data { - #[Computed] - public ?string $upper_name; - - public function __construct( - public ?string $name, - ) { - $this->upper_name = $name ? strtoupper($name) : null; - } - }; - - expect($dataObject::from(['name' => 'Ruben'])) - ->name->toBe('Ruben') - ->upper_name->toBe('RUBEN'); - - expect($dataObject::from(['name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); - - expect($dataObject::validateAndCreate(['name' => 'Ruben'])) - ->name->toBe('Ruben') - ->upper_name->toBe('RUBEN'); - - expect($dataObject::validateAndCreate(['name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); - - expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => 'RUBEN'])) - ->toThrow(CannotSetComputedValue::class); - - expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); // Case conflicts with DefaultsPipe, ignoring it for now -}); - -it('can have a hidden value', function () { - $dataObject = new class ('', '') extends Data { - public function __construct( - public string $show, - #[Hidden] - public string $hidden, - ) { - } - }; - - expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])) - ->show->toBe('Yes') - ->hidden->toBe('No'); - - expect($dataObject::validateAndCreate(['show' => 'Yes', 'hidden' => 'No'])) - ->show->toBe('Yes') - ->hidden->toBe('No'); - - expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])->toArray())->toBe(['show' => 'Yes']); -}); - -it('throws a readable exception message when the constructor fails', function ( - array $data, - string $message, -) { - try { - MultiData::from($data); - } catch (CannotCreateData $e) { - expect($e->getMessage())->toBe($message); - - return; - } - - throw new Exception('We should not reach this point'); -})->with(fn () => [ - yield 'no params' => [[], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 0 given. Parameters missing: first, second.'], - yield 'one param' => [['first' => 'First'], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 1 given. Parameters given: first. Parameters missing: second.'], -]); - -it('is possible to add extra global transformers when transforming using context', function () { - $dataClass = new class () extends Data { - public DateTime $dateTime; - }; - - $data = $dataClass::from([ - 'dateTime' => new DateTime(), - ]); - - $customTransformer = new class () implements Transformer { - public function transform(DataProperty $property, mixed $value, TransformationContext $context): string - { - return "Custom transformed date"; - } - }; - - $transformed = $data->transform( - TransformationContextFactory::create()->transformer(DateTimeInterface::class, $customTransformer) - ); - - expect($transformed)->toBe([ - 'dateTime' => 'Custom transformed date', - ]); -}); - it('can use data as an DTO', function () { $dto = SimpleDto::from('Hello World'); diff --git a/tests/Datasets/DataCollection.php b/tests/Datasets/DataCollection.php deleted file mode 100644 index cfda0e26a..000000000 --- a/tests/Datasets/DataCollection.php +++ /dev/null @@ -1,11 +0,0 @@ - 'filter', - 'arguments' => [fn (SimpleData $data) => $data->string !== 'B'], - 'expected' => [0 => ['string' => 'A'], 2 => ['string' => 'C']], - ]; -}); diff --git a/tests/Datasets/DataTest.php b/tests/Datasets/DataTest.php deleted file mode 100644 index b77992b72..000000000 --- a/tests/Datasets/DataTest.php +++ /dev/null @@ -1,197 +0,0 @@ - [ - 'directive' => ['first'], - 'expectedOnly' => [ - 'first' => 'A', - ], - 'expectedExcept' => [ - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi' => [ - 'directive' => ['first', 'second'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi-2' => [ - 'directive' => ['{first,second}'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'all' => [ - 'directive' => ['*'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [], - ]; - - yield 'nested' => [ - 'directive' => ['nested'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.single' => [ - 'directive' => ['nested.first'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.multi' => [ - 'directive' => ['nested.{first, second}'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested-all' => [ - 'directive' => ['nested.*'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'collection' => [ - 'directive' => ['collection'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - ]; - - yield 'collection-single' => [ - 'directive' => ['collection.first'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E'], - ['first' => 'G'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['second' => 'F'], - ['second' => 'H'], - ], - ], - ]; - - yield 'collection-multi' => [ - 'directive' => ['collection.first', 'collection.second'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; - - yield 'collection-all' => [ - 'directive' => ['collection.*'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; -}); diff --git a/tests/EmptyTest.php b/tests/EmptyTest.php new file mode 100644 index 000000000..7f9e4d836 --- /dev/null +++ b/tests/EmptyTest.php @@ -0,0 +1,54 @@ +toMatchArray([ + 'property' => null, + 'lazyProperty' => null, + 'array' => [], + 'collection' => [], + 'dataCollection' => [], + 'data' => [ + 'string' => null, + ], + 'lazyData' => [ + 'string' => null, + ], + 'defaultProperty' => true, + ]); +}); + +it('can overwrite properties in an empty version of a data object', function () { + expect(SimpleData::empty())->toMatchArray([ + 'string' => null, + ]); + + expect(SimpleData::empty(['string' => 'Ruben']))->toMatchArray([ + 'string' => 'Ruben', + ]); +}); diff --git a/tests/DataPipes/FillRouteParameterPropertiesPipeTest.php b/tests/FillRouteParametersTest.php similarity index 100% rename from tests/DataPipes/FillRouteParameterPropertiesPipeTest.php rename to tests/FillRouteParametersTest.php diff --git a/tests/LivewireTest.php b/tests/LivewireTest.php new file mode 100644 index 000000000..72709d562 --- /dev/null +++ b/tests/LivewireTest.php @@ -0,0 +1,19 @@ + 'Freek']); + + expect($data)->toEqual(new $class('Freek')); +}); diff --git a/tests/Resolvers/DataFromSomethingResolverTest.php b/tests/MagicalCreationTest.php similarity index 99% rename from tests/Resolvers/DataFromSomethingResolverTest.php rename to tests/MagicalCreationTest.php index de05a816e..9f8b2a9bf 100644 --- a/tests/Resolvers/DataFromSomethingResolverTest.php +++ b/tests/MagicalCreationTest.php @@ -1,5 +1,6 @@ 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ]); + + $dataClass = new class ('hello', $data, $data, $dataCollection, $dataCollection) extends Data { + public function __construct( + #[MapOutputName('property')] + public string $string, + public SimpleDataWithMappedProperty $nested, + #[MapOutputName('nested_other')] + public SimpleDataWithMappedProperty $nested_renamed, + #[DataCollectionOf(SimpleDataWithMappedProperty::class)] + public array $nested_collection, + #[ + MapOutputName('nested_other_collection'), + DataCollectionOf(SimpleDataWithMappedProperty::class) + ] + public array $nested_renamed_collection, + ) { + } + }; + + expect($dataClass->toArray())->toMatchArray([ + 'property' => 'hello', + 'nested' => [ + 'description' => 'hello', + ], + 'nested_other' => [ + 'description' => 'hello', + ], + 'nested_collection' => [ + ['description' => 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ], + 'nested_other_collection' => [ + ['description' => 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ], + ]); +}); + +it('can map the property names for the whole class using one attribute when transforming', function () { + $data = DataWithMapper::from([ + 'cased_property' => 'We are the knights who say, ni!', + 'data_cased_property' => + ['string' => 'Bring us a, shrubbery!'], + 'data_collection_cased_property' => [ + ['string' => 'One that looks nice!'], + ['string' => 'But not too expensive!'], + ], + ]); + + expect($data->toArray())->toMatchArray([ + 'cased_property' => 'We are the knights who say, ni!', + 'data_cased_property' => + ['string' => 'Bring us a, shrubbery!'], + 'data_collection_cased_property' => [ + ['string' => 'One that looks nice!'], + ['string' => 'But not too expensive!'], + ], + ]); +}); + +it('can transform the data object without mapping', function () { + $data = new class ('Freek') extends Data { + public function __construct( + #[MapOutputName('snake_name')] + public string $camelName + ) { + } + }; + + expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) + ->toMatchArray([ + 'camelName' => 'Freek', + ]); +}); + +it('can map an input property using string when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -21,7 +113,7 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('can map in nested objects using strings', function () { +it('can map an input property in nested objects using strings when creating', function () { $dataClass = new class () extends Data { #[MapInputName('nested.something')] public string $mapped; @@ -34,7 +126,7 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('replaces properties when a mapped alternative exists', function () { +it('replaces properties when a mapped alternative exists when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -48,7 +140,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('skips properties it cannot find ', function () { +it('skips properties it cannot find when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -61,7 +153,8 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('can use integers to map properties', function () { + +it('can use integers to map properties when creating', function () { $dataClass = new class () extends Data { #[MapInputName(1)] public string $mapped; @@ -75,7 +168,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can use integers to map properties in nested data', function () { +it('can use integers to map properties in nested data when creating', function () { $dataClass = new class () extends Data { #[MapInputName('1.0')] public string $mapped; @@ -89,7 +182,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can combine integers and strings to map properties', function () { +it('can combine integers and strings to map properties when creating', function () { $dataClass = new class () extends Data { #[MapInputName('lines.1')] public string $mapped; @@ -105,7 +198,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can use a dedicated mapper', function () { +it('can use a special mapping class which converts property names between standards', function () { $dataClass = new class () extends Data { #[MapInputName(SnakeCaseMapper::class)] public string $mappedLine; @@ -118,7 +211,7 @@ expect($data->mappedLine)->toEqual('We are the knights who say, ni!'); }); -it('can map properties into data objects', function () { +it('can use mapped properties to magically create data', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public SimpleData $mapped; @@ -135,7 +228,7 @@ ); }); -it('can map properties into data objects which map properties again', function () { +it('can use mapped properties (nested) to magically create data', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public SimpleDataWithMappedProperty $mapped; @@ -154,7 +247,7 @@ ); }); -it('can map properties into data collections', function () { +it('can map properties when creating a collection of data objects', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleData::class)] public array $mapped; @@ -177,7 +270,7 @@ ); }); -it('can map properties into data collections which map properties again', function () { +it('can map properties when creating a (nested) collection of data objects', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleDataWithMappedProperty::class)] public array $mapped; @@ -200,11 +293,11 @@ ); }); -it('can map properties from a complete class', function () { +it('can use one attribute on the class to map properties when creating', function () { $data = DataWithMapper::from([ 'cased_property' => 'We are the knights who say, ni!', 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], + ['string' => 'Bring us a, shrubbery!'], 'data_collection_cased_property' => [ ['string' => 'One that looks nice!'], ['string' => 'But not too expensive!'], diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index f4423b461..9cc3f93df 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; +use Spatie\LaravelData\Tests\Fakes\CircData; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\ExceptData; @@ -23,7 +24,10 @@ use Spatie\LaravelData\Tests\Fakes\NestedLazyData; use Spatie\LaravelData\Tests\Fakes\OnlyData; use Spatie\LaravelData\Tests\Fakes\PartialClassConditionalData; +use Spatie\LaravelData\Tests\Fakes\SimpleChildDataWithMappedOutputName; use Spatie\LaravelData\Tests\Fakes\SimpleData; +use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedOutputName; +use Spatie\LaravelData\Tests\Fakes\UlarData; it('can include a lazy property', function () { $data = new LazyData(Lazy::create(fn () => 'test')); @@ -1439,3 +1443,502 @@ public function __construct( ], ]); }); + +it('can use only when transforming', function (array $directive, array $expectedOnly) { + $dataClass = new class () extends Data { + public string $first; + + public string $second; + + public MultiData $nested; + + #[DataCollectionOf(MultiData::class)] + public DataCollection $collection; + }; + + $data = $dataClass::from([ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ]); + + expect($data->only(...$directive)) + ->toArray() + ->toMatchArray($expectedOnly); +})->with('only-inclusion'); + +it('can use except when transforming', function ( + array $directive, + array $expectedOnly, + array $expectedExcept +) { + $dataClass = new class () extends Data { + public string $first; + + public string $second; + + public MultiData $nested; + + #[DataCollectionOf(MultiData::class)] + public DataCollection $collection; + }; + + $data = $dataClass::from([ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ]); + + expect($data->except(...$directive)->toArray()) + ->toEqual($expectedExcept); +})->with('only-inclusion'); + +// Todo: replace +//it('will correctly reduce a tree based upon allowed includes', function ( +// ?array $lazyDataAllowedIncludes, +// ?array $dataAllowedIncludes, +// ?string $requestedAllowedIncludes, +// TreeNode $expectedIncludes +//) { +// LazyData::setAllowedIncludes($lazyDataAllowedIncludes); +// +// $data = new class ( +// 'Hello', +// LazyData::from('Hello'), +// LazyData::collect(['Hello', 'World']) +// ) extends Data { +// public static ?array $allowedIncludes; +// +// public function __construct( +// public string $property, +// public LazyData $nested, +// #[DataCollectionOf(LazyData::class)] +// public array $collection, +// ) { +// } +// +// public static function allowedRequestIncludes(): ?array +// { +// return static::$allowedIncludes; +// } +// }; +// +// $data::$allowedIncludes = $dataAllowedIncludes; +// +// $request = request(); +// +// if ($requestedAllowedIncludes !== null) { +// $request->merge([ +// 'include' => $requestedAllowedIncludes, +// ]); +// } +// +// $trees = $this->resolver->execute($data, $request); +// +// expect($trees->lazyIncluded)->toEqual($expectedIncludes); +//})->with(function () { +// yield 'disallowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => [], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new ExcludedTreeNode(), +// ]; +// +// yield 'allowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['property'], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'disallowed all nested data property inclusion ' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'multi property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => 'nested.*,property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'without property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => null, +// 'expectedIncludes' => new DisabledTreeNode(), +// ]; +//}); + +it('can combine request and manual includes', function () { + $dataclass = new class ( + Lazy::create(fn () => 'Rick Astley'), + Lazy::create(fn () => 'Never gonna give you up'), + Lazy::create(fn () => 1986), + ) extends MultiLazyData { + public static function allowedRequestIncludes(): ?array + { + return null; + } + }; + + $data = $dataclass->include('name')->toResponse(request()->merge([ + 'include' => 'artist', + ]))->getData(true); + + expect($data)->toMatchArray([ + 'artist' => 'Rick Astley', + 'name' => 'Never gonna give you up', + ]); +}); + +it('handles parsing includes from request', function (array $input, array $expected) { + $dataclass = new class ( + Lazy::create(fn () => 'Rick Astley'), + Lazy::create(fn () => 'Never gonna give you up'), + Lazy::create(fn () => 1986), + ) extends MultiLazyData { + public static function allowedRequestIncludes(): ?array + { + return ['*']; + } + }; + + $request = request()->merge($input); + + $data = $dataclass->toResponse($request)->getData(assoc: true); + + expect($data)->toHaveKeys($expected); +})->with(function () { + yield 'input as array' => [ + 'input' => ['include' => ['artist', 'name']], + 'expected' => ['artist', 'name'], + ]; + + yield 'input as comma separated' => [ + 'input' => ['include' => 'artist,name'], + 'expected' => ['artist', 'name'], + ]; +}); + +it('handles parsing except from request with mapped output name', function () { + $dataclass = SimpleDataWithMappedOutputName::from([ + 'id' => 1, + 'amount' => 1000, + 'any_string' => 'test', + 'child' => SimpleChildDataWithMappedOutputName::from([ + 'id' => 2, + 'amount' => 2000, + ]), + ]); + + $request = request()->merge(['except' => ['paid_amount', 'any_string', 'child.child_amount']]); + + $data = $dataclass->toResponse($request)->getData(assoc: true); + + expect($data)->toMatchArray([ + 'id' => 1, + 'child' => [ + 'id' => 2, + ], + ]); +}); + +it('handles circular dependencies', function () { + $dataClass = new CircData( + 'test', + new UlarData( + 'test', + new CircData('test', null) + ) + ); + + $data = $dataClass->toResponse(request())->getData(assoc: true); + + expect($data)->toBe([ + 'string' => 'test', + 'ular' => [ + 'string' => 'test', + 'circ' => [ + 'string' => 'test', + 'ular' => null, + ], + ], + ]); + + // Not really a test with expectation, we just want to check we don't end up in an infinite loop +}); + +dataset('only-inclusion', function () { + yield 'single' => [ + 'directive' => ['first'], + 'expectedOnly' => [ + 'first' => 'A', + ], + 'expectedExcept' => [ + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'multi' => [ + 'directive' => ['first', 'second'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + ], + 'expectedExcept' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'multi-2' => [ + 'directive' => ['{first,second}'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + ], + 'expectedExcept' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'all' => [ + 'directive' => ['*'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [], + ]; + + yield 'nested' => [ + 'directive' => ['nested'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested.single' => [ + 'directive' => ['nested.first'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested.multi' => [ + 'directive' => ['nested.{first, second}'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => [], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested-all' => [ + 'directive' => ['nested.*'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => [], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'collection' => [ + 'directive' => ['collection'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + ]; + + yield 'collection-single' => [ + 'directive' => ['collection.first'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E'], + ['first' => 'G'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['second' => 'F'], + ['second' => 'H'], + ], + ], + ]; + + yield 'collection-multi' => [ + 'directive' => ['collection.first', 'collection.second'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'collection-all' => [ + 'directive' => ['collection.*'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + [], + [], + ], + ], + ]; +}); diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php new file mode 100644 index 000000000..79ba038a9 --- /dev/null +++ b/tests/PipelineTest.php @@ -0,0 +1,60 @@ +through(DefaultValuesDataPipe::class) + ->through(CastPropertiesDataPipe::class) + ->firstThrough(AuthorizedDataPipe::class); + + $reflectionProperty = tap( + new ReflectionProperty(DataPipeline::class, 'pipes'), + static fn (ReflectionProperty $r) => $r->setAccessible(true), + ); + + $pipes = $reflectionProperty->getValue($pipeline); + + expect($pipes) + ->toHaveCount(3) + ->toMatchArray([ + AuthorizedDataPipe::class, + DefaultValuesDataPipe::class, + CastPropertiesDataPipe::class, + ]); +}); + +it('can restructure payload before entering the pipeline', function () { + $class = new class () extends Data { + public function __construct( + public string|null $name = null, + public string|null $address = null, + ) { + } + + public static function prepareForPipeline(Collection $properties): Collection + { + $properties->put('address', $properties->only(['line_1', 'city', 'state', 'zipcode'])->join(',')); + + return $properties; + } + }; + + $instance = $class::from([ + 'name' => 'Freek', + 'line_1' => '123 Sesame St', + 'city' => 'New York', + 'state' => 'NJ', + 'zipcode' => '10010', + ]); + + expect($instance->toArray())->toMatchArray([ + 'name' => 'Freek', + 'address' => '123 Sesame St,New York,NJ,10010', + ]); +}); diff --git a/tests/RequestDataTest.php b/tests/RequestTest.php similarity index 82% rename from tests/RequestDataTest.php rename to tests/RequestTest.php index b58dd1ac7..c9f66aee4 100644 --- a/tests/RequestDataTest.php +++ b/tests/RequestTest.php @@ -2,11 +2,13 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; +use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; +use Spatie\LaravelData\WithData; use function Pest\Laravel\handleExceptions; use function Pest\Laravel\postJson; @@ -138,31 +140,3 @@ public static function fromRequest(Request $request) ->assertJson(['name' => 'Rick Astley']); } ); - -it('can wrap data', function () { - Route::post('/example-route', function () { - return SimpleData::from(request()->input('string'))->wrap('data'); - }); - - performRequest('Hello World') - ->assertCreated() - ->assertJson(['data' => ['string' => 'Hello World']]); -}); - -it('can wrap data collections', function () { - Route::post('/example-route', function () { - return SimpleData::collect([ - request()->input('string'), - strtoupper(request()->input('string')), - ], DataCollection::class)->wrap('data'); - }); - - performRequest('Hello World') - ->assertCreated() - ->assertJson([ - 'data' => [ - ['string' => 'Hello World'], - ['string' => 'HELLO WORLD'], - ], - ]); -}); diff --git a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php deleted file mode 100644 index ac0ef7650..000000000 --- a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php +++ /dev/null @@ -1,262 +0,0 @@ -merge([ -// 'include' => $requestedAllowedIncludes, -// ]); -// } -// -// $trees = $this->resolver->execute($data, $request); -// -// expect($trees->lazyIncluded)->toEqual($expectedIncludes); -//})->with(function () { -// yield 'disallowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => [], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new ExcludedTreeNode(), -// ]; -// -// yield 'allowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['property'], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'disallowed all nested data property inclusion ' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'multi property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => 'nested.*,property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'without property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => null, -// 'expectedIncludes' => new DisabledTreeNode(), -// ]; -//}); - -it('can combine request and manual includes', function () { - $dataclass = new class ( - Lazy::create(fn () => 'Rick Astley'), - Lazy::create(fn () => 'Never gonna give you up'), - Lazy::create(fn () => 1986), - ) extends MultiLazyData { - public static function allowedRequestIncludes(): ?array - { - return null; - } - }; - - $data = $dataclass->include('name')->toResponse(request()->merge([ - 'include' => 'artist', - ]))->getData(true); - - expect($data)->toMatchArray([ - 'artist' => 'Rick Astley', - 'name' => 'Never gonna give you up', - ]); -}); - -it('handles parsing includes from request', function (array $input, array $expected) { - $dataclass = new class ( - Lazy::create(fn () => 'Rick Astley'), - Lazy::create(fn () => 'Never gonna give you up'), - Lazy::create(fn () => 1986), - ) extends MultiLazyData { - public static function allowedRequestIncludes(): ?array - { - return ['*']; - } - }; - - $request = request()->merge($input); - - $data = $dataclass->toResponse($request)->getData(assoc: true); - - expect($data)->toHaveKeys($expected); -})->with(function () { - yield 'input as array' => [ - 'input' => ['include' => ['artist', 'name']], - 'expected' => ['artist', 'name'], - ]; - - yield 'input as comma separated' => [ - 'input' => ['include' => 'artist,name'], - 'expected' => ['artist', 'name'], - ]; -}); - -it('handles parsing except from request with mapped output name', function () { - $dataclass = SimpleDataWithMappedOutputName::from([ - 'id' => 1, - 'amount' => 1000, - 'any_string' => 'test', - 'child' => SimpleChildDataWithMappedOutputName::from([ - 'id' => 2, - 'amount' => 2000, - ]), - ]); - - $request = request()->merge(['except' => ['paid_amount', 'any_string', 'child.child_amount']]); - - $data = $dataclass->toResponse($request)->getData(assoc: true); - - expect($data)->toMatchArray([ - 'id' => 1, - 'child' => [ - 'id' => 2, - ], - ]); -}); - -it('handles circular dependencies', function () { - $dataClass = new CircData( - 'test', - new UlarData( - 'test', - new CircData('test', null) - ) - ); - - $data = $dataClass->toResponse(request())->getData(assoc: true); - - expect($data)->toBe([ - 'string' => 'test', - 'ular' => [ - 'string' => 'test', - 'circ' => [ - 'string' => 'test', - 'ular' => null, - ], - ], - ]); - - // Not really a test with expectation, we just want to check we don't end up in an infinite loop -}); diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index cba407883..1ff83d1a9 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -29,23 +29,73 @@ function findVisibleFields( public string $hidden = 'hidden'; }; - expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ 'visible' => null, ]); }); -it('will hide optional fields which are unitialized', function () { +it('will hide fields which are uninitialized', function () { $dataClass = new class () extends Data { public string $visible = 'visible'; public Optional|string $optional; }; - expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ 'visible' => null, ]); }); +it('can execute excepts', function ( + TransformationContextFactory $factory, + array $expectedVisibleFields, + array $expectedTransformed +) { + $dataClass = new class() extends Data { + /** + * @param array $collection + */ + public function __construct( + public string $string = 'string', + public SimpleData $simple = new SimpleData('simple'), + public NestedData $nested = new NestedData(new SimpleData('simple')), + public array $collection = [ + new SimpleData('simple'), + new SimpleData('simple'), + ], + ) { + } + }; + + expect(findVisibleFields($dataClass, $factory))->toEqual($expectedVisibleFields); + + expect($dataClass->transform($factory))->toEqual($expectedTransformed); +})->with(function () { + yield 'single field' => [ + 'factory' => TransformationContextFactory::create() + ->except('simple'), + 'fields' => [ + 'string' => null, + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'string', + 'nested' => [ + 'simple' => ['string' => 'simple'], + ], + 'collection' => [ + [ + 'simple' => 'simple', + ], + [ + 'simple' => 'simple', + ], + ], + ], + ]; +}); + // TODO write tests it('can perform an excepts', function () { diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index e915ed777..0d22cab0e 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -13,7 +13,6 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\ContextableData; use Spatie\LaravelData\Contracts\DataObject; -use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\ResponsableData; @@ -594,7 +593,6 @@ function (object $class, array $expected) { AppendableData::class, ContextableData::class, BaseData::class, - DefaultableData::class, IncludeableData::class, ResponsableData::class, TransformableData::class, diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php new file mode 100644 index 000000000..69b8cbc07 --- /dev/null +++ b/tests/TransformationTest.php @@ -0,0 +1,371 @@ +toArray())->toMatchArray([ + 'string' => 'Ruben', + ]); +}); + +it('can transform a collection of data objects', function () { + $collection = SimpleData::collect(collect([ + 'Ruben', + 'Freek', + 'Brent', + ]), DataCollection::class); + + expect($collection->toArray()) + ->toMatchArray([ + ['string' => 'Ruben'], + ['string' => 'Freek'], + ['string' => 'Brent'], + ]); +}); + +it('will use global transformers to convert specific types', function () { + $date = new DateTime('16 may 1994'); + + $data = new class ($date) extends Data { + public function __construct(public DateTime $date) + { + } + }; + + expect($data->toArray())->toMatchArray(['date' => '1994-05-16T00:00:00+00:00']); +}); + +it('can use a manually specified transformer', function () { + $date = new DateTime('16 may 1994'); + + $data = new class ($date) extends Data { + public function __construct( + #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] + public $date + ) { + } + }; + + expect($data->toArray())->toMatchArray(['date' => '16-05-1994']); +}); + +test('a transformer will never handle a null value', function () { + $data = new class (null) extends Data { + public function __construct( + #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] + public $date + ) { + } + }; + + expect($data->toArray())->toMatchArray(['date' => null]); +}); + +it('can get the data object without transforming', function () { + $data = new class ( + $dataObject = new SimpleData('Test'), + $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), + Lazy::create(fn () => new SimpleData('Lazy')), + 'Test', + $transformable = new DateTime('16 may 1994') + ) extends Data { + public function __construct( + public SimpleData $data, + #[DataCollectionOf(SimpleData::class)] + public DataCollection $dataCollection, + public Lazy|Data $lazy, + public string $string, + public DateTime $transformable + ) { + } + }; + + expect($data->all())->toMatchArray([ + 'data' => $dataObject, + 'dataCollection' => $dataCollection, + 'string' => 'Test', + 'transformable' => $transformable, + ]); + + expect($data->include('lazy')->all())->toMatchArray([ + 'data' => $dataObject, + 'dataCollection' => $dataCollection, + 'lazy' => (new SimpleData('Lazy')), + 'string' => 'Test', + 'transformable' => $transformable, + ]); +}); + +it('can transform to JSON', function () { + expect('{"string":"Hello"}') + ->toEqual(SimpleData::from('Hello')->toJson()) + ->toEqual(json_encode(SimpleData::from('Hello'))); +}); + +it('can use a custom transformer for a data object and/or data collectable', function () { + $nestedData = new class (42, 'Hello World') extends Data { + public function __construct( + public int $integer, + public string $string, + ) { + } + }; + + $nestedDataCollection = $nestedData::collect([ + ['integer' => 314, 'string' => 'pi'], + ['integer' => '69', 'string' => 'Laravel after hours'], + ]); + + $dataWithDefaultTransformers = new class ($nestedData, $nestedDataCollection) extends Data { + public function __construct( + public Data $nestedData, + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection, + ) { + } + }; + + $dataWithSpecificTransformers = new class ($nestedData, $nestedDataCollection) extends Data { + public function __construct( + #[WithTransformer(ConfidentialDataTransformer::class)] + public Data $nestedData, + #[ + WithTransformer(ConfidentialDataCollectionTransformer::class), + DataCollectionOf(SimpleData::class) + ] + public array $nestedDataCollection, + ) { + } + }; + + expect($dataWithDefaultTransformers->toArray()) + ->toMatchArray([ + 'nestedData' => ['integer' => 42, 'string' => 'Hello World'], + 'nestedDataCollection' => [ + ['integer' => 314, 'string' => 'pi'], + ['integer' => '69', 'string' => 'Laravel after hours'], + ], + ]); + + expect($dataWithSpecificTransformers->toArray()) + ->toMatchArray([ + 'nestedData' => ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + 'nestedDataCollection' => [ + ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + ], + ]); +}); + +it('can transform built it types with custom transformers', function () { + $data = new class ('Hello World', 'Hello World') extends Data { + public function __construct( + public string $without_transformer, + #[WithTransformer(StringToUpperTransformer::class)] + public string $with_transformer + ) { + } + }; + + expect($data->toArray())->toMatchArray([ + 'without_transformer' => 'Hello World', + 'with_transformer' => 'HELLO WORLD', + ]); +}); + +it('will not transform optional values', function () { + $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { + public function __construct( + public string $string, + public string|Optional $undefinable_string, + #[WithTransformer(StringToUpperTransformer::class)] + public string|Optional $undefinable_string_with_transformer, + ) { + } + }; + + $partialData = $dataClass::from([ + 'string' => 'Hello World', + ]); + + $fullData = $dataClass::from([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_transformer' => 'Hello World', + ]); + + expect($partialData->toArray())->toMatchArray([ + 'string' => 'Hello World', + ]); + + expect($fullData->toArray())->toMatchArray([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_transformer' => 'HELLO WORLD', + ]); +}); + +it('will transform native enums', function () { + $data = EnumData::from([ + 'enum' => DummyBackedEnum::FOO, + ]); + + expect($data->toArray())->toMatchArray([ + 'enum' => 'foo', + ]) + ->and($data->all())->toMatchArray([ + 'enum' => DummyBackedEnum::FOO, + ]); +}); + +it('can have a circular dependency which will not go into an infinite loop', function () { + $data = CircData::from([ + 'string' => 'Hello World', + 'ular' => [ + 'string' => 'Hello World', + 'circ' => [ + 'string' => 'Hello World', + ], + ], + ]); + + expect($data)->toEqual( + new CircData('Hello World', new UlarData('Hello World', new CircData('Hello World', null))) + ); + + expect($data->toArray())->toMatchArray([ + 'string' => 'Hello World', + 'ular' => [ + 'string' => 'Hello World', + 'circ' => [ + 'string' => 'Hello World', + 'ular' => null, + ], + ], + ]); +}); + +it('can have a hidden value', function () { + $dataObject = new class ('', '') extends Data { + public function __construct( + public string $show, + #[Hidden] + public string $hidden, + ) { + } + }; + + expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])) + ->show->toBe('Yes') + ->hidden->toBe('No'); + + expect($dataObject::validateAndCreate(['show' => 'Yes', 'hidden' => 'No'])) + ->show->toBe('Yes') + ->hidden->toBe('No'); + + expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])->toArray())->toBe(['show' => 'Yes']); +}); + +it('is possible to add extra global transformers when transforming using context', function () { + $dataClass = new class () extends Data { + public DateTime $dateTime; + }; + + $data = $dataClass::from([ + 'dateTime' => new DateTime(), + ]); + + $customTransformer = new class () implements Transformer { + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string + { + return "Custom transformed date"; + } + }; + + $transformed = $data->transform( + TransformationContextFactory::create()->transformer(DateTimeInterface::class, $customTransformer) + ); + + expect($transformed)->toBe([ + 'dateTime' => 'Custom transformed date', + ]); +}); + +it('can transform a paginated data collection', function () { + $items = Collection::times(100, fn (int $index) => "Item {$index}"); + + $paginator = new LengthAwarePaginator( + $items->forPage(1, 15), + 100, + 15 + ); + + $collection = new PaginatedDataCollection(SimpleData::class, $paginator); + + expect($collection)->toBeInstanceOf(PaginatedDataCollection::class); + assertMatchesJsonSnapshot($collection->toJson()); +}); + +it('can transform a paginated cursor data collection', function () { + $items = Collection::times(100, fn (int $index) => "Item {$index}"); + + $paginator = new CursorPaginator( + $items, + 15, + ); + + $collection = new CursorPaginatedDataCollection(SimpleData::class, $paginator); + + if (version_compare(app()->version(), '9.0.0', '<=')) { + $this->markTestIncomplete('Laravel 8 uses a different format'); + } + + expect($collection)->toBeInstanceOf(CursorPaginatedDataCollection::class); + assertMatchesJsonSnapshot($collection->toJson()); +}); + +it('can transform a data collection', function () { + $collection = new DataCollection(SimpleData::class, ['A', 'B']); + + $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + + expect($filtered)->toMatchArray([ + ['string' => 'Ax'], + ['string' => 'Bx'], + ]); +}); + +it('can transform a data collection into JSON', function () { + $collection = (new DataCollection(SimpleData::class, ['A', 'B', 'C'])); + + expect('[{"string":"A"},{"string":"B"},{"string":"C"}]') + ->toEqual($collection->toJson()) + ->toEqual(json_encode($collection)); +}); diff --git a/tests/WithDataTest.php b/tests/WithDataTest.php new file mode 100644 index 000000000..613d5d042 --- /dev/null +++ b/tests/WithDataTest.php @@ -0,0 +1,61 @@ +fill([ + 'string' => 'Hello World', + ]); + + $data = $model->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); + +it('can define the WithData trait data class by method', function () { + $arrayable = new class () implements Arrayable { + use WithData; + + public function toArray() + { + return [ + 'string' => 'Hello World', + ]; + } + + protected function dataClass(): string + { + return SimpleData::class; + } + }; + + $data = $arrayable->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); + +it('can add the WithData trait to a request', function () { + $formRequest = new class () extends FormRequest { + use WithData; + + public string $dataClass = SimpleData::class; + }; + + $formRequest->replace([ + 'string' => 'Hello World', + ]); + + $data = $formRequest->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); diff --git a/tests/WrapTest.php b/tests/WrapTest.php new file mode 100644 index 000000000..50c395270 --- /dev/null +++ b/tests/WrapTest.php @@ -0,0 +1,233 @@ +wrap('wrap') + ->toResponse(\request()) + ->getData(true) + )->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class) + ->wrap('wrap') + ->toResponse(\request()) + ->getData(true) + )->toMatchArray([ + 'wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); +}); + + + +it('can wrap data objects using a global default', function () { + config()->set('data.wrap', 'wrap'); + + expect( + SimpleData::from('Hello World') + ->toResponse(\request())->getData(true) + )->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::from('Hello World') + ->wrap('other-wrap') + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class) + ->toResponse(\request())->getData(true) + ) + ->toMatchArray([ + 'wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); + + expect( + SimpleData::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); + + expect( + (new DataCollection(SimpleData::class, ['Hello', 'World'])) + ->wrap('other-wrap') + ->toResponse(\request()) + ->getData(true) + ) + ->toMatchArray([ + 'other-wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); + + expect( + (new DataCollection(SimpleData::class, ['Hello', 'World'])) + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray([ + ['string' => 'Hello'], + ['string' => 'World'], + ]); +}); + +it('can set a default wrap on a data object', function () { + expect( + SimpleDataWithWrap::from('Hello World') + ->toResponse(\request()) + ->getData(true) + ) + ->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleDataWithWrap::from('Hello World') + ->wrap('other-wrap') + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); + + expect( + SimpleDataWithWrap::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); +}); + +it('wraps additional data', function () { + $dataClass = new class ('Hello World') extends Data { + public function __construct( + public string $string + ) { + } + + public function with(): array + { + return ['with' => 'this']; + } + }; + + $data = $dataClass->additional(['additional' => 'this']) + ->wrap('wrap') + ->toResponse(\request()) + ->getData(true); + + expect($data)->toMatchArray([ + 'wrap' => ['string' => 'Hello World'], + 'additional' => 'this', + 'with' => 'this', + ]); +}); + +it('wraps complex data structures', function () { + $data = new MultiNestedData( + new NestedData(SimpleData::from('Hello')), + [ + new NestedData(SimpleData::from('World')), + ], + ); + + expect( + $data->wrap('wrap')->toResponse(\request())->getData(true) + )->toMatchArray([ + 'wrap' => [ + 'nested' => ['simple' => ['string' => 'Hello']], + 'nestedCollection' => [ + ['simple' => ['string' => 'World']], + ], + ], + ]); +}); + +it('wraps complex data structures with a global', function () { + config()->set('data.wrap', 'wrap'); + + $data = new MultiNestedData( + new NestedData(SimpleData::from('Hello')), + [ + new NestedData(SimpleData::from('World')), + ], + ); + + expect( + $data->wrap('wrap')->toResponse(\request())->getData(true) + )->toMatchArray([ + 'wrap' => [ + 'nested' => ['simple' => ['string' => 'Hello']], + 'nestedCollection' => [ + 'wrap' => [ + ['simple' => ['string' => 'World']], + ], + ], + ], + ]); +}); + +it('only wraps responses, default transformations will not wrap', function () { + expect( + SimpleData::from('Hello World')->wrap('wrap') + ) + ->toArray() + ->toMatchArray(['string' => 'Hello World']); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class)->wrap('wrap') + ) + ->toArray() + ->toMatchArray([ + ['string' => 'Hello'], + ['string' => 'World'], + ]); +}); + +it('will wrap responses which are data', function () { + Route::post('/example-route', function () { + return SimpleData::from(request()->input('string'))->wrap('data'); + }); + + performRequest('Hello World') + ->assertCreated() + ->assertJson(['data' => ['string' => 'Hello World']]); +}); + +it('will wrap responses which are data collections', function () { + Route::post('/example-route', function () { + return SimpleData::collect([ + request()->input('string'), + strtoupper(request()->input('string')), + ], DataCollection::class)->wrap('data'); + }); + + performRequest('Hello World') + ->assertCreated() + ->assertJson([ + 'data' => [ + ['string' => 'Hello World'], + ['string' => 'HELLO WORLD'], + ], + ]); +}); diff --git a/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json new file mode 100644 index 000000000..79c64f04b --- /dev/null +++ b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json @@ -0,0 +1,58 @@ +{ + "data": [ + { + "string": "Item 1" + }, + { + "string": "Item 2" + }, + { + "string": "Item 3" + }, + { + "string": "Item 4" + }, + { + "string": "Item 5" + }, + { + "string": "Item 6" + }, + { + "string": "Item 7" + }, + { + "string": "Item 8" + }, + { + "string": "Item 9" + }, + { + "string": "Item 10" + }, + { + "string": "Item 11" + }, + { + "string": "Item 12" + }, + { + "string": "Item 13" + }, + { + "string": "Item 14" + }, + { + "string": "Item 15" + } + ], + "links": [], + "meta": { + "path": "\/", + "per_page": 15, + "next_cursor": "eyJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "next_page_url": "\/?cursor=eyJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "prev_cursor": null, + "prev_page_url": null + } +} diff --git a/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json new file mode 100644 index 000000000..c4ce0e425 --- /dev/null +++ b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json @@ -0,0 +1,109 @@ +{ + "data": [ + { + "string": "Item 1" + }, + { + "string": "Item 2" + }, + { + "string": "Item 3" + }, + { + "string": "Item 4" + }, + { + "string": "Item 5" + }, + { + "string": "Item 6" + }, + { + "string": "Item 7" + }, + { + "string": "Item 8" + }, + { + "string": "Item 9" + }, + { + "string": "Item 10" + }, + { + "string": "Item 11" + }, + { + "string": "Item 12" + }, + { + "string": "Item 13" + }, + { + "string": "Item 14" + }, + { + "string": "Item 15" + } + ], + "links": [ + { + "url": null, + "label": "« Previous", + "active": false + }, + { + "url": "\/?page=1", + "label": "1", + "active": true + }, + { + "url": "\/?page=2", + "label": "2", + "active": false + }, + { + "url": "\/?page=3", + "label": "3", + "active": false + }, + { + "url": "\/?page=4", + "label": "4", + "active": false + }, + { + "url": "\/?page=5", + "label": "5", + "active": false + }, + { + "url": "\/?page=6", + "label": "6", + "active": false + }, + { + "url": "\/?page=7", + "label": "7", + "active": false + }, + { + "url": "\/?page=2", + "label": "Next »", + "active": false + } + ], + "meta": { + "current_page": 1, + "first_page_url": "\/?page=1", + "from": 1, + "last_page": 7, + "last_page_url": "\/?page=7", + "next_page_url": "\/?page=2", + "path": "\/", + "per_page": 15, + "prev_page_url": null, + "to": 15, + "total": 100 + } +}