Skip to content

Commit ff4e7af

Browse files
committed
Add support for json keys that include a colon character
1 parent 7af6655 commit ff4e7af

File tree

3 files changed

+192
-50
lines changed

3 files changed

+192
-50
lines changed

dotnet-json.Tests/Commands/MergeCommandTests.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,132 @@ await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
5555
}");
5656
}
5757

58+
[Fact]
59+
public async Task CorrectlyMergesKeysWithColons()
60+
{
61+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
62+
""Parent:Child"": ""value""
63+
}");
64+
65+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{
66+
""Parent:Child"": ""other""
67+
}");
68+
69+
var (exitCode, console) = await RunCommand(
70+
Path.Join(_tmpDir, "a.json"),
71+
Path.Join(_tmpDir, "b.json"));
72+
73+
exitCode.Should().Be(0);
74+
75+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
76+
content.Should().Be(@"{
77+
""Parent:Child"": ""other""
78+
}");
79+
}
80+
81+
[Fact]
82+
public async Task KeepsTheFormattingPerKey()
83+
{
84+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
85+
""Parent:Child"": ""value""
86+
}");
87+
88+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{
89+
""Parent"": {
90+
""Child"": ""other""
91+
}
92+
}");
93+
94+
var (exitCode, console) = await RunCommand(
95+
Path.Join(_tmpDir, "a.json"),
96+
Path.Join(_tmpDir, "b.json"));
97+
98+
exitCode.Should().Be(0);
99+
100+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
101+
content.Should().Be(@"{
102+
""Parent:Child"": ""other""
103+
}");
104+
}
105+
106+
[Fact]
107+
public async Task AddsKeyInObject()
108+
{
109+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
110+
""Parent"": {
111+
""Other"": ""value""
112+
},
113+
""Parent:Another"": ""value""
114+
}");
115+
116+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{
117+
""Parent"": {
118+
""Child"": ""other""
119+
}
120+
}");
121+
122+
var (exitCode, console) = await RunCommand(
123+
Path.Join(_tmpDir, "a.json"),
124+
Path.Join(_tmpDir, "b.json"));
125+
126+
exitCode.Should().Be(0);
127+
128+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
129+
content.Should().Be(@"{
130+
""Parent"": {
131+
""Other"": ""value"",
132+
""Child"": ""other""
133+
},
134+
""Parent:Another"": ""value""
135+
}");
136+
}
137+
138+
[Fact]
139+
public async Task AddsKeyInMostSpecificObject()
140+
{
141+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
142+
""Parent:Nested"": {
143+
""Key"": ""value""
144+
},
145+
""Parent"": {
146+
""Nested"": {
147+
""Another"": ""value""
148+
}
149+
}
150+
}");
151+
152+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{
153+
""Parent"": {
154+
""Nested"": {
155+
""Child"": ""other""
156+
},
157+
""Another:Child"": ""value""
158+
}
159+
}");
160+
161+
var (exitCode, console) = await RunCommand(
162+
Path.Join(_tmpDir, "a.json"),
163+
Path.Join(_tmpDir, "b.json"));
164+
165+
exitCode.Should().Be(0);
166+
167+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
168+
content.Should().Be(@"{
169+
""Parent:Nested"": {
170+
""Key"": ""value"",
171+
""Child"": ""other""
172+
},
173+
""Parent"": {
174+
""Nested"": {
175+
""Another"": ""value""
176+
},
177+
""Another"": {
178+
""Child"": ""value""
179+
}
180+
}
181+
}");
182+
}
183+
58184
private async Task<(int exitCode, IConsole console)> RunCommand(params string[] args)
59185
{
60186
var command = new MergeCommand();

dotnet-json/Core/JsonDocument.cs

Lines changed: 63 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Text;
56
using Newtonsoft.Json;
67
using Newtonsoft.Json.Linq;
@@ -53,23 +54,34 @@ public void Remove(string key)
5354
jValue?.Remove();
5455
}
5556

56-
internal static IEnumerable<KeyValuePair<string, JValue>> AllValues(JToken token, string prefix = "")
57+
internal static IEnumerable<KeyValuePair<string, JValue>> AllValues(JToken token)
5758
{
59+
return AllTokens(token)
60+
.Where(kv => kv.Value is JValue)
61+
.Select(kv => KeyValuePair.Create(kv.Key, (kv.Value as JValue)!));
62+
}
63+
64+
internal static IEnumerable<KeyValuePair<string, JToken>> AllTokens(JToken token, string prefix = "")
65+
{
66+
yield return KeyValuePair.Create(prefix, token);
67+
5868
switch (token)
5969
{
60-
case JValue jValue:
61-
yield return KeyValuePair.Create(prefix, jValue);
70+
case JValue:
6271
break;
6372

6473
case JProperty jProperty:
65-
foreach (var kv in AllValues(jProperty.Value, CreatePrefix(prefix, jProperty.Name)))
74+
foreach (var kv in AllTokens(jProperty.Value, CreatePrefix(prefix, jProperty.Name)))
6675
yield return kv;
6776
break;
6877

6978
case JArray jArray:
7079
for (var i = 0; i < jArray.Count; i++)
71-
foreach (var kv in AllValues(jArray[i], CreatePrefix(prefix, i.ToString())))
80+
{
81+
foreach (var kv in AllTokens(jArray[i], CreatePrefix(prefix, i.ToString())))
7282
yield return kv;
83+
}
84+
7385
break;
7486

7587
case JObject jObject:
@@ -78,7 +90,7 @@ internal static IEnumerable<KeyValuePair<string, JValue>> AllValues(JToken token
7890
if (value == null)
7991
continue;
8092

81-
foreach (var kv in AllValues(value, CreatePrefix(prefix, key)))
93+
foreach (var kv in AllTokens(value, CreatePrefix(prefix, key)))
8294
yield return kv;
8395
}
8496

@@ -101,62 +113,69 @@ internal static IEnumerable<KeyValuePair<string, JValue>> AllValues(JToken token
101113

102114
internal JToken? FindToken(string key, bool createNew = false)
103115
{
104-
var subKeys = string.IsNullOrWhiteSpace(key) ? new string[0] : key.Split(':');
116+
var parentKey = "";
117+
var bestParent = (JToken?)(_json as JObject) ?? (_json as JArray);
105118

106-
var current = _json;
107-
for (var i = 0; i < subKeys.Length; i++)
119+
foreach (var kv in AllTokens(_json))
108120
{
109-
var subkey = subKeys[i];
110-
var isLastKey = i == subKeys.Length - 1;
111-
112-
if (current is JValue && !createNew)
113-
return null;
121+
if (kv.Key == key)
122+
return kv.Value;
114123

115-
if (int.TryParse(subkey, out _) && current is JObject && ((JObject)current).Count == 0 && current.Parent != null)
124+
if (key.StartsWith(kv.Key) && kv.Value is JObject jObject && kv.Key.Length > parentKey.Length)
116125
{
117-
if (current.Parent is JProperty jProperty)
118-
jProperty.Value = current = new JArray();
119-
else if (current.Parent is JArray parentArray)
120-
parentArray[parentArray.IndexOf(current)] = current = new JArray();
126+
parentKey = kv.Key;
127+
bestParent = jObject;
121128
}
129+
}
122130

123-
if (current is JArray jArray)
124-
{
125-
current = FindTokenInArray(jArray, subkey, createNew, isLastKey);
126-
continue;
127-
}
131+
if (bestParent == null || !createNew)
132+
return null;
128133

129-
if (current is JValue)
130-
return null;
134+
var restKeys = key.Substring(parentKey.Length).TrimStart(':').Split(':');
135+
for (var i = 0; i < restKeys.Length - 1; i++)
136+
{
137+
JToken newValue;
131138

132-
var jObject = (JObject)current!; // At this point current can only be a JObject.
133-
current = jObject[subkey];
139+
if (restKeys.Length > i + 1 && int.TryParse(restKeys[i + 1], out _))
140+
newValue = new JArray();
141+
else
142+
newValue = new JObject();
134143

135-
if (createNew && (current is null || (current is JValue && !isLastKey)))
144+
if (bestParent is JArray jArray)
136145
{
137-
current = jObject[subkey] = isLastKey ? (JToken)new JValue((object?)null) : new JObject();
146+
if (!int.TryParse(restKeys[i], out var idx))
147+
throw new Exception($"Cannot index into array with key '{restKeys[^1]}'");
148+
149+
while (jArray.Count <= idx)
150+
jArray.Add(new JValue((object?)null));
151+
jArray[idx] = newValue;
152+
}
153+
else
154+
{
155+
bestParent[restKeys[i]] = newValue;
138156
}
157+
158+
bestParent = newValue;
139159
}
140160

141-
return current; // If current is no JValue here, throw an exception
142-
}
161+
var value = (JToken)new JValue((object?)null);
143162

144-
internal static JToken FindTokenInArray(JArray jArray, string subkey, bool createNew, bool isLastKey)
145-
{
146-
if (!int.TryParse(subkey, out var index))
147-
throw new Exception($"Cannot index into array at {GetPosition(jArray)} with index {subkey}.");
163+
if (bestParent is JArray array)
164+
{
165+
if (!int.TryParse(restKeys[^1], out var idx))
166+
throw new Exception($"Cannot index into array with key '{restKeys[^1]}'");
148167

149-
if (createNew && index >= jArray.Count)
168+
while (array.Count <= idx)
169+
array.Add(new JValue((object?)null));
170+
array[idx] = value;
171+
value = array[idx];
172+
}
173+
else
150174
{
151-
for (var j = jArray.Count; j < index; j++)
152-
jArray.Add(new JValue((object?)null));
153-
jArray.Add(isLastKey ? (JToken)new JValue((object?)null) : new JObject());
175+
bestParent[restKeys[^1]] = value;
154176
}
155177

156-
if (index >= jArray.Count)
157-
throw new IndexOutOfRangeException($"Index {index} does not exist for array at {GetPosition(jArray)}");
158-
159-
return jArray[index];
178+
return value;
160179
}
161180

162181
internal void SetValue(string key, object? value)
@@ -168,8 +187,5 @@ internal void SetValue(string key, object? value)
168187

169188
jValue.Value = value;
170189
}
171-
172-
internal static string GetPosition(JToken token)
173-
=> token.Path.Replace(".", ":").Replace("[", ".").Replace("]", "");
174190
}
175191
}

dotnet-json/dotnet-json.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<ToolCommandName>dotnet-json</ToolCommandName>
1515
<PackageOutputPath>./nupkg</PackageOutputPath>
1616

17-
<Version>1.0.0</Version>
17+
<Version>1.0.1</Version>
1818
<PackageId>dotnet-json</PackageId>
1919
<Authors>sleeuwen</Authors>
2020
<RepositoryUrl>https://github.com/sleeuwen/dotnet-json</RepositoryUrl>
@@ -31,9 +31,9 @@
3131
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
3232
<PackageReference Include="System.CommandLine" Version="2.0.0-beta2.21617.1" />
3333
</ItemGroup>
34-
34+
3535
<ItemGroup>
36-
<None Include="..\README.md" Pack="true" PackagePath="\"/>
36+
<None Include="..\README.md" Pack="true" PackagePath="\" />
3737
</ItemGroup>
3838

3939
</Project>

0 commit comments

Comments
 (0)