From 79062cb5504e8a4d3b1bc0e19ff10a17e5506914 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 15 Jan 2025 14:58:11 +0100 Subject: [PATCH 1/6] Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .../Resolver/InProcess/JsonEvaluator.cs | 36 ++++-- .../JsonEvaluatorTest.cs | 115 +++++++++++++++--- .../Utils.cs | 90 +++++++++++++- 3 files changed, 214 insertions(+), 27 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 6154982e..386e117b 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.RegularExpressions; using JsonLogic.Net; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,7 +10,6 @@ using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using OpenFeature.Error; using OpenFeature.Model; -using System.Text.RegularExpressions; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess { @@ -26,6 +26,8 @@ internal class FlagConfiguration internal object Targeting { get; set; } [JsonProperty("source")] internal string Source { get; set; } + [JsonProperty("metadata")] + internal Dictionary Metadata { get; set; } } internal class FlagSyncData @@ -34,6 +36,8 @@ internal class FlagSyncData internal Dictionary Flags { get; set; } [JsonProperty("$evaluators")] internal Dictionary Evaluators { get; set; } + [JsonProperty("metadata")] + internal Dictionary Metadata { get; set; } } internal class FlagConfigurationSync @@ -53,6 +57,7 @@ internal enum FlagConfigurationUpdateType internal class JsonEvaluator { private Dictionary _flags = new Dictionary(); + private Dictionary _flagSetMetadata = new Dictionary(); private string _selector; @@ -99,17 +104,17 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { case FlagConfigurationUpdateType.ALL: _flags = flagConfigsMap.Flags; + _flagSetMetadata = flagConfigsMap.Metadata; break; case FlagConfigurationUpdateType.ADD: + case FlagConfigurationUpdateType.UPDATE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags[keyAndValue.Key] = keyAndValue.Value; } - break; - case FlagConfigurationUpdateType.UPDATE: - foreach (var keyAndValue in flagConfigsMap.Flags) + foreach (var metadata in flagConfigsMap.Metadata) { - _flags[keyAndValue.Key] = keyAndValue.Value; + _flagSetMetadata[metadata.Key] = metadata.Value; } break; case FlagConfigurationUpdateType.DELETE: @@ -117,8 +122,11 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { _flags.Remove(keyAndValue.Key); } + foreach (var keyValuePair in flagConfigsMap.Metadata) + { + _flagSetMetadata.Remove(keyValuePair.Key); + } break; - } } @@ -157,6 +165,16 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); } + Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); + if(flagConfiguration.Metadata != null) + { + foreach (var (key,value) in flagConfiguration.Metadata) + { + combinedMetadata[key] = value; + } + } + + var flagMetadata = new ImmutableMetadata(combinedMetadata); var variant = flagConfiguration.DefaultVariant; if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}") { @@ -212,7 +230,8 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva flagKey: flagKey, value, reason: reason, - variant: variant + variant: variant, + flagMetadata: flagMetadata ); } else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue)) @@ -223,7 +242,8 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva flagKey: flagKey, value, reason: reason, - variant: variant + variant: variant, + flagMetadata: flagMetadata ); } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs index 930250a2..dd2501c8 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using AutoFixture; using OpenFeature.Constant; @@ -10,7 +11,6 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test { public class UnitTestJsonEvaluator { - [Fact] public void TestJsonEvaluatorAddFlagConfig() { @@ -23,7 +23,6 @@ public void TestJsonEvaluatorAddFlagConfig() var result = jsonEvaluator.ResolveBooleanValueAsync("validFlag", false); Assert.True(result.Value); - } [Fact] @@ -40,7 +39,6 @@ public void TestJsonEvaluatorAddStaticStringEvaluation() Assert.Equal("#CC0000", result.Value); Assert.Equal("red", result.Variant); Assert.Equal(Reason.Static, result.Reason); - } [Fact] @@ -57,7 +55,7 @@ public void TestJsonEvaluatorDynamicBoolEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlag", false, builder.Build()); @@ -80,7 +78,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyFlagKey() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -101,7 +100,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyTimestamp() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, builder.Build()); + var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -119,7 +119,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluator() var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -137,7 +138,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluatorReturningBoolTy var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("true", result.Variant); @@ -155,7 +158,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithMissingDefaultVariant() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, + builder.Build())); } [Fact] @@ -169,7 +174,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithUnexpectedVariantType() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, + builder.Build())); } [Fact] @@ -186,7 +193,7 @@ public void TestJsonEvaluatorDynamicStringEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStringValueAsync("targetingStringFlag", "", builder.Build()); @@ -209,7 +216,7 @@ public void TestJsonEvaluatorDynamicFloatEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveDoubleValueAsync("targetingFloatFlag", 0, builder.Build()); @@ -232,7 +239,7 @@ public void TestJsonEvaluatorDynamicIntEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveIntegerValueAsync("targetingNumberFlag", 0, builder.Build()); @@ -255,7 +262,7 @@ public void TestJsonEvaluatorDynamicObjectEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStructureValueAsync("targetingObjectFlag", null, builder.Build()); @@ -280,7 +287,8 @@ public void TestJsonEvaluatorDisabledBoolEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); } [Fact] @@ -299,7 +307,8 @@ public void TestJsonEvaluatorFlagNotFoundEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); } [Fact] @@ -318,7 +327,79 @@ public void TestJsonEvaluatorWrongTypeEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + } + + [Fact] + public void TestJsonEvaluatorReturnsFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.flags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetdata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.3", result.FlagMetadata.GetString("string")); + Assert.Equal(3, result.FlagMetadata.GetInt("integer")); + Assert.Equal(false, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.2, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata)); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata)); } } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs index c1db7aaa..de386478 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs @@ -301,10 +301,95 @@ public class Utils ""off"": false }, ""defaultVariant"": ""on"" + }, + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + } + } +}"; + + public static string metadataFlags = @"{ + ""flags"":{ + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"":{ + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + }, + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" } + }, + ""metadata"": { + ""string"": ""1.0.3"", + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, } }"; + public static string invalidFlagSetMetadata = @"{ + ""flags"":{ + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" + } + }, + ""metadata"": { + ""string"": {""in"": ""valid""}, + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, + } +}"; + public static string invalidFlagMetadata = @"{ + ""flags"":{ + ""invalid-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": {""in"": ""valid""}, + } + }, + } +}"; + + /// /// Repeatedly runs the supplied assertion until it doesn't throw, or the timeout is reached. /// @@ -312,11 +397,11 @@ public class Utils /// Timeout in millis (defaults to 1000) /// Poll interval (defaults to 100 /// - public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, int pollIntervalMillis = 100) + public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, + int pollIntervalMillis = 100) { using (var cts = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken))) { - cts.CancelAfter(timeoutMillis); var exceptions = new List(); @@ -347,6 +432,7 @@ public static async Task AssertUntilAsync(Action assertionFun throw new AggregateException(message, exceptions); } } + throw new AggregateException(message, exceptions); } } From 934e580ccb1394588fa3246e8e2635f9401001e2 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 15 Jan 2025 15:12:17 +0100 Subject: [PATCH 2/6] fixup! Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .../Resolver/InProcess/JsonEvaluator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 386e117b..2af0ca80 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -168,9 +168,9 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); if(flagConfiguration.Metadata != null) { - foreach (var (key,value) in flagConfiguration.Metadata) + foreach (var metadataEntry in flagConfiguration.Metadata) { - combinedMetadata[key] = value; + combinedMetadata[metadataEntry.Key] = metadataEntry.Value; } } From 0ff89cd787f9362eea4590c931586bf0f12c6cbd Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Wed, 15 Jan 2025 15:19:21 +0100 Subject: [PATCH 3/6] fixup! Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .../Resolver/InProcess/JsonEvaluator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 2af0ca80..472a0a53 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -104,7 +104,15 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { case FlagConfigurationUpdateType.ALL: _flags = flagConfigsMap.Flags; - _flagSetMetadata = flagConfigsMap.Metadata; + if (flagConfigsMap.Metadata == null) + { + _flagSetMetadata.Clear(); + } + else + { + _flagSetMetadata = flagConfigsMap.Metadata; + } + break; case FlagConfigurationUpdateType.ADD: case FlagConfigurationUpdateType.UPDATE: From 8ec5889218fb4a1c8c6be02756419496d3698113 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 16 Jan 2025 08:50:21 +0100 Subject: [PATCH 4/6] fixup! Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .../Resolver/InProcess/JsonEvaluator.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 472a0a53..0a784e4f 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -93,7 +93,12 @@ internal FlagSyncData Parse(string flagConfigurations) }); } - return JsonConvert.DeserializeObject(transformed); + Console.Error.WriteLine("flagConfigurations " + flagConfigurations); + + FlagSyncData data = JsonConvert.DeserializeObject(transformed); + Console.Error.WriteLine("metadata " + data.Metadata); + Console.Error.WriteLine("metadata count " + data.Metadata?.Count); + return data; } internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations) From 55609f56a0d2e8b2c1448a451c70ac7d1d4711a2 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Thu, 16 Jan 2025 08:57:02 +0100 Subject: [PATCH 5/6] fixup! Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .../Resolver/InProcess/JsonEvaluator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 0a784e4f..8740b004 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -98,6 +98,8 @@ internal FlagSyncData Parse(string flagConfigurations) FlagSyncData data = JsonConvert.DeserializeObject(transformed); Console.Error.WriteLine("metadata " + data.Metadata); Console.Error.WriteLine("metadata count " + data.Metadata?.Count); + Console.WriteLine("metadata " + data.Metadata);//.WriteLine(); + Console.WriteLine("metadata count " + data.Metadata?.Count); return data; } From 6b5210de97560e44ce734420598f813e3b870c92 Mon Sep 17 00:00:00 2001 From: "christian.lutnik" Date: Fri, 17 Jan 2025 09:54:49 +0100 Subject: [PATCH 6/6] fixup! Update in-process resolver to support flag metadata #305 Signed-off-by: christian.lutnik --- .fleet/run.json | 10 + .gitmodules | 2 +- .../Resolver/InProcess/JsonEvaluator.cs | 178 ++++++++++++------ .../JsonEvaluatorTest.cs | 7 +- 4 files changed, 132 insertions(+), 65 deletions(-) create mode 100644 .fleet/run.json diff --git a/.fleet/run.json b/.fleet/run.json new file mode 100644 index 00000000..9d1c42f1 --- /dev/null +++ b/.fleet/run.json @@ -0,0 +1,10 @@ +{ + "configurations": [ + { + "type": "command", + "name": "debug", + "program": "$PROJECT_DIR$\\test\\OpenFeature.Contrib.Providers.Flagd.Test\\bin\\Debug\\net6.0\\testhost.exe", + "args": [] + } + ] +} \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 6940ffbf..b426c70e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"] path = src/OpenFeature.Contrib.Providers.Flagd/schemas - url = git@github.com:open-feature/schemas.git + url = https://github.com/open-feature/schemas.git [submodule "spec"] path = spec url = https://github.com/open-feature/spec.git diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 8740b004..8e0579fa 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -13,31 +13,21 @@ namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess { - internal class FlagConfiguration { - [JsonProperty("state")] - internal string State { get; set; } - [JsonProperty("defaultVariant")] - internal string DefaultVariant { get; set; } - [JsonProperty("variants")] - internal Dictionary Variants { get; set; } - [JsonProperty("targeting")] - internal object Targeting { get; set; } - [JsonProperty("source")] - internal string Source { get; set; } - [JsonProperty("metadata")] - internal Dictionary Metadata { get; set; } + [JsonProperty("state")] internal string State { get; set; } + [JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; } + [JsonProperty("variants")] internal Dictionary Variants { get; set; } + [JsonProperty("targeting")] internal object Targeting { get; set; } + [JsonProperty("source")] internal string Source { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagSyncData { - [JsonProperty("flags")] - internal Dictionary Flags { get; set; } - [JsonProperty("$evaluators")] - internal Dictionary Evaluators { get; set; } - [JsonProperty("metadata")] - internal Dictionary Metadata { get; set; } + [JsonProperty("flags")] internal Dictionary Flags { get; set; } + [JsonProperty("$evaluators")] internal Dictionary Evaluators { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagConfigurationSync @@ -93,16 +83,61 @@ internal FlagSyncData Parse(string flagConfigurations) }); } - Console.Error.WriteLine("flagConfigurations " + flagConfigurations); - FlagSyncData data = JsonConvert.DeserializeObject(transformed); - Console.Error.WriteLine("metadata " + data.Metadata); - Console.Error.WriteLine("metadata count " + data.Metadata?.Count); - Console.WriteLine("metadata " + data.Metadata);//.WriteLine(); - Console.WriteLine("metadata count " + data.Metadata?.Count); + var data = JsonConvert.DeserializeObject(transformed); + if (data.Metadata == null) + { + data.Metadata = new Dictionary(); + } + else + { + foreach (var key in new List(data.Metadata.Keys)) + { + var value = data.Metadata[key]; + if (value is long longValue) + { + data.Metadata[key] = (int)longValue; + continue; + } + + VerifyMetadataValue(key, value); + } + } + + foreach (var flagConfig in data.Flags) + { + if (flagConfig.Value.Metadata == null) + { + continue; + } + + foreach (var key in new List(flagConfig.Value.Metadata.Keys)) + { + var value = flagConfig.Value.Metadata[key]; + if (value is long longValue) + { + flagConfig.Value.Metadata[key] = (int)longValue; + continue; + } + + VerifyMetadataValue(key, value); + } + } + return data; } + private static void VerifyMetadataValue(string key, object value) + { + if (value is int || value is double || value is string || value is bool) + { + return; + } + + throw new ParseErrorException("Metadata entry for key " + key + " and value " + value + + " is of unknown type"); + } + internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations) { var flagConfigsMap = Parse(flagConfigurations); @@ -111,14 +146,7 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { case FlagConfigurationUpdateType.ALL: _flags = flagConfigsMap.Flags; - if (flagConfigsMap.Metadata == null) - { - _flagSetMetadata.Clear(); - } - else - { - _flagSetMetadata = flagConfigsMap.Metadata; - } + _flagSetMetadata = flagConfigsMap.Metadata; break; case FlagConfigurationUpdateType.ADD: @@ -127,50 +155,60 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { _flags[keyAndValue.Key] = keyAndValue.Value; } + foreach (var metadata in flagConfigsMap.Metadata) { _flagSetMetadata[metadata.Key] = metadata.Value; } + break; case FlagConfigurationUpdateType.DELETE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags.Remove(keyAndValue.Key); } + foreach (var keyValuePair in flagConfigsMap.Metadata) { _flagSetMetadata.Remove(keyValuePair.Key); } + break; } } - public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - private ResolutionDetails ResolveValue(string flagKey, T defaultValue, EvaluationContext context = null) + private ResolutionDetails ResolveValue(string flagKey, T defaultValue, + EvaluationContext context = null) { // check if we find the flag key var reason = Reason.Static; @@ -178,10 +216,12 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { if ("DISABLED" == flagConfiguration.State) { - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); } + Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); - if(flagConfiguration.Metadata != null) + if (flagConfiguration.Metadata != null) { foreach (var metadataEntry in flagConfiguration.Metadata) { @@ -191,12 +231,15 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva var flagMetadata = new ImmutableMetadata(combinedMetadata); var variant = flagConfiguration.DefaultVariant; - if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}") + if (flagConfiguration.Targeting != null && + !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && + flagConfiguration.Targeting.ToString() != "{}") { reason = Reason.TargetingMatch; var flagdProperties = new Dictionary(); flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey)); - flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); + flagdProperties.Add(FlagdProperties.TimestampKey, + new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); if (context == null) { @@ -206,7 +249,7 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva var targetingContext = context.AsDictionary().Add( FlagdProperties.FlagdPropertiesKey, new Value(new Structure(flagdProperties)) - ); + ); var targetingString = flagConfiguration.Targeting.ToString(); // Parse json into hierarchical structure @@ -235,34 +278,39 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { // if variant is null, revert to default reason = Reason.Default; - flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue); + flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, + out var defaultVariantValue); if (defaultVariantValue == null) { - throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); + throw new FeatureProviderException(ErrorType.ParseError, + "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); } + var value = ExtractFoundVariant(defaultVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant, - flagMetadata: flagMetadata - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue)) { // if variant can be found, return it - this could be TARGETING_MATCH or STATIC. var value = ExtractFoundVariant(foundVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant, - flagMetadata: flagMetadata - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } } - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); + + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); } static T ExtractFoundVariant(object foundVariantValue, string flagKey) @@ -271,6 +319,7 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = Convert.ToInt32(foundVariantValue); } + if (typeof(T) == typeof(double)) { foundVariantValue = Convert.ToDouble(foundVariantValue); @@ -279,11 +328,14 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = ConvertJObjectToOpenFeatureValue(value); } + if (foundVariantValue is T castValue) { return castValue; } - throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); + + throw new FeatureProviderException(ErrorType.TypeMismatch, + "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); } static dynamic ConvertToDynamicObject(IImmutableDictionary dictionary) @@ -294,7 +346,9 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary dictio foreach (var kvp in dictionary) { expandoDict.Add(kvp.Key, - kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject); + kvp.Value.IsStructure + ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) + : kvp.Value.AsObject); } return expandoObject; @@ -337,4 +391,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue) return new Value(new Structure(result)); } } -} +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs index dd2501c8..781658eb 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs @@ -375,6 +375,9 @@ public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata() jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + + var a = result.FlagMetadata.GetInt("integer"); + Assert.NotNull(result.FlagMetadata); Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); Assert.Equal(2, result.FlagMetadata.GetInt("integer")); @@ -389,7 +392,7 @@ public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata() var jsonEvaluator = new JsonEvaluator(fixture.Create()); - Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata)); + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata)); } [Fact] @@ -399,7 +402,7 @@ public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata() var jsonEvaluator = new JsonEvaluator(fixture.Create()); - Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata)); + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata)); } } }