Skip to content

Commit 2ea74fc

Browse files
committed
Support out-of-order subtable extensions
1 parent df91ea5 commit 2ea74fc

File tree

7 files changed

+585
-31
lines changed

7 files changed

+585
-31
lines changed

src/Tomlyn.SourceGeneration/TomlSerializerContextGenerator.cs

Lines changed: 254 additions & 23 deletions
Large diffs are not rendered by default.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.Collections.Generic;
2+
using System.Text.Json;
3+
using System.Text.Json.Serialization;
4+
using NUnit.Framework;
5+
using Tomlyn.Model;
6+
using Tomlyn.Serialization;
7+
8+
namespace Tomlyn.Tests;
9+
10+
public sealed class OutOfOrderSubtableRoot
11+
{
12+
[JsonPropertyName("msbuild")]
13+
public OutOfOrderSubtableMsBuild MSBuild { get; } = new();
14+
15+
[JsonPropertyName("github")]
16+
public OutOfOrderSubtableGitHub GitHub { get; } = new();
17+
}
18+
19+
public sealed class OutOfOrderSubtableMsBuild
20+
{
21+
public string Project { get; set; } = string.Empty;
22+
23+
public Dictionary<string, object> Properties { get; } = new();
24+
}
25+
26+
public sealed class OutOfOrderSubtableGitHub
27+
{
28+
public string User { get; set; } = string.Empty;
29+
30+
public string Repo { get; set; } = string.Empty;
31+
}
32+
33+
[TomlSourceGenerationOptions(
34+
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
35+
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate)]
36+
[TomlSerializable(typeof(OutOfOrderSubtableRoot))]
37+
internal partial class TestOutOfOrderSubtableContext : TomlSerializerContext
38+
{
39+
}
40+
41+
public class NewApiOutOfOrderSubtableTests
42+
{
43+
private const string SampleToml =
44+
"""
45+
[msbuild]
46+
project = "HelloWorld.csproj"
47+
48+
[github]
49+
user = "u"
50+
repo = "r"
51+
52+
[msbuild.properties]
53+
PublishReadyToRun = false
54+
""";
55+
56+
[Test]
57+
public void Reflection_AllowsOutOfOrderSubtableExtensions()
58+
{
59+
var result = TomlSerializer.Deserialize<OutOfOrderSubtableRoot>(SampleToml, new TomlSerializerOptions
60+
{
61+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
62+
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
63+
SourceName = "repro.toml",
64+
});
65+
66+
Assert.That(result, Is.Not.Null);
67+
Assert.That(result!.MSBuild.Project, Is.EqualTo("HelloWorld.csproj"));
68+
Assert.That(result.GitHub.User, Is.EqualTo("u"));
69+
Assert.That(result.GitHub.Repo, Is.EqualTo("r"));
70+
Assert.That(result.MSBuild.Properties["PublishReadyToRun"], Is.EqualTo(false));
71+
}
72+
73+
[Test]
74+
public void SourceGenerated_AllowsOutOfOrderSubtableExtensions()
75+
{
76+
var context = TestOutOfOrderSubtableContext.Default;
77+
var result = TomlSerializer.Deserialize(SampleToml, context.OutOfOrderSubtableRoot);
78+
79+
Assert.That(result, Is.Not.Null);
80+
Assert.That(result!.MSBuild.Project, Is.EqualTo("HelloWorld.csproj"));
81+
Assert.That(result.GitHub.User, Is.EqualTo("u"));
82+
Assert.That(result.GitHub.Repo, Is.EqualTo("r"));
83+
Assert.That(result.MSBuild.Properties["PublishReadyToRun"], Is.EqualTo(false));
84+
}
85+
86+
[Test]
87+
public void TypedDictionary_AllowsOutOfOrderSubtableExtensions()
88+
{
89+
var result = TomlSerializer.Deserialize<Dictionary<string, object>>(SampleToml, new TomlSerializerOptions
90+
{
91+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
92+
SourceName = "repro.toml",
93+
});
94+
95+
Assert.That(result, Is.Not.Null);
96+
Assert.That(result!.TryGetValue("msbuild", out var rawMsBuild), Is.True);
97+
Assert.That(rawMsBuild, Is.TypeOf<TomlTable>());
98+
99+
var msbuild = (TomlTable)rawMsBuild!;
100+
Assert.That(msbuild["project"], Is.EqualTo("HelloWorld.csproj"));
101+
Assert.That(msbuild["properties"], Is.TypeOf<TomlTable>());
102+
103+
var properties = (TomlTable)msbuild["properties"];
104+
Assert.That(properties["PublishReadyToRun"], Is.EqualTo(false));
105+
}
106+
}

src/Tomlyn/Serialization/Converters/TomlPrimitiveConverters.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ internal static TomlTable ReadTable(TomlReader reader)
10971097
return table;
10981098
}
10991099

1100-
private static void ReadTableInto(TomlReader reader, TomlTable table)
1100+
internal static void ReadTableInto(TomlReader reader, TomlTable table)
11011101
{
11021102
if (reader.TokenType != TomlTokenType.StartTable)
11031103
{

src/Tomlyn/Serialization/Internal/TomlCollectionTypeInfos.cs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -871,12 +871,28 @@ public override void Write(TomlWriter writer, TDictionary value)
871871
}
872872

873873
var key = reader.PropertyName!;
874-
if (Options.DuplicateKeyHandling == TomlDuplicateKeyHandling.Error && dict.ContainsKey(key))
874+
reader.Read();
875+
if (Options.DuplicateKeyHandling == TomlDuplicateKeyHandling.Error &&
876+
dict.TryGetValue(key, out var existingValue))
875877
{
878+
if (TryReadTableHeaderExtension(reader, existingValue, out var mergedValue))
879+
{
880+
dict[key] = mergedValue;
881+
continue;
882+
}
883+
876884
throw reader.CreateException($"Duplicate key '{key}' was encountered.");
877885
}
878886

879-
reader.Read();
887+
if (TomlTableHeaderExtensionHelper.IsTableHeaderExtension(reader) && dict.TryGetValue(key, out existingValue))
888+
{
889+
if (TryReadTableHeaderExtension(reader, existingValue, out var mergedValue))
890+
{
891+
dict[key] = mergedValue;
892+
continue;
893+
}
894+
}
895+
880896
dict[key] = ReadValue(reader);
881897
}
882898

@@ -912,12 +928,27 @@ public override void Write(TomlWriter writer, TDictionary value)
912928
}
913929

914930
var key = reader.PropertyName!;
931+
reader.Read();
915932
if (seen is not null && !seen.Add(key))
916933
{
934+
if (dict.TryGetValue(key, out var duplicateExistingValue) && TryReadTableHeaderExtension(reader, duplicateExistingValue, out var mergedValue))
935+
{
936+
dict[key] = mergedValue;
937+
continue;
938+
}
939+
917940
throw reader.CreateException($"Duplicate key '{key}' was encountered.");
918941
}
919942

920-
reader.Read();
943+
if (TomlTableHeaderExtensionHelper.IsTableHeaderExtension(reader) && dict.TryGetValue(key, out var currentValue))
944+
{
945+
if (TryReadTableHeaderExtension(reader, currentValue, out var mergedValue))
946+
{
947+
dict[key] = mergedValue;
948+
continue;
949+
}
950+
}
951+
921952
dict[key] = ReadValue(reader);
922953
}
923954

@@ -966,4 +997,18 @@ private TValue ReadValue(TomlReader reader)
966997

967998
return (TValue)_valueTypeInfo!.ReadAsObject(reader)!;
968999
}
1000+
1001+
private bool TryReadTableHeaderExtension(TomlReader reader, TValue existingValue, out TValue mergedValue)
1002+
{
1003+
EnsureValueTypeInfo();
1004+
1005+
if (!TomlTableHeaderExtensionHelper.TryReadIntoExisting(reader, existingValue, _valueTypeInfo!, out var merged))
1006+
{
1007+
mergedValue = existingValue;
1008+
return false;
1009+
}
1010+
1011+
mergedValue = (TValue)merged!;
1012+
return true;
1013+
}
9691014
}

src/Tomlyn/Serialization/Internal/TomlReflectionTypeInfoResolver.cs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,14 @@ private object ReadIntoExistingInstance(TomlReader reader, object instance, Toml
833833

834834
if (_indexByName.TryGetValue(name, out var memberIndex))
835835
{
836+
var member = _members[memberIndex];
836837
if (seen is not null && seen[memberIndex])
837838
{
839+
if (TryReadTableHeaderExtension(reader, instance, member))
840+
{
841+
continue;
842+
}
843+
838844
throw reader.CreateException($"Duplicate key '{name}' was encountered.");
839845
}
840846

@@ -843,7 +849,6 @@ private object ReadIntoExistingInstance(TomlReader reader, object instance, Toml
843849
seen[memberIndex] = true;
844850
}
845851

846-
var member = _members[memberIndex];
847852
if (member.Setter is null && member.ObjectCreationHandling != JsonObjectCreationHandling.Populate && !member.HasSingleOrArray)
848853
{
849854
reader.Skip();
@@ -1154,6 +1159,19 @@ private object ReadWithConstructor(TomlReader reader, TomlSourceSpan? tableStart
11541159
{
11551160
if (ctorSeen[parameterIndex] && Options.DuplicateKeyHandling == TomlDuplicateKeyHandling.Error)
11561161
{
1162+
var duplicateBinding = _parameters[parameterIndex];
1163+
if (TryReadTableHeaderExtension(reader, ctorArgs, parameterIndex, duplicateBinding.ParameterType, out var updatedArgument))
1164+
{
1165+
ctorArgs[parameterIndex] = updatedArgument;
1166+
if (duplicateBinding.MemberIndex is { } duplicateLinkedMemberIndex && duplicateLinkedMemberIndex >= 0 && duplicateLinkedMemberIndex < _members.Count)
1167+
{
1168+
memberSeen[duplicateLinkedMemberIndex] = true;
1169+
memberValues[duplicateLinkedMemberIndex] = updatedArgument;
1170+
}
1171+
1172+
continue;
1173+
}
1174+
11571175
throw reader.CreateException($"Duplicate key '{name}' was encountered.");
11581176
}
11591177

@@ -1191,6 +1209,12 @@ private object ReadWithConstructor(TomlReader reader, TomlSourceSpan? tableStart
11911209
{
11921210
if (memberSeen[memberIndex] && Options.DuplicateKeyHandling == TomlDuplicateKeyHandling.Error)
11931211
{
1212+
var duplicateMember = _members[memberIndex];
1213+
if (TryReadTableHeaderExtension(reader, memberValues, memberIndex, duplicateMember))
1214+
{
1215+
continue;
1216+
}
1217+
11941218
throw reader.CreateException($"Duplicate key '{name}' was encountered.");
11951219
}
11961220

@@ -1325,6 +1349,68 @@ private object ReadWithConstructor(TomlReader reader, TomlSourceSpan? tableStart
13251349
return instance;
13261350
}
13271351

1352+
private bool TryReadTableHeaderExtension(TomlReader reader, object instance, MemberModel member)
1353+
{
1354+
if (member.Converter is not null)
1355+
{
1356+
return false;
1357+
}
1358+
1359+
var existingValue = member.Getter(instance);
1360+
if (existingValue is null)
1361+
{
1362+
return false;
1363+
}
1364+
1365+
var typeInfo = reader.ResolveTypeInfo(member.MemberType);
1366+
if (!TomlTableHeaderExtensionHelper.TryReadIntoExisting(reader, existingValue, typeInfo, out var populatedValue))
1367+
{
1368+
return false;
1369+
}
1370+
1371+
if (member.Setter is not null)
1372+
{
1373+
member.Setter(instance, populatedValue);
1374+
return true;
1375+
}
1376+
1377+
if (!ReferenceEquals(existingValue, populatedValue))
1378+
{
1379+
throw reader.CreateException(
1380+
$"Member '{member.Member.Name}' on '{Type.FullName}' cannot be extended by an additional TOML table definition because '{member.MemberType.FullName}' does not support in-place population.");
1381+
}
1382+
1383+
return true;
1384+
}
1385+
1386+
private bool TryReadTableHeaderExtension(TomlReader reader, object?[] values, int index, Type valueType, out object? populatedValue)
1387+
{
1388+
populatedValue = values[index];
1389+
if (populatedValue is null)
1390+
{
1391+
return false;
1392+
}
1393+
1394+
var typeInfo = reader.ResolveTypeInfo(valueType);
1395+
return TomlTableHeaderExtensionHelper.TryReadIntoExisting(reader, populatedValue, typeInfo, out populatedValue);
1396+
}
1397+
1398+
private bool TryReadTableHeaderExtension(TomlReader reader, object?[] values, int index, MemberModel member)
1399+
{
1400+
if (member.Converter is not null)
1401+
{
1402+
return false;
1403+
}
1404+
1405+
if (!TryReadTableHeaderExtension(reader, values, index, member.MemberType, out var populatedValue))
1406+
{
1407+
return false;
1408+
}
1409+
1410+
values[index] = populatedValue;
1411+
return true;
1412+
}
1413+
13281414
private static void CapturePropertyMetadata(
13291415
TomlPropertiesMetadata? propertiesMetadata,
13301416
string name,

0 commit comments

Comments
 (0)