Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 69 additions & 11 deletions src/Encoder/ArrayToDocumentConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ public function convert(array $data): Document
$nodes = [];
$position = new Position(1, 1, 0);

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

// Categorize flattened data into root entries, tables, and table arrays
[$rootEntries, $tables, $tableArrays] = $this->categorizeData($flatData);

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

/**
* Flattens nested array structure into dotted keys.
*
* Example:
* ['github' => ['token' => ['key' => 'val']]]
* becomes:
* ['github.token' => ['key' => 'val']]
*
* @param array<string, mixed> $data
* @param string $prefix
* @return array<string, mixed>
*/
private function flattenData(array $data, string $prefix = ''): array
{
$result = [];

foreach ($data as $key => $value) {
if (!\is_string($key)) {
throw new \InvalidArgumentException('TOML keys must be strings, got: ' . \get_debug_type($key));
}

$fullKey = $prefix === '' ? $key : $prefix . '.' . $key;

if (!\is_array($value)) {
// Scalar value
$result[$fullKey] = $value;
} elseif ($this->isTableArray($value)) {
// Table array - keep as is
$result[$fullKey] = $value;
} elseif ($this->isAssociativeArray($value)) {
// Check if this table has only scalar/array values (leaf table)
// or if it has nested tables
$hasNestedTables = false;
foreach ($value as $subValue) {
if (\is_array($subValue) && $this->isAssociativeArray($subValue) && !$this->isTableArray($subValue)) {
$hasNestedTables = true;
break;
}
}

if ($hasNestedTables) {
// Recursively flatten nested tables
$flattened = $this->flattenData($value, $fullKey);
foreach ($flattened as $flatKey => $flatValue) {
$result[$flatKey] = $flatValue;
}
} else {
// Leaf table - keep as is
$result[$fullKey] = $value;
}
} else {
// Simple array (list of scalars)
$result[$fullKey] = $value;
}
}

return $result;
}

/**
* Categorizes data into root entries, tables, and table arrays.
*
Expand Down Expand Up @@ -102,18 +164,14 @@ private function createTable(string $name, array $data): Table
$entries = [];
$position = new Position(0, 0, 0);

// Categorize table data
[$rootEntries, $nestedTables, $nestedTableArrays] = $this->categorizeData($data);

// Add root entries
foreach ($rootEntries as $key => $value) {
// Add all entries (data is already flattened, so no nested tables here)
foreach ($data as $key => $value) {
if (!\is_string($key)) {
throw new \InvalidArgumentException('TOML keys must be strings, got: ' . \get_debug_type($key));
}
$entries[] = $this->createEntry($key, $value);
}

// Nested tables and table arrays will be handled at document level
// For now, we only handle simple table entries
// TODO: Handle nested structures properly

return new Table($keyNode, $entries, null, $position);
}

Expand Down
36 changes: 36 additions & 0 deletions tests/Unit/TomlEncodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public static function provideRoundTripData(): \Generator
['str' => 'text', 'int' => 42, 'float' => 3.14, 'bool' => true],
];
}

public static function provideFixtureFiles(): \Generator
{
yield 'roadrunner config' => ['rr.toml'];
}
// ============================================
// Basic Encoding Tests
// ============================================
Expand Down Expand Up @@ -345,4 +350,35 @@ public function testEncodeStringWithNewlinesProducesMultilineString(): void
$lines = \explode("\n", $toml);
self::assertGreaterThan(3, \count($lines), 'Should have multiple lines');
}

// ============================================
// Fixture-based Round-Trip Tests
// ============================================

#[DataProvider('provideFixtureFiles')]
public function testEncodeFixtureFileRoundTrip(string $filename): void
{
// Arrange
$fixturePath = __DIR__ . '/fixtures/' . $filename;
$tomlContent = \file_get_contents($fixturePath);
self::assertNotFalse($tomlContent, "Failed to read fixture file: {$filename}");

// Parse original TOML to array
$originalArray = Toml::parseToArray($tomlContent);

// Act
// Encode array back to TOML string
$encodedToml = (string) Toml::encode($originalArray);

// Parse the encoded TOML back to array
$reEncodedArray = Toml::parseToArray($encodedToml);

// Assert
// The re-encoded array should match the original parsed array
self::assertEquals(
$originalArray,
$reEncodedArray,
"Round-trip encoding/decoding changed the data structure for {$filename}"
);
}
}
34 changes: 34 additions & 0 deletions tests/Unit/fixtures/rr.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[roadrunner]
ref = "v2025.1.1"

[log]
level = "info"
mode = "production"

[github.token]
token = "123adryen7y123"

[github.plugins.temporal]
ref = "v5.7.0"
owner = "temporalio"
repository = "roadrunner-temporal"

[github.plugins.kv]
ref = "v5.2.8"
owner = "roadrunner-server"
repository = "kv"

[github.plugins.logger]
ref = "v5.1.8"
owner = "roadrunner-server"
repository = "logger"

[github.plugins.server]
ref = "v5.2.9"
owner = "roadrunner-server"
repository = "server"

[github.plugins.rpc]
ref = "v5.1.8"
owner = "roadrunner-server"
repository = "rpc"
Loading