Skip to content

Commit ddfa601

Browse files
committed
fix: Encode nested JsonSerializable values
1 parent b3aad57 commit ddfa601

File tree

3 files changed

+243
-0
lines changed

3 files changed

+243
-0
lines changed
File renamed without changes.

src/Encoder/ValueFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static function create(mixed $value): Value
3434
\is_bool($value) => self::createBoolean($value),
3535
$value instanceof \DateTimeInterface => self::createDateTime($value),
3636
\is_array($value) => self::createArray($value),
37+
$value instanceof \JsonSerializable => self::create($value->jsonSerialize()),
3738
default => throw new InvalidTypeException('Unsupported value type: ' . \get_debug_type($value)),
3839
};
3940
}

tests/Unit/Encoder/ValueFactoryTest.php

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,141 @@ public static function provideUnsupportedTypes(): \Generator
4545
yield 'object without DateTimeInterface' => [new \stdClass(), 'stdClass'];
4646
yield 'null' => [null, 'null'];
4747
}
48+
49+
public static function provideJsonSerializableData(): \Generator
50+
{
51+
yield 'string value' => [
52+
new class implements \JsonSerializable {
53+
public function jsonSerialize(): string
54+
{
55+
return 'serialized string';
56+
}
57+
},
58+
StringValue::class,
59+
'serialized string',
60+
];
61+
62+
yield 'integer value' => [
63+
new class implements \JsonSerializable {
64+
public function jsonSerialize(): int
65+
{
66+
return 42;
67+
}
68+
},
69+
IntegerValue::class,
70+
42,
71+
];
72+
73+
yield 'float value' => [
74+
new class implements \JsonSerializable {
75+
public function jsonSerialize(): float
76+
{
77+
return 3.14;
78+
}
79+
},
80+
FloatValue::class,
81+
3.14,
82+
];
83+
84+
yield 'boolean true' => [
85+
new class implements \JsonSerializable {
86+
public function jsonSerialize(): bool
87+
{
88+
return true;
89+
}
90+
},
91+
BooleanValue::class,
92+
true,
93+
];
94+
95+
yield 'boolean false' => [
96+
new class implements \JsonSerializable {
97+
public function jsonSerialize(): bool
98+
{
99+
return false;
100+
}
101+
},
102+
BooleanValue::class,
103+
false,
104+
];
105+
106+
yield 'indexed array' => [
107+
new class implements \JsonSerializable {
108+
public function jsonSerialize(): array
109+
{
110+
return [1, 2, 3];
111+
}
112+
},
113+
ArrayValue::class,
114+
[1, 2, 3],
115+
];
116+
117+
yield 'associative array' => [
118+
new class implements \JsonSerializable {
119+
public function jsonSerialize(): array
120+
{
121+
return ['name' => 'John', 'age' => 30];
122+
}
123+
},
124+
InlineTableValue::class,
125+
['name' => 'John', 'age' => 30],
126+
];
127+
128+
yield 'empty array' => [
129+
new class implements \JsonSerializable {
130+
public function jsonSerialize(): array
131+
{
132+
return [];
133+
}
134+
},
135+
ArrayValue::class,
136+
[],
137+
];
138+
139+
yield 'datetime value' => [
140+
new class implements \JsonSerializable {
141+
public function jsonSerialize(): \DateTimeImmutable
142+
{
143+
return new \DateTimeImmutable('2024-01-15T10:30:00Z');
144+
}
145+
},
146+
DateTimeValue::class,
147+
'2024-01-15T10:30:00Z',
148+
];
149+
}
150+
151+
public static function provideJsonSerializableInvalidData(): \Generator
152+
{
153+
yield 'null value' => [
154+
new class implements \JsonSerializable {
155+
public function jsonSerialize(): mixed
156+
{
157+
return null;
158+
}
159+
},
160+
'null',
161+
];
162+
163+
yield 'stdClass object' => [
164+
new class implements \JsonSerializable {
165+
public function jsonSerialize(): object
166+
{
167+
return new \stdClass();
168+
}
169+
},
170+
'stdClass',
171+
];
172+
173+
yield 'resource' => [
174+
new class implements \JsonSerializable {
175+
public function jsonSerialize(): mixed
176+
{
177+
return \fopen('php://memory', 'r');
178+
}
179+
},
180+
'resource (stream)',
181+
];
182+
}
48183
// ============================================
49184
// String Value Tests
50185
// ============================================
@@ -332,4 +467,111 @@ public function testCreateArrayHandlesNestedAssociativeArrays(): void
332467
self::assertArrayHasKey('outer', $result->pairs);
333468
self::assertInstanceOf(InlineTableValue::class, $result->pairs['outer']);
334469
}
470+
471+
// ============================================
472+
// JsonSerializable Tests
473+
// ============================================
474+
475+
#[DataProvider('provideJsonSerializableData')]
476+
public function testCreateJsonSerializableReturnsCorrectValueType(
477+
\JsonSerializable $jsonSerializable,
478+
string $expectedClass,
479+
mixed $expectedValue,
480+
): void {
481+
// Act
482+
$result = ValueFactory::create($jsonSerializable);
483+
484+
// Assert
485+
self::assertInstanceOf($expectedClass, $result);
486+
487+
// Verify the actual value based on type
488+
match ($expectedClass) {
489+
StringValue::class => self::assertSame($expectedValue, $result->value),
490+
IntegerValue::class => self::assertSame($expectedValue, $result->value),
491+
FloatValue::class => self::assertSame($expectedValue, $result->value),
492+
BooleanValue::class => self::assertSame($expectedValue, $result->value),
493+
DateTimeValue::class => self::assertSame($expectedValue, $result->raw),
494+
ArrayValue::class => self::assertCount(\count($expectedValue), $result->elements),
495+
InlineTableValue::class => self::assertCount(\count($expectedValue), $result->pairs),
496+
default => self::fail("Unexpected value class: $expectedClass"),
497+
};
498+
}
499+
500+
#[DataProvider('provideJsonSerializableInvalidData')]
501+
public function testCreateJsonSerializableThrowsExceptionForInvalidData(
502+
\JsonSerializable $jsonSerializable,
503+
string $expectedType,
504+
): void {
505+
// Assert (before Act for exceptions)
506+
$this->expectException(InvalidTypeException::class);
507+
$this->expectExceptionMessage("Unsupported value type: $expectedType");
508+
509+
// Act
510+
ValueFactory::create($jsonSerializable);
511+
}
512+
513+
public function testCreateJsonSerializableWithNestedJsonSerializable(): void
514+
{
515+
// Arrange
516+
$innerJsonSerializable = new class implements \JsonSerializable {
517+
public function jsonSerialize(): string
518+
{
519+
return 'inner value';
520+
}
521+
};
522+
523+
$outerJsonSerializable = new class($innerJsonSerializable) implements \JsonSerializable {
524+
public function __construct(private readonly \JsonSerializable $inner)
525+
{
526+
}
527+
528+
public function jsonSerialize(): array
529+
{
530+
return ['nested' => $this->inner];
531+
}
532+
};
533+
534+
// Act
535+
$result = ValueFactory::create($outerJsonSerializable);
536+
537+
// Assert
538+
self::assertInstanceOf(InlineTableValue::class, $result);
539+
self::assertArrayHasKey('nested', $result->pairs);
540+
self::assertInstanceOf(StringValue::class, $result->pairs['nested']);
541+
self::assertSame('inner value', $result->pairs['nested']->value);
542+
}
543+
544+
public function testCreateJsonSerializableWithComplexStructure(): void
545+
{
546+
// Arrange
547+
$jsonSerializable = new class implements \JsonSerializable {
548+
public function jsonSerialize(): array
549+
{
550+
return [
551+
'title' => 'Test',
552+
'count' => 10,
553+
'enabled' => true,
554+
'ratio' => 0.5,
555+
'tags' => ['php', 'toml'],
556+
'metadata' => [
557+
'created' => '2024-01-15',
558+
'version' => 1,
559+
],
560+
];
561+
}
562+
};
563+
564+
// Act
565+
$result = ValueFactory::create($jsonSerializable);
566+
567+
// Assert
568+
self::assertInstanceOf(InlineTableValue::class, $result);
569+
self::assertCount(6, $result->pairs);
570+
self::assertInstanceOf(StringValue::class, $result->pairs['title']);
571+
self::assertInstanceOf(IntegerValue::class, $result->pairs['count']);
572+
self::assertInstanceOf(BooleanValue::class, $result->pairs['enabled']);
573+
self::assertInstanceOf(FloatValue::class, $result->pairs['ratio']);
574+
self::assertInstanceOf(ArrayValue::class, $result->pairs['tags']);
575+
self::assertInstanceOf(InlineTableValue::class, $result->pairs['metadata']);
576+
}
335577
}

0 commit comments

Comments
 (0)