From 72ffc27919a150cba7848bd2a471cddd1f2d21a2 Mon Sep 17 00:00:00 2001 From: dcb6 Date: Mon, 30 Sep 2024 19:56:41 -0400 Subject: [PATCH 1/3] done --- generators/php/codegen/src/AsIs.ts | 1 + .../codegen/src/asIs/JsonDecoder.Template.php | 42 +++++-- .../src/asIs/JsonDeserializer.Template.php | 37 ++++-- .../src/asIs/JsonSerializer.Template.php | 33 +++-- .../src/asIs/SerializableType.Template.php | 16 ++- .../php/codegen/src/asIs/Union.Template.php | 55 ++++++-- .../asIs/UnionPropertyTypeTest.Template.php | 118 ++++++++++++++++++ generators/php/codegen/src/ast/Attribute.ts | 7 +- generators/php/codegen/src/ast/Type.ts | 24 +++- .../context/AbstractPhpGeneratorContext.ts | 3 +- .../codegen/src/context/PhpAttributeMapper.ts | 55 ++++++-- .../php/codegen/src/context/PhpTypeMapper.ts | 11 +- .../endpoint/http/HttpEndpointGenerator.ts | 34 ++++- .../src/endpoint/request/EndpointRequest.ts | 2 +- .../php-sdk/examples/src/Core/JsonDecoder.php | 13 ++ .../examples/src/Core/JsonDeserializer.php | 37 ++++-- .../examples/src/Core/JsonSerializer.php | 33 +++-- .../examples/src/Core/SerializableType.php | 14 +++ seed/php-sdk/examples/src/Core/Union.php | 49 +++++++- .../php-sdk/examples/src/Types/Identifier.php | 6 +- .../examples/src/Types/Types/Entity.php | 8 +- .../examples/src/Types/Types/ResponseType.php | 8 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 +++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../undiscriminated-unions/src/Core/Union.php | 49 +++++++- .../src/Union/UnionClient.php | 14 ++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 +++++++++++++++++ 30 files changed, 878 insertions(+), 120 deletions(-) create mode 100644 generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php create mode 100644 seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php diff --git a/generators/php/codegen/src/AsIs.ts b/generators/php/codegen/src/AsIs.ts index 3a496d31d8..afe44fb227 100644 --- a/generators/php/codegen/src/AsIs.ts +++ b/generators/php/codegen/src/AsIs.ts @@ -16,6 +16,7 @@ export enum AsIsFiles { NestedUnionArrayTypeTest = "NestedUnionArrayTypeTest.Template.php", NullableArrayTypeTest = "NullableArrayTypeTest.Template.php", NullPropertyTypeTest = "NullPropertyTypeTest.Template.php", + UnionPropertyTypeTest = "UnionPropertyTypeTest.Template.php", ScalarTypesTest = "ScalarTypesTest.Template.php", EnumTest = "EnumTest.Template.php", TestTypeTest = "TestTypeTest.Template.php", diff --git a/generators/php/codegen/src/asIs/JsonDecoder.Template.php b/generators/php/codegen/src/asIs/JsonDecoder.Template.php index cc4238be49..e34af48812 100644 --- a/generators/php/codegen/src/asIs/JsonDecoder.Template.php +++ b/generators/php/codegen/src/asIs/JsonDecoder.Template.php @@ -15,7 +15,8 @@ class JsonDecoder * @return string The decoded string. * @throws JsonException If the decoded value is not a string. */ - public static function decodeString(string $json): string { + public static function decodeString(string $json): string + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value: " . $json); @@ -30,7 +31,8 @@ public static function decodeString(string $json): string { * @return bool The decoded boolean. * @throws JsonException If the decoded value is not a boolean. */ - public static function decodeBool(string $json): bool { + public static function decodeBool(string $json): bool + { $decoded = self::decode($json); if (!is_bool($decoded)) { throw new JsonException("Unexpected non-boolean json value: " . $json); @@ -45,7 +47,8 @@ public static function decodeBool(string $json): bool { * @return DateTime The decoded DateTime object. * @throws JsonException If the decoded value is not a valid datetime string. */ - public static function decodeDateTime(string $json): DateTime { + public static function decodeDateTime(string $json): DateTime + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value for datetime: " . $json); @@ -60,7 +63,8 @@ public static function decodeDateTime(string $json): DateTime { * @return DateTime The decoded DateTime object. * @throws JsonException If the decoded value is not a valid date string. */ - public static function decodeDate(string $json): DateTime { + public static function decodeDate(string $json): DateTime + { $decoded = self::decode($json); if (!is_string($decoded)) { throw new JsonException("Unexpected non-string json value for date: " . $json); @@ -75,7 +79,8 @@ public static function decodeDate(string $json): DateTime { * @return float The decoded float. * @throws JsonException If the decoded value is not a float. */ - public static function decodeFloat(string $json): float { + public static function decodeFloat(string $json): float + { $decoded = self::decode($json); if (!is_float($decoded)) { throw new JsonException("Unexpected non-float json value: " . $json); @@ -90,7 +95,8 @@ public static function decodeFloat(string $json): float { * @return int The decoded integer. * @throws JsonException If the decoded value is not an integer. */ - public static function decodeInt(string $json): int { + public static function decodeInt(string $json): int + { $decoded = self::decode($json); if (!is_int($decoded)) { throw new JsonException("Unexpected non-integer json value: " . $json); @@ -106,7 +112,8 @@ public static function decodeInt(string $json): int { * @return mixed[]|array The deserialized array. * @throws JsonException If the decoded value is not an array. */ - public static function decodeArray(string $json, array $type): array { + public static function decodeArray(string $json, array $type): array + { $decoded = self::decode($json); if (!is_array($decoded)) { throw new JsonException("Unexpected non-array json value: " . $json); @@ -114,6 +121,19 @@ public static function decodeArray(string $json, array $type): array { return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * @@ -121,7 +141,8 @@ public static function decodeArray(string $json, array $type): array { * @return mixed The decoded mixed. * @throws JsonException If the decoded value is not an mixed. */ - public static function decodeMixed(string $json): mixed { + public static function decodeMixed(string $json): mixed + { return self::decode($json); } @@ -132,7 +153,8 @@ public static function decodeMixed(string $json): mixed { * @return mixed The decoded value. * @throws JsonException If an error occurs during JSON decoding. */ - public static function decode(string $json): mixed { + public static function decode(string $json): mixed + { return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/JsonDeserializer.Template.php b/generators/php/codegen/src/asIs/JsonDeserializer.Template.php index 987e816467..f0a4229011 100644 --- a/generators/php/codegen/src/asIs/JsonDeserializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonDeserializer.Template.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/generators/php/codegen/src/asIs/JsonSerializer.Template.php b/generators/php/codegen/src/asIs/JsonSerializer.Template.php index 27ce301c90..bd04619680 100644 --- a/generators/php/codegen/src/asIs/JsonSerializer.Template.php +++ b/generators/php/codegen/src/asIs/JsonSerializer.Template.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/generators/php/codegen/src/asIs/SerializableType.Template.php b/generators/php/codegen/src/asIs/SerializableType.Template.php index 39acdc2b8c..6abe274c4c 100644 --- a/generators/php/codegen/src/asIs/SerializableType.Template.php +++ b/generators/php/codegen/src/asIs/SerializableType.Template.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { @@ -162,4 +176,4 @@ private static function getJsonKey(ReflectionProperty $property): ?string $jsonPropertyAttr = $property->getAttributes(JsonProperty::class)[0] ?? null; return $jsonPropertyAttr?->newInstance()?->name; } -} \ No newline at end of file +} diff --git a/generators/php/codegen/src/asIs/Union.Template.php b/generators/php/codegen/src/asIs/Union.Template.php index 891e1788f9..78aa129b4c 100644 --- a/generators/php/codegen/src/asIs/Union.Template.php +++ b/generators/php/codegen/src/asIs/Union.Template.php @@ -2,20 +2,61 @@ namespace <%= coreNamespace%>; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } - public function __toString(): string { - return implode(' | ', $this->types); + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php b/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php new file mode 100644 index 0000000000..4377a6fee9 --- /dev/null +++ b/generators/php/codegen/src/asIs/UnionPropertyTypeTest.Template.php @@ -0,0 +1,118 @@ +; + +use PHPUnit\Framework\TestCase; +use <%= coreNamespace%>\JsonProperty; +use <%= coreNamespace%>\SerializableType; +use <%= coreNamespace%>\Union; + +class UnionPropertyType extends SerializableType +{ + + #[Union(new Union('string', 'integer'), 'null', ['integer' => 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) + { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} \ No newline at end of file diff --git a/generators/php/codegen/src/ast/Attribute.ts b/generators/php/codegen/src/ast/Attribute.ts index 102dc26bd0..5b5d1b45d5 100644 --- a/generators/php/codegen/src/ast/Attribute.ts +++ b/generators/php/codegen/src/ast/Attribute.ts @@ -26,13 +26,16 @@ export class Attribute extends AstNode { writer.write(`${this.reference.name}`); if (this.arguments.length > 0) { writer.write("("); - for (const argument of this.arguments) { + this.arguments.forEach((argument, index) => { + if (index > 0) { + writer.write(","); + } if (typeof argument === "string") { writer.write(argument); } else { argument.write(writer); } - } + }); writer.write(")"); } } diff --git a/generators/php/codegen/src/ast/Type.ts b/generators/php/codegen/src/ast/Type.ts index 64e8866d99..ba3fd62da3 100644 --- a/generators/php/codegen/src/ast/Type.ts +++ b/generators/php/codegen/src/ast/Type.ts @@ -176,14 +176,30 @@ export class Type extends AstNode { writer.write("}"); break; } - case "union": - this.internalType.types.forEach((type, index) => { + case "union": { + let index = 0; + const typeStrings = new Set(); + for (const type of this.internalType.types) { + if (!comment) { + const typeString = type.toString({ + namespace: writer.namespace, + rootNamespace: writer.rootNamespace, + customConfig: writer.customConfig + }); + // handle potential duplicates, such as strings (due to enums) and arrays + if (typeStrings.has(typeString)) { + continue; + } + typeStrings.add(typeString); + } if (index > 0) { writer.write("|"); } - type.write(writer); - }); + type.write(writer, { comment }); + index++; + } break; + } case "optional": { const isUnion = this.internalType.value.internalType.type === "union"; if (!isUnion) { diff --git a/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts b/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts index b47f704338..d8678d182d 100644 --- a/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts +++ b/generators/php/codegen/src/context/AbstractPhpGeneratorContext.ts @@ -296,7 +296,8 @@ export abstract class AbstractPhpGeneratorContext< AsIsFiles.ScalarTypesTest, AsIsFiles.TestTypeTest, AsIsFiles.UnionArrayTypeTest, - AsIsFiles.EnumTest + AsIsFiles.EnumTest, + AsIsFiles.UnionPropertyTypeTest ]; } diff --git a/generators/php/codegen/src/context/PhpAttributeMapper.ts b/generators/php/codegen/src/context/PhpAttributeMapper.ts index 750840c7b8..4af3b4f27a 100644 --- a/generators/php/codegen/src/context/PhpAttributeMapper.ts +++ b/generators/php/codegen/src/context/PhpAttributeMapper.ts @@ -1,7 +1,11 @@ import { assertNever } from "@fern-api/core-utils"; +import { Arguments, UnnamedArgument } from "@fern-api/generator-commons"; import { ObjectProperty } from "@fern-fern/ir-sdk/api"; +import { isEqual, uniq, uniqWith } from "lodash-es"; import { php } from ".."; +import { ClassInstantiation } from "../ast"; import { BasePhpCustomConfigSchema } from "../custom-config/BasePhpCustomConfigSchema"; +import { parameter } from "../php"; import { AbstractPhpGeneratorContext } from "./AbstractPhpGeneratorContext"; export declare namespace PhpAttributeMapper { @@ -41,14 +45,41 @@ export class PhpAttributeMapper { attributes.push( php.attribute({ reference: this.context.getArrayTypeClassReference(), - arguments: [this.getArrayTypeAttributeArgument(type.underlyingType())] + arguments: [this.getTypeAttributeArgument(type.underlyingType())] }) ); } + if (underlyingInternalType.type === "union") { + const unionTypeParameters = this.getUnionTypeParameters(underlyingInternalType.types); + // only add the attribute if deduping in getUnionTypeParameters resulted in more than one type + if (unionTypeParameters.length > 1) { + attributes.push( + php.attribute({ + reference: this.context.getUnionClassReference(), + arguments: this.getUnionTypeParameters(underlyingInternalType.types) + }) + ); + } + } return attributes; } - public getArrayTypeAttributeArgument(type: php.Type): php.AstNode { + public getUnionTypeClassRepresentation(arguments_: php.AstNode[]): ClassInstantiation { + return php.instantiateClass({ + classReference: this.context.getUnionClassReference(), + arguments_ + }); + } + + public getUnionTypeParameters(types: php.Type[]): php.AstNode[] { + // remove duplicates, such as "string" and "string" if enums and strings are both in the union + return uniqWith( + types.map((type) => this.getTypeAttributeArgument(type)), + isEqual + ); + } + + public getTypeAttributeArgument(type: php.Type): php.AstNode { switch (type.internalType.type) { case "int": return php.codeblock("'integer'"); @@ -69,14 +100,14 @@ export class PhpAttributeMapper { return php.codeblock("'object'"); case "array": return php.array({ - entries: [this.getArrayTypeAttributeArgument(type.internalType.value)] + entries: [this.getTypeAttributeArgument(type.internalType.value)] }); case "map": { return php.map({ entries: [ { - key: this.getArrayTypeAttributeArgument(type.internalType.keyType), - value: this.getArrayTypeAttributeArgument(type.internalType.valueType) + key: this.getTypeAttributeArgument(type.internalType.keyType), + value: this.getTypeAttributeArgument(type.internalType.valueType) } ] }); @@ -92,17 +123,17 @@ export class PhpAttributeMapper { }); } case "union": { - return php.instantiateClass({ - classReference: this.context.getUnionClassReference(), - arguments_: type.internalType.types.map((unionType) => - this.getArrayTypeAttributeArgument(unionType) - ) - }); + const unionTypeParameters = this.getUnionTypeParameters(type.internalType.types); + // dedupe in getUnionTypeParameters could result in a single value + if (unionTypeParameters.length === 1) { + return unionTypeParameters[0]!; + } + return this.getUnionTypeClassRepresentation(unionTypeParameters); } case "optional": return php.instantiateClass({ classReference: this.context.getUnionClassReference(), - arguments_: [this.getArrayTypeAttributeArgument(type.internalType.value), php.codeblock("'null'")] + arguments_: [this.getTypeAttributeArgument(type.internalType.value), php.codeblock("'null'")] }); case "reference": { const reference = type.internalType.value; diff --git a/generators/php/codegen/src/context/PhpTypeMapper.ts b/generators/php/codegen/src/context/PhpTypeMapper.ts index cddb80ddea..baab3e63be 100644 --- a/generators/php/codegen/src/context/PhpTypeMapper.ts +++ b/generators/php/codegen/src/context/PhpTypeMapper.ts @@ -9,6 +9,7 @@ import { TypeId, TypeReference } from "@fern-fern/ir-sdk/api"; +import { isEqual, uniqWith } from "lodash-es"; import { php } from "../"; import { ClassReference, Type } from "../ast"; import { BasePhpCustomConfigSchema } from "../custom-config/BasePhpCustomConfigSchema"; @@ -118,7 +119,15 @@ export class PhpTypeMapper { case "union": return php.Type.mixed(); case "undiscriminatedUnion": { - return php.Type.mixed(); + return php.Type.union( + // need to dedupe because lists and sets are both represented as array, + uniqWith( + typeDeclaration.shape.members.map((member) => + this.convert({ reference: member.type, preserveEnums }) + ), + isEqual + ) + ); } default: assertNever(typeDeclaration.shape); diff --git a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts index 065b51fb50..43629e63c9 100644 --- a/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts +++ b/generators/php/sdk/src/endpoint/http/HttpEndpointGenerator.ts @@ -221,6 +221,10 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { methodSuffix: "String" }); case "union": + return this.decodeJsonResponseForUnion({ + arguments_, + types: internalType.types + }); case "object": case "optional": case "typeDict": @@ -259,7 +263,7 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { php.invokeMethod({ on: this.context.getJsonDecoderClassReference(), method: "decodeArray", - arguments_: [...arguments_, this.context.phpAttributeMapper.getArrayTypeAttributeArgument(type)], + arguments_: [...arguments_, this.context.phpAttributeMapper.getTypeAttributeArgument(type)], static_: true }) ); @@ -291,6 +295,34 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator { }); } + private decodeJsonResponseForUnion({ + arguments_, + types + }: { + arguments_: UnnamedArgument[]; + types: php.Type[]; + }): php.CodeBlock { + const unionTypeParameters = this.context.phpAttributeMapper.getUnionTypeParameters(types); + // if deduping in getUnionTypeParameters results in one type, treat it like just that type + if (unionTypeParameters.length === 1) { + return this.decodeJsonResponse(types[0]); + } + return php.codeblock((writer) => { + writer.writeNode( + php.invokeMethod({ + on: this.context.getJsonDecoderClassReference(), + method: "decodeUnion", + arguments_: [ + ...arguments_, + this.context.phpAttributeMapper.getUnionTypeClassRepresentation(unionTypeParameters) + ], + static_: true + }) + ); + writer.writeLine("; // @phpstan-ignore-line"); + }); + } + private getResponseBodyContent(): php.CodeBlock { return php.codeblock((writer) => { writer.write(`${JSON_VARIABLE_NAME} = ${RESPONSE_VARIABLE_NAME}->getBody()->getContents()`); diff --git a/generators/php/sdk/src/endpoint/request/EndpointRequest.ts b/generators/php/sdk/src/endpoint/request/EndpointRequest.ts index 39997730a2..6c49515f96 100644 --- a/generators/php/sdk/src/endpoint/request/EndpointRequest.ts +++ b/generators/php/sdk/src/endpoint/request/EndpointRequest.ts @@ -91,7 +91,7 @@ export abstract class EndpointRequest { methodInvocation: php.invokeMethod({ on: this.context.getJsonSerializerClassReference(), method: "serializeArray", - arguments_: [bodyArgument, this.context.phpAttributeMapper.getArrayTypeAttributeArgument(type)], + arguments_: [bodyArgument, this.context.phpAttributeMapper.getTypeAttributeArgument(type)], static_: true }), isOptional diff --git a/seed/php-sdk/examples/src/Core/JsonDecoder.php b/seed/php-sdk/examples/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/examples/src/Core/JsonDecoder.php +++ b/seed/php-sdk/examples/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/examples/src/Core/JsonDeserializer.php b/seed/php-sdk/examples/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/examples/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/examples/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/examples/src/Core/JsonSerializer.php b/seed/php-sdk/examples/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/examples/src/Core/JsonSerializer.php +++ b/seed/php-sdk/examples/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/examples/src/Core/SerializableType.php b/seed/php-sdk/examples/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/examples/src/Core/SerializableType.php +++ b/seed/php-sdk/examples/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/examples/src/Core/Union.php b/seed/php-sdk/examples/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/examples/src/Core/Union.php +++ b/seed/php-sdk/examples/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/examples/src/Types/Identifier.php b/seed/php-sdk/examples/src/Types/Identifier.php index 891355cc1f..efcb8cc827 100644 --- a/seed/php-sdk/examples/src/Types/Identifier.php +++ b/seed/php-sdk/examples/src/Types/Identifier.php @@ -8,10 +8,10 @@ class Identifier extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $value @@ -27,7 +27,7 @@ class Identifier extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * value: string, * label: string, * } $values diff --git a/seed/php-sdk/examples/src/Types/Types/Entity.php b/seed/php-sdk/examples/src/Types/Types/Entity.php index 7316bb6b39..830aef6b2f 100644 --- a/seed/php-sdk/examples/src/Types/Types/Entity.php +++ b/seed/php-sdk/examples/src/Types/Types/Entity.php @@ -3,15 +3,17 @@ namespace Seed\Types\Types; use Seed\Core\SerializableType; +use Seed\Types\BasicType; +use Seed\Types\ComplexType; use Seed\Core\JsonProperty; class Entity extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $name @@ -21,7 +23,7 @@ class Entity extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * name: string, * } $values */ diff --git a/seed/php-sdk/examples/src/Types/Types/ResponseType.php b/seed/php-sdk/examples/src/Types/Types/ResponseType.php index 48d032f4eb..71faed3cee 100644 --- a/seed/php-sdk/examples/src/Types/Types/ResponseType.php +++ b/seed/php-sdk/examples/src/Types/Types/ResponseType.php @@ -3,19 +3,21 @@ namespace Seed\Types\Types; use Seed\Core\SerializableType; +use Seed\Types\BasicType; +use Seed\Types\ComplexType; use Seed\Core\JsonProperty; class ResponseType extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @param array{ - * type: mixed, + * type: value-of|value-of, * } $values */ public function __construct( diff --git a/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-sdk/undiscriminated-unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php b/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php index fe592455cb..c707e452fd 100644 --- a/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php +++ b/seed/php-sdk/undiscriminated-unions/src/Union/UnionClient.php @@ -8,8 +8,10 @@ use Seed\Core\JsonApiRequest; use Seed\Core\HttpMethod; use Seed\Core\JsonDecoder; +use Seed\Core\Union; use JsonException; use Psr\Http\Client\ClientExceptionInterface; +use Seed\Union\Types\KeyType; class UnionClient { @@ -28,15 +30,15 @@ public function __construct( } /** - * @param mixed $request + * @param string|array|int|array|array> $request * @param ?array{ * baseUrl?: string, * } $options - * @return mixed + * @return string|array|int|array|array> * @throws SeedException * @throws SeedApiException */ - public function get(mixed $request, ?array $options = null): mixed + public function get(string|array|int $request, ?array $options = null): string|array|int { try { $response = $this->client->sendRequest( @@ -50,7 +52,7 @@ public function get(mixed $request, ?array $options = null): mixed $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 400) { $json = $response->getBody()->getContents(); - return JsonDecoder::decodeMixed($json); + return JsonDecoder::decodeUnion($json, new Union('string', ['string'], 'integer', ['integer'], [['integer']])); // @phpstan-ignore-line } } catch (JsonException $e) { throw new SeedException(message: "Failed to deserialize response: {$e->getMessage()}", previous: $e); @@ -68,7 +70,7 @@ public function get(mixed $request, ?array $options = null): mixed * @param ?array{ * baseUrl?: string, * } $options - * @return array + * @return array|string, string> * @throws SeedException * @throws SeedApiException */ @@ -85,7 +87,7 @@ public function getMetadata(?array $options = null): array $statusCode = $response->getStatusCode(); if ($statusCode >= 200 && $statusCode < 400) { $json = $response->getBody()->getContents(); - return JsonDecoder::decodeArray($json, ['mixed' => 'string']); // @phpstan-ignore-line + return JsonDecoder::decodeArray($json, ['string' => 'string']); // @phpstan-ignore-line } } catch (JsonException $e) { throw new SeedException(message: "Failed to deserialize response: {$e->getMessage()}", previous: $e); diff --git a/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} From 4e4e491650f70f6f3e23732ea7dd61972a470676 Mon Sep 17 00:00:00 2001 From: dcb6 Date: Mon, 30 Sep 2024 19:58:27 -0400 Subject: [PATCH 2/3] seed --- .../alias-extends/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../alias-extends/src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../alias-extends/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-model/alias/src/Core/JsonDecoder.php | 13 ++ .../alias/src/Core/JsonDeserializer.php | 37 ++++-- .../alias/src/Core/JsonSerializer.php | 33 +++-- .../alias/src/Core/SerializableType.php | 14 +++ seed/php-model/alias/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../any-auth/src/Core/JsonDecoder.php | 13 ++ .../any-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../any-auth/src/Core/JsonSerializer.php | 33 +++-- .../any-auth/src/Core/SerializableType.php | 14 +++ seed/php-model/any-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../api-wide-base-path/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../audiences/src/Core/JsonDecoder.php | 13 ++ .../audiences/src/Core/JsonDeserializer.php | 37 ++++-- .../audiences/src/Core/JsonSerializer.php | 33 +++-- .../audiences/src/Core/SerializableType.php | 14 +++ seed/php-model/audiences/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../basic-auth/src/Core/JsonDecoder.php | 13 ++ .../basic-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../basic-auth/src/Core/JsonSerializer.php | 33 +++-- .../basic-auth/src/Core/SerializableType.php | 14 +++ seed/php-model/basic-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-model/bytes/src/Core/JsonDecoder.php | 13 ++ .../bytes/src/Core/JsonDeserializer.php | 37 ++++-- .../bytes/src/Core/JsonSerializer.php | 33 +++-- .../bytes/src/Core/SerializableType.php | 14 +++ seed/php-model/bytes/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../circular-references/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../custom-auth/src/Core/JsonDecoder.php | 13 ++ .../custom-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../custom-auth/src/Core/JsonSerializer.php | 33 +++-- .../custom-auth/src/Core/SerializableType.php | 14 +++ seed/php-model/custom-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-model/enum/src/Core/JsonDecoder.php | 13 ++ .../enum/src/Core/JsonDeserializer.php | 37 ++++-- .../enum/src/Core/JsonSerializer.php | 33 +++-- .../enum/src/Core/SerializableType.php | 14 +++ seed/php-model/enum/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../error-property/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../error-property/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../examples/src/Core/JsonDecoder.php | 13 ++ .../examples/src/Core/JsonDeserializer.php | 37 ++++-- .../examples/src/Core/JsonSerializer.php | 33 +++-- .../examples/src/Core/SerializableType.php | 14 +++ seed/php-model/examples/src/Core/Union.php | 49 +++++++- seed/php-model/examples/src/Identifier.php | 6 +- seed/php-model/examples/src/Types/Entity.php | 8 +- .../examples/src/Types/ResponseType.php | 8 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../exhaustive/src/Core/JsonDecoder.php | 13 ++ .../exhaustive/src/Core/JsonDeserializer.php | 37 ++++-- .../exhaustive/src/Core/JsonSerializer.php | 33 +++-- .../exhaustive/src/Core/SerializableType.php | 14 +++ seed/php-model/exhaustive/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../extends/src/Core/JsonDecoder.php | 13 ++ .../extends/src/Core/JsonDeserializer.php | 37 ++++-- .../extends/src/Core/JsonSerializer.php | 33 +++-- .../extends/src/Core/SerializableType.php | 14 +++ seed/php-model/extends/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../extra-properties/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../extra-properties/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../file-download/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../file-download/src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../file-download/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../file-upload/src/Core/JsonDecoder.php | 13 ++ .../file-upload/src/Core/JsonDeserializer.php | 37 ++++-- .../file-upload/src/Core/JsonSerializer.php | 33 +++-- .../file-upload/src/Core/SerializableType.php | 14 +++ seed/php-model/file-upload/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../folders/src/Core/JsonDecoder.php | 13 ++ .../folders/src/Core/JsonDeserializer.php | 37 ++++-- .../folders/src/Core/JsonSerializer.php | 33 +++-- .../folders/src/Core/SerializableType.php | 14 +++ seed/php-model/folders/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../grpc-proto-exhaustive/src/Column.php | 11 +- .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../grpc-proto-exhaustive/src/Core/Union.php | 49 +++++++- .../grpc-proto-exhaustive/src/QueryColumn.php | 11 +- .../src/ScoredColumn.php | 11 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../grpc-proto/src/Core/JsonDecoder.php | 13 ++ .../grpc-proto/src/Core/JsonDeserializer.php | 37 ++++-- .../grpc-proto/src/Core/JsonSerializer.php | 33 +++-- .../grpc-proto/src/Core/SerializableType.php | 14 +++ seed/php-model/grpc-proto/src/Core/Union.php | 49 +++++++- seed/php-model/grpc-proto/src/UserModel.php | 13 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../idempotency-headers/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-model/imdb/src/Core/JsonDecoder.php | 13 ++ .../imdb/src/Core/JsonDeserializer.php | 37 ++++-- .../imdb/src/Core/JsonSerializer.php | 33 +++-- .../imdb/src/Core/SerializableType.php | 14 +++ seed/php-model/imdb/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../literal/src/Core/JsonDecoder.php | 13 ++ .../literal/src/Core/JsonDeserializer.php | 37 ++++-- .../literal/src/Core/JsonSerializer.php | 33 +++-- .../literal/src/Core/SerializableType.php | 14 +++ seed/php-model/literal/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../mixed-case/src/Core/JsonDecoder.php | 13 ++ .../mixed-case/src/Core/JsonDeserializer.php | 37 ++++-- .../mixed-case/src/Core/JsonSerializer.php | 33 +++-- .../mixed-case/src/Core/SerializableType.php | 14 +++ seed/php-model/mixed-case/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../mixed-file-directory/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../multi-line-docs/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../multi-line-docs/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../multi-url-environment/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../no-environment/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../no-environment/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../php-model/object/src/Core/JsonDecoder.php | 13 ++ .../object/src/Core/JsonDeserializer.php | 37 ++++-- .../object/src/Core/JsonSerializer.php | 33 +++-- .../object/src/Core/SerializableType.php | 14 +++ seed/php-model/object/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../objects-with-imports/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../optional/src/Core/JsonDecoder.php | 13 ++ .../optional/src/Core/JsonDeserializer.php | 37 ++++-- .../optional/src/Core/JsonSerializer.php | 33 +++-- .../optional/src/Core/SerializableType.php | 14 +++ seed/php-model/optional/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../package-yml/src/Core/JsonDecoder.php | 13 ++ .../package-yml/src/Core/JsonDeserializer.php | 37 ++++-- .../package-yml/src/Core/JsonSerializer.php | 33 +++-- .../package-yml/src/Core/SerializableType.php | 14 +++ seed/php-model/package-yml/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../pagination/src/Core/JsonDecoder.php | 13 ++ .../pagination/src/Core/JsonDeserializer.php | 37 ++++-- .../pagination/src/Core/JsonSerializer.php | 33 +++-- .../pagination/src/Core/SerializableType.php | 14 +++ seed/php-model/pagination/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../plain-text/src/Core/JsonDecoder.php | 13 ++ .../plain-text/src/Core/JsonDeserializer.php | 37 ++++-- .../plain-text/src/Core/JsonSerializer.php | 33 +++-- .../plain-text/src/Core/SerializableType.php | 14 +++ seed/php-model/plain-text/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../query-parameters/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../query-parameters/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../reserved-keywords/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../response-property/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../simple-fhir/src/BaseResource.php | 7 +- .../simple-fhir/src/Core/JsonDecoder.php | 13 ++ .../simple-fhir/src/Core/JsonDeserializer.php | 37 ++++-- .../simple-fhir/src/Core/JsonSerializer.php | 33 +++-- .../simple-fhir/src/Core/SerializableType.php | 14 +++ seed/php-model/simple-fhir/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../streaming-parameter/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../streaming/src/Core/JsonDecoder.php | 13 ++ .../streaming/src/Core/JsonDeserializer.php | 37 ++++-- .../streaming/src/Core/JsonSerializer.php | 33 +++-- .../streaming/src/Core/SerializableType.php | 14 +++ seed/php-model/streaming/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-model/trace/src/Core/JsonDecoder.php | 13 ++ .../trace/src/Core/JsonDeserializer.php | 37 ++++-- .../trace/src/Core/JsonSerializer.php | 33 +++-- .../trace/src/Core/SerializableType.php | 14 +++ seed/php-model/trace/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../undiscriminated-unions/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../php-model/unions/src/Core/JsonDecoder.php | 13 ++ .../unions/src/Core/JsonDeserializer.php | 37 ++++-- .../unions/src/Core/JsonSerializer.php | 33 +++-- .../unions/src/Core/SerializableType.php | 14 +++ seed/php-model/unions/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../unknown/src/Core/JsonDecoder.php | 13 ++ .../unknown/src/Core/JsonDeserializer.php | 37 ++++-- .../unknown/src/Core/JsonSerializer.php | 33 +++-- .../unknown/src/Core/SerializableType.php | 14 +++ seed/php-model/unknown/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../validation/src/Core/JsonDecoder.php | 13 ++ .../validation/src/Core/JsonDeserializer.php | 37 ++++-- .../validation/src/Core/JsonSerializer.php | 33 +++-- .../validation/src/Core/SerializableType.php | 14 +++ seed/php-model/validation/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../variables/src/Core/JsonDecoder.php | 13 ++ .../variables/src/Core/JsonDeserializer.php | 37 ++++-- .../variables/src/Core/JsonSerializer.php | 33 +++-- .../variables/src/Core/SerializableType.php | 14 +++ seed/php-model/variables/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../version-no-default/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../version/src/Core/JsonDecoder.php | 13 ++ .../version/src/Core/JsonDeserializer.php | 37 ++++-- .../version/src/Core/JsonSerializer.php | 33 +++-- .../version/src/Core/SerializableType.php | 14 +++ seed/php-model/version/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../websocket/src/Core/JsonDecoder.php | 13 ++ .../websocket/src/Core/JsonDeserializer.php | 37 ++++-- .../websocket/src/Core/JsonSerializer.php | 33 +++-- .../websocket/src/Core/SerializableType.php | 14 +++ seed/php-model/websocket/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../alias-extends/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../alias-extends/src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ seed/php-sdk/alias-extends/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/alias/src/Core/JsonDecoder.php | 13 ++ .../alias/src/Core/JsonDeserializer.php | 37 ++++-- .../php-sdk/alias/src/Core/JsonSerializer.php | 33 +++-- .../alias/src/Core/SerializableType.php | 14 +++ seed/php-sdk/alias/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../php-sdk/any-auth/src/Core/JsonDecoder.php | 13 ++ .../any-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../any-auth/src/Core/JsonSerializer.php | 33 +++-- .../any-auth/src/Core/SerializableType.php | 14 +++ seed/php-sdk/any-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../api-wide-base-path/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../audiences/src/Core/JsonDecoder.php | 13 ++ .../audiences/src/Core/JsonDeserializer.php | 37 ++++-- .../audiences/src/Core/JsonSerializer.php | 33 +++-- .../audiences/src/Core/SerializableType.php | 14 +++ seed/php-sdk/audiences/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../basic-auth/src/Core/JsonDecoder.php | 13 ++ .../basic-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../basic-auth/src/Core/JsonSerializer.php | 33 +++-- .../basic-auth/src/Core/SerializableType.php | 14 +++ seed/php-sdk/basic-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/bytes/src/Core/JsonDecoder.php | 13 ++ .../bytes/src/Core/JsonDeserializer.php | 37 ++++-- .../php-sdk/bytes/src/Core/JsonSerializer.php | 33 +++-- .../bytes/src/Core/SerializableType.php | 14 +++ seed/php-sdk/bytes/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../circular-references/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../custom-auth/src/Core/JsonDecoder.php | 13 ++ .../custom-auth/src/Core/JsonDeserializer.php | 37 ++++-- .../custom-auth/src/Core/JsonSerializer.php | 33 +++-- .../custom-auth/src/Core/SerializableType.php | 14 +++ seed/php-sdk/custom-auth/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/enum/src/Core/JsonDecoder.php | 13 ++ .../enum/src/Core/JsonDeserializer.php | 37 ++++-- seed/php-sdk/enum/src/Core/JsonSerializer.php | 33 +++-- .../enum/src/Core/SerializableType.php | 14 +++ seed/php-sdk/enum/src/Core/Union.php | 49 +++++++- .../Requests/SendEnumInlinedRequest.php | 15 +-- .../enum/src/PathParam/PathParamClient.php | 7 +- .../Requests/SendEnumAsQueryParamRequest.php | 15 +-- .../SendEnumListAsQueryParamRequest.php | 9 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../error-property/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../php-sdk/error-property/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../exhaustive/src/Core/JsonDecoder.php | 13 ++ .../exhaustive/src/Core/JsonDeserializer.php | 37 ++++-- .../exhaustive/src/Core/JsonSerializer.php | 33 +++-- .../exhaustive/src/Core/SerializableType.php | 14 +++ seed/php-sdk/exhaustive/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/extends/src/Core/JsonDecoder.php | 13 ++ .../extends/src/Core/JsonDeserializer.php | 37 ++++-- .../extends/src/Core/JsonSerializer.php | 33 +++-- .../extends/src/Core/SerializableType.php | 14 +++ seed/php-sdk/extends/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../extra-properties/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../extra-properties/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../file-download/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../file-download/src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ seed/php-sdk/file-download/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../file-upload/src/Core/JsonDecoder.php | 13 ++ .../file-upload/src/Core/JsonDeserializer.php | 37 ++++-- .../file-upload/src/Core/JsonSerializer.php | 33 +++-- .../file-upload/src/Core/SerializableType.php | 14 +++ seed/php-sdk/file-upload/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/folders/src/Core/JsonDecoder.php | 13 ++ .../folders/src/Core/JsonDeserializer.php | 37 ++++-- .../folders/src/Core/JsonSerializer.php | 33 +++-- .../folders/src/Core/SerializableType.php | 14 +++ seed/php-sdk/folders/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../grpc-proto-exhaustive/src/Core/Union.php | 49 +++++++- .../Dataservice/Requests/DeleteRequest.php | 13 +- .../Dataservice/Requests/DescribeRequest.php | 13 +- .../src/Dataservice/Requests/QueryRequest.php | 11 +- .../Dataservice/Requests/UpdateRequest.php | 11 +- .../src/Types/Column.php | 11 +- .../src/Types/QueryColumn.php | 11 +- .../src/Types/ScoredColumn.php | 11 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../grpc-proto/src/Core/JsonDecoder.php | 13 ++ .../grpc-proto/src/Core/JsonDeserializer.php | 37 ++++-- .../grpc-proto/src/Core/JsonSerializer.php | 33 +++-- .../grpc-proto/src/Core/SerializableType.php | 14 +++ seed/php-sdk/grpc-proto/src/Core/Union.php | 49 +++++++- .../grpc-proto/src/Types/UserModel.php | 13 +- .../Userservice/Requests/CreateRequest.php | 13 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../idempotency-headers/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/imdb/src/Core/JsonDecoder.php | 13 ++ .../imdb/src/Core/JsonDeserializer.php | 37 ++++-- seed/php-sdk/imdb/src/Core/JsonSerializer.php | 33 +++-- .../imdb/src/Core/SerializableType.php | 14 +++ seed/php-sdk/imdb/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/literal/src/Core/JsonDecoder.php | 13 ++ .../literal/src/Core/JsonDeserializer.php | 37 ++++-- .../literal/src/Core/JsonSerializer.php | 33 +++-- .../literal/src/Core/SerializableType.php | 14 +++ seed/php-sdk/literal/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../mixed-case/src/Core/JsonDecoder.php | 13 ++ .../mixed-case/src/Core/JsonDeserializer.php | 37 ++++-- .../mixed-case/src/Core/JsonSerializer.php | 33 +++-- .../mixed-case/src/Core/SerializableType.php | 14 +++ seed/php-sdk/mixed-case/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../mixed-file-directory/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../multi-line-docs/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../multi-line-docs/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../no-environment/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../php-sdk/no-environment/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/object/src/Core/JsonDecoder.php | 13 ++ .../object/src/Core/JsonDeserializer.php | 37 ++++-- .../object/src/Core/JsonSerializer.php | 33 +++-- .../object/src/Core/SerializableType.php | 14 +++ seed/php-sdk/object/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../objects-with-imports/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../php-sdk/optional/src/Core/JsonDecoder.php | 13 ++ .../optional/src/Core/JsonDeserializer.php | 37 ++++-- .../optional/src/Core/JsonSerializer.php | 33 +++-- .../optional/src/Core/SerializableType.php | 14 +++ seed/php-sdk/optional/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../package-yml/src/Core/JsonDecoder.php | 13 ++ .../package-yml/src/Core/JsonDeserializer.php | 37 ++++-- .../package-yml/src/Core/JsonSerializer.php | 33 +++-- .../package-yml/src/Core/SerializableType.php | 14 +++ seed/php-sdk/package-yml/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../pagination/src/Core/JsonDecoder.php | 13 ++ .../pagination/src/Core/JsonDeserializer.php | 37 ++++-- .../pagination/src/Core/JsonSerializer.php | 33 +++-- .../pagination/src/Core/SerializableType.php | 14 +++ seed/php-sdk/pagination/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../plain-text/src/Core/JsonDecoder.php | 13 ++ .../plain-text/src/Core/JsonDeserializer.php | 37 ++++-- .../plain-text/src/Core/JsonSerializer.php | 33 +++-- .../plain-text/src/Core/SerializableType.php | 14 +++ seed/php-sdk/plain-text/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../query-parameters/src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../query-parameters/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../reserved-keywords/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../response-property/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../simple-fhir/src/Core/JsonDecoder.php | 13 ++ .../simple-fhir/src/Core/JsonDeserializer.php | 37 ++++-- .../simple-fhir/src/Core/JsonSerializer.php | 33 +++-- .../simple-fhir/src/Core/SerializableType.php | 14 +++ seed/php-sdk/simple-fhir/src/Core/Union.php | 49 +++++++- .../simple-fhir/src/Types/BaseResource.php | 7 +- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../streaming-parameter/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../streaming/src/Core/JsonDecoder.php | 13 ++ .../streaming/src/Core/JsonDeserializer.php | 37 ++++-- .../streaming/src/Core/JsonSerializer.php | 33 +++-- .../streaming/src/Core/SerializableType.php | 14 +++ seed/php-sdk/streaming/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/trace/src/Core/JsonDecoder.php | 13 ++ .../trace/src/Core/JsonDeserializer.php | 37 ++++-- .../php-sdk/trace/src/Core/JsonSerializer.php | 33 +++-- .../trace/src/Core/SerializableType.php | 14 +++ seed/php-sdk/trace/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/unions/src/Core/JsonDecoder.php | 13 ++ .../unions/src/Core/JsonDeserializer.php | 37 ++++-- .../unions/src/Core/JsonSerializer.php | 33 +++-- .../unions/src/Core/SerializableType.php | 14 +++ seed/php-sdk/unions/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/unknown/src/Core/JsonDecoder.php | 13 ++ .../unknown/src/Core/JsonDeserializer.php | 37 ++++-- .../unknown/src/Core/JsonSerializer.php | 33 +++-- .../unknown/src/Core/SerializableType.php | 14 +++ seed/php-sdk/unknown/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../validation/src/Core/JsonDecoder.php | 13 ++ .../validation/src/Core/JsonDeserializer.php | 37 ++++-- .../validation/src/Core/JsonSerializer.php | 33 +++-- .../validation/src/Core/SerializableType.php | 14 +++ seed/php-sdk/validation/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../variables/src/Core/JsonDecoder.php | 13 ++ .../variables/src/Core/JsonDeserializer.php | 37 ++++-- .../variables/src/Core/JsonSerializer.php | 33 +++-- .../variables/src/Core/SerializableType.php | 14 +++ seed/php-sdk/variables/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../src/Core/JsonDecoder.php | 13 ++ .../src/Core/JsonDeserializer.php | 37 ++++-- .../src/Core/JsonSerializer.php | 33 +++-- .../src/Core/SerializableType.php | 14 +++ .../version-no-default/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ seed/php-sdk/version/src/Core/JsonDecoder.php | 13 ++ .../version/src/Core/JsonDeserializer.php | 37 ++++-- .../version/src/Core/JsonSerializer.php | 33 +++-- .../version/src/Core/SerializableType.php | 14 +++ seed/php-sdk/version/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ .../websocket/src/Core/JsonDecoder.php | 13 ++ .../websocket/src/Core/JsonDeserializer.php | 37 ++++-- .../websocket/src/Core/JsonSerializer.php | 33 +++-- .../websocket/src/Core/SerializableType.php | 14 +++ seed/php-sdk/websocket/src/Core/Union.php | 49 +++++++- .../tests/Seed/Core/UnionPropertyTypeTest.php | 116 ++++++++++++++++++ 730 files changed, 28331 insertions(+), 2820 deletions(-) create mode 100644 seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php create mode 100644 seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php diff --git a/seed/php-model/alias-extends/src/Core/JsonDecoder.php b/seed/php-model/alias-extends/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/alias-extends/src/Core/JsonDecoder.php +++ b/seed/php-model/alias-extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/alias-extends/src/Core/JsonSerializer.php b/seed/php-model/alias-extends/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-model/alias-extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/alias-extends/src/Core/SerializableType.php b/seed/php-model/alias-extends/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/alias-extends/src/Core/SerializableType.php +++ b/seed/php-model/alias-extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/alias-extends/src/Core/Union.php b/seed/php-model/alias-extends/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/alias-extends/src/Core/Union.php +++ b/seed/php-model/alias-extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/alias/src/Core/JsonDecoder.php b/seed/php-model/alias/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/alias/src/Core/JsonDecoder.php +++ b/seed/php-model/alias/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/alias/src/Core/JsonDeserializer.php b/seed/php-model/alias/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/alias/src/Core/JsonDeserializer.php +++ b/seed/php-model/alias/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/alias/src/Core/JsonSerializer.php b/seed/php-model/alias/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/alias/src/Core/JsonSerializer.php +++ b/seed/php-model/alias/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/alias/src/Core/SerializableType.php b/seed/php-model/alias/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/alias/src/Core/SerializableType.php +++ b/seed/php-model/alias/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/alias/src/Core/Union.php b/seed/php-model/alias/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/alias/src/Core/Union.php +++ b/seed/php-model/alias/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/alias/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/any-auth/src/Core/JsonDecoder.php b/seed/php-model/any-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/any-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/any-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/any-auth/src/Core/JsonDeserializer.php b/seed/php-model/any-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/any-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/any-auth/src/Core/JsonSerializer.php b/seed/php-model/any-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/any-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/any-auth/src/Core/SerializableType.php b/seed/php-model/any-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/any-auth/src/Core/SerializableType.php +++ b/seed/php-model/any-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/any-auth/src/Core/Union.php b/seed/php-model/any-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/any-auth/src/Core/Union.php +++ b/seed/php-model/any-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php b/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php b/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php b/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php +++ b/seed/php-model/api-wide-base-path/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/api-wide-base-path/src/Core/SerializableType.php b/seed/php-model/api-wide-base-path/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/api-wide-base-path/src/Core/SerializableType.php +++ b/seed/php-model/api-wide-base-path/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/api-wide-base-path/src/Core/Union.php b/seed/php-model/api-wide-base-path/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/api-wide-base-path/src/Core/Union.php +++ b/seed/php-model/api-wide-base-path/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/audiences/src/Core/JsonDecoder.php b/seed/php-model/audiences/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/audiences/src/Core/JsonDecoder.php +++ b/seed/php-model/audiences/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/audiences/src/Core/JsonDeserializer.php b/seed/php-model/audiences/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-model/audiences/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/audiences/src/Core/JsonSerializer.php b/seed/php-model/audiences/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/audiences/src/Core/JsonSerializer.php +++ b/seed/php-model/audiences/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/audiences/src/Core/SerializableType.php b/seed/php-model/audiences/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/audiences/src/Core/SerializableType.php +++ b/seed/php-model/audiences/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/audiences/src/Core/Union.php b/seed/php-model/audiences/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/audiences/src/Core/Union.php +++ b/seed/php-model/audiences/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/audiences/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/auth-environment-variables/src/Core/SerializableType.php b/seed/php-model/auth-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/auth-environment-variables/src/Core/Union.php b/seed/php-model/auth-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/auth-environment-variables/src/Core/Union.php +++ b/seed/php-model/auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php b/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/basic-auth-environment-variables/src/Core/Union.php b/seed/php-model/basic-auth-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/basic-auth-environment-variables/src/Core/Union.php +++ b/seed/php-model/basic-auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/basic-auth/src/Core/JsonDecoder.php b/seed/php-model/basic-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/basic-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/basic-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/basic-auth/src/Core/JsonSerializer.php b/seed/php-model/basic-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/basic-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/basic-auth/src/Core/SerializableType.php b/seed/php-model/basic-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/basic-auth/src/Core/SerializableType.php +++ b/seed/php-model/basic-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/basic-auth/src/Core/Union.php b/seed/php-model/basic-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/basic-auth/src/Core/Union.php +++ b/seed/php-model/basic-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php b/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php b/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/bearer-token-environment-variable/src/Core/Union.php b/seed/php-model/bearer-token-environment-variable/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/bearer-token-environment-variable/src/Core/Union.php +++ b/seed/php-model/bearer-token-environment-variable/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/bytes/src/Core/JsonDecoder.php b/seed/php-model/bytes/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/bytes/src/Core/JsonDecoder.php +++ b/seed/php-model/bytes/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/bytes/src/Core/JsonDeserializer.php b/seed/php-model/bytes/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-model/bytes/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/bytes/src/Core/JsonSerializer.php b/seed/php-model/bytes/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/bytes/src/Core/JsonSerializer.php +++ b/seed/php-model/bytes/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/bytes/src/Core/SerializableType.php b/seed/php-model/bytes/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/bytes/src/Core/SerializableType.php +++ b/seed/php-model/bytes/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/bytes/src/Core/Union.php b/seed/php-model/bytes/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/bytes/src/Core/Union.php +++ b/seed/php-model/bytes/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/bytes/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php b/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php b/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php b/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references-advanced/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/circular-references-advanced/src/Core/SerializableType.php b/seed/php-model/circular-references-advanced/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-model/circular-references-advanced/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/circular-references-advanced/src/Core/Union.php b/seed/php-model/circular-references-advanced/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/circular-references-advanced/src/Core/Union.php +++ b/seed/php-model/circular-references-advanced/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/circular-references/src/Core/JsonDecoder.php b/seed/php-model/circular-references/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/circular-references/src/Core/JsonDecoder.php +++ b/seed/php-model/circular-references/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/circular-references/src/Core/JsonDeserializer.php b/seed/php-model/circular-references/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-model/circular-references/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/circular-references/src/Core/JsonSerializer.php b/seed/php-model/circular-references/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-model/circular-references/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/circular-references/src/Core/SerializableType.php b/seed/php-model/circular-references/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/circular-references/src/Core/SerializableType.php +++ b/seed/php-model/circular-references/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/circular-references/src/Core/Union.php b/seed/php-model/circular-references/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/circular-references/src/Core/Union.php +++ b/seed/php-model/circular-references/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php b/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php b/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php b/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php +++ b/seed/php-model/cross-package-type-names/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/cross-package-type-names/src/Core/SerializableType.php b/seed/php-model/cross-package-type-names/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/cross-package-type-names/src/Core/SerializableType.php +++ b/seed/php-model/cross-package-type-names/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/cross-package-type-names/src/Core/Union.php b/seed/php-model/cross-package-type-names/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/cross-package-type-names/src/Core/Union.php +++ b/seed/php-model/cross-package-type-names/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/custom-auth/src/Core/JsonDecoder.php b/seed/php-model/custom-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/custom-auth/src/Core/JsonDecoder.php +++ b/seed/php-model/custom-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/custom-auth/src/Core/JsonSerializer.php b/seed/php-model/custom-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-model/custom-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/custom-auth/src/Core/SerializableType.php b/seed/php-model/custom-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/custom-auth/src/Core/SerializableType.php +++ b/seed/php-model/custom-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/custom-auth/src/Core/Union.php b/seed/php-model/custom-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/custom-auth/src/Core/Union.php +++ b/seed/php-model/custom-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/enum/src/Core/JsonDecoder.php b/seed/php-model/enum/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/enum/src/Core/JsonDecoder.php +++ b/seed/php-model/enum/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/enum/src/Core/JsonDeserializer.php b/seed/php-model/enum/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/enum/src/Core/JsonDeserializer.php +++ b/seed/php-model/enum/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/enum/src/Core/JsonSerializer.php b/seed/php-model/enum/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/enum/src/Core/JsonSerializer.php +++ b/seed/php-model/enum/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/enum/src/Core/SerializableType.php b/seed/php-model/enum/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/enum/src/Core/SerializableType.php +++ b/seed/php-model/enum/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/enum/src/Core/Union.php b/seed/php-model/enum/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/enum/src/Core/Union.php +++ b/seed/php-model/enum/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/enum/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/error-property/src/Core/JsonDecoder.php b/seed/php-model/error-property/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/error-property/src/Core/JsonDecoder.php +++ b/seed/php-model/error-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/error-property/src/Core/JsonDeserializer.php b/seed/php-model/error-property/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/error-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/error-property/src/Core/JsonSerializer.php b/seed/php-model/error-property/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/error-property/src/Core/JsonSerializer.php +++ b/seed/php-model/error-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/error-property/src/Core/SerializableType.php b/seed/php-model/error-property/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/error-property/src/Core/SerializableType.php +++ b/seed/php-model/error-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/error-property/src/Core/Union.php b/seed/php-model/error-property/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/error-property/src/Core/Union.php +++ b/seed/php-model/error-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/error-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/examples/src/Core/JsonDecoder.php b/seed/php-model/examples/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/examples/src/Core/JsonDecoder.php +++ b/seed/php-model/examples/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/examples/src/Core/JsonDeserializer.php b/seed/php-model/examples/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/examples/src/Core/JsonDeserializer.php +++ b/seed/php-model/examples/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/examples/src/Core/JsonSerializer.php b/seed/php-model/examples/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/examples/src/Core/JsonSerializer.php +++ b/seed/php-model/examples/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/examples/src/Core/SerializableType.php b/seed/php-model/examples/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/examples/src/Core/SerializableType.php +++ b/seed/php-model/examples/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/examples/src/Core/Union.php b/seed/php-model/examples/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/examples/src/Core/Union.php +++ b/seed/php-model/examples/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/examples/src/Identifier.php b/seed/php-model/examples/src/Identifier.php index e673d13197..48363b1db2 100644 --- a/seed/php-model/examples/src/Identifier.php +++ b/seed/php-model/examples/src/Identifier.php @@ -8,10 +8,10 @@ class Identifier extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $value @@ -27,7 +27,7 @@ class Identifier extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * value: string, * label: string, * } $values diff --git a/seed/php-model/examples/src/Types/Entity.php b/seed/php-model/examples/src/Types/Entity.php index c1dd816bc6..c94f68a979 100644 --- a/seed/php-model/examples/src/Types/Entity.php +++ b/seed/php-model/examples/src/Types/Entity.php @@ -3,15 +3,17 @@ namespace Seed\Types; use Seed\Core\SerializableType; +use Seed\BasicType; +use Seed\ComplexType; use Seed\Core\JsonProperty; class Entity extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @var string $name @@ -21,7 +23,7 @@ class Entity extends SerializableType /** * @param array{ - * type: mixed, + * type: value-of|value-of, * name: string, * } $values */ diff --git a/seed/php-model/examples/src/Types/ResponseType.php b/seed/php-model/examples/src/Types/ResponseType.php index fd26020ff9..6bdca9a0b7 100644 --- a/seed/php-model/examples/src/Types/ResponseType.php +++ b/seed/php-model/examples/src/Types/ResponseType.php @@ -3,19 +3,21 @@ namespace Seed\Types; use Seed\Core\SerializableType; +use Seed\BasicType; +use Seed\ComplexType; use Seed\Core\JsonProperty; class ResponseType extends SerializableType { /** - * @var mixed $type + * @var value-of|value-of $type */ #[JsonProperty('type')] - public mixed $type; + public string $type; /** * @param array{ - * type: mixed, + * type: value-of|value-of, * } $values */ public function __construct( diff --git a/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/examples/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/exhaustive/src/Core/JsonDecoder.php b/seed/php-model/exhaustive/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-model/exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/exhaustive/src/Core/JsonSerializer.php b/seed/php-model/exhaustive/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/exhaustive/src/Core/SerializableType.php b/seed/php-model/exhaustive/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/exhaustive/src/Core/Union.php b/seed/php-model/exhaustive/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/exhaustive/src/Core/Union.php +++ b/seed/php-model/exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/extends/src/Core/JsonDecoder.php b/seed/php-model/extends/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/extends/src/Core/JsonDecoder.php +++ b/seed/php-model/extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/extends/src/Core/JsonDeserializer.php b/seed/php-model/extends/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/extends/src/Core/JsonDeserializer.php +++ b/seed/php-model/extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/extends/src/Core/JsonSerializer.php b/seed/php-model/extends/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/extends/src/Core/JsonSerializer.php +++ b/seed/php-model/extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/extends/src/Core/SerializableType.php b/seed/php-model/extends/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/extends/src/Core/SerializableType.php +++ b/seed/php-model/extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/extends/src/Core/Union.php b/seed/php-model/extends/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/extends/src/Core/Union.php +++ b/seed/php-model/extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/extra-properties/src/Core/JsonDecoder.php b/seed/php-model/extra-properties/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/extra-properties/src/Core/JsonDecoder.php +++ b/seed/php-model/extra-properties/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/extra-properties/src/Core/JsonSerializer.php b/seed/php-model/extra-properties/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-model/extra-properties/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/extra-properties/src/Core/SerializableType.php b/seed/php-model/extra-properties/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/extra-properties/src/Core/SerializableType.php +++ b/seed/php-model/extra-properties/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/extra-properties/src/Core/Union.php b/seed/php-model/extra-properties/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/extra-properties/src/Core/Union.php +++ b/seed/php-model/extra-properties/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/file-download/src/Core/JsonDecoder.php b/seed/php-model/file-download/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/file-download/src/Core/JsonDecoder.php +++ b/seed/php-model/file-download/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/file-download/src/Core/JsonDeserializer.php b/seed/php-model/file-download/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-download/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/file-download/src/Core/JsonSerializer.php b/seed/php-model/file-download/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/file-download/src/Core/JsonSerializer.php +++ b/seed/php-model/file-download/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/file-download/src/Core/SerializableType.php b/seed/php-model/file-download/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/file-download/src/Core/SerializableType.php +++ b/seed/php-model/file-download/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/file-download/src/Core/Union.php b/seed/php-model/file-download/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/file-download/src/Core/Union.php +++ b/seed/php-model/file-download/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/file-download/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/file-upload/src/Core/JsonDecoder.php b/seed/php-model/file-upload/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/file-upload/src/Core/JsonDecoder.php +++ b/seed/php-model/file-upload/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/file-upload/src/Core/JsonDeserializer.php b/seed/php-model/file-upload/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-model/file-upload/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/file-upload/src/Core/JsonSerializer.php b/seed/php-model/file-upload/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-model/file-upload/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/file-upload/src/Core/SerializableType.php b/seed/php-model/file-upload/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/file-upload/src/Core/SerializableType.php +++ b/seed/php-model/file-upload/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/file-upload/src/Core/Union.php b/seed/php-model/file-upload/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/file-upload/src/Core/Union.php +++ b/seed/php-model/file-upload/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/folders/src/Core/JsonDecoder.php b/seed/php-model/folders/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/folders/src/Core/JsonDecoder.php +++ b/seed/php-model/folders/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/folders/src/Core/JsonDeserializer.php b/seed/php-model/folders/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/folders/src/Core/JsonDeserializer.php +++ b/seed/php-model/folders/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/folders/src/Core/JsonSerializer.php b/seed/php-model/folders/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/folders/src/Core/JsonSerializer.php +++ b/seed/php-model/folders/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/folders/src/Core/SerializableType.php b/seed/php-model/folders/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/folders/src/Core/SerializableType.php +++ b/seed/php-model/folders/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/folders/src/Core/Union.php b/seed/php-model/folders/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/folders/src/Core/Union.php +++ b/seed/php-model/folders/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/folders/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/grpc-proto-exhaustive/src/Column.php b/seed/php-model/grpc-proto-exhaustive/src/Column.php index 0d6262dcad..c0cc5b4386 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Column.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Column.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class Column extends SerializableType { @@ -21,10 +22,10 @@ class Column extends SerializableType public array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -36,7 +37,7 @@ class Column extends SerializableType * @param array{ * id: string, * values: array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -45,7 +46,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values']; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php b/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php b/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-model/grpc-proto-exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php index a21ff3222f..c30b57e5a5 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/QueryColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class QueryColumn extends SerializableType { @@ -27,10 +28,10 @@ class QueryColumn extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class QueryColumn extends SerializableType * values: array, * topK?: ?int, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->values = $values['values']; $this->topK = $values['topK'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php index 021ff382e1..22ce1d2861 100644 --- a/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php +++ b/seed/php-model/grpc-proto-exhaustive/src/ScoredColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class ScoredColumn extends SerializableType { @@ -27,10 +28,10 @@ class ScoredColumn extends SerializableType public ?array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class ScoredColumn extends SerializableType * id: string, * score?: ?float, * values?: ?array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->id = $values['id']; $this->score = $values['score'] ?? null; $this->values = $values['values'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/grpc-proto/src/Core/JsonDecoder.php b/seed/php-model/grpc-proto/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonDecoder.php +++ b/seed/php-model/grpc-proto/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-model/grpc-proto/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/grpc-proto/src/Core/SerializableType.php b/seed/php-model/grpc-proto/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-model/grpc-proto/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/grpc-proto/src/Core/Union.php b/seed/php-model/grpc-proto/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/grpc-proto/src/Core/Union.php +++ b/seed/php-model/grpc-proto/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/grpc-proto/src/UserModel.php b/seed/php-model/grpc-proto/src/UserModel.php index 4ba096631e..54516ab941 100644 --- a/seed/php-model/grpc-proto/src/UserModel.php +++ b/seed/php-model/grpc-proto/src/UserModel.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class UserModel extends SerializableType { @@ -32,10 +33,10 @@ class UserModel extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class UserModel extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php b/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-model/idempotency-headers/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/idempotency-headers/src/Core/SerializableType.php b/seed/php-model/idempotency-headers/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-model/idempotency-headers/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/idempotency-headers/src/Core/Union.php b/seed/php-model/idempotency-headers/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/idempotency-headers/src/Core/Union.php +++ b/seed/php-model/idempotency-headers/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/imdb/src/Core/JsonDecoder.php b/seed/php-model/imdb/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/imdb/src/Core/JsonDecoder.php +++ b/seed/php-model/imdb/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/imdb/src/Core/JsonDeserializer.php b/seed/php-model/imdb/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-model/imdb/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/imdb/src/Core/JsonSerializer.php b/seed/php-model/imdb/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/imdb/src/Core/JsonSerializer.php +++ b/seed/php-model/imdb/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/imdb/src/Core/SerializableType.php b/seed/php-model/imdb/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/imdb/src/Core/SerializableType.php +++ b/seed/php-model/imdb/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/imdb/src/Core/Union.php b/seed/php-model/imdb/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/imdb/src/Core/Union.php +++ b/seed/php-model/imdb/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/imdb/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/literal/src/Core/JsonDecoder.php b/seed/php-model/literal/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/literal/src/Core/JsonDecoder.php +++ b/seed/php-model/literal/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/literal/src/Core/JsonDeserializer.php b/seed/php-model/literal/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/literal/src/Core/JsonDeserializer.php +++ b/seed/php-model/literal/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/literal/src/Core/JsonSerializer.php b/seed/php-model/literal/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/literal/src/Core/JsonSerializer.php +++ b/seed/php-model/literal/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/literal/src/Core/SerializableType.php b/seed/php-model/literal/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/literal/src/Core/SerializableType.php +++ b/seed/php-model/literal/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/literal/src/Core/Union.php b/seed/php-model/literal/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/literal/src/Core/Union.php +++ b/seed/php-model/literal/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/literal/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/mixed-case/src/Core/JsonDecoder.php b/seed/php-model/mixed-case/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/mixed-case/src/Core/JsonDecoder.php +++ b/seed/php-model/mixed-case/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/mixed-case/src/Core/JsonSerializer.php b/seed/php-model/mixed-case/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-case/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/mixed-case/src/Core/SerializableType.php b/seed/php-model/mixed-case/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/mixed-case/src/Core/SerializableType.php +++ b/seed/php-model/mixed-case/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/mixed-case/src/Core/Union.php b/seed/php-model/mixed-case/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/mixed-case/src/Core/Union.php +++ b/seed/php-model/mixed-case/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php b/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php b/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php b/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-model/mixed-file-directory/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/mixed-file-directory/src/Core/SerializableType.php b/seed/php-model/mixed-file-directory/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-model/mixed-file-directory/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/mixed-file-directory/src/Core/Union.php b/seed/php-model/mixed-file-directory/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/mixed-file-directory/src/Core/Union.php +++ b/seed/php-model/mixed-file-directory/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php b/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php b/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php b/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-line-docs/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/multi-line-docs/src/Core/SerializableType.php b/seed/php-model/multi-line-docs/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-model/multi-line-docs/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/multi-line-docs/src/Core/Union.php b/seed/php-model/multi-line-docs/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/multi-line-docs/src/Core/Union.php +++ b/seed/php-model/multi-line-docs/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php b/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/multi-url-environment-no-default/src/Core/Union.php b/seed/php-model/multi-url-environment-no-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/multi-url-environment-no-default/src/Core/Union.php +++ b/seed/php-model/multi-url-environment-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/multi-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php b/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php b/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php b/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/multi-url-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/multi-url-environment/src/Core/SerializableType.php b/seed/php-model/multi-url-environment/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/multi-url-environment/src/Core/SerializableType.php +++ b/seed/php-model/multi-url-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/multi-url-environment/src/Core/Union.php b/seed/php-model/multi-url-environment/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/multi-url-environment/src/Core/Union.php +++ b/seed/php-model/multi-url-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/multi-url-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/no-environment/src/Core/JsonDecoder.php b/seed/php-model/no-environment/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/no-environment/src/Core/JsonDecoder.php +++ b/seed/php-model/no-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/no-environment/src/Core/JsonDeserializer.php b/seed/php-model/no-environment/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-model/no-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/no-environment/src/Core/JsonSerializer.php b/seed/php-model/no-environment/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-model/no-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/no-environment/src/Core/SerializableType.php b/seed/php-model/no-environment/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/no-environment/src/Core/SerializableType.php +++ b/seed/php-model/no-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/no-environment/src/Core/Union.php b/seed/php-model/no-environment/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/no-environment/src/Core/Union.php +++ b/seed/php-model/no-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/oauth-client-credentials-default/src/Core/Union.php b/seed/php-model/oauth-client-credentials-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/oauth-client-credentials-default/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials-nested-root/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php b/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php b/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php b/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-model/oauth-client-credentials/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php b/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-model/oauth-client-credentials/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/oauth-client-credentials/src/Core/Union.php b/seed/php-model/oauth-client-credentials/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-model/oauth-client-credentials/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/object/src/Core/JsonDecoder.php b/seed/php-model/object/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/object/src/Core/JsonDecoder.php +++ b/seed/php-model/object/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/object/src/Core/JsonDeserializer.php b/seed/php-model/object/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/object/src/Core/JsonDeserializer.php +++ b/seed/php-model/object/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/object/src/Core/JsonSerializer.php b/seed/php-model/object/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/object/src/Core/JsonSerializer.php +++ b/seed/php-model/object/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/object/src/Core/SerializableType.php b/seed/php-model/object/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/object/src/Core/SerializableType.php +++ b/seed/php-model/object/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/object/src/Core/Union.php b/seed/php-model/object/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/object/src/Core/Union.php +++ b/seed/php-model/object/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/object/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php b/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php b/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php b/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-model/objects-with-imports/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/objects-with-imports/src/Core/SerializableType.php b/seed/php-model/objects-with-imports/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-model/objects-with-imports/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/objects-with-imports/src/Core/Union.php b/seed/php-model/objects-with-imports/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/objects-with-imports/src/Core/Union.php +++ b/seed/php-model/objects-with-imports/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/optional/src/Core/JsonDecoder.php b/seed/php-model/optional/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/optional/src/Core/JsonDecoder.php +++ b/seed/php-model/optional/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/optional/src/Core/JsonDeserializer.php b/seed/php-model/optional/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/optional/src/Core/JsonDeserializer.php +++ b/seed/php-model/optional/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/optional/src/Core/JsonSerializer.php b/seed/php-model/optional/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/optional/src/Core/JsonSerializer.php +++ b/seed/php-model/optional/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/optional/src/Core/SerializableType.php b/seed/php-model/optional/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/optional/src/Core/SerializableType.php +++ b/seed/php-model/optional/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/optional/src/Core/Union.php b/seed/php-model/optional/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/optional/src/Core/Union.php +++ b/seed/php-model/optional/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/optional/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/package-yml/src/Core/JsonDecoder.php b/seed/php-model/package-yml/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/package-yml/src/Core/JsonDecoder.php +++ b/seed/php-model/package-yml/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/package-yml/src/Core/JsonDeserializer.php b/seed/php-model/package-yml/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-model/package-yml/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/package-yml/src/Core/JsonSerializer.php b/seed/php-model/package-yml/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-model/package-yml/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/package-yml/src/Core/SerializableType.php b/seed/php-model/package-yml/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/package-yml/src/Core/SerializableType.php +++ b/seed/php-model/package-yml/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/package-yml/src/Core/Union.php b/seed/php-model/package-yml/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/package-yml/src/Core/Union.php +++ b/seed/php-model/package-yml/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/pagination/src/Core/JsonDecoder.php b/seed/php-model/pagination/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/pagination/src/Core/JsonDecoder.php +++ b/seed/php-model/pagination/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/pagination/src/Core/JsonDeserializer.php b/seed/php-model/pagination/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-model/pagination/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/pagination/src/Core/JsonSerializer.php b/seed/php-model/pagination/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/pagination/src/Core/JsonSerializer.php +++ b/seed/php-model/pagination/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/pagination/src/Core/SerializableType.php b/seed/php-model/pagination/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/pagination/src/Core/SerializableType.php +++ b/seed/php-model/pagination/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/pagination/src/Core/Union.php b/seed/php-model/pagination/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/pagination/src/Core/Union.php +++ b/seed/php-model/pagination/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/pagination/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/plain-text/src/Core/JsonDecoder.php b/seed/php-model/plain-text/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/plain-text/src/Core/JsonDecoder.php +++ b/seed/php-model/plain-text/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/plain-text/src/Core/JsonDeserializer.php b/seed/php-model/plain-text/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-model/plain-text/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/plain-text/src/Core/JsonSerializer.php b/seed/php-model/plain-text/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-model/plain-text/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/plain-text/src/Core/SerializableType.php b/seed/php-model/plain-text/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/plain-text/src/Core/SerializableType.php +++ b/seed/php-model/plain-text/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/plain-text/src/Core/Union.php b/seed/php-model/plain-text/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/plain-text/src/Core/Union.php +++ b/seed/php-model/plain-text/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/query-parameters/src/Core/JsonDecoder.php b/seed/php-model/query-parameters/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/query-parameters/src/Core/JsonDecoder.php +++ b/seed/php-model/query-parameters/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/query-parameters/src/Core/JsonSerializer.php b/seed/php-model/query-parameters/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-model/query-parameters/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/query-parameters/src/Core/SerializableType.php b/seed/php-model/query-parameters/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/query-parameters/src/Core/SerializableType.php +++ b/seed/php-model/query-parameters/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/query-parameters/src/Core/Union.php b/seed/php-model/query-parameters/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/query-parameters/src/Core/Union.php +++ b/seed/php-model/query-parameters/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php b/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-model/reserved-keywords/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/reserved-keywords/src/Core/SerializableType.php b/seed/php-model/reserved-keywords/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-model/reserved-keywords/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/reserved-keywords/src/Core/Union.php b/seed/php-model/reserved-keywords/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/reserved-keywords/src/Core/Union.php +++ b/seed/php-model/reserved-keywords/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/response-property/src/Core/JsonDecoder.php b/seed/php-model/response-property/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/response-property/src/Core/JsonDecoder.php +++ b/seed/php-model/response-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/response-property/src/Core/JsonDeserializer.php b/seed/php-model/response-property/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-model/response-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/response-property/src/Core/JsonSerializer.php b/seed/php-model/response-property/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/response-property/src/Core/JsonSerializer.php +++ b/seed/php-model/response-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/response-property/src/Core/SerializableType.php b/seed/php-model/response-property/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/response-property/src/Core/SerializableType.php +++ b/seed/php-model/response-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/response-property/src/Core/Union.php b/seed/php-model/response-property/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/response-property/src/Core/Union.php +++ b/seed/php-model/response-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/response-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/simple-fhir/src/BaseResource.php b/seed/php-model/simple-fhir/src/BaseResource.php index fe335daac3..ea9ceafd1d 100644 --- a/seed/php-model/simple-fhir/src/BaseResource.php +++ b/seed/php-model/simple-fhir/src/BaseResource.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class BaseResource extends SerializableType { @@ -15,9 +16,9 @@ class BaseResource extends SerializableType public string $id; /** - * @var array $relatedResources + * @var array $relatedResources */ - #[JsonProperty('related_resources'), ArrayType(['mixed'])] + #[JsonProperty('related_resources'), ArrayType([new Union(Account::class, Patient::class, Practitioner::class, Script::class)])] public array $relatedResources; /** @@ -29,7 +30,7 @@ class BaseResource extends SerializableType /** * @param array{ * id: string, - * relatedResources: array, + * relatedResources: array, * memo: Memo, * } $values */ diff --git a/seed/php-model/simple-fhir/src/Core/JsonDecoder.php b/seed/php-model/simple-fhir/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonDecoder.php +++ b/seed/php-model/simple-fhir/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-model/simple-fhir/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/simple-fhir/src/Core/SerializableType.php b/seed/php-model/simple-fhir/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-model/simple-fhir/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/simple-fhir/src/Core/Union.php b/seed/php-model/simple-fhir/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/simple-fhir/src/Core/Union.php +++ b/seed/php-model/simple-fhir/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php b/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php b/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php b/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php +++ b/seed/php-model/single-url-environment-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/single-url-environment-default/src/Core/SerializableType.php b/seed/php-model/single-url-environment-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/single-url-environment-default/src/Core/SerializableType.php +++ b/seed/php-model/single-url-environment-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/single-url-environment-default/src/Core/Union.php b/seed/php-model/single-url-environment-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/single-url-environment-default/src/Core/Union.php +++ b/seed/php-model/single-url-environment-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php b/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/single-url-environment-no-default/src/Core/Union.php b/seed/php-model/single-url-environment-no-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/single-url-environment-no-default/src/Core/Union.php +++ b/seed/php-model/single-url-environment-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php b/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming-parameter/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/streaming-parameter/src/Core/SerializableType.php b/seed/php-model/streaming-parameter/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-model/streaming-parameter/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/streaming-parameter/src/Core/Union.php b/seed/php-model/streaming-parameter/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/streaming-parameter/src/Core/Union.php +++ b/seed/php-model/streaming-parameter/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/streaming/src/Core/JsonDecoder.php b/seed/php-model/streaming/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/streaming/src/Core/JsonDecoder.php +++ b/seed/php-model/streaming/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/streaming/src/Core/JsonDeserializer.php b/seed/php-model/streaming/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-model/streaming/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/streaming/src/Core/JsonSerializer.php b/seed/php-model/streaming/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/streaming/src/Core/JsonSerializer.php +++ b/seed/php-model/streaming/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/streaming/src/Core/SerializableType.php b/seed/php-model/streaming/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/streaming/src/Core/SerializableType.php +++ b/seed/php-model/streaming/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/streaming/src/Core/Union.php b/seed/php-model/streaming/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/streaming/src/Core/Union.php +++ b/seed/php-model/streaming/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/streaming/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/trace/src/Core/JsonDecoder.php b/seed/php-model/trace/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/trace/src/Core/JsonDecoder.php +++ b/seed/php-model/trace/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/trace/src/Core/JsonDeserializer.php b/seed/php-model/trace/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/trace/src/Core/JsonDeserializer.php +++ b/seed/php-model/trace/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/trace/src/Core/JsonSerializer.php b/seed/php-model/trace/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/trace/src/Core/JsonSerializer.php +++ b/seed/php-model/trace/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/trace/src/Core/SerializableType.php b/seed/php-model/trace/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/trace/src/Core/SerializableType.php +++ b/seed/php-model/trace/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/trace/src/Core/Union.php b/seed/php-model/trace/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/trace/src/Core/Union.php +++ b/seed/php-model/trace/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/trace/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php b/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php +++ b/seed/php-model/undiscriminated-unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php +++ b/seed/php-model/undiscriminated-unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/undiscriminated-unions/src/Core/Union.php b/seed/php-model/undiscriminated-unions/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/undiscriminated-unions/src/Core/Union.php +++ b/seed/php-model/undiscriminated-unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/undiscriminated-unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/unions/src/Core/JsonDecoder.php b/seed/php-model/unions/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/unions/src/Core/JsonDecoder.php +++ b/seed/php-model/unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/unions/src/Core/JsonDeserializer.php b/seed/php-model/unions/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/unions/src/Core/JsonDeserializer.php +++ b/seed/php-model/unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/unions/src/Core/JsonSerializer.php b/seed/php-model/unions/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/unions/src/Core/JsonSerializer.php +++ b/seed/php-model/unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/unions/src/Core/SerializableType.php b/seed/php-model/unions/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/unions/src/Core/SerializableType.php +++ b/seed/php-model/unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/unions/src/Core/Union.php b/seed/php-model/unions/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/unions/src/Core/Union.php +++ b/seed/php-model/unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/unknown/src/Core/JsonDecoder.php b/seed/php-model/unknown/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/unknown/src/Core/JsonDecoder.php +++ b/seed/php-model/unknown/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/unknown/src/Core/JsonDeserializer.php b/seed/php-model/unknown/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-model/unknown/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/unknown/src/Core/JsonSerializer.php b/seed/php-model/unknown/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/unknown/src/Core/JsonSerializer.php +++ b/seed/php-model/unknown/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/unknown/src/Core/SerializableType.php b/seed/php-model/unknown/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/unknown/src/Core/SerializableType.php +++ b/seed/php-model/unknown/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/unknown/src/Core/Union.php b/seed/php-model/unknown/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/unknown/src/Core/Union.php +++ b/seed/php-model/unknown/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/unknown/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/validation/src/Core/JsonDecoder.php b/seed/php-model/validation/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/validation/src/Core/JsonDecoder.php +++ b/seed/php-model/validation/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/validation/src/Core/JsonDeserializer.php b/seed/php-model/validation/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/validation/src/Core/JsonDeserializer.php +++ b/seed/php-model/validation/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/validation/src/Core/JsonSerializer.php b/seed/php-model/validation/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/validation/src/Core/JsonSerializer.php +++ b/seed/php-model/validation/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/validation/src/Core/SerializableType.php b/seed/php-model/validation/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/validation/src/Core/SerializableType.php +++ b/seed/php-model/validation/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/validation/src/Core/Union.php b/seed/php-model/validation/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/validation/src/Core/Union.php +++ b/seed/php-model/validation/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/validation/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/variables/src/Core/JsonDecoder.php b/seed/php-model/variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/variables/src/Core/JsonDecoder.php +++ b/seed/php-model/variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/variables/src/Core/JsonDeserializer.php b/seed/php-model/variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/variables/src/Core/JsonDeserializer.php +++ b/seed/php-model/variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/variables/src/Core/JsonSerializer.php b/seed/php-model/variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/variables/src/Core/JsonSerializer.php +++ b/seed/php-model/variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/variables/src/Core/SerializableType.php b/seed/php-model/variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/variables/src/Core/SerializableType.php +++ b/seed/php-model/variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/variables/src/Core/Union.php b/seed/php-model/variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/variables/src/Core/Union.php +++ b/seed/php-model/variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/version-no-default/src/Core/JsonDecoder.php b/seed/php-model/version-no-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/version-no-default/src/Core/JsonDecoder.php +++ b/seed/php-model/version-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/version-no-default/src/Core/JsonDeserializer.php b/seed/php-model/version-no-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/version-no-default/src/Core/JsonSerializer.php b/seed/php-model/version-no-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-model/version-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/version-no-default/src/Core/SerializableType.php b/seed/php-model/version-no-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/version-no-default/src/Core/SerializableType.php +++ b/seed/php-model/version-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/version-no-default/src/Core/Union.php b/seed/php-model/version-no-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/version-no-default/src/Core/Union.php +++ b/seed/php-model/version-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/version/src/Core/JsonDecoder.php b/seed/php-model/version/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/version/src/Core/JsonDecoder.php +++ b/seed/php-model/version/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/version/src/Core/JsonDeserializer.php b/seed/php-model/version/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/version/src/Core/JsonDeserializer.php +++ b/seed/php-model/version/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/version/src/Core/JsonSerializer.php b/seed/php-model/version/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/version/src/Core/JsonSerializer.php +++ b/seed/php-model/version/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/version/src/Core/SerializableType.php b/seed/php-model/version/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/version/src/Core/SerializableType.php +++ b/seed/php-model/version/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/version/src/Core/Union.php b/seed/php-model/version/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/version/src/Core/Union.php +++ b/seed/php-model/version/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/version/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/websocket/src/Core/JsonDecoder.php b/seed/php-model/websocket/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-model/websocket/src/Core/JsonDecoder.php +++ b/seed/php-model/websocket/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-model/websocket/src/Core/JsonDeserializer.php b/seed/php-model/websocket/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-model/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-model/websocket/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-model/websocket/src/Core/JsonSerializer.php b/seed/php-model/websocket/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-model/websocket/src/Core/JsonSerializer.php +++ b/seed/php-model/websocket/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-model/websocket/src/Core/SerializableType.php b/seed/php-model/websocket/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-model/websocket/src/Core/SerializableType.php +++ b/seed/php-model/websocket/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-model/websocket/src/Core/Union.php b/seed/php-model/websocket/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-model/websocket/src/Core/Union.php +++ b/seed/php-model/websocket/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-model/websocket/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php b/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias-extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/alias-extends/src/Core/SerializableType.php b/seed/php-sdk/alias-extends/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/alias-extends/src/Core/SerializableType.php +++ b/seed/php-sdk/alias-extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/alias-extends/src/Core/Union.php b/seed/php-sdk/alias-extends/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/alias-extends/src/Core/Union.php +++ b/seed/php-sdk/alias-extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/alias-extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/alias/src/Core/JsonDecoder.php b/seed/php-sdk/alias/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/alias/src/Core/JsonDecoder.php +++ b/seed/php-sdk/alias/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/alias/src/Core/JsonDeserializer.php b/seed/php-sdk/alias/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/alias/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/alias/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/alias/src/Core/JsonSerializer.php b/seed/php-sdk/alias/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/alias/src/Core/JsonSerializer.php +++ b/seed/php-sdk/alias/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/alias/src/Core/SerializableType.php b/seed/php-sdk/alias/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/alias/src/Core/SerializableType.php +++ b/seed/php-sdk/alias/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/alias/src/Core/Union.php b/seed/php-sdk/alias/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/alias/src/Core/Union.php +++ b/seed/php-sdk/alias/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/alias/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/any-auth/src/Core/JsonDecoder.php b/seed/php-sdk/any-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/any-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/any-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/any-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/any-auth/src/Core/SerializableType.php b/seed/php-sdk/any-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/any-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/any-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/any-auth/src/Core/Union.php b/seed/php-sdk/any-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/any-auth/src/Core/Union.php +++ b/seed/php-sdk/any-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/any-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php b/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php b/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/api-wide-base-path/src/Core/Union.php b/seed/php-sdk/api-wide-base-path/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/api-wide-base-path/src/Core/Union.php +++ b/seed/php-sdk/api-wide-base-path/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/api-wide-base-path/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/audiences/src/Core/JsonDecoder.php b/seed/php-sdk/audiences/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/audiences/src/Core/JsonDecoder.php +++ b/seed/php-sdk/audiences/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/audiences/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/audiences/src/Core/JsonSerializer.php b/seed/php-sdk/audiences/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/audiences/src/Core/JsonSerializer.php +++ b/seed/php-sdk/audiences/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/audiences/src/Core/SerializableType.php b/seed/php-sdk/audiences/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/audiences/src/Core/SerializableType.php +++ b/seed/php-sdk/audiences/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/audiences/src/Core/Union.php b/seed/php-sdk/audiences/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/audiences/src/Core/Union.php +++ b/seed/php-sdk/audiences/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/audiences/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/auth-environment-variables/src/Core/Union.php b/seed/php-sdk/auth-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/auth-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php b/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/basic-auth-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/basic-auth-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php b/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/basic-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/basic-auth/src/Core/SerializableType.php b/seed/php-sdk/basic-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/basic-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/basic-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/basic-auth/src/Core/Union.php b/seed/php-sdk/basic-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/basic-auth/src/Core/Union.php +++ b/seed/php-sdk/basic-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/basic-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php b/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php +++ b/seed/php-sdk/bearer-token-environment-variable/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/bearer-token-environment-variable/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/bytes/src/Core/JsonDecoder.php b/seed/php-sdk/bytes/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/bytes/src/Core/JsonDecoder.php +++ b/seed/php-sdk/bytes/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/bytes/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/bytes/src/Core/JsonSerializer.php b/seed/php-sdk/bytes/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/bytes/src/Core/JsonSerializer.php +++ b/seed/php-sdk/bytes/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/bytes/src/Core/SerializableType.php b/seed/php-sdk/bytes/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/bytes/src/Core/SerializableType.php +++ b/seed/php-sdk/bytes/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/bytes/src/Core/Union.php b/seed/php-sdk/bytes/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/bytes/src/Core/Union.php +++ b/seed/php-sdk/bytes/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/bytes/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php b/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php b/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/circular-references-advanced/src/Core/Union.php b/seed/php-sdk/circular-references-advanced/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/circular-references-advanced/src/Core/Union.php +++ b/seed/php-sdk/circular-references-advanced/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/circular-references-advanced/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/circular-references/src/Core/JsonDecoder.php b/seed/php-sdk/circular-references/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonDecoder.php +++ b/seed/php-sdk/circular-references/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/circular-references/src/Core/JsonSerializer.php +++ b/seed/php-sdk/circular-references/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/circular-references/src/Core/SerializableType.php b/seed/php-sdk/circular-references/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/circular-references/src/Core/SerializableType.php +++ b/seed/php-sdk/circular-references/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/circular-references/src/Core/Union.php b/seed/php-sdk/circular-references/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/circular-references/src/Core/Union.php +++ b/seed/php-sdk/circular-references/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/circular-references/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php b/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php b/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/cross-package-type-names/src/Core/Union.php b/seed/php-sdk/cross-package-type-names/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/cross-package-type-names/src/Core/Union.php +++ b/seed/php-sdk/cross-package-type-names/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/cross-package-type-names/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php b/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php +++ b/seed/php-sdk/custom-auth/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/custom-auth/src/Core/SerializableType.php b/seed/php-sdk/custom-auth/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/custom-auth/src/Core/SerializableType.php +++ b/seed/php-sdk/custom-auth/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/custom-auth/src/Core/Union.php b/seed/php-sdk/custom-auth/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/custom-auth/src/Core/Union.php +++ b/seed/php-sdk/custom-auth/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/custom-auth/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/enum/src/Core/JsonDecoder.php b/seed/php-sdk/enum/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/enum/src/Core/JsonDecoder.php +++ b/seed/php-sdk/enum/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/enum/src/Core/JsonDeserializer.php b/seed/php-sdk/enum/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/enum/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/enum/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/enum/src/Core/JsonSerializer.php b/seed/php-sdk/enum/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/enum/src/Core/JsonSerializer.php +++ b/seed/php-sdk/enum/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/enum/src/Core/SerializableType.php b/seed/php-sdk/enum/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/enum/src/Core/SerializableType.php +++ b/seed/php-sdk/enum/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/enum/src/Core/Union.php b/seed/php-sdk/enum/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/enum/src/Core/Union.php +++ b/seed/php-sdk/enum/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php b/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php index 55b714944c..eca5e93593 100644 --- a/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php +++ b/seed/php-sdk/enum/src/InlinedRequest/Requests/SendEnumInlinedRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; use Seed\Core\JsonProperty; +use Seed\Types\Color; class SendEnumInlinedRequest extends SerializableType { @@ -21,23 +22,23 @@ class SendEnumInlinedRequest extends SerializableType public ?string $maybeOperand; /** - * @var mixed $operandOrColor + * @var value-of|value-of $operandOrColor */ #[JsonProperty('operandOrColor')] - public mixed $operandOrColor; + public string $operandOrColor; /** - * @var mixed $maybeOperandOrColor + * @var value-of|value-of|null $maybeOperandOrColor */ #[JsonProperty('maybeOperandOrColor')] - public mixed $maybeOperandOrColor; + public string|null $maybeOperandOrColor; /** * @param array{ * operand: value-of, * maybeOperand?: ?value-of, - * operandOrColor: mixed, - * maybeOperandOrColor: mixed, + * operandOrColor: value-of|value-of, + * maybeOperandOrColor?: value-of|value-of|null, * } $values */ public function __construct( @@ -46,6 +47,6 @@ public function __construct( $this->operand = $values['operand']; $this->maybeOperand = $values['maybeOperand'] ?? null; $this->operandOrColor = $values['operandOrColor']; - $this->maybeOperandOrColor = $values['maybeOperandOrColor']; + $this->maybeOperandOrColor = $values['maybeOperandOrColor'] ?? null; } } diff --git a/seed/php-sdk/enum/src/PathParam/PathParamClient.php b/seed/php-sdk/enum/src/PathParam/PathParamClient.php index 621b9958a1..99bacb4426 100644 --- a/seed/php-sdk/enum/src/PathParam/PathParamClient.php +++ b/seed/php-sdk/enum/src/PathParam/PathParamClient.php @@ -4,6 +4,7 @@ use Seed\Core\RawClient; use Seed\Types\Operand; +use Seed\Types\Color; use Seed\Exceptions\SeedException; use Seed\Exceptions\SeedApiException; use Seed\Core\JsonApiRequest; @@ -29,15 +30,15 @@ public function __construct( /** * @param value-of $operand * @param ?value-of $maybeOperand - * @param mixed $operandOrColor - * @param mixed $maybeOperandOrColor + * @param value-of|value-of $operandOrColor + * @param value-of|value-of|null $maybeOperandOrColor * @param ?array{ * baseUrl?: string, * } $options * @throws SeedException * @throws SeedApiException */ - public function send(string $operand, ?string $maybeOperand = null, mixed $operandOrColor, mixed $maybeOperandOrColor, ?array $options = null): void + public function send(string $operand, ?string $maybeOperand = null, string $operandOrColor, string|null $maybeOperandOrColor = null, ?array $options = null): void { try { $response = $this->client->sendRequest( diff --git a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php index b026f8a66f..d73c5650fb 100644 --- a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php +++ b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumAsQueryParamRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; +use Seed\Types\Color; class SendEnumAsQueryParamRequest extends SerializableType { @@ -18,21 +19,21 @@ class SendEnumAsQueryParamRequest extends SerializableType public ?string $maybeOperand; /** - * @var mixed $operandOrColor + * @var value-of|value-of $operandOrColor */ - public mixed $operandOrColor; + public string $operandOrColor; /** - * @var mixed $maybeOperandOrColor + * @var value-of|value-of|null $maybeOperandOrColor */ - public mixed $maybeOperandOrColor; + public string|null $maybeOperandOrColor; /** * @param array{ * operand: value-of, * maybeOperand?: ?value-of, - * operandOrColor: mixed, - * maybeOperandOrColor: mixed, + * operandOrColor: value-of|value-of, + * maybeOperandOrColor?: value-of|value-of|null, * } $values */ public function __construct( @@ -41,6 +42,6 @@ public function __construct( $this->operand = $values['operand']; $this->maybeOperand = $values['maybeOperand'] ?? null; $this->operandOrColor = $values['operandOrColor']; - $this->maybeOperandOrColor = $values['maybeOperandOrColor']; + $this->maybeOperandOrColor = $values['maybeOperandOrColor'] ?? null; } } diff --git a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php index 5acfab2c4e..b7c0a49cde 100644 --- a/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php +++ b/seed/php-sdk/enum/src/QueryParam/Requests/SendEnumListAsQueryParamRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Types\Operand; +use Seed\Types\Color; class SendEnumListAsQueryParamRequest extends SerializableType { @@ -18,12 +19,12 @@ class SendEnumListAsQueryParamRequest extends SerializableType public array $maybeOperand; /** - * @var array $operandOrColor + * @var array|value-of> $operandOrColor */ public array $operandOrColor; /** - * @var array $maybeOperandOrColor + * @var array|value-of|null> $maybeOperandOrColor */ public array $maybeOperandOrColor; @@ -31,8 +32,8 @@ class SendEnumListAsQueryParamRequest extends SerializableType * @param array{ * operand: array>, * maybeOperand: array>, - * operandOrColor: array, - * maybeOperandOrColor: array, + * operandOrColor: array|value-of>, + * maybeOperandOrColor: array|value-of|null>, * } $values */ public function __construct( diff --git a/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/enum/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/error-property/src/Core/JsonDecoder.php b/seed/php-sdk/error-property/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/error-property/src/Core/JsonDecoder.php +++ b/seed/php-sdk/error-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/error-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/error-property/src/Core/JsonSerializer.php b/seed/php-sdk/error-property/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/error-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/error-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/error-property/src/Core/SerializableType.php b/seed/php-sdk/error-property/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/error-property/src/Core/SerializableType.php +++ b/seed/php-sdk/error-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/error-property/src/Core/Union.php b/seed/php-sdk/error-property/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/error-property/src/Core/Union.php +++ b/seed/php-sdk/error-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/error-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php b/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/exhaustive/src/Core/SerializableType.php b/seed/php-sdk/exhaustive/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/exhaustive/src/Core/Union.php b/seed/php-sdk/exhaustive/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/exhaustive/src/Core/Union.php +++ b/seed/php-sdk/exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/extends/src/Core/JsonDecoder.php b/seed/php-sdk/extends/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/extends/src/Core/JsonDecoder.php +++ b/seed/php-sdk/extends/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/extends/src/Core/JsonDeserializer.php b/seed/php-sdk/extends/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/extends/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extends/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/extends/src/Core/JsonSerializer.php b/seed/php-sdk/extends/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/extends/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extends/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/extends/src/Core/SerializableType.php b/seed/php-sdk/extends/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/extends/src/Core/SerializableType.php +++ b/seed/php-sdk/extends/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/extends/src/Core/Union.php b/seed/php-sdk/extends/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/extends/src/Core/Union.php +++ b/seed/php-sdk/extends/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/extends/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php b/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php +++ b/seed/php-sdk/extra-properties/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/extra-properties/src/Core/SerializableType.php b/seed/php-sdk/extra-properties/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/extra-properties/src/Core/SerializableType.php +++ b/seed/php-sdk/extra-properties/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/extra-properties/src/Core/Union.php b/seed/php-sdk/extra-properties/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/extra-properties/src/Core/Union.php +++ b/seed/php-sdk/extra-properties/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/extra-properties/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/file-download/src/Core/JsonDecoder.php b/seed/php-sdk/file-download/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/file-download/src/Core/JsonDecoder.php +++ b/seed/php-sdk/file-download/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/file-download/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/file-download/src/Core/JsonSerializer.php b/seed/php-sdk/file-download/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/file-download/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-download/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/file-download/src/Core/SerializableType.php b/seed/php-sdk/file-download/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/file-download/src/Core/SerializableType.php +++ b/seed/php-sdk/file-download/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/file-download/src/Core/Union.php b/seed/php-sdk/file-download/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/file-download/src/Core/Union.php +++ b/seed/php-sdk/file-download/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/file-download/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/file-upload/src/Core/JsonDecoder.php b/seed/php-sdk/file-upload/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonDecoder.php +++ b/seed/php-sdk/file-upload/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/file-upload/src/Core/JsonSerializer.php +++ b/seed/php-sdk/file-upload/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/file-upload/src/Core/SerializableType.php b/seed/php-sdk/file-upload/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/file-upload/src/Core/SerializableType.php +++ b/seed/php-sdk/file-upload/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/file-upload/src/Core/Union.php b/seed/php-sdk/file-upload/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/file-upload/src/Core/Union.php +++ b/seed/php-sdk/file-upload/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/file-upload/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/folders/src/Core/JsonDecoder.php b/seed/php-sdk/folders/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/folders/src/Core/JsonDecoder.php +++ b/seed/php-sdk/folders/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/folders/src/Core/JsonDeserializer.php b/seed/php-sdk/folders/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/folders/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/folders/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/folders/src/Core/JsonSerializer.php b/seed/php-sdk/folders/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/folders/src/Core/JsonSerializer.php +++ b/seed/php-sdk/folders/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/folders/src/Core/SerializableType.php b/seed/php-sdk/folders/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/folders/src/Core/SerializableType.php +++ b/seed/php-sdk/folders/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/folders/src/Core/Union.php b/seed/php-sdk/folders/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/folders/src/Core/Union.php +++ b/seed/php-sdk/folders/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/folders/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php index ebd3d19a26..2882f1b047 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DeleteRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class DeleteRequest extends SerializableType { @@ -27,25 +28,25 @@ class DeleteRequest extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @param array{ * ids?: ?array, * deleteAll?: ?bool, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->ids = $values['ids'] ?? null; $this->deleteAll = $values['deleteAll'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php index 7219ecab76..0706e7ce2f 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/DescribeRequest.php @@ -4,23 +4,24 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class DescribeRequest extends SerializableType { /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @param array{ - * filter: mixed, + * filter?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php index 9fd063c8cf..88f2351d06 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/QueryRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; use Seed\Types\QueryColumn; use Seed\Core\ArrayType; use Seed\Types\IndexedData; @@ -23,10 +24,10 @@ class QueryRequest extends SerializableType public int $topK; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?bool $includeValues @@ -68,7 +69,7 @@ class QueryRequest extends SerializableType * @param array{ * namespace?: ?string, * topK: int, - * filter: mixed, + * filter?: array|array|null, * includeValues?: ?bool, * includeMetadata?: ?bool, * queries?: ?array, @@ -82,7 +83,7 @@ public function __construct( ) { $this->namespace = $values['namespace'] ?? null; $this->topK = $values['topK']; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->includeValues = $values['includeValues'] ?? null; $this->includeMetadata = $values['includeMetadata'] ?? null; $this->queries = $values['queries'] ?? null; diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php index 7e2afca26a..21a4c59b46 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Dataservice/Requests/UpdateRequest.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; use Seed\Types\IndexedData; class UpdateRequest extends SerializableType @@ -22,10 +23,10 @@ class UpdateRequest extends SerializableType public ?array $values; /** - * @var mixed $setMetadata + * @var array|array|null $setMetadata */ - #[JsonProperty('setMetadata')] - public mixed $setMetadata; + #[JsonProperty('setMetadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $setMetadata; /** * @var ?string $namespace @@ -43,7 +44,7 @@ class UpdateRequest extends SerializableType * @param array{ * id: string, * values?: ?array, - * setMetadata: mixed, + * setMetadata?: array|array|null, * namespace?: ?string, * indexedData?: ?IndexedData, * } $values @@ -53,7 +54,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values'] ?? null; - $this->setMetadata = $values['setMetadata']; + $this->setMetadata = $values['setMetadata'] ?? null; $this->namespace = $values['namespace'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php index c5e2615ac0..c696c3d3bc 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/Column.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class Column extends SerializableType { @@ -21,10 +22,10 @@ class Column extends SerializableType public array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -36,7 +37,7 @@ class Column extends SerializableType * @param array{ * id: string, * values: array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -45,7 +46,7 @@ public function __construct( ) { $this->id = $values['id']; $this->values = $values['values']; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php index 9e23e5acb3..27e8ae75d2 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/QueryColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class QueryColumn extends SerializableType { @@ -27,10 +28,10 @@ class QueryColumn extends SerializableType public ?string $namespace; /** - * @var mixed $filter + * @var array|array|null $filter */ - #[JsonProperty('filter')] - public mixed $filter; + #[JsonProperty('filter'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $filter; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class QueryColumn extends SerializableType * values: array, * topK?: ?int, * namespace?: ?string, - * filter: mixed, + * filter?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->values = $values['values']; $this->topK = $values['topK'] ?? null; $this->namespace = $values['namespace'] ?? null; - $this->filter = $values['filter']; + $this->filter = $values['filter'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php index e770ca3b3e..9c0399df33 100644 --- a/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php +++ b/seed/php-sdk/grpc-proto-exhaustive/src/Types/ScoredColumn.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class ScoredColumn extends SerializableType { @@ -27,10 +28,10 @@ class ScoredColumn extends SerializableType public ?array $values; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @var ?IndexedData $indexedData @@ -43,7 +44,7 @@ class ScoredColumn extends SerializableType * id: string, * score?: ?float, * values?: ?array, - * metadata: mixed, + * metadata?: array|array|null, * indexedData?: ?IndexedData, * } $values */ @@ -53,7 +54,7 @@ public function __construct( $this->id = $values['id']; $this->score = $values['score'] ?? null; $this->values = $values['values'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; $this->indexedData = $values['indexedData'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/grpc-proto-exhaustive/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php b/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php +++ b/seed/php-sdk/grpc-proto/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/grpc-proto/src/Core/SerializableType.php +++ b/seed/php-sdk/grpc-proto/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/grpc-proto/src/Core/Union.php b/seed/php-sdk/grpc-proto/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/grpc-proto/src/Core/Union.php +++ b/seed/php-sdk/grpc-proto/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/grpc-proto/src/Types/UserModel.php b/seed/php-sdk/grpc-proto/src/Types/UserModel.php index aa7089f7af..cff03d8ec8 100644 --- a/seed/php-sdk/grpc-proto/src/Types/UserModel.php +++ b/seed/php-sdk/grpc-proto/src/Types/UserModel.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class UserModel extends SerializableType { @@ -32,10 +33,10 @@ class UserModel extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class UserModel extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php index ba5b06a57f..a239f9835b 100644 --- a/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php +++ b/seed/php-sdk/grpc-proto/src/Userservice/Requests/CreateRequest.php @@ -4,6 +4,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; +use Seed\Core\Union; class CreateRequest extends SerializableType { @@ -32,10 +33,10 @@ class CreateRequest extends SerializableType public ?float $weight; /** - * @var mixed $metadata + * @var array|array|null $metadata */ - #[JsonProperty('metadata')] - public mixed $metadata; + #[JsonProperty('metadata'), Union(['string' => new Union('float', 'string', 'bool')], ['string' => 'mixed'])] + public array|null $metadata; /** * @param array{ @@ -43,16 +44,16 @@ class CreateRequest extends SerializableType * email?: ?string, * age?: ?int, * weight?: ?float, - * metadata: mixed, + * metadata?: array|array|null, * } $values */ public function __construct( - array $values, + array $values = [], ) { $this->username = $values['username'] ?? null; $this->email = $values['email'] ?? null; $this->age = $values['age'] ?? null; $this->weight = $values['weight'] ?? null; - $this->metadata = $values['metadata']; + $this->metadata = $values['metadata'] ?? null; } } diff --git a/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/grpc-proto/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php b/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php +++ b/seed/php-sdk/idempotency-headers/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php +++ b/seed/php-sdk/idempotency-headers/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/idempotency-headers/src/Core/Union.php b/seed/php-sdk/idempotency-headers/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/idempotency-headers/src/Core/Union.php +++ b/seed/php-sdk/idempotency-headers/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/idempotency-headers/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/imdb/src/Core/JsonDecoder.php b/seed/php-sdk/imdb/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/imdb/src/Core/JsonDecoder.php +++ b/seed/php-sdk/imdb/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/imdb/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/imdb/src/Core/JsonSerializer.php b/seed/php-sdk/imdb/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/imdb/src/Core/JsonSerializer.php +++ b/seed/php-sdk/imdb/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/imdb/src/Core/SerializableType.php b/seed/php-sdk/imdb/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/imdb/src/Core/SerializableType.php +++ b/seed/php-sdk/imdb/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/imdb/src/Core/Union.php b/seed/php-sdk/imdb/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/imdb/src/Core/Union.php +++ b/seed/php-sdk/imdb/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/imdb/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/literal/src/Core/JsonDecoder.php b/seed/php-sdk/literal/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/literal/src/Core/JsonDecoder.php +++ b/seed/php-sdk/literal/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/literal/src/Core/JsonDeserializer.php b/seed/php-sdk/literal/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/literal/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/literal/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/literal/src/Core/JsonSerializer.php b/seed/php-sdk/literal/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/literal/src/Core/JsonSerializer.php +++ b/seed/php-sdk/literal/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/literal/src/Core/SerializableType.php b/seed/php-sdk/literal/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/literal/src/Core/SerializableType.php +++ b/seed/php-sdk/literal/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/literal/src/Core/Union.php b/seed/php-sdk/literal/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/literal/src/Core/Union.php +++ b/seed/php-sdk/literal/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/literal/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php b/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-case/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/mixed-case/src/Core/SerializableType.php b/seed/php-sdk/mixed-case/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/mixed-case/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-case/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/mixed-case/src/Core/Union.php b/seed/php-sdk/mixed-case/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/mixed-case/src/Core/Union.php +++ b/seed/php-sdk/mixed-case/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/mixed-case/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php b/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php b/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/mixed-file-directory/src/Core/Union.php b/seed/php-sdk/mixed-file-directory/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/mixed-file-directory/src/Core/Union.php +++ b/seed/php-sdk/mixed-file-directory/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/mixed-file-directory/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php b/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php b/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php b/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php +++ b/seed/php-sdk/multi-line-docs/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php b/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php +++ b/seed/php-sdk/multi-line-docs/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/multi-line-docs/src/Core/Union.php b/seed/php-sdk/multi-line-docs/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/multi-line-docs/src/Core/Union.php +++ b/seed/php-sdk/multi-line-docs/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/multi-line-docs/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/no-environment/src/Core/JsonDecoder.php b/seed/php-sdk/no-environment/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonDecoder.php +++ b/seed/php-sdk/no-environment/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/no-environment/src/Core/JsonSerializer.php +++ b/seed/php-sdk/no-environment/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/no-environment/src/Core/SerializableType.php b/seed/php-sdk/no-environment/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/no-environment/src/Core/SerializableType.php +++ b/seed/php-sdk/no-environment/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/no-environment/src/Core/Union.php b/seed/php-sdk/no-environment/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/no-environment/src/Core/Union.php +++ b/seed/php-sdk/no-environment/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/no-environment/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-environment-variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials-nested-root/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials-nested-root/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php b/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php b/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/oauth-client-credentials/src/Core/Union.php b/seed/php-sdk/oauth-client-credentials/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/oauth-client-credentials/src/Core/Union.php +++ b/seed/php-sdk/oauth-client-credentials/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/oauth-client-credentials/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/object/src/Core/JsonDecoder.php b/seed/php-sdk/object/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/object/src/Core/JsonDecoder.php +++ b/seed/php-sdk/object/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/object/src/Core/JsonDeserializer.php b/seed/php-sdk/object/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/object/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/object/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/object/src/Core/JsonSerializer.php b/seed/php-sdk/object/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/object/src/Core/JsonSerializer.php +++ b/seed/php-sdk/object/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/object/src/Core/SerializableType.php b/seed/php-sdk/object/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/object/src/Core/SerializableType.php +++ b/seed/php-sdk/object/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/object/src/Core/Union.php b/seed/php-sdk/object/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/object/src/Core/Union.php +++ b/seed/php-sdk/object/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/object/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php b/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php b/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php b/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php +++ b/seed/php-sdk/objects-with-imports/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php b/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php +++ b/seed/php-sdk/objects-with-imports/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/objects-with-imports/src/Core/Union.php b/seed/php-sdk/objects-with-imports/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/objects-with-imports/src/Core/Union.php +++ b/seed/php-sdk/objects-with-imports/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/objects-with-imports/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/optional/src/Core/JsonDecoder.php b/seed/php-sdk/optional/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/optional/src/Core/JsonDecoder.php +++ b/seed/php-sdk/optional/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/optional/src/Core/JsonDeserializer.php b/seed/php-sdk/optional/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/optional/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/optional/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/optional/src/Core/JsonSerializer.php b/seed/php-sdk/optional/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/optional/src/Core/JsonSerializer.php +++ b/seed/php-sdk/optional/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/optional/src/Core/SerializableType.php b/seed/php-sdk/optional/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/optional/src/Core/SerializableType.php +++ b/seed/php-sdk/optional/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/optional/src/Core/Union.php b/seed/php-sdk/optional/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/optional/src/Core/Union.php +++ b/seed/php-sdk/optional/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/optional/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/package-yml/src/Core/JsonDecoder.php b/seed/php-sdk/package-yml/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonDecoder.php +++ b/seed/php-sdk/package-yml/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/package-yml/src/Core/JsonSerializer.php +++ b/seed/php-sdk/package-yml/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/package-yml/src/Core/SerializableType.php b/seed/php-sdk/package-yml/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/package-yml/src/Core/SerializableType.php +++ b/seed/php-sdk/package-yml/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/package-yml/src/Core/Union.php b/seed/php-sdk/package-yml/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/package-yml/src/Core/Union.php +++ b/seed/php-sdk/package-yml/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/package-yml/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/pagination/src/Core/JsonDecoder.php b/seed/php-sdk/pagination/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/pagination/src/Core/JsonDecoder.php +++ b/seed/php-sdk/pagination/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/pagination/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/pagination/src/Core/JsonSerializer.php b/seed/php-sdk/pagination/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/pagination/src/Core/JsonSerializer.php +++ b/seed/php-sdk/pagination/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/pagination/src/Core/SerializableType.php b/seed/php-sdk/pagination/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/pagination/src/Core/SerializableType.php +++ b/seed/php-sdk/pagination/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/pagination/src/Core/Union.php b/seed/php-sdk/pagination/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/pagination/src/Core/Union.php +++ b/seed/php-sdk/pagination/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/pagination/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/plain-text/src/Core/JsonDecoder.php b/seed/php-sdk/plain-text/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonDecoder.php +++ b/seed/php-sdk/plain-text/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/plain-text/src/Core/JsonSerializer.php +++ b/seed/php-sdk/plain-text/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/plain-text/src/Core/SerializableType.php b/seed/php-sdk/plain-text/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/plain-text/src/Core/SerializableType.php +++ b/seed/php-sdk/plain-text/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/plain-text/src/Core/Union.php b/seed/php-sdk/plain-text/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/plain-text/src/Core/Union.php +++ b/seed/php-sdk/plain-text/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/plain-text/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php b/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php +++ b/seed/php-sdk/query-parameters/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/query-parameters/src/Core/SerializableType.php b/seed/php-sdk/query-parameters/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/query-parameters/src/Core/SerializableType.php +++ b/seed/php-sdk/query-parameters/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/query-parameters/src/Core/Union.php b/seed/php-sdk/query-parameters/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/query-parameters/src/Core/Union.php +++ b/seed/php-sdk/query-parameters/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/query-parameters/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php b/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php +++ b/seed/php-sdk/reserved-keywords/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php +++ b/seed/php-sdk/reserved-keywords/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/reserved-keywords/src/Core/Union.php b/seed/php-sdk/reserved-keywords/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/reserved-keywords/src/Core/Union.php +++ b/seed/php-sdk/reserved-keywords/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/reserved-keywords/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/response-property/src/Core/JsonDecoder.php b/seed/php-sdk/response-property/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/response-property/src/Core/JsonDecoder.php +++ b/seed/php-sdk/response-property/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/response-property/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/response-property/src/Core/JsonSerializer.php b/seed/php-sdk/response-property/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/response-property/src/Core/JsonSerializer.php +++ b/seed/php-sdk/response-property/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/response-property/src/Core/SerializableType.php b/seed/php-sdk/response-property/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/response-property/src/Core/SerializableType.php +++ b/seed/php-sdk/response-property/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/response-property/src/Core/Union.php b/seed/php-sdk/response-property/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/response-property/src/Core/Union.php +++ b/seed/php-sdk/response-property/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/response-property/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php b/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php +++ b/seed/php-sdk/simple-fhir/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/simple-fhir/src/Core/SerializableType.php +++ b/seed/php-sdk/simple-fhir/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/simple-fhir/src/Core/Union.php b/seed/php-sdk/simple-fhir/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/simple-fhir/src/Core/Union.php +++ b/seed/php-sdk/simple-fhir/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/simple-fhir/src/Types/BaseResource.php b/seed/php-sdk/simple-fhir/src/Types/BaseResource.php index 505b309bd0..83ac40eb59 100644 --- a/seed/php-sdk/simple-fhir/src/Types/BaseResource.php +++ b/seed/php-sdk/simple-fhir/src/Types/BaseResource.php @@ -5,6 +5,7 @@ use Seed\Core\SerializableType; use Seed\Core\JsonProperty; use Seed\Core\ArrayType; +use Seed\Core\Union; class BaseResource extends SerializableType { @@ -15,9 +16,9 @@ class BaseResource extends SerializableType public string $id; /** - * @var array $relatedResources + * @var array $relatedResources */ - #[JsonProperty('related_resources'), ArrayType(['mixed'])] + #[JsonProperty('related_resources'), ArrayType([new Union(Account::class, Patient::class, Practitioner::class, Script::class)])] public array $relatedResources; /** @@ -29,7 +30,7 @@ class BaseResource extends SerializableType /** * @param array{ * id: string, - * relatedResources: array, + * relatedResources: array, * memo: Memo, * } $values */ diff --git a/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/simple-fhir/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php b/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php b/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/single-url-environment-default/src/Core/Union.php b/seed/php-sdk/single-url-environment-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/single-url-environment-default/src/Core/Union.php +++ b/seed/php-sdk/single-url-environment-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/single-url-environment-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php b/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php b/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php +++ b/seed/php-sdk/single-url-environment-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/single-url-environment-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php b/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming-parameter/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming-parameter/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/streaming-parameter/src/Core/Union.php b/seed/php-sdk/streaming-parameter/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/streaming-parameter/src/Core/Union.php +++ b/seed/php-sdk/streaming-parameter/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/streaming-parameter/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/streaming/src/Core/JsonDecoder.php b/seed/php-sdk/streaming/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/streaming/src/Core/JsonDecoder.php +++ b/seed/php-sdk/streaming/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/streaming/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/streaming/src/Core/JsonSerializer.php b/seed/php-sdk/streaming/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/streaming/src/Core/JsonSerializer.php +++ b/seed/php-sdk/streaming/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/streaming/src/Core/SerializableType.php b/seed/php-sdk/streaming/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/streaming/src/Core/SerializableType.php +++ b/seed/php-sdk/streaming/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/streaming/src/Core/Union.php b/seed/php-sdk/streaming/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/streaming/src/Core/Union.php +++ b/seed/php-sdk/streaming/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/streaming/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/trace/src/Core/JsonDecoder.php b/seed/php-sdk/trace/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/trace/src/Core/JsonDecoder.php +++ b/seed/php-sdk/trace/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/trace/src/Core/JsonDeserializer.php b/seed/php-sdk/trace/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/trace/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/trace/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/trace/src/Core/JsonSerializer.php b/seed/php-sdk/trace/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/trace/src/Core/JsonSerializer.php +++ b/seed/php-sdk/trace/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/trace/src/Core/SerializableType.php b/seed/php-sdk/trace/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/trace/src/Core/SerializableType.php +++ b/seed/php-sdk/trace/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/trace/src/Core/Union.php b/seed/php-sdk/trace/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/trace/src/Core/Union.php +++ b/seed/php-sdk/trace/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/trace/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/unions/src/Core/JsonDecoder.php b/seed/php-sdk/unions/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/unions/src/Core/JsonDecoder.php +++ b/seed/php-sdk/unions/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/unions/src/Core/JsonDeserializer.php b/seed/php-sdk/unions/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/unions/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unions/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/unions/src/Core/JsonSerializer.php b/seed/php-sdk/unions/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/unions/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unions/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/unions/src/Core/SerializableType.php b/seed/php-sdk/unions/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/unions/src/Core/SerializableType.php +++ b/seed/php-sdk/unions/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/unions/src/Core/Union.php b/seed/php-sdk/unions/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/unions/src/Core/Union.php +++ b/seed/php-sdk/unions/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/unions/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/unknown/src/Core/JsonDecoder.php b/seed/php-sdk/unknown/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/unknown/src/Core/JsonDecoder.php +++ b/seed/php-sdk/unknown/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/unknown/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/unknown/src/Core/JsonSerializer.php b/seed/php-sdk/unknown/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/unknown/src/Core/JsonSerializer.php +++ b/seed/php-sdk/unknown/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/unknown/src/Core/SerializableType.php b/seed/php-sdk/unknown/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/unknown/src/Core/SerializableType.php +++ b/seed/php-sdk/unknown/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/unknown/src/Core/Union.php b/seed/php-sdk/unknown/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/unknown/src/Core/Union.php +++ b/seed/php-sdk/unknown/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/unknown/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/validation/src/Core/JsonDecoder.php b/seed/php-sdk/validation/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/validation/src/Core/JsonDecoder.php +++ b/seed/php-sdk/validation/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/validation/src/Core/JsonDeserializer.php b/seed/php-sdk/validation/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/validation/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/validation/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/validation/src/Core/JsonSerializer.php b/seed/php-sdk/validation/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/validation/src/Core/JsonSerializer.php +++ b/seed/php-sdk/validation/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/validation/src/Core/SerializableType.php b/seed/php-sdk/validation/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/validation/src/Core/SerializableType.php +++ b/seed/php-sdk/validation/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/validation/src/Core/Union.php b/seed/php-sdk/validation/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/validation/src/Core/Union.php +++ b/seed/php-sdk/validation/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/validation/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/variables/src/Core/JsonDecoder.php b/seed/php-sdk/variables/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/variables/src/Core/JsonDecoder.php +++ b/seed/php-sdk/variables/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/variables/src/Core/JsonDeserializer.php b/seed/php-sdk/variables/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/variables/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/variables/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/variables/src/Core/JsonSerializer.php b/seed/php-sdk/variables/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/variables/src/Core/JsonSerializer.php +++ b/seed/php-sdk/variables/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/variables/src/Core/SerializableType.php b/seed/php-sdk/variables/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/variables/src/Core/SerializableType.php +++ b/seed/php-sdk/variables/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/variables/src/Core/Union.php b/seed/php-sdk/variables/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/variables/src/Core/Union.php +++ b/seed/php-sdk/variables/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/variables/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php b/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php b/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php b/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version-no-default/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/version-no-default/src/Core/SerializableType.php b/seed/php-sdk/version-no-default/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/version-no-default/src/Core/SerializableType.php +++ b/seed/php-sdk/version-no-default/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/version-no-default/src/Core/Union.php b/seed/php-sdk/version-no-default/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/version-no-default/src/Core/Union.php +++ b/seed/php-sdk/version-no-default/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/version-no-default/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/version/src/Core/JsonDecoder.php b/seed/php-sdk/version/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/version/src/Core/JsonDecoder.php +++ b/seed/php-sdk/version/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/version/src/Core/JsonDeserializer.php b/seed/php-sdk/version/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/version/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/version/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/version/src/Core/JsonSerializer.php b/seed/php-sdk/version/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/version/src/Core/JsonSerializer.php +++ b/seed/php-sdk/version/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/version/src/Core/SerializableType.php b/seed/php-sdk/version/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/version/src/Core/SerializableType.php +++ b/seed/php-sdk/version/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/version/src/Core/Union.php b/seed/php-sdk/version/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/version/src/Core/Union.php +++ b/seed/php-sdk/version/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/version/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/websocket/src/Core/JsonDecoder.php b/seed/php-sdk/websocket/src/Core/JsonDecoder.php index 34651d0b9e..c7f9629e01 100644 --- a/seed/php-sdk/websocket/src/Core/JsonDecoder.php +++ b/seed/php-sdk/websocket/src/Core/JsonDecoder.php @@ -121,6 +121,19 @@ public static function decodeArray(string $json, array $type): array return JsonDeserializer::deserializeArray($decoded, $type); } + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } /** * Decodes a JSON string and returns a mixed. * diff --git a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php index 4c9998886e..b1de7d141a 100644 --- a/seed/php-sdk/websocket/src/Core/JsonDeserializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonDeserializer.php @@ -66,18 +66,9 @@ public static function deserializeArray(array $data, array $type): array private static function deserializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::deserializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - $readableType = Utils::getReadableType($data); - throw new JsonException( - "Cannot deserialize value of type $readableType with any of the union types: " . $type - ); + return self::deserializeUnion($data, $type); } + if (is_array($type)) { return self::deserializeArray((array)$data, $type); } @@ -85,9 +76,33 @@ private static function deserializeValue(mixed $data, mixed $type): mixed if (gettype($type) != "string") { throw new JsonException("Unexpected non-string type."); } + return self::deserializeSingleValue($data, $type); } + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + /** * Deserializes a single value based on its expected type. * diff --git a/seed/php-sdk/websocket/src/Core/JsonSerializer.php b/seed/php-sdk/websocket/src/Core/JsonSerializer.php index eae0c08744..1e37550f15 100644 --- a/seed/php-sdk/websocket/src/Core/JsonSerializer.php +++ b/seed/php-sdk/websocket/src/Core/JsonSerializer.php @@ -57,14 +57,7 @@ public static function serializeArray(array $data, array $type): array private static function serializeValue(mixed $data, mixed $type): mixed { if ($type instanceof Union) { - foreach ($type->types as $unionType) { - try { - return self::serializeSingleValue($data, $unionType); - } catch (Exception) { - continue; - } - } - throw new JsonException("Cannot serialize value with any of the union types."); + return self::serializeUnion($data, $type); } if (is_array($type)) { @@ -78,6 +71,30 @@ private static function serializeValue(mixed $data, mixed $type): mixed return self::serializeSingleValue($data, $type); } + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + /** * Serializes a single value based on its type. * diff --git a/seed/php-sdk/websocket/src/Core/SerializableType.php b/seed/php-sdk/websocket/src/Core/SerializableType.php index ecb6c6abc1..9121bdca01 100644 --- a/seed/php-sdk/websocket/src/Core/SerializableType.php +++ b/seed/php-sdk/websocket/src/Core/SerializableType.php @@ -56,6 +56,13 @@ public function jsonSerialize(): array : JsonSerializer::serializeDateTime($value); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + // Handle arrays with type annotations $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; if ($arrayTypeAttr && is_array($value)) { @@ -135,6 +142,13 @@ public static function jsonDeserialize(array $data): static $value = JsonDeserializer::deserializeArray($value, $arrayType); } + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + // Handle object $type = $property->getType(); if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { diff --git a/seed/php-sdk/websocket/src/Core/Union.php b/seed/php-sdk/websocket/src/Core/Union.php index 8608d2cae4..1e9fe801ee 100644 --- a/seed/php-sdk/websocket/src/Core/Union.php +++ b/seed/php-sdk/websocket/src/Core/Union.php @@ -2,20 +2,61 @@ namespace Seed\Core; +use Attribute; + +/** + * Union type attribute for flexible type declarations. + * + * This class is used to define a union of multiple types for a property. + * It allows a property to accept one of several types, including strings, arrays, or other Union types. + * This class supports complex types, nested unions, and arrays, providing a flexible mechanism for property type validation and serialization. + * + * Example: + * + * ```php + * #[Union('string', 'int', 'null', new Union('boolean', 'float'))] + * private mixed $property; + * ``` + */ +#[Attribute(Attribute::TARGET_PROPERTY)] class Union { /** - * @var string[] + * @var array> The types allowed for this property, which can be strings, arrays, or nested Union types. */ public array $types; - public function __construct(string ...$strings) + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) { - $this->types = $strings; + $this->types = $types; } + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ public function __toString(): string { - return implode(' | ', $this->types); + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); } } diff --git a/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php b/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php new file mode 100644 index 0000000000..e278eb4288 --- /dev/null +++ b/seed/php-sdk/websocket/tests/Seed/Core/UnionPropertyTypeTest.php @@ -0,0 +1,116 @@ + 'integer'], UnionPropertyType::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionPropertyType + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $data = [ + 'complexUnion' => [1 => 100, 2 => 200] // Map + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for map + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + // Nested instance of UnionPropertyType + $nestedData = [ + 'complexUnion' => 'Nested String' + ]; + + $data = [ + 'complexUnion' => new UnionPropertyType($nestedData) // UnionPropertyType instance + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for UnionPropertyType instance + $this->assertInstanceOf(UnionPropertyType::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $data = []; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for null + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $data = [ + 'complexUnion' => 42 // Integer + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for integer + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $data = [ + 'complexUnion' => 'Some String' // String + ]; + + $json = json_encode($data, JSON_THROW_ON_ERROR); + $object = UnionPropertyType::fromJson($json); + + // Test for string + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $serializedJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($json, $serializedJson, 'Serialized JSON does not match the original JSON.'); + } +} From 1accc22d34b6d7da39f5e417c2a0d79b4bc5a850 Mon Sep 17 00:00:00 2001 From: dcb6 Date: Mon, 30 Sep 2024 19:59:26 -0400 Subject: [PATCH 3/3] changelog --- generators/php/sdk/versions.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/generators/php/sdk/versions.yml b/generators/php/sdk/versions.yml index 8fd359a75d..c9a63e645a 100644 --- a/generators/php/sdk/versions.yml +++ b/generators/php/sdk/versions.yml @@ -1,3 +1,8 @@ +- version: 0.1.5 + changelogEntry: + - type: feat + summary: >- + Support undiscriminated unions. - version: 0.1.4 changelogEntry: - type: fix