Skip to content

Commit bd804b6

Browse files
authored
refactor: add key value pair json decoder (#103)
1 parent 217b14c commit bd804b6

File tree

6 files changed

+245
-68
lines changed

6 files changed

+245
-68
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using System.Text.Json;
2+
3+
namespace AzureAppConfigurationEmulator.Common;
4+
5+
public interface IKeyValuePairJsonDecoder
6+
{
7+
IEnumerable<KeyValuePair<string, string?>> Decode(
8+
JsonDocument document,
9+
string? prefix = null,
10+
string? separator = null);
11+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Globalization;
2+
using System.Text.Json;
3+
4+
namespace AzureAppConfigurationEmulator.Common;
5+
6+
public class KeyValuePairJsonDecoder : IKeyValuePairJsonDecoder
7+
{
8+
public IEnumerable<KeyValuePair<string, string?>> Decode(
9+
JsonDocument document,
10+
string? prefix = null,
11+
string? separator = null)
12+
{
13+
return Decode(document.RootElement, prefix, separator);
14+
}
15+
16+
private IEnumerable<KeyValuePair<string, string?>> Decode(
17+
JsonElement element,
18+
string? prefix = null,
19+
string? separator = null)
20+
{
21+
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(KeyValuePairJsonDecoder)}.{nameof(Decode)}");
22+
23+
switch (element.ValueKind)
24+
{
25+
case JsonValueKind.Object:
26+
foreach (var innerProperty in element.EnumerateObject())
27+
{
28+
var innerPrefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{innerProperty.Name}" : innerProperty.Name;
29+
30+
foreach (var setting in Decode(innerProperty.Value, innerPrefix, separator))
31+
{
32+
yield return setting;
33+
}
34+
}
35+
36+
break;
37+
case JsonValueKind.Array:
38+
var index = 0;
39+
40+
foreach (var innerElement in element.EnumerateArray())
41+
{
42+
var innerPrefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{index}" : index.ToString();
43+
44+
foreach (var setting in Decode(innerElement, innerPrefix, separator))
45+
{
46+
yield return setting;
47+
}
48+
49+
index += 1;
50+
}
51+
52+
break;
53+
default:
54+
if (!string.IsNullOrEmpty(prefix))
55+
{
56+
var value = element.ValueKind switch
57+
{
58+
JsonValueKind.Undefined => null,
59+
JsonValueKind.Object => null,
60+
JsonValueKind.Array => null,
61+
JsonValueKind.String => element.GetString(),
62+
JsonValueKind.Number => element.GetDouble().ToString(CultureInfo.InvariantCulture),
63+
JsonValueKind.True => true.ToString(),
64+
JsonValueKind.False => false.ToString(),
65+
JsonValueKind.Null => null,
66+
_ => null
67+
};
68+
69+
yield return new KeyValuePair<string, string?>(prefix, value);
70+
}
71+
72+
break;
73+
}
74+
}
75+
}

src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor

Lines changed: 9 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@inject IConfigurationSettingFactory ConfigurationSettingFactory
22
@inject IConfigurationSettingRepository ConfigurationSettingRepository
3+
@inject IKeyValuePairJsonDecoder KeyValuePairJsonDecoder
34
@page "/kvdata"
45
@using System.Security.Cryptography
56
@using System.Text
@@ -170,69 +171,6 @@
170171
Model ??= new InputModel();
171172
}
172173

173-
private static IEnumerable<KeyValuePair<string, object?>> FlattenJsonElement(JsonElement element, string? prefix = null, string? separator = null)
174-
{
175-
switch (element.ValueKind)
176-
{
177-
case JsonValueKind.Object:
178-
foreach (var property in element.EnumerateObject())
179-
{
180-
foreach (var pair in FlattenJsonElement(property.Value, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{property.Name}" : property.Name, separator))
181-
{
182-
yield return pair;
183-
}
184-
}
185-
186-
break;
187-
case JsonValueKind.Array:
188-
var index = 0;
189-
190-
foreach (var inner in element.EnumerateArray())
191-
{
192-
foreach (var pair in FlattenJsonElement(inner, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{index}" : index.ToString(), separator))
193-
{
194-
yield return pair;
195-
}
196-
197-
index += 1;
198-
}
199-
200-
break;
201-
case JsonValueKind.String:
202-
if (!string.IsNullOrEmpty(prefix))
203-
{
204-
yield return new KeyValuePair<string, object?>(prefix, element.GetString());
205-
}
206-
207-
break;
208-
case JsonValueKind.Number:
209-
if (!string.IsNullOrEmpty(prefix))
210-
{
211-
yield return new KeyValuePair<string, object?>(prefix, element.GetDouble());
212-
}
213-
214-
break;
215-
case JsonValueKind.True:
216-
case JsonValueKind.False:
217-
if (!string.IsNullOrEmpty(prefix))
218-
{
219-
yield return new KeyValuePair<string, object?>(prefix, element.GetBoolean());
220-
}
221-
222-
break;
223-
case JsonValueKind.Undefined:
224-
case JsonValueKind.Null:
225-
if (!string.IsNullOrEmpty(prefix))
226-
{
227-
yield return new KeyValuePair<string, object?>(prefix, null);
228-
}
229-
230-
break;
231-
default:
232-
throw new ArgumentOutOfRangeException();
233-
}
234-
}
235-
236174
private async Task HandleSourceConnectionStringChange(string? connectionString)
237175
{
238176
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleSourceConnectionStringChange)}");
@@ -312,13 +250,16 @@
312250
destinationSetting.LastModified = date;
313251
destinationSetting.ContentType = sourceSetting.ContentType;
314252
destinationSetting.Value = sourceSetting.Value;
253+
destinationSetting.Locked = sourceSetting.Locked;
315254
destinationSetting.Tags = sourceSetting.Tags;
316255

317256
await ConfigurationSettingRepository.Update(destinationSetting);
318257
}
319258
else
320259
{
321-
destinationSetting = ConfigurationSettingFactory.Create(sourceSetting.Key, sourceSetting.Label, sourceSetting.ContentType, sourceSetting.Value, sourceSetting.Tags);
260+
var date = DateTimeOffset.UtcNow;
261+
262+
destinationSetting = ConfigurationSettingFactory.Create(Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(date.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss")))), sourceSetting.Key, date, sourceSetting.Locked, sourceSetting.Label, sourceSetting.ContentType, sourceSetting.Value, sourceSetting.Tags);
322263

323264
await ConfigurationSettingRepository.Add(destinationSetting);
324265
}
@@ -334,7 +275,7 @@
334275

335276
using var document = await JsonDocument.ParseAsync(stream);
336277

337-
foreach (var (sourceKey, sourceValue) in FlattenJsonElement(document.RootElement, Model.Prefix, Model.Separator))
278+
foreach (var (sourceKey, sourceValue) in KeyValuePairJsonDecoder.Decode(document, Model.Prefix, Model.Separator))
338279
{
339280
if (await ConfigurationSettingRepository.Get(sourceKey, Model.Label ?? LabelFilter.Null).SingleOrDefaultAsync() is { } destinationSetting)
340281
{
@@ -349,7 +290,9 @@
349290
}
350291
else
351292
{
352-
destinationSetting = ConfigurationSettingFactory.Create(sourceKey, Model.Label, Model.ContentType, sourceValue?.ToString());
293+
var date = DateTimeOffset.UtcNow;
294+
295+
destinationSetting = ConfigurationSettingFactory.Create(Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(date.UtcDateTime.ToString("yyyy-MM-dd HH:mm:ss")))), sourceKey, date, false, Model.Label, Model.ContentType, sourceValue, null);
353296

354297
await ConfigurationSettingRepository.Add(destinationSetting);
355298
}

src/AzureAppConfigurationEmulator/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
builder.Services.AddSingleton<IDbConnectionFactory, SqliteDbConnectionFactory>();
8787
builder.Services.AddSingleton<IDbParameterFactory, SqliteDbParameterFactory>();
8888
builder.Services.AddSingleton<IEventGridEventFactory, HttpContextEventGridEventFactory>();
89+
builder.Services.AddSingleton<IKeyValuePairJsonDecoder, KeyValuePairJsonDecoder>();
8990

9091
var app = builder.Build();
9192

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System.Text.Json;
2+
using AzureAppConfigurationEmulator.Common;
3+
using NUnit.Framework;
4+
5+
namespace AzureAppConfigurationEmulator.Tests.Common;
6+
7+
public class KeyValuePairJsonDecoderTests
8+
{
9+
private KeyValuePairJsonDecoder Decoder { get; set; }
10+
11+
[SetUp]
12+
public void SetUp()
13+
{
14+
Decoder = new KeyValuePairJsonDecoder();
15+
}
16+
17+
[TestCaseSource(nameof(Decode_KeyValuePairs_DocumentAndPrefixAndSeparator_TestCases))]
18+
public void Decode_KeyValuePairs_DocumentAndPrefixAndSeparator(string json, string? prefix, string? separator, IDictionary<string, string?> expected)
19+
{
20+
// Arrange
21+
using var document = JsonDocument.Parse(json);
22+
23+
// Act
24+
var settings = Decoder.Decode(document, prefix, separator);
25+
26+
// Assert
27+
Assert.That(settings.ToDictionary(), Is.EqualTo(expected));
28+
}
29+
30+
// ReSharper disable once InconsistentNaming
31+
private static object[] Decode_KeyValuePairs_DocumentAndPrefixAndSeparator_TestCases =
32+
[
33+
new object?[]
34+
{
35+
"{\"TestKey\":\"TestValue\"}",
36+
null,
37+
null,
38+
new Dictionary<string, string?> { { "TestKey", "TestValue" } }
39+
},
40+
new object?[]
41+
{
42+
"{\"TestKey\":\"TestValue\"}",
43+
"TestPrefix",
44+
null,
45+
new Dictionary<string, string?> { { "TestPrefixTestKey", "TestValue" } }
46+
},
47+
new object?[]
48+
{
49+
"{\"TestKey\":\"TestValue\"}",
50+
null,
51+
".",
52+
new Dictionary<string, string?> { { "TestKey", "TestValue" } }
53+
},
54+
new object?[]
55+
{
56+
"{\"TestKey\":\"TestValue\"}",
57+
"TestPrefix",
58+
".",
59+
new Dictionary<string, string?> { { "TestPrefix.TestKey", "TestValue" } }
60+
},
61+
new object?[]
62+
{
63+
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
64+
null,
65+
null,
66+
new Dictionary<string, string?> { { "TestOuterKeyTestInnerKey", "TestValue" } }
67+
},
68+
new object?[]
69+
{
70+
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
71+
"TestPrefix",
72+
null,
73+
new Dictionary<string, string?> { { "TestPrefixTestOuterKeyTestInnerKey", "TestValue" } }
74+
},
75+
new object?[]
76+
{
77+
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
78+
null,
79+
".",
80+
new Dictionary<string, string?> { { "TestOuterKey.TestInnerKey", "TestValue" } }
81+
},
82+
new object?[]
83+
{
84+
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
85+
"TestPrefix",
86+
".",
87+
new Dictionary<string, string?> { { "TestPrefix.TestOuterKey.TestInnerKey", "TestValue" } }
88+
},
89+
new object?[]
90+
{
91+
"{\"TestKey\":[\"TestValue\"]}",
92+
null,
93+
null,
94+
new Dictionary<string, string?> { { "TestKey0", "TestValue" } }
95+
},
96+
new object?[]
97+
{
98+
"{\"TestKey\":[\"TestValue\"]}",
99+
"TestPrefix",
100+
null,
101+
new Dictionary<string, string?> { { "TestPrefixTestKey0", "TestValue" } }
102+
},
103+
new object?[]
104+
{
105+
"{\"TestKey\":[\"TestValue\"]}",
106+
null,
107+
".",
108+
new Dictionary<string, string?> { { "TestKey.0", "TestValue" } }
109+
},
110+
new object?[]
111+
{
112+
"{\"TestKey\":[\"TestValue\"]}",
113+
"TestPrefix",
114+
".",
115+
new Dictionary<string, string?> { { "TestPrefix.TestKey.0", "TestValue" } }
116+
},
117+
new object?[]
118+
{
119+
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
120+
null,
121+
null,
122+
new Dictionary<string, string?> { { "TestOuterKey0TestInnerKey", "TestValue" } }
123+
},
124+
new object?[]
125+
{
126+
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
127+
"TestPrefix",
128+
null,
129+
new Dictionary<string, string?> { { "TestPrefixTestOuterKey0TestInnerKey", "TestValue" } }
130+
},
131+
new object?[]
132+
{
133+
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
134+
null,
135+
".",
136+
new Dictionary<string, string?> { { "TestOuterKey.0.TestInnerKey", "TestValue" } }
137+
},
138+
new object?[]
139+
{
140+
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
141+
"TestPrefix",
142+
".",
143+
new Dictionary<string, string?> { { "TestPrefix.TestOuterKey.0.TestInnerKey", "TestValue" } }
144+
}
145+
];
146+
}

tests/AzureAppConfigurationEmulator.Tests/ConfigurationSettings/ConfigurationSettingFactoryTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ public void SetUp()
2222
public void Create_ConfigurationSetting_ContentType(string? contentType, Type expected)
2323
{
2424
// Arrange
25+
const string etag = "TestEtag";
2526
const string key = "TestKey";
26-
const string label = "TestLabel";
27+
var date = DateTimeOffset.UtcNow;
2728
const string value = "{\"id\":\"TestId\",\"enabled\":true}";
2829

2930
// Act
30-
var setting = Factory.Create(key, label, contentType, value);
31+
var setting = Factory.Create(etag, key, date, false, null, contentType, value);
3132

3233
// Assert
3334
Assert.That(setting, Is.TypeOf(expected));

0 commit comments

Comments
 (0)