Skip to content

Commit de32d25

Browse files
authored
Handle nested tables in TOML encoder (#2)
2 parents 2ac3c17 + f2ea717 commit de32d25

File tree

3 files changed

+139
-11
lines changed

3 files changed

+139
-11
lines changed

src/Encoder/ArrayToDocumentConverter.php

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ public function convert(array $data): Document
2626
$nodes = [];
2727
$position = new Position(1, 1, 0);
2828

29-
// Categorize data into root entries, tables, and table arrays
30-
[$rootEntries, $tables, $tableArrays] = $this->categorizeData($data);
29+
// Flatten the data structure to handle nested tables
30+
$flatData = $this->flattenData($data);
31+
32+
// Categorize flattened data into root entries, tables, and table arrays
33+
[$rootEntries, $tables, $tableArrays] = $this->categorizeData($flatData);
3134

3235
// Root key-value pairs first
3336
foreach ($rootEntries as $key => $value) {
@@ -49,6 +52,65 @@ public function convert(array $data): Document
4952
return new Document($nodes, $position);
5053
}
5154

55+
/**
56+
* Flattens nested array structure into dotted keys.
57+
*
58+
* Example:
59+
* ['github' => ['token' => ['key' => 'val']]]
60+
* becomes:
61+
* ['github.token' => ['key' => 'val']]
62+
*
63+
* @param array<string, mixed> $data
64+
* @param string $prefix
65+
* @return array<string, mixed>
66+
*/
67+
private function flattenData(array $data, string $prefix = ''): array
68+
{
69+
$result = [];
70+
71+
foreach ($data as $key => $value) {
72+
if (!\is_string($key)) {
73+
throw new \InvalidArgumentException('TOML keys must be strings, got: ' . \get_debug_type($key));
74+
}
75+
76+
$fullKey = $prefix === '' ? $key : $prefix . '.' . $key;
77+
78+
if (!\is_array($value)) {
79+
// Scalar value
80+
$result[$fullKey] = $value;
81+
} elseif ($this->isTableArray($value)) {
82+
// Table array - keep as is
83+
$result[$fullKey] = $value;
84+
} elseif ($this->isAssociativeArray($value)) {
85+
// Check if this table has only scalar/array values (leaf table)
86+
// or if it has nested tables
87+
$hasNestedTables = false;
88+
foreach ($value as $subValue) {
89+
if (\is_array($subValue) && $this->isAssociativeArray($subValue) && !$this->isTableArray($subValue)) {
90+
$hasNestedTables = true;
91+
break;
92+
}
93+
}
94+
95+
if ($hasNestedTables) {
96+
// Recursively flatten nested tables
97+
$flattened = $this->flattenData($value, $fullKey);
98+
foreach ($flattened as $flatKey => $flatValue) {
99+
$result[$flatKey] = $flatValue;
100+
}
101+
} else {
102+
// Leaf table - keep as is
103+
$result[$fullKey] = $value;
104+
}
105+
} else {
106+
// Simple array (list of scalars)
107+
$result[$fullKey] = $value;
108+
}
109+
}
110+
111+
return $result;
112+
}
113+
52114
/**
53115
* Categorizes data into root entries, tables, and table arrays.
54116
*
@@ -102,18 +164,14 @@ private function createTable(string $name, array $data): Table
102164
$entries = [];
103165
$position = new Position(0, 0, 0);
104166

105-
// Categorize table data
106-
[$rootEntries, $nestedTables, $nestedTableArrays] = $this->categorizeData($data);
107-
108-
// Add root entries
109-
foreach ($rootEntries as $key => $value) {
167+
// Add all entries (data is already flattened, so no nested tables here)
168+
foreach ($data as $key => $value) {
169+
if (!\is_string($key)) {
170+
throw new \InvalidArgumentException('TOML keys must be strings, got: ' . \get_debug_type($key));
171+
}
110172
$entries[] = $this->createEntry($key, $value);
111173
}
112174

113-
// Nested tables and table arrays will be handled at document level
114-
// For now, we only handle simple table entries
115-
// TODO: Handle nested structures properly
116-
117175
return new Table($keyNode, $entries, null, $position);
118176
}
119177

tests/Unit/TomlEncodeTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public static function provideRoundTripData(): \Generator
4040
['str' => 'text', 'int' => 42, 'float' => 3.14, 'bool' => true],
4141
];
4242
}
43+
44+
public static function provideFixtureFiles(): \Generator
45+
{
46+
yield 'roadrunner config' => ['rr.toml'];
47+
}
4348
// ============================================
4449
// Basic Encoding Tests
4550
// ============================================
@@ -345,4 +350,35 @@ public function testEncodeStringWithNewlinesProducesMultilineString(): void
345350
$lines = \explode("\n", $toml);
346351
self::assertGreaterThan(3, \count($lines), 'Should have multiple lines');
347352
}
353+
354+
// ============================================
355+
// Fixture-based Round-Trip Tests
356+
// ============================================
357+
358+
#[DataProvider('provideFixtureFiles')]
359+
public function testEncodeFixtureFileRoundTrip(string $filename): void
360+
{
361+
// Arrange
362+
$fixturePath = __DIR__ . '/fixtures/' . $filename;
363+
$tomlContent = \file_get_contents($fixturePath);
364+
self::assertNotFalse($tomlContent, "Failed to read fixture file: {$filename}");
365+
366+
// Parse original TOML to array
367+
$originalArray = Toml::parseToArray($tomlContent);
368+
369+
// Act
370+
// Encode array back to TOML string
371+
$encodedToml = (string) Toml::encode($originalArray);
372+
373+
// Parse the encoded TOML back to array
374+
$reEncodedArray = Toml::parseToArray($encodedToml);
375+
376+
// Assert
377+
// The re-encoded array should match the original parsed array
378+
self::assertEquals(
379+
$originalArray,
380+
$reEncodedArray,
381+
"Round-trip encoding/decoding changed the data structure for {$filename}"
382+
);
383+
}
348384
}

tests/Unit/fixtures/rr.toml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[roadrunner]
2+
ref = "v2025.1.1"
3+
4+
[log]
5+
level = "info"
6+
mode = "production"
7+
8+
[github.token]
9+
token = "123adryen7y123"
10+
11+
[github.plugins.temporal]
12+
ref = "v5.7.0"
13+
owner = "temporalio"
14+
repository = "roadrunner-temporal"
15+
16+
[github.plugins.kv]
17+
ref = "v5.2.8"
18+
owner = "roadrunner-server"
19+
repository = "kv"
20+
21+
[github.plugins.logger]
22+
ref = "v5.1.8"
23+
owner = "roadrunner-server"
24+
repository = "logger"
25+
26+
[github.plugins.server]
27+
ref = "v5.2.9"
28+
owner = "roadrunner-server"
29+
repository = "server"
30+
31+
[github.plugins.rpc]
32+
ref = "v5.1.8"
33+
owner = "roadrunner-server"
34+
repository = "rpc"

0 commit comments

Comments
 (0)