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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json;

namespace AzureAppConfigurationEmulator.Common;

public interface IKeyValuePairJsonDecoder
{
IEnumerable<KeyValuePair<string, string?>> Decode(
JsonDocument document,
string? prefix = null,
string? separator = null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Globalization;
using System.Text.Json;

namespace AzureAppConfigurationEmulator.Common;

public class KeyValuePairJsonDecoder : IKeyValuePairJsonDecoder
{
public IEnumerable<KeyValuePair<string, string?>> Decode(
JsonDocument document,
string? prefix = null,
string? separator = null)
{
return Decode(document.RootElement, prefix, separator);
}

private IEnumerable<KeyValuePair<string, string?>> 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<string, string?>(prefix, value);
}

break;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@inject IConfigurationSettingFactory ConfigurationSettingFactory
@inject IConfigurationSettingRepository ConfigurationSettingRepository
@inject IKeyValuePairJsonDecoder KeyValuePairJsonDecoder
@page "/kvdata"
@using System.Security.Cryptography
@using System.Text
Expand Down Expand Up @@ -170,69 +171,6 @@
Model ??= new InputModel();
}

private static IEnumerable<KeyValuePair<string, object?>> 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<string, object?>(prefix, element.GetString());
}

break;
case JsonValueKind.Number:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, element.GetDouble());
}

break;
case JsonValueKind.True:
case JsonValueKind.False:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, element.GetBoolean());
}

break;
case JsonValueKind.Undefined:
case JsonValueKind.Null:
if (!string.IsNullOrEmpty(prefix))
{
yield return new KeyValuePair<string, object?>(prefix, null);
}

break;
default:
throw new ArgumentOutOfRangeException();
}
}

private async Task HandleSourceConnectionStringChange(string? connectionString)
{
using var activity = Telemetry.ActivitySource.StartActivity($"{nameof(ImportExport)}.{nameof(HandleSourceConnectionStringChange)}");
Expand Down Expand Up @@ -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);
}
Expand All @@ -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)
{
Expand All @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions src/AzureAppConfigurationEmulator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
builder.Services.AddSingleton<IDbConnectionFactory, SqliteDbConnectionFactory>();
builder.Services.AddSingleton<IDbParameterFactory, SqliteDbParameterFactory>();
builder.Services.AddSingleton<IEventGridEventFactory, HttpContextEventGridEventFactory>();
builder.Services.AddSingleton<IKeyValuePairJsonDecoder, KeyValuePairJsonDecoder>();

var app = builder.Build();

Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string?> 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<string, string?> { { "TestKey", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":\"TestValue\"}",
"TestPrefix",
null,
new Dictionary<string, string?> { { "TestPrefixTestKey", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":\"TestValue\"}",
null,
".",
new Dictionary<string, string?> { { "TestKey", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":\"TestValue\"}",
"TestPrefix",
".",
new Dictionary<string, string?> { { "TestPrefix.TestKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
null,
null,
new Dictionary<string, string?> { { "TestOuterKeyTestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
"TestPrefix",
null,
new Dictionary<string, string?> { { "TestPrefixTestOuterKeyTestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
null,
".",
new Dictionary<string, string?> { { "TestOuterKey.TestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":{\"TestInnerKey\":\"TestValue\"}}",
"TestPrefix",
".",
new Dictionary<string, string?> { { "TestPrefix.TestOuterKey.TestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":[\"TestValue\"]}",
null,
null,
new Dictionary<string, string?> { { "TestKey0", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":[\"TestValue\"]}",
"TestPrefix",
null,
new Dictionary<string, string?> { { "TestPrefixTestKey0", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":[\"TestValue\"]}",
null,
".",
new Dictionary<string, string?> { { "TestKey.0", "TestValue" } }
},
new object?[]
{
"{\"TestKey\":[\"TestValue\"]}",
"TestPrefix",
".",
new Dictionary<string, string?> { { "TestPrefix.TestKey.0", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
null,
null,
new Dictionary<string, string?> { { "TestOuterKey0TestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
"TestPrefix",
null,
new Dictionary<string, string?> { { "TestPrefixTestOuterKey0TestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
null,
".",
new Dictionary<string, string?> { { "TestOuterKey.0.TestInnerKey", "TestValue" } }
},
new object?[]
{
"{\"TestOuterKey\":[{\"TestInnerKey\":\"TestValue\"}]}",
"TestPrefix",
".",
new Dictionary<string, string?> { { "TestPrefix.TestOuterKey.0.TestInnerKey", "TestValue" } }
}
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading