diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d26c0721..57f241c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,12 +50,12 @@ jobs: services: # flagd-testbed for flagd RPC provider e2e tests flagd: - image: ghcr.io/open-feature/flagd-testbed:v0.5.4 + image: ghcr.io/open-feature/flagd-testbed:v0.5.6 ports: - 8013:8013 # sync-testbed for flagd in-process provider e2e tests sync: - image: ghcr.io/open-feature/sync-testbed:v0.5.4 + image: ghcr.io/open-feature/sync-testbed:v0.5.6 ports: - 9090:9090 steps: diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs index e9560af7..ce336ac9 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs @@ -17,7 +17,7 @@ internal class FlagdProperties internal FlagdProperties(object from) { //object value; - if (from is Dictionary dict) + if (from is IDictionary dict) { if (dict.TryGetValue(TargetingKeyKey, out object targetingKeyValue) && targetingKeyValue is string targetingKeyString) @@ -25,7 +25,7 @@ internal FlagdProperties(object from) TargetingKey = targetingKeyString; } if (dict.TryGetValue(FlagdPropertiesKey, out object flagdPropertiesObj) - && flagdPropertiesObj is Dictionary flagdProperties) + && flagdPropertiesObj is IDictionary flagdProperties) { if (flagdProperties.TryGetValue(FlagKeyKey, out object flagKeyObj) && flagKeyObj is string flagKey) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs index 73b6c5aa..928f9d1a 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs @@ -33,7 +33,7 @@ internal FractionalEvaluator() class FractionalEvaluationDistribution { public string variant; - public int percentage; + public int weight; } internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) @@ -49,17 +49,20 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) var flagdProperties = new FlagdProperties(data); - // check if the first argument is a string (i.e. the property to base the distribution on - var propertyValue = flagdProperties.TargetingKey; var bucketStartIndex = 0; var arg0 = p.Apply(args[0], data); + string propertyValue; if (arg0 is string stringValue) { propertyValue = stringValue; bucketStartIndex = 1; } + else + { + propertyValue = flagdProperties.FlagKey + flagdProperties.TargetingKey; + } var distributions = new List(); var distributionSum = 0; @@ -75,34 +78,28 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) var bucketArr = bucket.MakeEnumerable().ToArray(); - if (bucketArr.Count() < 2) + if (!bucketArr.Any()) { continue; } - if (!bucketArr.ElementAt(1).IsNumeric()) + var weight = 1; + + if (bucketArr.Length >= 2 && bucketArr.ElementAt(1).IsNumeric()) { - continue; + weight = Convert.ToInt32(bucketArr.ElementAt(1)); } - - var percentage = Convert.ToInt32(bucketArr.ElementAt(1)); distributions.Add(new FractionalEvaluationDistribution { variant = bucketArr.ElementAt(0).ToString(), - percentage = percentage + weight = weight }); - distributionSum += percentage; - } - - if (distributionSum != 100) - { - Logger.LogDebug("Sum of distribution values is not eqyal to 100"); - return null; + distributionSum += weight; } - var valueToDistribute = flagdProperties.FlagKey + propertyValue; + var valueToDistribute = propertyValue; var murmur32 = MurmurHash.Create32(); var bytes = Encoding.ASCII.GetBytes(valueToDistribute); var hashBytes = murmur32.ComputeHash(bytes); @@ -110,11 +107,11 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) var bucketValue = (int)(Math.Abs((float)hash) / Int32.MaxValue * 100); - var rangeEnd = 0; + var rangeEnd = 0.0; foreach (var dist in distributions) { - rangeEnd += dist.percentage; + rangeEnd += 100 * (dist.weight / (float)distributionSum); if (bucketValue < rangeEnd) { return dist.variant; diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml index b688327f..f6b0c0a2 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml @@ -1,25 +1,17 @@ services: flagd: - build: - context: flagd-testbed - dockerfile: flagd/Dockerfile + image: ghcr.io/open-feature/flagd-testbed:v0.5.6 ports: - 8013:8013 flagd-unstable: - build: - context: flagd-testbed - dockerfile: flagd/Dockerfile.unstable + image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.6 ports: - 8014:8013 flagd-sync: - build: - context: flagd-testbed - dockerfile: sync/Dockerfile + image: ghcr.io/open-feature/sync-testbed:v0.5.6 ports: - 9090:9090 flagd-sync-unstable: - build: - context: flagd-testbed - dockerfile: sync/Dockerfile.unstable + image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.6 ports: - 9091:9090 \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed b/src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed index efcbf72d..ed7e0ba6 160000 --- a/src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed +++ b/src/OpenFeature.Contrib.Providers.Flagd/flagd-testbed @@ -1 +1 @@ -Subproject commit efcbf72d34593be47e03ea920b77db29050e47eb +Subproject commit ed7e0ba660b01e1a22849e1b28ec37453921552e diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.Test/Steps/EvaluationStepDefinitionBase.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.Test/Steps/EvaluationStepDefinitionBase.cs index 7887312c..8762c932 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.Test/Steps/EvaluationStepDefinitionBase.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.Test/Steps/EvaluationStepDefinitionBase.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.ComponentModel.DataAnnotations; -using System.Text.RegularExpressions; +using System.ComponentModel; +using System.Reflection; using System.Threading.Tasks; using OpenFeature.Constant; using OpenFeature.Model; @@ -245,7 +244,7 @@ public void Thenthedefaultstringvalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason); - Assert.Contains(errorCode, notFoundDetails.ErrorMessage); + Assert.Contains(errorCode, GetErrorTypeDescription(notFoundDetails.ErrorType)); } [When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")] @@ -266,7 +265,15 @@ public void Thenthedefaultintegervalueshouldbereturned() public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode) { Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason); - Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage); + Assert.Contains(errorCode, GetErrorTypeDescription(typeErrorDetails.ErrorType)); + } + + // convenience method to get the enum description. + private string GetErrorTypeDescription(Enum value) + { + FieldInfo info = value.GetType().GetField(value.ToString()); + DescriptionAttribute[] attributes = (DescriptionAttribute[])info.GetCustomAttributes(typeof(DescriptionAttribute)); + return attributes[0].Description; } } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs index b2ea621c..cd23b3ca 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs @@ -37,8 +37,9 @@ public void Evaluate(string email, string flagKey, string expected) var targetingString = @"{""fractional"": [ { - ""var"": [ - ""email"" + ""cat"": [ + { ""var"":""$flagd.flagKey"" }, + { ""var"":""email"" } ] }, [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25], @@ -55,7 +56,70 @@ public void Evaluate(string email, string flagKey, string expected) // Act & Assert var result = evaluator.Apply(rule, data); Assert.Equal(expected, result.ToString()); + } + + [Theory] + [MemberData(nameof(FractionalEvaluationTestData.FractionalEvaluationContext), MemberType = typeof(FractionalEvaluationTestData))] + public void EvaluateUsingRelativeWeights(string email, string flagKey, string expected) + { + // Arrange + var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); + var fractionalEvaluator = new FractionalEvaluator(); + EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + + var targetingString = @"{""fractional"": [ + { + ""cat"": [ + { ""var"":""$flagd.flagKey"" }, + { ""var"":""email"" } + ] + }, + [""red"", 5], [""blue"", 5], [""green"", 5], [""yellow"", 5], + ]}"; + + // Parse json into hierarchical structure + var rule = JObject.Parse(targetingString); + + var data = new Dictionary { + { "email", email }, + {"$flagd", new Dictionary { {"flagKey", flagKey } } } + }; + + // Act & Assert + var result = evaluator.Apply(rule, data); + Assert.Equal(expected, result.ToString()); + } + + [Theory] + [MemberData(nameof(FractionalEvaluationTestData.FractionalEvaluationContext), MemberType = typeof(FractionalEvaluationTestData))] + public void EvaluateUsingDefaultWeights(string email, string flagKey, string expected) + { + // Arrange + var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); + var fractionalEvaluator = new FractionalEvaluator(); + EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + + var targetingString = @"{""fractional"": [ + { + ""cat"": [ + { ""var"":""$flagd.flagKey"" }, + { ""var"":""email"" } + ] + }, + [""red""], [""blue""], [""green""], [""yellow""], + ]}"; + // Parse json into hierarchical structure + var rule = JObject.Parse(targetingString); + + var data = new Dictionary { + { "email", email }, + {"$flagd", new Dictionary { {"flagKey", flagKey } } } + }; + + // Act & Assert + var result = evaluator.Apply(rule, data); + Assert.Equal(expected, result.ToString()); } [Theory]