diff --git a/src/AzureAppConfigurationEmulator/Common/IKeyValuePairJsonDecoder.cs b/src/AzureAppConfigurationEmulator/Common/IKeyValuePairJsonDecoder.cs new file mode 100644 index 0000000..b991447 --- /dev/null +++ b/src/AzureAppConfigurationEmulator/Common/IKeyValuePairJsonDecoder.cs @@ -0,0 +1,11 @@ +using System.Text.Json; + +namespace AzureAppConfigurationEmulator.Common; + +public interface IKeyValuePairJsonDecoder +{ + IEnumerable> Decode( + JsonDocument document, + string? prefix = null, + string? separator = null); +} diff --git a/src/AzureAppConfigurationEmulator/Common/KeyValuePairJsonDecoder.cs b/src/AzureAppConfigurationEmulator/Common/KeyValuePairJsonDecoder.cs new file mode 100644 index 0000000..1e52b86 --- /dev/null +++ b/src/AzureAppConfigurationEmulator/Common/KeyValuePairJsonDecoder.cs @@ -0,0 +1,75 @@ +using System.Globalization; +using System.Text.Json; + +namespace AzureAppConfigurationEmulator.Common; + +public class KeyValuePairJsonDecoder : IKeyValuePairJsonDecoder +{ + public IEnumerable> Decode( + JsonDocument document, + string? prefix = null, + string? separator = null) + { + return Decode(document.RootElement, prefix, separator); + } + + private IEnumerable> Decode( + JsonElement element, + string? prefix = null, + string? separator = null) + { + using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(KeyValuePairJsonDecoder)}.{nameof(Decode)}"); + + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var innerProperty in element.EnumerateObject()) + { + var innerPrefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{innerProperty.Name}" : innerProperty.Name; + + foreach (var setting in Decode(innerProperty.Value, innerPrefix, separator)) + { + yield return setting; + } + } + + break; + case JsonValueKind.Array: + var index = 0; + + foreach (var innerElement in element.EnumerateArray()) + { + var innerPrefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{index}" : index.ToString(); + + foreach (var setting in Decode(innerElement, innerPrefix, separator)) + { + yield return setting; + } + + index += 1; + } + + break; + default: + if (!string.IsNullOrEmpty(prefix)) + { + var value = element.ValueKind switch + { + JsonValueKind.Undefined => null, + JsonValueKind.Object => null, + JsonValueKind.Array => null, + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.GetDouble().ToString(CultureInfo.InvariantCulture), + JsonValueKind.True => true.ToString(), + JsonValueKind.False => false.ToString(), + JsonValueKind.Null => null, + _ => null + }; + + yield return new KeyValuePair(prefix, value); + } + + break; + } + } +} diff --git a/src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor b/src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor index a41abf5..6e55e47 100644 --- a/src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor +++ b/src/AzureAppConfigurationEmulator/Components/Pages/ImportExport.razor @@ -1,5 +1,6 @@ @inject IConfigurationSettingFactory ConfigurationSettingFactory @inject IConfigurationSettingRepository ConfigurationSettingRepository +@inject IKeyValuePairJsonDecoder KeyValuePairJsonDecoder @page "/kvdata" @using System.Security.Cryptography @using System.Text @@ -170,69 +171,6 @@ Model ??= new InputModel(); } - private static IEnumerable> FlattenJsonElement(JsonElement element, string? prefix = null, string? separator = null) - { - switch (element.ValueKind) - { - case JsonValueKind.Object: - foreach (var property in element.EnumerateObject()) - { - foreach (var pair in FlattenJsonElement(property.Value, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{property.Name}" : property.Name, separator)) - { - yield return pair; - } - } - - break; - case JsonValueKind.Array: - var index = 0; - - foreach (var inner in element.EnumerateArray()) - { - foreach (var pair in FlattenJsonElement(inner, !string.IsNullOrEmpty(prefix) ? $"{prefix}{separator}{index}" : index.ToString(), separator)) - { - yield return pair; - } - - index += 1; - } - - break; - case JsonValueKind.String: - if (!string.IsNullOrEmpty(prefix)) - { - yield return new KeyValuePair(prefix, element.GetString()); - } - - break; - case JsonValueKind.Number: - if (!string.IsNullOrEmpty(prefix)) - { - yield return new KeyValuePair(prefix, element.GetDouble()); - } - - break; - case JsonValueKind.True: - case JsonValueKind.False: - if (!string.IsNullOrEmpty(prefix)) - { - yield return new KeyValuePair(prefix, element.GetBoolean()); - } - - break; - case JsonValueKind.Undefined: - case JsonValueKind.Null: - if (!string.IsNullOrEmpty(prefix)) - { - yield return new KeyValuePair(prefix, null); - } - - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - private async Task HandleSourceConnectionStringChange(string? connectionString) { using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleSourceConnectionStringChange)}"); @@ -312,13 +250,16 @@ destinationSetting.LastModified = date; destinationSetting.ContentType = sourceSetting.ContentType; destinationSetting.Value = sourceSetting.Value; + destinationSetting.Locked = sourceSetting.Locked; destinationSetting.Tags = sourceSetting.Tags; await ConfigurationSettingRepository.Update(destinationSetting); } else { - destinationSetting = ConfigurationSettingFactory.Create(sourceSetting.Key, sourceSetting.Label, sourceSetting.ContentType, sourceSetting.Value, sourceSetting.Tags); + var date = DateTimeOffset.UtcNow; + + 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); await ConfigurationSettingRepository.Add(destinationSetting); } @@ -334,7 +275,7 @@ using var document = await JsonDocument.ParseAsync(stream); - foreach (var (sourceKey, sourceValue) in FlattenJsonElement(document.RootElement, Model.Prefix, Model.Separator)) + foreach (var (sourceKey, sourceValue) in KeyValuePairJsonDecoder.Decode(document, Model.Prefix, Model.Separator)) { if (await ConfigurationSettingRepository.Get(sourceKey, Model.Label ?? LabelFilter.Null).SingleOrDefaultAsync() is { } destinationSetting) { @@ -349,7 +290,9 @@ } else { - destinationSetting = ConfigurationSettingFactory.Create(sourceKey, Model.Label, Model.ContentType, sourceValue?.ToString()); + var date = DateTimeOffset.UtcNow; + + 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); await ConfigurationSettingRepository.Add(destinationSetting); } diff --git a/src/AzureAppConfigurationEmulator/Program.cs b/src/AzureAppConfigurationEmulator/Program.cs index 84f0032..69af054 100644 --- a/src/AzureAppConfigurationEmulator/Program.cs +++ b/src/AzureAppConfigurationEmulator/Program.cs @@ -86,6 +86,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/tests/AzureAppConfigurationEmulator.Tests/Common/KeyValuePairJsonDecoderTests.cs b/tests/AzureAppConfigurationEmulator.Tests/Common/KeyValuePairJsonDecoderTests.cs new file mode 100644 index 0000000..6995111 --- /dev/null +++ b/tests/AzureAppConfigurationEmulator.Tests/Common/KeyValuePairJsonDecoderTests.cs @@ -0,0 +1,146 @@ +using System.Text.Json; +using AzureAppConfigurationEmulator.Common; +using NUnit.Framework; + +namespace AzureAppConfigurationEmulator.Tests.Common; + +public class KeyValuePairJsonDecoderTests +{ + private KeyValuePairJsonDecoder Decoder { get; set; } + + [SetUp] + public void SetUp() + { + Decoder = new KeyValuePairJsonDecoder(); + } + + [TestCaseSource(nameof(Decode_KeyValuePairs_DocumentAndPrefixAndSeparator_TestCases))] + public void Decode_KeyValuePairs_DocumentAndPrefixAndSeparator(string json, string? prefix, string? separator, IDictionary expected) + { + // Arrange + using var document = JsonDocument.Parse(json); + + // Act + var settings = Decoder.Decode(document, prefix, separator); + + // Assert + Assert.That(settings.ToDictionary(), Is.EqualTo(expected)); + } + + // ReSharper disable once InconsistentNaming + private static object[] Decode_KeyValuePairs_DocumentAndPrefixAndSeparator_TestCases = + [ + new object?[] + { + "{\"TestKey\":\"TestValue\"}", + null, + null, + new Dictionary { { "TestKey", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":\"TestValue\"}", + "TestPrefix", + null, + new Dictionary { { "TestPrefixTestKey", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":\"TestValue\"}", + null, + ".", + new Dictionary { { "TestKey", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":\"TestValue\"}", + "TestPrefix", + ".", + new Dictionary { { "TestPrefix.TestKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}", + null, + null, + new Dictionary { { "TestOuterKeyTestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}", + "TestPrefix", + null, + new Dictionary { { "TestPrefixTestOuterKeyTestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}", + null, + ".", + new Dictionary { { "TestOuterKey.TestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}", + "TestPrefix", + ".", + new Dictionary { { "TestPrefix.TestOuterKey.TestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":[\"TestValue\"]}", + null, + null, + new Dictionary { { "TestKey0", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":[\"TestValue\"]}", + "TestPrefix", + null, + new Dictionary { { "TestPrefixTestKey0", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":[\"TestValue\"]}", + null, + ".", + new Dictionary { { "TestKey.0", "TestValue" } } + }, + new object?[] + { + "{\"TestKey\":[\"TestValue\"]}", + "TestPrefix", + ".", + new Dictionary { { "TestPrefix.TestKey.0", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}", + null, + null, + new Dictionary { { "TestOuterKey0TestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}", + "TestPrefix", + null, + new Dictionary { { "TestPrefixTestOuterKey0TestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}", + null, + ".", + new Dictionary { { "TestOuterKey.0.TestInnerKey", "TestValue" } } + }, + new object?[] + { + "{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}", + "TestPrefix", + ".", + new Dictionary { { "TestPrefix.TestOuterKey.0.TestInnerKey", "TestValue" } } + } + ]; +} diff --git a/tests/AzureAppConfigurationEmulator.Tests/ConfigurationSettings/ConfigurationSettingFactoryTests.cs b/tests/AzureAppConfigurationEmulator.Tests/ConfigurationSettings/ConfigurationSettingFactoryTests.cs index 8fbd30c..e917173 100644 --- a/tests/AzureAppConfigurationEmulator.Tests/ConfigurationSettings/ConfigurationSettingFactoryTests.cs +++ b/tests/AzureAppConfigurationEmulator.Tests/ConfigurationSettings/ConfigurationSettingFactoryTests.cs @@ -22,12 +22,13 @@ public void SetUp() public void Create_ConfigurationSetting_ContentType(string? contentType, Type expected) { // Arrange + const string etag = "TestEtag"; const string key = "TestKey"; - const string label = "TestLabel"; + var date = DateTimeOffset.UtcNow; const string value = "{\"id\":\"TestId\",\"enabled\":true}"; // Act - var setting = Factory.Create(key, label, contentType, value); + var setting = Factory.Create(etag, key, date, false, null, contentType, value); // Assert Assert.That(setting, Is.TypeOf(expected));