diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 10e3ad5fd..88d1b2bc9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -807,7 +807,6 @@ - @@ -815,10 +814,14 @@ format]]> format]]> + + + + + totalMicroseconds * 1000]]> - format)->totalMicroseconds * 1000]]> @@ -828,9 +831,6 @@ - - - classFQCN::from($value)]]> classFQCN::from($value['value'])]]> @@ -849,11 +849,6 @@ - - - - - diff --git a/src/Internal/Marshaller/Type/ActivityCancellationType.php b/src/Internal/Marshaller/Type/ActivityCancellationType.php index 22797bba0..b3c008ac6 100644 --- a/src/Internal/Marshaller/Type/ActivityCancellationType.php +++ b/src/Internal/Marshaller/Type/ActivityCancellationType.php @@ -10,7 +10,7 @@ * Converts a boolean value to an activity cancellation policy. * * @see Policy - * @extends Type + * @extends Type * @internal */ final class ActivityCancellationType extends Type diff --git a/src/Internal/Marshaller/Type/ArrayType.php b/src/Internal/Marshaller/Type/ArrayType.php index 6f19b6945..6ccb28079 100644 --- a/src/Internal/Marshaller/Type/ArrayType.php +++ b/src/Internal/Marshaller/Type/ArrayType.php @@ -15,7 +15,7 @@ use Temporal\Internal\Marshaller\MarshallingRule; /** - * @extends Type + * @extends Type */ class ArrayType extends Type implements DetectableTypeInterface, RuleFactoryInterface { @@ -57,7 +57,6 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule } /** - * @psalm-assert array $value * @param mixed $value * @param array $current */ diff --git a/src/Internal/Marshaller/Type/AssocArrayType.php b/src/Internal/Marshaller/Type/AssocArrayType.php index 96c325b2f..ef657ad6f 100644 --- a/src/Internal/Marshaller/Type/AssocArrayType.php +++ b/src/Internal/Marshaller/Type/AssocArrayType.php @@ -17,7 +17,7 @@ /** * Force the value to be an associative array (object) on serialization. * - * @extends Type + * @extends Type */ class AssocArrayType extends Type { @@ -40,17 +40,13 @@ public function __construct(MarshallerInterface $marshaller, MarshallingRule|str parent::__construct($marshaller); } - /** - * @psalm-assert array $value - * @psalm-assert array $current - * @param mixed $value - * @param mixed $current - */ public function parse($value, $current): array { - \is_array($value) or throw new \InvalidArgumentException( - \sprintf(self::ERROR_INVALID_TYPE, \get_debug_type($value)), - ); + if (!\is_array($value)) { + throw new \InvalidArgumentException( + \sprintf(self::ERROR_INVALID_TYPE, \get_debug_type($value)), + ); + } if ($this->type) { $result = []; diff --git a/src/Internal/Marshaller/Type/ChildWorkflowCancellationType.php b/src/Internal/Marshaller/Type/ChildWorkflowCancellationType.php index 72e9a542f..3967e6d2b 100644 --- a/src/Internal/Marshaller/Type/ChildWorkflowCancellationType.php +++ b/src/Internal/Marshaller/Type/ChildWorkflowCancellationType.php @@ -11,7 +11,7 @@ * * @see Policy * - * @extends Type + * @extends Type * @internal */ final class ChildWorkflowCancellationType extends Type diff --git a/src/Internal/Marshaller/Type/CronType.php b/src/Internal/Marshaller/Type/CronType.php index 5dec24744..c08d62026 100644 --- a/src/Internal/Marshaller/Type/CronType.php +++ b/src/Internal/Marshaller/Type/CronType.php @@ -12,7 +12,8 @@ namespace Temporal\Internal\Marshaller\Type; /** - * @extends Type + * @psalm-type TValue = null|string|\Stringable + * @extends Type */ class CronType extends Type { diff --git a/src/Internal/Marshaller/Type/DateIntervalType.php b/src/Internal/Marshaller/Type/DateIntervalType.php index 819394213..46e4a9353 100644 --- a/src/Internal/Marshaller/Type/DateIntervalType.php +++ b/src/Internal/Marshaller/Type/DateIntervalType.php @@ -20,7 +20,8 @@ /** * @psalm-import-type DateIntervalFormat from DateInterval - * @extends Type + * @psalm-import-type DateIntervalValue from DateInterval + * @extends Type */ class DateIntervalType extends Type implements DetectableTypeInterface, RuleFactoryInterface { @@ -53,10 +54,6 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule public function serialize($value): int|Duration { - if ($this->format === DateInterval::FORMAT_NANOSECONDS) { - return (int) (DateInterval::parse($value, $this->format)->totalMicroseconds * 1000); - } - if ($this->format === Duration::class) { return match (true) { $value instanceof \DateInterval => DateInterval::toDuration($value), @@ -69,8 +66,27 @@ public function serialize($value): int|Duration }; } - $method = 'total' . \ucfirst($this->format); - return (int) (DateInterval::parse($value, $this->format)->$method); + $interval = DateInterval::parse($value, $this->format); + + return (int) match ($this->format) { + DateInterval::FORMAT_YEARS => $interval->totalYears, + DateInterval::FORMAT_MONTHS => $interval->totalMonths, + DateInterval::FORMAT_WEEKS => $interval->totalWeeks, + DateInterval::FORMAT_DAYS => $interval->totalDays, + DateInterval::FORMAT_HOURS => $interval->totalHours, + DateInterval::FORMAT_MINUTES => $interval->totalMinutes, + DateInterval::FORMAT_SECONDS => $interval->totalSeconds, + DateInterval::FORMAT_MILLISECONDS => $interval->totalMilliseconds, + DateInterval::FORMAT_MICROSECONDS => $interval->totalMicroseconds, + DateInterval::FORMAT_NANOSECONDS => (int) \round($interval->totalMicroseconds * 1000), + default => throw new \InvalidArgumentException( + \sprintf( + 'Unsupported format: "%s". See %s for available formats.', + $this->format, + DateInterval::class, + ), + ), + }; } public function parse($value, $current): CarbonInterval diff --git a/src/Internal/Marshaller/Type/DateTimeType.php b/src/Internal/Marshaller/Type/DateTimeType.php index f9be701f3..0383504a8 100644 --- a/src/Internal/Marshaller/Type/DateTimeType.php +++ b/src/Internal/Marshaller/Type/DateTimeType.php @@ -19,7 +19,7 @@ use Temporal\Internal\Support\Inheritance; /** - * @extends Type + * @extends Type */ class DateTimeType extends Type implements DetectableTypeInterface, RuleFactoryInterface { diff --git a/src/Internal/Marshaller/Type/DurationJsonType.php b/src/Internal/Marshaller/Type/DurationJsonType.php index fca3dc1f3..094dfb7ca 100644 --- a/src/Internal/Marshaller/Type/DurationJsonType.php +++ b/src/Internal/Marshaller/Type/DurationJsonType.php @@ -20,7 +20,8 @@ /** * @psalm-import-type DateIntervalFormat from DateInterval - * @extends Type + * @psalm-import-type DateIntervalValue from DateInterval + * @extends Type */ class DurationJsonType extends Type implements DetectableTypeInterface, RuleFactoryInterface { diff --git a/src/Internal/Marshaller/Type/EncodedCollectionType.php b/src/Internal/Marshaller/Type/EncodedCollectionType.php index 98e853d74..9bf27fde3 100644 --- a/src/Internal/Marshaller/Type/EncodedCollectionType.php +++ b/src/Internal/Marshaller/Type/EncodedCollectionType.php @@ -23,7 +23,7 @@ /** * Read only type. - * @extends Type + * @extends Type */ final class EncodedCollectionType extends Type implements DetectableTypeInterface, RuleFactoryInterface { @@ -52,9 +52,6 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule return new MarshallingRule($property->getName(), self::class, $type->getName()); } - /** - * @psalm-assert string $value - */ public function parse(mixed $value, mixed $current): EncodedCollection { return match (true) { diff --git a/src/Internal/Marshaller/Type/EnumType.php b/src/Internal/Marshaller/Type/EnumType.php index 372e22665..af2bfffc0 100644 --- a/src/Internal/Marshaller/Type/EnumType.php +++ b/src/Internal/Marshaller/Type/EnumType.php @@ -15,16 +15,20 @@ use Temporal\Internal\Marshaller\MarshallingRule; /** - * @extends Type + * @template TClass of \UnitEnum + * @extends Type */ class EnumType extends Type implements RuleFactoryInterface { private const ERROR_INVALID_TYPE = 'Invalid Enum value. Expected: int or string scalar value for BackedEnum; ' . 'array with `name` or `value` keys; a case of the Enum. %s given.'; - /** @var class-string<\UnitEnum> */ + /** @var class-string */ private string $classFQCN; + /** + * @param class-string|null $class + */ public function __construct(MarshallerInterface $marshaller, ?string $class = null) { if ($class === null) { @@ -54,7 +58,7 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule public function parse($value, $current) { - if (\is_object($value)) { + if ($value instanceof $this->classFQCN) { return $value; } @@ -78,10 +82,6 @@ public function parse($value, $current) throw new \InvalidArgumentException(\sprintf(self::ERROR_INVALID_TYPE, \ucfirst(\get_debug_type($value)))); } - /** - * @psalm-suppress UndefinedDocblockClass - * @param mixed $value - */ public function serialize($value): array { return $value instanceof \BackedEnum diff --git a/src/Internal/Marshaller/Type/EnumValueType.php b/src/Internal/Marshaller/Type/EnumValueType.php index 9ad8a12dd..909ba3723 100644 --- a/src/Internal/Marshaller/Type/EnumValueType.php +++ b/src/Internal/Marshaller/Type/EnumValueType.php @@ -15,7 +15,7 @@ use Temporal\Internal\Marshaller\MarshallingRule; /** - * @extends Type + * @extends Type */ class EnumValueType extends Type implements RuleFactoryInterface { @@ -55,7 +55,7 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule public function parse($value, $current) { - if (\is_object($value)) { + if ($value instanceof \BackedEnum) { return $value; } diff --git a/src/Internal/Marshaller/Type/NullableType.php b/src/Internal/Marshaller/Type/NullableType.php index ad2dd8294..b6f4e4985 100644 --- a/src/Internal/Marshaller/Type/NullableType.php +++ b/src/Internal/Marshaller/Type/NullableType.php @@ -15,7 +15,7 @@ use Temporal\Internal\Marshaller\MarshallingRule; /** - * @extends Type + * @extends Type */ class NullableType extends Type { @@ -33,11 +33,6 @@ public function __construct(MarshallerInterface $marshaller, MarshallingRule|str parent::__construct($marshaller); } - /** - * @param mixed $value - * @param mixed $current - * @return mixed - */ public function parse($value, $current) { if ($value === null) { diff --git a/src/Internal/Marshaller/Type/ObjectType.php b/src/Internal/Marshaller/Type/ObjectType.php index 06e87f646..abc873267 100644 --- a/src/Internal/Marshaller/Type/ObjectType.php +++ b/src/Internal/Marshaller/Type/ObjectType.php @@ -16,7 +16,7 @@ /** * @template TClass of object - * @extends Type + * @extends Type */ class ObjectType extends Type implements DetectableTypeInterface, RuleFactoryInterface { @@ -60,13 +60,11 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule public function parse($value, $current): object { - if (\is_object($value)) { + if (\is_object($value) && $this->reflection->isInstance($value)) { return $value; } - if ($current === null) { - $current = $this->emptyInstance(); - } + $current ??= $this->emptyInstance(); if ($current::class === \stdClass::class && $this->reflection->getName() === \stdClass::class) { foreach ($value as $key => $val) { diff --git a/src/Internal/Marshaller/Type/OneOfType.php b/src/Internal/Marshaller/Type/OneOfType.php index fc5d8a7c0..44b7e5960 100644 --- a/src/Internal/Marshaller/Type/OneOfType.php +++ b/src/Internal/Marshaller/Type/OneOfType.php @@ -15,7 +15,7 @@ /** * @template TClass of object - * @extends Type + * @extends Type */ class OneOfType extends Type { @@ -34,7 +34,7 @@ public function __construct( public function parse(mixed $value, mixed $current): ?object { - if (\is_object($value)) { + if ($this->parentClass !== null && $value instanceof $this->parentClass) { return $value; } @@ -58,10 +58,12 @@ public function parse(mixed $value, mixed $current): ?object } if ($dtoClass === null) { - $this->nullable or throw new \InvalidArgumentException(\sprintf( - 'Unable to detect OneOf case for non-nullable type%s.', - $this->parentClass ? " `{$this->parentClass}`" : '', - )); + if (!$this->nullable) { + throw new \InvalidArgumentException(\sprintf( + 'Unable to detect OneOf case for non-nullable type%s.', + $this->parentClass ? " `{$this->parentClass}`" : '', + )); + } return null; } @@ -87,16 +89,23 @@ public function serialize(mixed $value): array return []; } - \is_object($value) or throw new \InvalidArgumentException(\sprintf( - 'Passed value must be a type of object, but %s given.', - \get_debug_type($value), - )); + if (!\is_object($value)) { + throw new \InvalidArgumentException(\sprintf( + 'Passed value must be a type of object, but %s given.', + \get_debug_type($value), + )); + } foreach ($this->cases as $field => $class) { if ($value::class === $class) { return [$field => $this->marshaller->marshal($value)]; } } + + throw new \InvalidArgumentException(\sprintf( + 'Passed value must be a type of one of the allowed classes, but %s given.', + \get_debug_type($value), + )); } /** diff --git a/src/Internal/Marshaller/Type/RuleFactoryInterface.php b/src/Internal/Marshaller/Type/RuleFactoryInterface.php index b8a3a747a..ac3c65ee0 100644 --- a/src/Internal/Marshaller/Type/RuleFactoryInterface.php +++ b/src/Internal/Marshaller/Type/RuleFactoryInterface.php @@ -15,10 +15,8 @@ /** * The type can detect the property type information from its reflection. - * - * @extends TypeInterface */ -interface RuleFactoryInterface extends TypeInterface +interface RuleFactoryInterface { /** * Make a marshalling rule for the given property. diff --git a/src/Internal/Marshaller/Type/Type.php b/src/Internal/Marshaller/Type/Type.php index 9aeaac4e5..876376b72 100644 --- a/src/Internal/Marshaller/Type/Type.php +++ b/src/Internal/Marshaller/Type/Type.php @@ -17,7 +17,8 @@ /** * @template-covariant TSerializeType of mixed - * @implements TypeInterface + * @template TParseType of mixed + * @implements TypeInterface */ abstract class Type implements TypeInterface { diff --git a/src/Internal/Marshaller/Type/TypeInterface.php b/src/Internal/Marshaller/Type/TypeInterface.php index c872464a2..c03ac12c8 100644 --- a/src/Internal/Marshaller/Type/TypeInterface.php +++ b/src/Internal/Marshaller/Type/TypeInterface.php @@ -15,6 +15,7 @@ /** * @template-covariant TMarshalType of mixed + * @template TParseType of mixed */ interface TypeInterface { @@ -26,12 +27,12 @@ public function __construct(MarshallerInterface $marshaller); /** * @param mixed $value * @param mixed $current - * @return mixed + * @return TParseType */ public function parse($value, $current); /** - * @param mixed $value + * @param TParseType $value * @return TMarshalType */ public function serialize($value); diff --git a/src/Internal/Marshaller/Type/UuidType.php b/src/Internal/Marshaller/Type/UuidType.php index afcd091ad..c1440893c 100644 --- a/src/Internal/Marshaller/Type/UuidType.php +++ b/src/Internal/Marshaller/Type/UuidType.php @@ -17,7 +17,7 @@ use Temporal\Internal\Support\Inheritance; /** - * @extends Type + * @extends Type */ final class UuidType extends Type implements DetectableTypeInterface, RuleFactoryInterface { @@ -44,17 +44,11 @@ public static function makeRule(\ReflectionProperty $property): ?MarshallingRule : new MarshallingRule($property->getName(), self::class, $type->getName()); } - /** - * @psalm-assert string $value - */ public function parse(mixed $value, mixed $current): UuidInterface { return Uuid::fromString($value); } - /** - * @psalm-assert UuidInterface $value - */ public function serialize(mixed $value): string { return $value->toString(); diff --git a/src/Internal/Support/DateInterval.php b/src/Internal/Support/DateInterval.php index 893f064f9..2863aded0 100644 --- a/src/Internal/Support/DateInterval.php +++ b/src/Internal/Support/DateInterval.php @@ -51,7 +51,7 @@ final class DateInterval ]; /** - * @param DateIntervalFormat $format + * @psalm-param DateIntervalFormat $format * * @psalm-assert DateIntervalValue|null $interval * @psalm-suppress InvalidOperand diff --git a/tests/Unit/Internal/Marshaller/Type/ActivityCancellationTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/ActivityCancellationTypeTestCase.php new file mode 100644 index 000000000..47bcfb8b7 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/ActivityCancellationTypeTestCase.php @@ -0,0 +1,51 @@ +createMock(MarshallerInterface::class); + $this->type = new ActivityCancellationType($marshaller); + } + + public function testParseTrue(): void + { + $this->assertSame(Policy::WAIT_CANCELLATION_COMPLETED, $this->type->parse(true, null)); + } + + public function testParseFalse(): void + { + $this->assertSame(Policy::TRY_CANCEL, $this->type->parse(false, null)); + } + + public function testSerializeWaitCancellationCompleted(): void + { + $this->assertTrue($this->type->serialize(Policy::WAIT_CANCELLATION_COMPLETED)); + } + + public function testSerializeTryCancel(): void + { + $this->assertFalse($this->type->serialize(Policy::TRY_CANCEL)); + } + + public function testSerializeUnsupportedOption(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('is currently not supported'); + + $this->type->serialize(Policy::ABANDON); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/ArrayTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/ArrayTypeTestCase.php new file mode 100644 index 000000000..258c3d15d --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/ArrayTypeTestCase.php @@ -0,0 +1,189 @@ +createReflectionNamedType('array'); + $this->assertTrue(ArrayType::match($type)); + } + + public function testMatchIterable(): void + { + $type = $this->createReflectionNamedType('iterable'); + $this->assertTrue(ArrayType::match($type)); + } + + public function testMatchReturnsFalseForString(): void + { + $type = $this->createReflectionNamedType('string'); + $this->assertFalse(ArrayType::match($type)); + } + + public function testParseNullReturnsEmptyArray(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ArrayType($marshaller); + + $this->assertSame([], $type->parse(null, [])); + } + + public function testParseInvalidTypeThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ArrayType($marshaller); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('must be a type of array'); + + $type->parse('not an array', []); + } + + public function testParseArrayWithoutInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ArrayType($marshaller); + + $this->assertSame([1, 2, 3], $type->parse([1, 2, 3], [])); + } + + public function testParseArrayWithInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('unmarshal')->willReturnCallback( + fn(array $data, object $obj) => (object) $data, + ); + $type = new ArrayType($marshaller, \stdClass::class); + + $result = $type->parse([['a' => 1], ['b' => 2]], []); + + $this->assertCount(2, $result); + $this->assertEquals((object) ['a' => 1], $result[0]); + $this->assertEquals((object) ['b' => 2], $result[1]); + } + + public function testSerializeArrayWithoutInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ArrayType($marshaller); + + $this->assertSame([1, 2, 3], $type->serialize([1, 2, 3])); + } + + public function testSerializeWithInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('marshal')->willReturnCallback( + fn(object $obj) => (array) $obj, + ); + $type = new ArrayType($marshaller, \stdClass::class); + + $result = $type->serialize([(object) ['a' => 1]]); + + $this->assertSame([['a' => 1]], $result); + } + + public function testSerializeIterableWithoutInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ArrayType($marshaller); + + $generator = (static function () { + yield 'a'; + yield 'b'; + })(); + + $this->assertSame(['a', 'b'], $type->serialize($generator)); + } + + public function testMakeRuleReturnsNullForNonArrayType(): void + { + $property = $this->createPropertyWithType('string', true, false); + + $this->assertNull(ArrayType::makeRule($property)); + } + + public function testMakeRuleForNonNullableArray(): void + { + $property = $this->createPropertyWithType('array', true, false); + + $rule = ArrayType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(ArrayType::class, $rule->type); + } + + public function testMakeRuleForNullableArray(): void + { + $property = $this->createPropertyWithType('array', true, true); + + $rule = ArrayType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(\Temporal\Internal\Marshaller\Type\NullableType::class, $rule->type); + } + + public function testMakeRuleForIterableType(): void + { + $property = $this->createPropertyWithType('iterable', true, false); + + $rule = ArrayType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(ArrayType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(ArrayType::makeRule($property)); + } + + public function testConstructWithMarshallingRule(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('unmarshal')->willReturnCallback( + fn(array $data, object $obj) => (object) $data, + ); + + $rule = new MarshallingRule(type: \stdClass::class); + $type = new ArrayType($marshaller, $rule); + + $result = $type->parse([['x' => 1]], []); + $this->assertEquals((object) ['x' => 1], $result[0]); + } + + private function createReflectionNamedType(string $name): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/AssocArrayTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/AssocArrayTypeTestCase.php new file mode 100644 index 000000000..33d5ba976 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/AssocArrayTypeTestCase.php @@ -0,0 +1,86 @@ +createMock(MarshallerInterface::class); + $type = new AssocArrayType($marshaller); + + $this->assertSame(['a' => 1, 'b' => 2], $type->parse(['a' => 1, 'b' => 2], null)); + } + + public function testParseNonArrayThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new AssocArrayType($marshaller); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('must be a type of array'); + + $type->parse('not an array', null); + } + + public function testParseWithInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('unmarshal')->willReturnCallback( + fn(array $data, object $obj) => (object) $data, + ); + $type = new AssocArrayType($marshaller, \stdClass::class); + + $result = $type->parse(['key' => ['foo' => 'bar']], []); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + $this->assertEquals((object) ['foo' => 'bar'], $result['key']); + } + + public function testSerializeArrayWithoutInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new AssocArrayType($marshaller); + + $result = $type->serialize(['a' => 1, 'b' => 2]); + + $this->assertEquals((object) ['a' => 1, 'b' => 2], $result); + } + + public function testSerializeWithInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('marshal')->willReturnCallback( + fn(object $obj) => (array) $obj, + ); + $type = new AssocArrayType($marshaller, \stdClass::class); + + $result = $type->serialize(['key' => (object) ['foo' => 'bar']]); + + $this->assertEquals((object) ['key' => ['foo' => 'bar']], $result); + } + + public function testSerializeIterableWithoutInnerType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new AssocArrayType($marshaller); + + $generator = (static function () { + yield 'a' => 1; + yield 'b' => 2; + })(); + + $result = $type->serialize($generator); + + $this->assertEquals((object) ['a' => 1, 'b' => 2], $result); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/ChildWorkflowCancellationTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/ChildWorkflowCancellationTypeTestCase.php new file mode 100644 index 000000000..102ea59c0 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/ChildWorkflowCancellationTypeTestCase.php @@ -0,0 +1,51 @@ +createMock(MarshallerInterface::class); + $this->type = new ChildWorkflowCancellationType($marshaller); + } + + public function testParseTrue(): void + { + $this->assertSame(Policy::WAIT_CANCELLATION_COMPLETED, $this->type->parse(true, null)); + } + + public function testParseFalse(): void + { + $this->assertSame(Policy::TRY_CANCEL, $this->type->parse(false, null)); + } + + public function testSerializeWaitCancellationCompleted(): void + { + $this->assertTrue($this->type->serialize(Policy::WAIT_CANCELLATION_COMPLETED)); + } + + public function testSerializeTryCancel(): void + { + $this->assertFalse($this->type->serialize(Policy::TRY_CANCEL)); + } + + public function testSerializeUnsupportedOption(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('is currently not supported'); + + $this->type->serialize(Policy::ABANDON); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/CronTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/CronTypeTestCase.php new file mode 100644 index 000000000..ee08beed3 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/CronTypeTestCase.php @@ -0,0 +1,65 @@ +createMock(MarshallerInterface::class); + $this->type = new CronType($marshaller); + } + + public function testParseEmptyStringReturnsNull(): void + { + $this->assertNull($this->type->parse('', null)); + } + + public function testParseValidCronString(): void + { + $this->assertSame('*/5 * * * *', $this->type->parse('*/5 * * * *', null)); + } + + public function testParseNonStringThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('cron-like string'); + + $this->type->parse(42, null); + } + + public function testSerializeString(): void + { + $this->assertSame('*/5 * * * *', $this->type->serialize('*/5 * * * *')); + } + + public function testSerializeStringable(): void + { + $stringable = new class implements \Stringable { + public function __toString(): string + { + return '0 0 * * *'; + } + }; + + $this->assertSame('0 0 * * *', $this->type->serialize($stringable)); + } + + public function testSerializeInvalidTypeThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('cron-like string'); + + $this->type->serialize(42); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/DateIntervalTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/DateIntervalTypeTestCase.php new file mode 100644 index 000000000..7f7c3670b --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/DateIntervalTypeTestCase.php @@ -0,0 +1,296 @@ +createReflectionNamedType(\DateInterval::class, false); + $this->assertTrue(DateIntervalType::match($type)); + } + + public function testMatchCarbonInterval(): void + { + $type = $this->createReflectionNamedType(CarbonInterval::class, false); + $this->assertTrue(DateIntervalType::match($type)); + } + + public function testMatchReturnsFalseForBuiltin(): void + { + $type = $this->createReflectionNamedType('int', true); + $this->assertFalse(DateIntervalType::match($type)); + } + + public function testMatchReturnsFalseForNonDateInterval(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + $this->assertFalse(DateIntervalType::match($type)); + } + + public function testSerializeNanoseconds(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_NANOSECONDS); + + $interval = CarbonInterval::seconds(5); + $result = $type->serialize($interval); + + $this->assertIsInt($result); + $this->assertSame(5_000_000_000, $result); + } + + public function testSerializeSeconds(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_SECONDS); + + $interval = CarbonInterval::minutes(2); + $result = $type->serialize($interval); + + $this->assertSame(120, $result); + } + + public function testSerializeMilliseconds(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_MILLISECONDS); + + $interval = CarbonInterval::seconds(3); + $result = $type->serialize($interval); + + $this->assertSame(3000, $result); + } + + public function testSerializeMicroseconds(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_MICROSECONDS); + + $interval = CarbonInterval::seconds(1); + $result = $type->serialize($interval); + + $this->assertSame(1_000_000, $result); + } + + public function testSerializeMinutes(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_MINUTES); + + $interval = CarbonInterval::hours(1); + $result = $type->serialize($interval); + + $this->assertSame(60, $result); + } + + public function testSerializeHours(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_HOURS); + + $interval = CarbonInterval::days(1); + $result = $type->serialize($interval); + + $this->assertSame(24, $result); + } + + public function testSerializeDays(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_DAYS); + + $interval = CarbonInterval::weeks(1); + $result = $type->serialize($interval); + + $this->assertSame(7, $result); + } + + public function testSerializeWeeks(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_WEEKS); + + $interval = CarbonInterval::weeks(2); + $result = $type->serialize($interval); + + $this->assertSame(2, $result); + } + + public function testSerializeDurationFromDateInterval(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $interval = new \DateInterval('PT10S'); + $result = $type->serialize($interval); + + $this->assertInstanceOf(Duration::class, $result); + $this->assertSame(10, $result->getSeconds()); + } + + public function testSerializeDurationFromCarbonInterval(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $interval = CarbonInterval::seconds(30)->microseconds(500000); + $result = $type->serialize($interval); + + $this->assertInstanceOf(Duration::class, $result); + $this->assertSame(30, $result->getSeconds()); + $this->assertSame(500000000, $result->getNanos()); + } + + public function testSerializeDurationFromInt(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $result = $type->serialize(30); + + $this->assertInstanceOf(Duration::class, $result); + $this->assertSame(30, $result->getSeconds()); + $this->assertSame(0, $result->getNanos()); + } + + public function testSerializeDurationFromString(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $result = $type->serialize('60'); + + $this->assertInstanceOf(Duration::class, $result); + $this->assertSame(60, $result->getSeconds()); + } + + public function testSerializeDurationFromFloat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $result = $type->serialize(10.5); + + $this->assertInstanceOf(Duration::class, $result); + $this->assertSame(10, $result->getSeconds()); + $this->assertSame(500000000, $result->getNanos()); + } + + public function testSerializeDurationFromInvalidTypeThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, Duration::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value type.'); + + $type->serialize(null); + } + + public function testSerializeYears(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_YEARS); + + $interval = CarbonInterval::years(3); + $result = $type->serialize($interval); + + $this->assertSame(3, $result); + } + + public function testSerializeMonths(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_MONTHS); + + $interval = CarbonInterval::years(1); + $result = $type->serialize($interval); + + $this->assertSame(12, $result); + } + + public function testParse(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateIntervalType($marshaller, DateInterval::FORMAT_SECONDS); + + $result = $type->parse(60, null); + + $this->assertInstanceOf(CarbonInterval::class, $result); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('int', true, false); + $this->assertNull(DateIntervalType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForNonDateInterval(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + $this->assertNull(DateIntervalType::makeRule($property)); + } + + public function testMakeRuleForNonNullable(): void + { + $property = $this->createPropertyWithType(\DateInterval::class, false, false); + + $rule = DateIntervalType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(DateIntervalType::class, $rule->type); + } + + public function testMakeRuleForNullable(): void + { + $property = $this->createPropertyWithType(\DateInterval::class, false, true); + + $rule = DateIntervalType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(DateIntervalType::makeRule($property)); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/DateTimeTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/DateTimeTypeTestCase.php new file mode 100644 index 000000000..a619b0a7f --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/DateTimeTypeTestCase.php @@ -0,0 +1,154 @@ +createReflectionNamedType(\DateTimeImmutable::class, false); + $this->assertTrue(DateTimeType::match($type)); + } + + public function testMatchDateTime(): void + { + $type = $this->createReflectionNamedType(\DateTime::class, false); + $this->assertTrue(DateTimeType::match($type)); + } + + public function testMatchReturnsFalseForBuiltin(): void + { + $type = $this->createReflectionNamedType('string', true); + $this->assertFalse(DateTimeType::match($type)); + } + + public function testMatchReturnsFalseForNonDateTime(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + $this->assertFalse(DateTimeType::match($type)); + } + + public function testParseString(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateTimeType($marshaller); + + $result = $type->parse('2020-01-01T00:00:00+00:00', null); + $this->assertInstanceOf(\DateTimeInterface::class, $result); + $this->assertSame('2020-01-01', $result->format('Y-m-d')); + } + + public function testSerializeDefaultFormat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateTimeType($marshaller); + + $result = $type->serialize(new \DateTimeImmutable('2020-01-01T12:00:00+00:00')); + $this->assertIsString($result); + $this->assertSame('2020-01-01T12:00:00+00:00', $result); + } + + public function testSerializeTimestampFormat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateTimeType($marshaller, format: Timestamp::class); + + $dt = new \DateTimeImmutable('2020-01-01T00:00:00+00:00'); + $result = $type->serialize($dt); + + $this->assertInstanceOf(Timestamp::class, $result); + $this->assertSame($dt->getTimestamp(), $result->getSeconds()); + } + + public function testSerializeCustomFormat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateTimeType($marshaller, format: 'Y-m-d'); + + $result = $type->serialize(new \DateTimeImmutable('2020-06-15T12:00:00+00:00')); + $this->assertSame('2020-06-15', $result); + } + + public function testMakeRuleReturnsNullForNonDateTimeType(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + + $this->assertNull(DateTimeType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('string', true, false); + + $this->assertNull(DateTimeType::makeRule($property)); + } + + public function testMakeRuleForNonNullableDateTime(): void + { + $property = $this->createPropertyWithType(\DateTimeImmutable::class, false, false); + + $rule = DateTimeType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(DateTimeType::class, $rule->type); + } + + public function testMakeRuleForNullableDateTime(): void + { + $property = $this->createPropertyWithType(\DateTimeImmutable::class, false, true); + + $rule = DateTimeType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(DateTimeType::makeRule($property)); + } + + public function testParseWithSpecificClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DateTimeType($marshaller, \DateTimeImmutable::class); + + $result = $type->parse('2021-01-01T00:00:00+00:00', null); + $this->assertInstanceOf(\DateTimeImmutable::class, $result); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/DurationJsonTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/DurationJsonTypeTestCase.php new file mode 100644 index 000000000..005bc3444 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/DurationJsonTypeTestCase.php @@ -0,0 +1,193 @@ +createReflectionNamedType(\DateInterval::class, false); + $this->assertTrue(DurationJsonType::match($type)); + } + + public function testMatchCarbonInterval(): void + { + $type = $this->createReflectionNamedType(CarbonInterval::class, false); + $this->assertTrue(DurationJsonType::match($type)); + } + + public function testMatchReturnsFalseForBuiltin(): void + { + $type = $this->createReflectionNamedType('int', true); + $this->assertFalse(DurationJsonType::match($type)); + } + + public function testMatchReturnsFalseForNonDateInterval(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + $this->assertFalse(DurationJsonType::match($type)); + } + + public function testSerializeDateInterval(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $interval = CarbonInterval::seconds(5)->microseconds(500000); + $result = $type->serialize($interval); + + $this->assertIsArray($result); + $this->assertArrayHasKey('seconds', $result); + $this->assertArrayHasKey('nanos', $result); + } + + public function testSerializeInt(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $result = $type->serialize(30); + + $this->assertIsArray($result); + $this->assertSame(30, $result['seconds']); + $this->assertSame(0, $result['nanos']); + } + + public function testSerializeString(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $result = $type->serialize('60'); + + $this->assertIsArray($result); + $this->assertSame(60, $result['seconds']); + } + + public function testSerializeFloat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $result = $type->serialize(10.5); + + $this->assertIsArray($result); + $this->assertSame(10, $result['seconds']); + $this->assertSame(500000000, $result['nanos']); + } + + public function testSerializeInvalidTypeThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value type'); + + $type->serialize(null); + } + + public function testParseArrayWithSecondsAndNanos(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $result = $type->parse(['seconds' => 5, 'nanos' => 500000000], null); + + $this->assertInstanceOf(CarbonInterval::class, $result); + } + + public function testParseNull(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller); + + $result = $type->parse(null, null); + + $this->assertInstanceOf(CarbonInterval::class, $result); + } + + public function testParseFallbackFormat(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new DurationJsonType($marshaller, DateInterval::FORMAT_SECONDS); + + $result = $type->parse(60, null); + + $this->assertInstanceOf(CarbonInterval::class, $result); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('int', true, false); + $this->assertNull(DurationJsonType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForNonDateInterval(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + $this->assertNull(DurationJsonType::makeRule($property)); + } + + public function testMakeRuleForNonNullable(): void + { + $property = $this->createPropertyWithType(\DateInterval::class, false, false); + + $rule = DurationJsonType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(DurationJsonType::class, $rule->type); + } + + public function testMakeRuleForNullable(): void + { + $property = $this->createPropertyWithType(\DateInterval::class, false, true); + + $rule = DurationJsonType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(DurationJsonType::makeRule($property)); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/EncodedCollectionTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/EncodedCollectionTypeTestCase.php new file mode 100644 index 000000000..4a228e229 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/EncodedCollectionTypeTestCase.php @@ -0,0 +1,222 @@ +createReflectionNamedType(EncodedCollection::class, false); + + $this->assertTrue(EncodedCollectionType::match($type)); + } + + public function testMatchReturnsFalseForBuiltinType(): void + { + $type = $this->createReflectionNamedType('array', true); + + $this->assertFalse(EncodedCollectionType::match($type)); + } + + public function testMatchReturnsFalseForUnrelatedClass(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + + $this->assertFalse(EncodedCollectionType::match($type)); + } + + public function testParseNull(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $result = $type->parse(null, null); + + $this->assertInstanceOf(EncodedCollection::class, $result); + $this->assertTrue($result->isEmpty()); + } + + public function testParseArray(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $result = $type->parse(['key' => 'value'], null); + + $this->assertInstanceOf(EncodedCollection::class, $result); + $this->assertSame('value', $result->getValue('key')); + } + + public function testParseEncodedCollection(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $collection = EncodedCollection::fromValues(['k' => 'v']); + $result = $type->parse($collection, null); + + $this->assertSame($collection, $result); + } + + public function testParseInvalidTypeThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported value type'); + + $type->parse(42, null); + } + + public function testSerializeWithoutMarshalTo(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $collection = EncodedCollection::fromValues(['a' => 1, 'b' => 2]); + $result = $type->serialize($collection); + + $this->assertSame(['a' => 1, 'b' => 2], $result); + } + + public function testSerializeNonEncodedCollectionThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported value type'); + + $type->serialize('not a collection'); + } + + public function testSerializeWithUnsupportedMarshalToThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller, \stdClass::class); + + $collection = EncodedCollection::empty(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported target type'); + + $type->serialize($collection); + } + + public function testSerializeWithSearchAttributesMarshalTo(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller, SearchAttributes::class); + + $converter = $this->createMock(DataConverterInterface::class); + $converter->method('toPayload')->willReturn(new \Temporal\Api\Common\V1\Payload()); + + $collection = EncodedCollection::fromValues(['key' => 'val'], $converter); + $result = $type->serialize($collection); + + $this->assertInstanceOf(SearchAttributes::class, $result); + } + + public function testSerializeWithMemoMarshalTo(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller, Memo::class); + + $converter = $this->createMock(DataConverterInterface::class); + $converter->method('toPayload')->willReturn(new \Temporal\Api\Common\V1\Payload()); + + $collection = EncodedCollection::fromValues(['key' => 'val'], $converter); + $result = $type->serialize($collection); + + $this->assertInstanceOf(Memo::class, $result); + } + + public function testSerializeWithPayloadsMarshalTo(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller, Payloads::class); + + $converter = $this->createMock(DataConverterInterface::class); + $converter->method('toPayload')->willReturn(new \Temporal\Api\Common\V1\Payload()); + + $collection = EncodedCollection::fromValues(['key' => 'val'], $converter); + $result = $type->serialize($collection); + + $this->assertInstanceOf(Payloads::class, $result); + } + + public function testSerializeWithHeaderMarshalTo(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EncodedCollectionType($marshaller, Header::class); + + $converter = $this->createMock(DataConverterInterface::class); + $converter->method('toPayload')->willReturn(new \Temporal\Api\Common\V1\Payload()); + + $collection = EncodedCollection::fromValues(['key' => 'val'], $converter); + $result = $type->serialize($collection); + + $this->assertInstanceOf(Header::class, $result); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('array', true, false); + + $this->assertNull(EncodedCollectionType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForNonMatchingClass(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + + $this->assertNull(EncodedCollectionType::makeRule($property)); + } + + public function testMakeRuleReturnsRuleForEncodedCollection(): void + { + $property = $this->createPropertyWithType(EncodedCollection::class, false, false); + + $rule = EncodedCollectionType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(EncodedCollectionType::class, $rule->type); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/EnumTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/EnumTypeTestCase.php new file mode 100644 index 000000000..39e374e09 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/EnumTypeTestCase.php @@ -0,0 +1,196 @@ +createMock(MarshallerInterface::class); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Enum is required'); + + new EnumType($marshaller, null); + } + + public function testParseEnumInstance(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, StringBackedEnum::class); + + $result = $type->parse(StringBackedEnum::Foo, null); + + $this->assertSame(StringBackedEnum::Foo, $result); + } + + public function testParseScalarValueForBackedEnum(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, StringBackedEnum::class); + + $result = $type->parse('foo', null); + + $this->assertSame(StringBackedEnum::Foo, $result); + } + + public function testParseIntScalarValue(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, IntBackedEnum::class); + + $result = $type->parse(1, null); + + $this->assertSame(IntBackedEnum::One, $result); + } + + public function testParseArrayWithValueKey(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, StringBackedEnum::class); + + $result = $type->parse(['value' => 'bar'], null); + + $this->assertSame(StringBackedEnum::Bar, $result); + } + + public function testParseArrayWithNameKey(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, SimpleEnum::class); + + $result = $type->parse(['name' => 'Alpha'], null); + + $this->assertSame(SimpleEnum::Alpha, $result); + } + + public function testParseArrayWithValueKeyTakesPrecedenceOverName(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, StringBackedEnum::class); + + $result = $type->parse(['value' => 'foo', 'name' => 'Bar'], null); + + $this->assertSame(StringBackedEnum::Foo, $result); + } + + public function testParseNonScalarNonArrayNonObjectThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, SimpleEnum::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Enum value'); + + // null is not scalar, not array, not object matching the enum + $type->parse(null, null); + } + + public function testParseScalarOnNonBackedEnumThrowsError(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, SimpleEnum::class); + + // SimpleEnum is non-backed and doesn't have from(), calling from() triggers Error + $this->expectException(\Error::class); + + $type->parse('Alpha', null); + } + + public function testParseArrayWithoutValueOrNameThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, SimpleEnum::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Enum value'); + + $type->parse(['unknown' => 'x'], null); + } + + public function testSerializeBackedEnum(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, StringBackedEnum::class); + + $result = $type->serialize(StringBackedEnum::Foo); + + $this->assertSame(['name' => 'Foo', 'value' => 'foo'], $result); + } + + public function testSerializeSimpleEnum(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumType($marshaller, SimpleEnum::class); + + $result = $type->serialize(SimpleEnum::Alpha); + + $this->assertSame(['name' => 'Alpha'], $result); + } + + public function testMakeRuleReturnsNullForNonEnumType(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + $this->assertNull(EnumType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('string', true, false); + $this->assertNull(EnumType::makeRule($property)); + } + + public function testMakeRuleForNonNullableEnum(): void + { + $property = $this->createPropertyWithType(StringBackedEnum::class, false, false); + + $rule = EnumType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(EnumType::class, $rule->type); + } + + public function testMakeRuleForNullableEnum(): void + { + $property = $this->createPropertyWithType(StringBackedEnum::class, false, true); + + $rule = EnumType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(EnumType::makeRule($property)); + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/EnumValueTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/EnumValueTypeTestCase.php new file mode 100644 index 000000000..d2414c8c1 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/EnumValueTypeTestCase.php @@ -0,0 +1,164 @@ +createMock(MarshallerInterface::class); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Enum is required'); + + new EnumValueType($marshaller, null); + } + + public function testConstructorThrowsForNonBackedEnum(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('must be an instance of BackedEnum'); + + new EnumValueType($marshaller, SimpleEnum::class); + } + + public function testParseBackedEnumInstance(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, StringBackedEnum::class); + + $result = $type->parse(StringBackedEnum::Foo, null); + + $this->assertSame(StringBackedEnum::Foo, $result); + } + + public function testParseStringValue(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, StringBackedEnum::class); + + $result = $type->parse('bar', null); + + $this->assertSame(StringBackedEnum::Bar, $result); + } + + public function testParseIntValue(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, IntBackedEnum::class); + + $result = $type->parse(2, null); + + $this->assertSame(IntBackedEnum::Two, $result); + } + + public function testParseInvalidTypeThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, StringBackedEnum::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Enum value'); + + $type->parse(12.5, null); + } + + public function testParseArrayThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, StringBackedEnum::class); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Enum value'); + + $type->parse(['value' => 'foo'], null); + } + + public function testSerializeStringBacked(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, StringBackedEnum::class); + + $result = $type->serialize(StringBackedEnum::Foo); + + $this->assertSame('foo', $result); + } + + public function testSerializeIntBacked(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new EnumValueType($marshaller, IntBackedEnum::class); + + $result = $type->serialize(IntBackedEnum::One); + + $this->assertSame(1, $result); + } + + public function testMakeRuleReturnsNullForNonEnumType(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + $this->assertNull(EnumValueType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('string', true, false); + $this->assertNull(EnumValueType::makeRule($property)); + } + + public function testMakeRuleForNonNullableEnum(): void + { + $property = $this->createPropertyWithType(StringBackedEnum::class, false, false); + + $rule = EnumValueType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(EnumValueType::class, $rule->type); + } + + public function testMakeRuleForNullableEnum(): void + { + $property = $this->createPropertyWithType(StringBackedEnum::class, false, true); + + $rule = EnumValueType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(EnumValueType::makeRule($property)); + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/NullableTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/NullableTypeTestCase.php new file mode 100644 index 000000000..07973063c --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/NullableTypeTestCase.php @@ -0,0 +1,63 @@ +createMock(MarshallerInterface::class); + $type = new NullableType($marshaller); + + $this->assertNull($type->parse(null, null)); + } + + public function testParseNonNullWithoutInnerTypeReturnsValue(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new NullableType($marshaller); + + $this->assertSame('hello', $type->parse('hello', null)); + } + + public function testParseNonNullWithInnerTypeDelegates(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new NullableType($marshaller, ArrayType::class); + + $this->assertSame([1, 2, 3], $type->parse([1, 2, 3], [])); + } + + public function testSerializeNullReturnsNull(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new NullableType($marshaller); + + $this->assertNull($type->serialize(null)); + } + + public function testSerializeNonNullWithoutInnerTypeReturnsValue(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new NullableType($marshaller); + + $this->assertSame('hello', $type->serialize('hello')); + } + + public function testSerializeNonNullWithInnerTypeDelegates(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new NullableType($marshaller, ArrayType::class); + + $this->assertSame([1, 2], $type->serialize([1, 2])); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/ObjectTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/ObjectTypeTestCase.php new file mode 100644 index 000000000..f9a585607 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/ObjectTypeTestCase.php @@ -0,0 +1,258 @@ +createReflectionNamedType('object', true); + $this->assertTrue(ObjectType::match($type)); + } + + public function testMatchNonBuiltinClass(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + $this->assertTrue(ObjectType::match($type)); + } + + public function testMatchReturnsFalseForBuiltinNonObject(): void + { + $type = $this->createReflectionNamedType('string', true); + $this->assertFalse(ObjectType::match($type)); + } + + public function testParseObjectInstanceOf(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller, \stdClass::class); + + $obj = (object) ['foo' => 'bar']; + $result = $type->parse($obj, null); + + $this->assertSame($obj, $result); + } + + public function testParseArrayIntoStdClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller, \stdClass::class); + + $result = $type->parse(['foo' => 'bar'], null); + + $this->assertInstanceOf(\stdClass::class, $result); + $this->assertSame('bar', $result->foo); + } + + public function testParseArrayIntoStdClassWithCurrent(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller, \stdClass::class); + + $current = (object) ['existing' => 'value']; + $result = $type->parse(['foo' => 'bar'], $current); + + $this->assertSame('bar', $result->foo); + } + + public function testParseArrayIntoTypedObjectUsesMarshaller(): void + { + $target = new class { + public string $foo = ''; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->with(['foo' => 'bar'], $this->isInstanceOf($targetClass)) + ->willReturnCallback(function (array $data, object $obj) { + $obj->foo = $data['foo']; + return $obj; + }); + + $type = new ObjectType($marshaller, $targetClass); + $result = $type->parse(['foo' => 'bar'], null); + + $this->assertSame('bar', $result->foo); + } + + public function testParseNullIntoTypedObject(): void + { + $target = new class { + public string $foo = 'default'; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->willReturnArgument(1); + + $type = new ObjectType($marshaller, $targetClass); + $result = $type->parse(null, null); + + $this->assertInstanceOf($targetClass, $result); + } + + public function testSerializeStdClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller, \stdClass::class); + + $result = $type->serialize((object) ['a' => 1, 'b' => 2]); + + $this->assertSame(['a' => 1, 'b' => 2], $result); + } + + public function testSerializeTypedObjectUsesMarshaller(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('marshal') + ->willReturn(['foo' => 'bar']); + + $target = new class { + public string $foo = 'bar'; + }; + $type = new ObjectType($marshaller, $target::class); + + $result = $type->serialize($target); + + $this->assertSame(['foo' => 'bar'], $result); + } + + public function testMakeRuleReturnsNullForBuiltinType(): void + { + $property = $this->createPropertyWithType('string', true, false); + $this->assertNull(ObjectType::makeRule($property)); + } + + public function testMakeRuleForNonNullableObject(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + + $rule = ObjectType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(ObjectType::class, $rule->type); + } + + public function testMakeRuleForNullableObject(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, true); + + $rule = ObjectType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(ObjectType::makeRule($property)); + } + + public function testDefaultClassIsStdClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller); + + $result = $type->parse(['x' => 1], null); + + $this->assertInstanceOf(\stdClass::class, $result); + $this->assertSame(1, $result->x); + } + + public function testParseWithExistingTypedObject(): void + { + $target = new class { + public string $foo = ''; + }; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->willReturnCallback(function (array $data, object $obj) { + $obj->foo = $data['foo']; + return $obj; + }); + + $type = new ObjectType($marshaller, $target::class); + $existing = clone $target; + $result = $type->parse(['foo' => 'updated'], $existing); + + $this->assertSame('updated', $result->foo); + } + + public function testDeprecatedInstanceMethodWithStdClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new ObjectType($marshaller, \stdClass::class); + + $method = new \ReflectionMethod($type, 'instance'); + $result = $method->invoke($type, ['a' => 1, 'b' => 2]); + + $this->assertInstanceOf(\stdClass::class, $result); + $this->assertSame(1, $result->a); + $this->assertSame(2, $result->b); + } + + public function testDeprecatedInstanceMethodWithTypedClass(): void + { + $target = new class { + public string $foo = ''; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->willReturnCallback(function (array $data, object $obj) { + $obj->foo = $data['foo']; + return $obj; + }); + + $type = new ObjectType($marshaller, $targetClass); + + $method = new \ReflectionMethod($type, 'instance'); + $result = $method->invoke($type, ['foo' => 'bar']); + + $this->assertInstanceOf($targetClass, $result); + $this->assertSame('bar', $result->foo); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/OneOfTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/OneOfTypeTestCase.php new file mode 100644 index 000000000..e7926f45d --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/OneOfTypeTestCase.php @@ -0,0 +1,193 @@ +createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, \stdClass::class, ['field' => \stdClass::class]); + + $obj = (object) ['foo' => 'bar']; + $result = $type->parse($obj, null); + + $this->assertSame($obj, $result); + } + + public function testParseNonArrayThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, \stdClass::class, ['field' => \stdClass::class]); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('must be a type of array'); + + $type->parse('string', null); + } + + public function testParseNullableWithNoCaseDetected(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, \stdClass::class, ['field' => \stdClass::class], nullable: true); + + $result = $type->parse(['unknown' => 'data'], null); + + $this->assertNull($result); + } + + public function testParseNonNullableWithNoCaseDetectedThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, \stdClass::class, ['field' => \stdClass::class], nullable: false); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to detect OneOf case'); + + $type->parse(['unknown' => 'data'], null); + } + + public function testParseNonNullableWithNoCaseShowsParentClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, \stdClass::class, ['field' => \stdClass::class], nullable: false); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('`stdClass`'); + + $type->parse(['unknown' => 'data'], null); + } + + public function testParseNonNullableWithoutParentClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, null, ['field' => \stdClass::class], nullable: false); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to detect OneOf case for non-nullable type.'); + + $type->parse(['unknown' => 'data'], null); + } + + public function testParseDetectedCaseUsesMarshaller(): void + { + $target = new class { + public string $name = ''; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->willReturnCallback(function (array $data, object $obj) { + $obj->name = $data['name']; + return $obj; + }); + + $type = new OneOfType($marshaller, null, ['typeA' => $targetClass]); + + $result = $type->parse(['typeA' => ['name' => 'test']], null); + + $this->assertInstanceOf($targetClass, $result); + $this->assertSame('test', $result->name); + } + + public function testParseDetectedCaseWithExistingCurrentOfSameClass(): void + { + $target = new class { + public string $name = ''; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('unmarshal') + ->willReturnCallback(function (array $data, object $obj) { + $obj->name = $data['name']; + return $obj; + }); + + $type = new OneOfType($marshaller, null, ['typeA' => $targetClass]); + + $existing = clone $target; + $result = $type->parse(['typeA' => ['name' => 'updated']], $existing); + + $this->assertSame($existing, $result); + $this->assertSame('updated', $result->name); + } + + public function testParseStdClassCase(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, null, ['typeA' => \stdClass::class]); + + $current = new \stdClass(); + $result = $type->parse(['typeA' => ['foo' => 'bar']], $current); + + $this->assertSame($current, $result); + $this->assertSame('bar', $result->foo); + } + + public function testSerializeNullableNull(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, null, ['typeA' => \stdClass::class], nullable: true); + + $result = $type->serialize(null); + + $this->assertSame([], $result); + } + + public function testSerializeNonObjectThrows(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, null, ['typeA' => \stdClass::class], nullable: false); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('must be a type of object'); + + $type->serialize('string'); + } + + public function testSerializeMatchingCase(): void + { + $target = new class { + public string $foo = 'bar'; + }; + $targetClass = $target::class; + + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->expects($this->once()) + ->method('marshal') + ->with($target) + ->willReturn(['foo' => 'bar']); + + $type = new OneOfType($marshaller, null, ['typeA' => $targetClass]); + + $result = $type->serialize($target); + + $this->assertSame(['typeA' => ['foo' => 'bar']], $result); + } + + public function testSerializeNoMatchingCaseThrowsTypeError(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new OneOfType($marshaller, null, ['typeA' => \stdClass::class]); + + $unregistered = new class {}; + + // serialize method doesn't return when no case matches, causing TypeError + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Passed value must be a type of one of the allowed classes, but class@anonymous given.'); + + $type->serialize($unregistered); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/Stub/IntBackedEnum.php b/tests/Unit/Internal/Marshaller/Type/Stub/IntBackedEnum.php new file mode 100644 index 000000000..aef5fdb42 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/Stub/IntBackedEnum.php @@ -0,0 +1,11 @@ +createMock(MarshallerInterface::class); + // NullableType with null argument creates a type with no inner type + $type = new NullableType($marshaller, null); + + // Parse should just return the value as-is (no inner type) + $this->assertSame('value', $type->parse('value', null)); + } + + public function testOfTypeWithTypeInterfaceClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + // ArrayType implements TypeInterface, so ofType should instantiate it directly + $type = new NullableType($marshaller, ArrayType::class); + + // Non-null value delegates to inner ArrayType + $this->assertSame([1, 2], $type->parse([1, 2], [])); + } + + public function testOfTypeWithNonTypeClass(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('unmarshal')->willReturnCallback( + fn(array $data, object $obj) => (object) $data, + ); + + // stdClass is not a TypeInterface, so ofType wraps it in ObjectType + $type = new NullableType($marshaller, \stdClass::class); + + $result = $type->parse(['foo' => 'bar'], null); + $this->assertInstanceOf(\stdClass::class, $result); + $this->assertSame('bar', $result->foo); + } + + public function testOfTypeWithMarshallingRule(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + + $rule = new MarshallingRule(type: ArrayType::class); + $type = new NullableType($marshaller, $rule); + + $this->assertSame([1, 2], $type->parse([1, 2], [])); + } + + public function testOfTypeWithMarshallingRuleNullType(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + + $rule = new MarshallingRule(type: null); + $type = new NullableType($marshaller, $rule); + + // When rule type is null, ofType returns null, and NullableType has no inner type + $this->assertSame('value', $type->parse('value', null)); + } + + public function testOfTypeWithMarshallingRuleAndArgs(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $marshaller->method('unmarshal')->willReturnCallback( + fn(array $data, object $obj) => (object) $data, + ); + + // MarshallingRule with of= argument passes it as constructor arg to the type + $rule = new MarshallingRule(type: ObjectType::class, of: \stdClass::class); + $type = new NullableType($marshaller, $rule); + + $result = $type->parse(['x' => 1], null); + $this->assertInstanceOf(\stdClass::class, $result); + } +} diff --git a/tests/Unit/Internal/Marshaller/Type/UuidTypeTestCase.php b/tests/Unit/Internal/Marshaller/Type/UuidTypeTestCase.php new file mode 100644 index 000000000..4a40c23d3 --- /dev/null +++ b/tests/Unit/Internal/Marshaller/Type/UuidTypeTestCase.php @@ -0,0 +1,125 @@ +createReflectionNamedType(UuidInterface::class, false); + $this->assertTrue(UuidType::match($type)); + } + + public function testMatchUuidV4(): void + { + $type = $this->createReflectionNamedType(UuidV4::class, false); + $this->assertTrue(UuidType::match($type)); + } + + public function testMatchReturnsFalseForBuiltin(): void + { + $type = $this->createReflectionNamedType('string', true); + $this->assertFalse(UuidType::match($type)); + } + + public function testMatchReturnsFalseForNonUuid(): void + { + $type = $this->createReflectionNamedType(\stdClass::class, false); + $this->assertFalse(UuidType::match($type)); + } + + public function testParse(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new UuidType($marshaller); + + $result = $type->parse('d1fb065d-f118-477d-a62a-ef93dc7ee03f', null); + + $this->assertInstanceOf(UuidInterface::class, $result); + $this->assertSame('d1fb065d-f118-477d-a62a-ef93dc7ee03f', $result->toString()); + } + + public function testSerialize(): void + { + $marshaller = $this->createMock(MarshallerInterface::class); + $type = new UuidType($marshaller); + + $uuid = UuidV4::fromString('d1fb065d-f118-477d-a62a-ef93dc7ee03f'); + $result = $type->serialize($uuid); + + $this->assertSame('d1fb065d-f118-477d-a62a-ef93dc7ee03f', $result); + } + + public function testMakeRuleReturnsNullForNonUuid(): void + { + $property = $this->createPropertyWithType(\stdClass::class, false, false); + $this->assertNull(UuidType::makeRule($property)); + } + + public function testMakeRuleForNonNullableUuid(): void + { + $property = $this->createPropertyWithType(UuidInterface::class, false, false); + + $rule = UuidType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(UuidType::class, $rule->type); + } + + public function testMakeRuleForNullableUuid(): void + { + $property = $this->createPropertyWithType(UuidInterface::class, false, true); + + $rule = UuidType::makeRule($property); + + $this->assertNotNull($rule); + $this->assertSame(NullableType::class, $rule->type); + } + + public function testMakeRuleReturnsNullForBuiltin(): void + { + $property = $this->createPropertyWithType('string', true, false); + $this->assertNull(UuidType::makeRule($property)); + } + + public function testMakeRuleReturnsNullForNonReflectionNamedType(): void + { + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn(null); + + $this->assertNull(UuidType::makeRule($property)); + } + + private function createReflectionNamedType(string $name, bool $isBuiltin): \ReflectionNamedType + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($name); + $type->method('isBuiltin')->willReturn($isBuiltin); + return $type; + } + + private function createPropertyWithType(string $typeName, bool $isBuiltin, bool $allowsNull): \ReflectionProperty + { + $type = $this->createMock(\ReflectionNamedType::class); + $type->method('getName')->willReturn($typeName); + $type->method('isBuiltin')->willReturn($isBuiltin); + $type->method('allowsNull')->willReturn($allowsNull); + + $property = $this->createMock(\ReflectionProperty::class); + $property->method('getType')->willReturn($type); + $property->method('getName')->willReturn('test'); + + return $property; + } +} diff --git a/tests/Unit/Internal/Support/DateIntervalTestCase.php b/tests/Unit/Internal/Support/DateIntervalTestCase.php index 270b7dcac..dd203729e 100644 --- a/tests/Unit/Internal/Support/DateIntervalTestCase.php +++ b/tests/Unit/Internal/Support/DateIntervalTestCase.php @@ -92,6 +92,109 @@ public function testParseFromDuration(): void self::assertSame(123_456, $i->microseconds); } + public function testParseWithFractionalHours(): void + { + $i = DateInterval::parse(1.5, DateInterval::FORMAT_HOURS); + + $this->assertSame('0/1/30/0', $i->format('%d/%h/%i/%s')); + } + + public function testParseWithFractionalDays(): void + { + $i = DateInterval::parse(1.5, DateInterval::FORMAT_DAYS); + + $this->assertSame('1/12/0/0', $i->format('%d/%h/%i/%s')); + } + + public function testParseWithFractionalWeeks(): void + { + $i = DateInterval::parse(0.5, DateInterval::FORMAT_WEEKS); + + $this->assertSame('3/12/0/0', $i->format('%d/%h/%i/%s')); + } + + public function testParseInvalidType(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unrecognized date time interval format'); + + DateInterval::parse(new \stdClass()); + } + + public function testParseInvalidFormat(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid date interval format'); + + DateInterval::parse(100, 'invalid_format'); + } + + public function testToDurationWithNullEmptyAndEmptyInterval(): void + { + $interval = new \DateInterval('PT0S'); + $result = DateInterval::toDuration($interval, nullEmpty: true); + + $this->assertNull($result); + } + + public function testToDurationWithNullEmptyAndNonEmptyInterval(): void + { + $interval = new \DateInterval('PT5S'); + $result = DateInterval::toDuration($interval, nullEmpty: true); + + $this->assertNotNull($result); + $this->assertSame(5, $result->getSeconds()); + } + + public function testToDurationWithNull(): void + { + $result = DateInterval::toDuration(null); + + $this->assertNull($result); + } + + public function testParseOrNullReturnsNullForNull(): void + { + $this->assertNull(DateInterval::parseOrNull(null)); + } + + public function testParseOrNullReturnsIntervalForValue(): void + { + $result = DateInterval::parseOrNull(5, DateInterval::FORMAT_SECONDS); + + $this->assertInstanceOf(\Carbon\CarbonInterval::class, $result); + } + + public function testAssertReturnsTrueForString(): void + { + $this->assertTrue(DateInterval::assert('PT5S')); + } + + public function testAssertReturnsTrueForInt(): void + { + $this->assertTrue(DateInterval::assert(5)); + } + + public function testAssertReturnsTrueForFloat(): void + { + $this->assertTrue(DateInterval::assert(1.5)); + } + + public function testAssertReturnsTrueForDateInterval(): void + { + $this->assertTrue(DateInterval::assert(new \DateInterval('PT5S'))); + } + + public function testAssertReturnsFalseForNull(): void + { + $this->assertFalse(DateInterval::assert(null)); + } + + public function testAssertReturnsFalseForArray(): void + { + $this->assertFalse(DateInterval::assert([])); + } + #[DataProvider('provideIso8601DurationFormats')] public function testParseDetectsIso8601FormatCorrectly(string $interval, bool $shouldBeIso8601): void {