diff --git a/src/OpenFeature.Providers.Ofrep/Extensions/MetadataExtensions.cs b/src/OpenFeature.Providers.Ofrep/Extensions/MetadataExtensions.cs new file mode 100644 index 00000000..01a01bf0 --- /dev/null +++ b/src/OpenFeature.Providers.Ofrep/Extensions/MetadataExtensions.cs @@ -0,0 +1,78 @@ +using System.Text.Json; + +namespace OpenFeature.Providers.Ofrep.Extensions; + +/// +/// Extension methods for handling metadata conversion. +/// +internal static class MetadataExtensions +{ + /// + /// Converts a dictionary with JsonElement values to a dictionary with primitive types. + /// + /// The metadata dictionary with potential JsonElement values. + /// A new dictionary with primitive type values (string, double, bool). + internal static Dictionary ToPrimitiveTypes(this Dictionary metadata) + { + return metadata.ToDictionary( + kvp => kvp.Key, + kvp => ExtractPrimitiveValue(kvp.Value) + ); + } + + /// + /// Extracts a primitive value from an object that may be a JsonElement. + /// + /// The value to extract. + /// The extracted primitive value. + private static object ExtractPrimitiveValue(object value) + { + if (value is JsonElement jsonElement) + { + return jsonElement.ValueKind switch + { + JsonValueKind.String => jsonElement.GetString() ?? string.Empty, + JsonValueKind.Number => jsonElement.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Object => ConvertJsonObject(jsonElement), + JsonValueKind.Array => ConvertJsonArray(jsonElement), + JsonValueKind.Null => string.Empty, + _ => jsonElement.GetRawText() + }; + } + + // If it's already a primitive type, return as-is + return value; + } + + /// + /// Converts a JsonElement object to a Dictionary with primitive values. + /// + /// The JSON element containing the object. + /// A dictionary with string keys and primitive values. + private static Dictionary ConvertJsonObject(JsonElement jsonElement) + { + var result = new Dictionary(); + foreach (var property in jsonElement.EnumerateObject()) + { + result[property.Name] = ExtractPrimitiveValue(property.Value); + } + return result; + } + + /// + /// Converts a JsonElement array to a List with primitive values. + /// + /// The JSON element containing the array. + /// A list with primitive values. + private static List ConvertJsonArray(JsonElement jsonElement) + { + var result = new List(); + foreach (var element in jsonElement.EnumerateArray()) + { + result.Add(ExtractPrimitiveValue(element)); + } + return result; + } +} diff --git a/src/OpenFeature.Providers.Ofrep/OfrepProvider.cs b/src/OpenFeature.Providers.Ofrep/OfrepProvider.cs index a1054b50..eaf54018 100644 --- a/src/OpenFeature.Providers.Ofrep/OfrepProvider.cs +++ b/src/OpenFeature.Providers.Ofrep/OfrepProvider.cs @@ -3,6 +3,7 @@ using OpenFeature.Providers.Ofrep.Client; using OpenFeature.Providers.Ofrep.Client.Constants; using OpenFeature.Providers.Ofrep.Configuration; +using OpenFeature.Providers.Ofrep.Extensions; namespace OpenFeature.Providers.Ofrep; @@ -129,7 +130,7 @@ await this._client.EvaluateFlag(flagKey, defaultValue.AsObject, reason: response.Reason, variant: response.Variant, errorMessage: response.ErrorMessage, - flagMetadata: response.Metadata != null ? new ImmutableMetadata(response.Metadata) : null); + flagMetadata: response.Metadata != null ? new ImmutableMetadata(response.Metadata.ToPrimitiveTypes()) : null); } /// @@ -163,7 +164,7 @@ private async Task> ResolveFlag( reason: response.Reason, variant: response.Variant, errorMessage: response.ErrorMessage, - flagMetadata: response.Metadata != null ? new ImmutableMetadata(response.Metadata) : null); + flagMetadata: response.Metadata != null ? new ImmutableMetadata(response.Metadata.ToPrimitiveTypes()) : null); } /// diff --git a/test/OpenFeature.Providers.Ofrep.Test/Extensions/MetadataExtensionsTest.cs b/test/OpenFeature.Providers.Ofrep.Test/Extensions/MetadataExtensionsTest.cs new file mode 100644 index 00000000..e1f66c53 --- /dev/null +++ b/test/OpenFeature.Providers.Ofrep.Test/Extensions/MetadataExtensionsTest.cs @@ -0,0 +1,416 @@ +using System.Text.Json; +using OpenFeature.Model; +using OpenFeature.Providers.Ofrep.Extensions; +using Xunit; + +namespace OpenFeature.Providers.Ofrep.Test.Extensions; + +public class MetadataExtensionsTest +{ + [Fact] + public void ToPrimitiveTypes_ShouldConvertJsonElementString_ToString() + { + // Arrange + var json = JsonSerializer.Deserialize("\"hello world\""); + var metadata = new Dictionary + { + { "stringKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["stringKey"]); + Assert.Equal("hello world", result["stringKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldConvertJsonElementNumber_ToDouble() + { + // Arrange + var json = JsonSerializer.Deserialize("42.5"); + var metadata = new Dictionary + { + { "numberKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["numberKey"]); + Assert.Equal(42.5, result["numberKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldConvertJsonElementInteger_ToDouble() + { + // Arrange + var json = JsonSerializer.Deserialize("100"); + var metadata = new Dictionary + { + { "intKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["intKey"]); + Assert.Equal(100.0, result["intKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldConvertJsonElementTrue_ToBoolTrue() + { + // Arrange + var json = JsonSerializer.Deserialize("true"); + var metadata = new Dictionary + { + { "boolKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["boolKey"]); + Assert.True((bool)result["boolKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldConvertJsonElementFalse_ToBoolFalse() + { + // Arrange + var json = JsonSerializer.Deserialize("false"); + var metadata = new Dictionary + { + { "boolKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["boolKey"]); + Assert.False((bool)result["boolKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleNullJsonElement_AsEmptyString() + { + // Arrange + var json = JsonSerializer.Deserialize("null"); + var metadata = new Dictionary + { + { "nullKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["nullKey"]); + Assert.Equal(string.Empty, result["nullKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleJsonElementObject_AsDictionary() + { + // Arrange + var json = JsonSerializer.Deserialize("{\"nested\": \"value\", \"number\": 42}"); + var metadata = new Dictionary + { + { "objectKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["objectKey"]); + var dict = (Dictionary)result["objectKey"]; + Assert.Equal(2, dict.Count); + Assert.Equal("value", dict["nested"]); + Assert.Equal(42.0, dict["number"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleNestedJsonObject() + { + // Arrange + var json = JsonSerializer.Deserialize("{\"outer\": {\"inner\": \"value\"}}"); + var metadata = new Dictionary + { + { "objectKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["objectKey"]); + var outerDict = (Dictionary)result["objectKey"]; + Assert.IsType>(outerDict["outer"]); + var innerDict = (Dictionary)outerDict["outer"]; + Assert.Equal("value", innerDict["inner"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleJsonElementArray_AsList() + { + // Arrange + var json = JsonSerializer.Deserialize("[1, 2, 3]"); + var metadata = new Dictionary + { + { "arrayKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["arrayKey"]); + var list = (List)result["arrayKey"]; + Assert.Equal(3, list.Count); + Assert.Equal(1.0, list[0]); + Assert.Equal(2.0, list[1]); + Assert.Equal(3.0, list[2]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleMixedTypeArray() + { + // Arrange + var json = JsonSerializer.Deserialize("[\"hello\", 42, true, null]"); + var metadata = new Dictionary + { + { "arrayKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["arrayKey"]); + var list = (List)result["arrayKey"]; + Assert.Equal(4, list.Count); + Assert.Equal("hello", list[0]); + Assert.Equal(42.0, list[1]); + Assert.Equal(true, list[2]); + Assert.Equal(string.Empty, list[3]); // null converts to empty string + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleNestedArrays() + { + // Arrange + var json = JsonSerializer.Deserialize("[[1, 2], [3, 4]]"); + var metadata = new Dictionary + { + { "arrayKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["arrayKey"]); + var outerList = (List)result["arrayKey"]; + Assert.Equal(2, outerList.Count); + Assert.IsType>(outerList[0]); + var innerList = (List)outerList[0]; + Assert.Equal(1.0, innerList[0]); + Assert.Equal(2.0, innerList[1]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleArrayOfObjects() + { + // Arrange + var json = JsonSerializer.Deserialize("[{\"name\": \"Alice\"}, {\"name\": \"Bob\"}]"); + var metadata = new Dictionary + { + { "arrayKey", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType>(result["arrayKey"]); + var list = (List)result["arrayKey"]; + Assert.Equal(2, list.Count); + Assert.IsType>(list[0]); + var firstItem = (Dictionary)list[0]; + Assert.Equal("Alice", firstItem["name"]); + var secondItem = (Dictionary)list[1]; + Assert.Equal("Bob", secondItem["name"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldPreservePrimitiveString() + { + // Arrange + var metadata = new Dictionary + { + { "stringKey", "already a string" } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["stringKey"]); + Assert.Equal("already a string", result["stringKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldPreservePrimitiveDouble() + { + // Arrange + var metadata = new Dictionary + { + { "doubleKey", 3.14 } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["doubleKey"]); + Assert.Equal(3.14, result["doubleKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldPreservePrimitiveInt() + { + // Arrange + var metadata = new Dictionary + { + { "intKey", 42 } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["intKey"]); + Assert.Equal(42, result["intKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldPreservePrimitiveBool() + { + // Arrange + var metadata = new Dictionary + { + { "boolKey", true } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["boolKey"]); + Assert.True((bool)result["boolKey"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleMixedTypes() + { + // Arrange + var jsonString = JsonSerializer.Deserialize("\"json string\""); + var jsonNumber = JsonSerializer.Deserialize("99.9"); + var jsonBool = JsonSerializer.Deserialize("true"); + + var metadata = new Dictionary + { + { "jsonString", jsonString }, + { "jsonNumber", jsonNumber }, + { "jsonBool", jsonBool }, + { "primitiveString", "native string" }, + { "primitiveInt", 123 }, + { "primitiveBool", false } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.Equal("json string", result["jsonString"]); + Assert.Equal(99.9, result["jsonNumber"]); + Assert.True((bool)result["jsonBool"]); + Assert.Equal("native string", result["primitiveString"]); + Assert.Equal(123, result["primitiveInt"]); + Assert.False((bool)result["primitiveBool"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldReturnEmptyDictionary_WhenInputIsEmpty() + { + // Arrange + var metadata = new Dictionary(); + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.Empty(result); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleEmptyString() + { + // Arrange + var json = JsonSerializer.Deserialize("\"\""); + var metadata = new Dictionary + { + { "emptyString", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["emptyString"]); + Assert.Equal(string.Empty, result["emptyString"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleNegativeNumbers() + { + // Arrange + var json = JsonSerializer.Deserialize("-42.5"); + var metadata = new Dictionary + { + { "negativeNumber", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["negativeNumber"]); + Assert.Equal(-42.5, result["negativeNumber"]); + } + + [Fact] + public void ToPrimitiveTypes_ShouldHandleZero() + { + // Arrange + var json = JsonSerializer.Deserialize("0"); + var metadata = new Dictionary + { + { "zero", json } + }; + + // Act + var result = metadata.ToPrimitiveTypes(); + + // Assert + Assert.IsType(result["zero"]); + Assert.Equal(0.0, result["zero"]); + } +}