diff --git a/src/Attributes/Validation/Sometimes.php b/src/Attributes/Validation/Sometimes.php new file mode 100644 index 000000000..32fd4ee8b --- /dev/null +++ b/src/Attributes/Validation/Sometimes.php @@ -0,0 +1,14 @@ +name()] ?? null; + $value = array_key_exists($property->name(), $values) ? $values[$property->name()] ?? null : Undefined::make(); if ($value === null) { return $value; } + if ($value instanceof Undefined) { + return $value; + } + if ($value instanceof Lazy) { return $value; } diff --git a/src/RuleInferrers/SometimesRuleInferrer.php b/src/RuleInferrers/SometimesRuleInferrer.php new file mode 100644 index 000000000..2acdd3715 --- /dev/null +++ b/src/RuleInferrers/SometimesRuleInferrer.php @@ -0,0 +1,17 @@ +isUndefinable() && ! in_array('sometimes', $rules)) { + $rules[] = 'sometimes'; + } + + return $rules; + } +} diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index fd9caa3f9..de430ccaa 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -15,6 +15,7 @@ use Spatie\LaravelData\Exceptions\CannotFindDataTypeForProperty; use Spatie\LaravelData\Exceptions\InvalidDataPropertyType; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Undefined; use TypeError; class DataProperty @@ -23,6 +24,8 @@ class DataProperty protected bool $isNullable; + protected bool $isUndefinable; + protected bool $isData; protected bool $isDataCollection; @@ -75,6 +78,11 @@ public function isNullable(): bool return $this->isNullable; } + public function isUndefinable(): bool + { + return $this->isUndefinable; + } + public function isPromoted(): bool { return $this->property->isPromoted(); @@ -177,6 +185,7 @@ private function processNoType(): void { $this->isLazy = false; $this->isNullable = true; + $this->isUndefinable = false; $this->isData = false; $this->isDataCollection = false; $this->types = new DataPropertyTypes(); @@ -194,6 +203,7 @@ private function processNamedType(ReflectionNamedType $type) $this->isData = is_a($name, Data::class, true); $this->isDataCollection = is_a($name, DataCollection::class, true); $this->isNullable = $type->allowsNull(); + $this->isUndefinable = is_a($name, Undefined::class, true); $this->types = new DataPropertyTypes([$name]); } @@ -201,6 +211,7 @@ private function processListType(ReflectionUnionType|ReflectionIntersectionType { $this->isLazy = false; $this->isNullable = false; + $this->isUndefinable = false; $this->isData = false; $this->isDataCollection = false; $this->types = new DataPropertyTypes(); @@ -214,6 +225,12 @@ private function processListType(ReflectionUnionType|ReflectionIntersectionType continue; } + if ($name === Undefined::class) { + $this->isUndefinable = true; + + continue; + } + if ($name === Lazy::class) { $this->isLazy = true; diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index e43740cdc..0c5b208d0 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -27,6 +27,7 @@ protected function typeProcessors(): array $this->config->getDefaultTypeReplacements() ), new RemoveLazyTypeProcessor(), + new RemoveUndefinedTypeProcessor(), new DtoCollectionTypeProcessor(), ]; } diff --git a/src/Support/TypeScriptTransformer/RemoveUndefinedTypeProcessor.php b/src/Support/TypeScriptTransformer/RemoveUndefinedTypeProcessor.php new file mode 100644 index 000000000..cf6a06246 --- /dev/null +++ b/src/Support/TypeScriptTransformer/RemoveUndefinedTypeProcessor.php @@ -0,0 +1,48 @@ +getIterator())) + ->reject(function (Type $type) { + if (! $type instanceof Object_) { + return false; + } + + return is_a((string)$type->getFqsen(), Undefined::class, true); + }); + + if ($types->isEmpty()) { + throw new Exception("Type {$reflection->getDeclaringClass()->name}:{$reflection->getName()} cannot be only Undefined"); + } + + if ($types->count() === 1) { + return $types->first(); + } + + return new Compound($types->all()); + } +} diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php index 1d673a18b..279f0f60b 100644 --- a/src/Transformers/DataTransformer.php +++ b/src/Transformers/DataTransformer.php @@ -8,6 +8,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\TransformationType; +use Spatie\LaravelData\Undefined; class DataTransformer { @@ -71,6 +72,10 @@ protected function shouldIncludeProperty( ?array $allowedIncludes, ?array $allowedExcludes, ): bool { + if ($value instanceof Undefined) { + return false; + } + if (! $value instanceof Lazy) { return true; } diff --git a/src/Undefined.php b/src/Undefined.php new file mode 100644 index 000000000..dd4edf505 --- /dev/null +++ b/src/Undefined.php @@ -0,0 +1,12 @@ + new Uuid(), 'expected' => ['uuid'], ]; + + yield [ + 'attribute' => new Sometimes(), + 'expected' => ['sometimes'], + ]; } public function acceptedIfAttributesDataProvider(): Generator diff --git a/tests/DataTest.php b/tests/DataTest.php index c1fb1a7e8..b09f0272f 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Tests\Factories\DataBlueprintFactory; use Spatie\LaravelData\Tests\Factories\DataPropertyBlueprintFactory; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; +use Spatie\LaravelData\Tests\Fakes\DefaultUndefinedData; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\DummyModel; use Spatie\LaravelData\Tests\Fakes\DummyModelWithCasts; @@ -773,4 +774,25 @@ public function it_can_construct_a_data_object_with_default_values_and_overwrite $this->assertEquals('Test', $data->default_property); $this->assertEquals('Test Again', $data->default_promoted_property); } + + + /** @test */ + public function it_excludes_undefined_values_data() + { + $data = DefaultUndefinedData::from([]); + + $this->assertEquals([], $data->toArray()); + } + + /** @test */ + public function it_includes_value_if_not_undefined_data() + { + $data = DefaultUndefinedData::from([ + 'name' => 'Freek' + ]); + + $this->assertEquals([ + 'name' => 'Freek' + ], $data->toArray()); + } } diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index 74bed5a8c..3693b8b2a 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -8,6 +8,7 @@ use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; +use Spatie\LaravelData\Undefined; class ComplicatedData extends Data { @@ -19,6 +20,7 @@ public function __construct( public string $string, public array $array, public ?int $nullable, + public int|Undefined $undefinable, public mixed $mixed, #[WithCast(DateTimeInterfaceCast::class, format: 'd-m-Y', type: CarbonImmutable::class)] public $explicitCast, diff --git a/tests/Fakes/DefaultUndefinedData.php b/tests/Fakes/DefaultUndefinedData.php new file mode 100644 index 000000000..5c6481787 --- /dev/null +++ b/tests/Fakes/DefaultUndefinedData.php @@ -0,0 +1,14 @@ +assertEquals('Hello world', $data->string); $this->assertEquals([1, 1, 2, 3, 5, 8], $data->array); $this->assertNull($data->nullable); + $this->assertInstanceOf(Undefined::class, $data->undefinable); $this->assertEquals(42, $data->mixed); $this->assertEquals(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00'), $data->defaultCast); $this->assertEquals(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994'), $data->explicitCast); diff --git a/tests/Resolvers/EmptyDataResolverTest.php b/tests/Resolvers/EmptyDataResolverTest.php index bee466b74..b480d6312 100644 --- a/tests/Resolvers/EmptyDataResolverTest.php +++ b/tests/Resolvers/EmptyDataResolverTest.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Resolvers\EmptyDataResolver; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\TestCase; +use Spatie\LaravelData\Undefined; class EmptyDataResolverTest extends TestCase { @@ -105,6 +106,37 @@ public function it_will_return_the_base_type_for_lazy_types_that_can_be_null() }); } + public function it_will_return_the_base_type_for_lazy_types_that_can_be_undefined() + { + $this->assertEmptyPropertyValue(null, new class () { + public Lazy | string | Undefined $property; + }); + + $this->assertEmptyPropertyValue([], new class () { + public Lazy | array | Undefined $property; + }); + + $this->assertEmptyPropertyValue(['string' => null], new class () { + public Lazy | SimpleData | Undefined $property; + }); + } + + /** @test */ + public function it_will_return_the_base_type_for_undefinable_types() + { + $this->assertEmptyPropertyValue(null, new class() { + public Undefined | string $property; + }); + + $this->assertEmptyPropertyValue([], new class () { + public Undefined | array $property; + }); + + $this->assertEmptyPropertyValue(['string' => null], new class () { + public Undefined | SimpleData $property; + }); + } + /** @test */ public function it_cannot_have_multiple_types() { @@ -135,6 +167,26 @@ public function it_cannot_have_multiple_types_with_a_nullable_lazy() }); } + /** @test */ + public function it_cannot_have_multiple_types_with_a_undefined() + { + $this->expectException(DataPropertyCanOnlyHaveOneType::class); + + $this->assertEmptyPropertyValue(null, new class () { + public int | string | Undefined $property; + }); + } + + /** @test */ + public function it_cannot_have_multiple_types_with_a_nullable_undefined() + { + $this->expectException(DataPropertyCanOnlyHaveOneType::class); + + $this->assertEmptyPropertyValue(null, new class () { + public int | string | Undefined | null $property; + }); + } + /** @test */ public function it_can_overwrite_empty_properties() { diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 2538a4fe4..b5e147a1f 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -21,6 +21,7 @@ use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\TestCase; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; +use Spatie\LaravelData\Undefined; class DataPropertyTest extends TestCase { @@ -33,6 +34,7 @@ public function it_works_with_non_typed_properties() $this->assertFalse($helper->isLazy()); $this->assertTrue($helper->isNullable()); + $this->assertFalse($helper->isUndefinable()); $this->assertFalse($helper->isData()); $this->assertFalse($helper->isDataCollection()); $this->assertTrue($helper->types()->isEmpty()); @@ -84,6 +86,29 @@ public function it_can_check_if_a_property_is_nullable() $this->assertTrue($helper->isNullable()); } + /** @test */ + public function it_can_check_if_a_property_is_undefinable() + { + $helper = $this->resolveHelper(new class () { + public int $property; + }); + + $this->assertFalse($helper->isUndefinable()); + + $helper = $this->resolveHelper(new class () { + public Undefined $property; + }); + + $this->assertTrue($helper->isUndefinable()); + + $helper = $this->resolveHelper(new class () { + public Undefined|int $property; + }); + + $this->assertTrue($helper->isUndefinable()); + } + + /** @test */ public function it_can_check_if_a_property_is_a_data_object() { diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index 4f8cfb762..7e5213276 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\TestCase; +use Spatie\LaravelData\Undefined; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class DataTypeScriptTransformerTest extends TestCase @@ -18,9 +19,12 @@ public function it_can_covert_a_data_object_to_typescript() { $config = TypeScriptTransformerConfig::create(); - $data = new class (null, 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), SimpleData::from('Simple data'), SimpleData::collection([]), ) extends Data { + $data = new class (null, Undefined::make(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], + Lazy::create(fn + () => 'Lazy'), SimpleData::from('Simple data'), SimpleData::collection([]), ) extends Data { public function __construct( public null | int $nullable, + public Undefined | int $undefineable, public int $int, public bool $bool, public string $string, diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt index 4cf1f171d..6084c05d7 100644 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt +++ b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt @@ -1,5 +1,6 @@ { nullable: number | null; +undefineable: number; int: number; bool: boolean; string: string;