From 8520aeb7b5c2e4965b1dc0d873c76e95ecfc41f3 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 27 May 2024 15:26:08 -0600 Subject: [PATCH 01/27] Renaming Shard to Sharder --- dot-net-sdk/EppoClient.cs | 8 ++++---- dot-net-sdk/helpers/{Shard.cs => Sharder.cs} | 2 +- eppo-sdk-test/helpers/{ShardTest.cs => SharderTest.cs} | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) rename dot-net-sdk/helpers/{Shard.cs => Sharder.cs} (97%) rename eppo-sdk-test/helpers/{ShardTest.cs => SharderTest.cs} (52%) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index 6ec518f..3f416fe 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -109,20 +109,20 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC private bool IsInExperimentSample(string subjectKey, string flagKey, int subjectShards, float percentageExposure) { - var shard = Shard.GetShard($"exposure-{subjectKey}-{flagKey}", subjectShards); + var shard = Sharder.GetShard($"exposure-{subjectKey}-{flagKey}", subjectShards); return shard <= percentageExposure * subjectShards; } private Variation GetAssignedVariation(string subjectKey, string flagKey, int subjectShards, List variations) { - var shard = Shard.GetShard($"assignment-{subjectKey}-{flagKey}", subjectShards); - return variations.Find(config => Shard.IsInRange(shard, config.shardRange))!; + var shard = Sharder.GetShard($"assignment-{subjectKey}-{flagKey}", subjectShards); + return variations.Find(config => Sharder.IsInRange(shard, config.shardRange))!; } public EppoValue GetSubjectVariationOverride(string subjectKey, ExperimentConfiguration experimentConfiguration) { - var hexedSubjectKey = Shard.GetHex(subjectKey); + var hexedSubjectKey = Sharder.GetHex(subjectKey); return experimentConfiguration.typedOverrides.GetValueOrDefault(hexedSubjectKey, new EppoValue()); } diff --git a/dot-net-sdk/helpers/Shard.cs b/dot-net-sdk/helpers/Sharder.cs similarity index 97% rename from dot-net-sdk/helpers/Shard.cs rename to dot-net-sdk/helpers/Sharder.cs index 4a43af9..0f5117b 100644 --- a/dot-net-sdk/helpers/Shard.cs +++ b/dot-net-sdk/helpers/Sharder.cs @@ -5,7 +5,7 @@ namespace eppo_sdk.helpers; -public class Shard +public class Sharder { public static string GetHex(string input) { diff --git a/eppo-sdk-test/helpers/ShardTest.cs b/eppo-sdk-test/helpers/SharderTest.cs similarity index 52% rename from eppo-sdk-test/helpers/ShardTest.cs rename to eppo-sdk-test/helpers/SharderTest.cs index 312255c..a0188ac 100644 --- a/eppo-sdk-test/helpers/ShardTest.cs +++ b/eppo-sdk-test/helpers/SharderTest.cs @@ -2,20 +2,20 @@ namespace eppo_sdk_test.helpers; -public class ShardTest +public class SharderTest { [Test] public void ShouldReturnHexString() { - Assert.That(Shard.GetHex("hello-world"), Is.EqualTo("2095312189753de6ad47dfe20cbe97ec")); - Assert.That(Shard.GetHex("another-string-with-experiment-subject"), Is.EqualTo("fd6bfc667b1bcdb901173f3d712e6c50")); + Assert.That(Sharder.GetHex("hello-world"), Is.EqualTo("2095312189753de6ad47dfe20cbe97ec")); + Assert.That(Sharder.GetHex("another-string-with-experiment-subject"), Is.EqualTo("fd6bfc667b1bcdb901173f3d712e6c50")); } [Test] public void ShouldReturnShard() { const int maxShards = 100; - var shardValue = Shard.GetShard("test-user", maxShards); + var shardValue = Sharder.GetShard("test-user", maxShards); Assert.That(shardValue, Is.GreaterThanOrEqualTo(0)); Assert.That(shardValue, Is.LessThanOrEqualTo(maxShards)); } From f017209eb858cdc6a0595e5a5db802543082ddb7 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 27 May 2024 22:32:09 -0600 Subject: [PATCH 02/27] New DTOs --- dot-net-sdk/dto/Allocation.cs | 24 ++- dot-net-sdk/dto/Condition.cs | 10 +- dot-net-sdk/dto/EppoValue.cs | 2 + dot-net-sdk/dto/Flag.cs | 21 +++ dot-net-sdk/dto/Rule.cs | 1 - dot-net-sdk/dto/Shard.cs | 13 ++ dot-net-sdk/dto/ShardRange.cs | 18 +- dot-net-sdk/dto/Split.cs | 16 ++ dot-net-sdk/dto/Variation.cs | 6 +- eppo-sdk-test/ValidateJsonConvertion.cs | 215 +++++++++++++++++++++--- 10 files changed, 284 insertions(+), 42 deletions(-) create mode 100644 dot-net-sdk/dto/Flag.cs create mode 100644 dot-net-sdk/dto/Shard.cs create mode 100644 dot-net-sdk/dto/Split.cs diff --git a/dot-net-sdk/dto/Allocation.cs b/dot-net-sdk/dto/Allocation.cs index 3db80c3..82eeebd 100644 --- a/dot-net-sdk/dto/Allocation.cs +++ b/dot-net-sdk/dto/Allocation.cs @@ -1,7 +1,21 @@ namespace eppo_sdk.dto; -public class Allocation -{ - public float percentExposure { get; set; } - public List variations { get; set; } -} \ No newline at end of file + public class Allocation + { + public string key { get; } + public List rules { get; set; } + public List splits { get; set; } + public bool doLog { get; set; } + public int? startAt { get; set; } + public int? endAt { get; set; } + + public Allocation(string key, IEnumerable rules, IEnumerable splits, bool doLog, int? startAt = null, int? endAt = null) + { + this.key = key; + this.rules = new List(rules); + this.splits = new List(splits); + this.doLog = doLog; + this.startAt = startAt; + this.endAt = endAt; + } + } diff --git a/dot-net-sdk/dto/Condition.cs b/dot-net-sdk/dto/Condition.cs index 7a7609a..3b574f8 100644 --- a/dot-net-sdk/dto/Condition.cs +++ b/dot-net-sdk/dto/Condition.cs @@ -5,14 +5,14 @@ namespace eppo_sdk.dto; public class Condition { - public string attribute { get; set; } - public EppoValue value { get; set; } - [JsonProperty(PropertyName = "operator", NamingStrategyType = typeof(DefaultNamingStrategy))] - public OperatorType operatorType { get; set; } + public string Attribute { get; set; } + + public OperatorType Operator { get; set; } + public EppoValue Value { get; set; } public override string ToString() { - return $"operator: {operatorType} | Attribute: {attribute} | value: {value}"; + return $"Operator: {Operator} | Attribute: {Attribute} | Value: {Value}"; } } \ No newline at end of file diff --git a/dot-net-sdk/dto/EppoValue.cs b/dot-net-sdk/dto/EppoValue.cs index 746c628..ed338b8 100644 --- a/dot-net-sdk/dto/EppoValue.cs +++ b/dot-net-sdk/dto/EppoValue.cs @@ -33,6 +33,8 @@ public EppoValue(EppoValueType type) this.type = type; } + public static EppoValue Bool(bool value) => new(value ? "true" : "false", EppoValueType.BOOLEAN); + public bool BoolValue() { return bool.Parse(value); diff --git a/dot-net-sdk/dto/Flag.cs b/dot-net-sdk/dto/Flag.cs new file mode 100644 index 0000000..8f76c69 --- /dev/null +++ b/dot-net-sdk/dto/Flag.cs @@ -0,0 +1,21 @@ +namespace eppo_sdk.dto; + +public class Flag +{ + public string key { get; set; } + public bool enabled { get; set; } + public List allocations { get; set; } + public EppoValueType variationType { get; set; } + public List variations { get; set; } + public int totalShards { get; set; } + + public Flag(string key, bool enabled, IEnumerable allocations, EppoValueType variationType, IEnumerable variations, int totalShards) + { + this.key = key; + this.enabled = enabled; + this.allocations = new List(allocations); + this.variationType = variationType; + this.variations = new List(variations); + this.totalShards = totalShards; + } +} \ No newline at end of file diff --git a/dot-net-sdk/dto/Rule.cs b/dot-net-sdk/dto/Rule.cs index d63f012..c726e61 100644 --- a/dot-net-sdk/dto/Rule.cs +++ b/dot-net-sdk/dto/Rule.cs @@ -2,6 +2,5 @@ namespace eppo_sdk.dto; public class Rule { - public string allocationKey { get; set; } public List conditions { get; set; } } \ No newline at end of file diff --git a/dot-net-sdk/dto/Shard.cs b/dot-net-sdk/dto/Shard.cs new file mode 100644 index 0000000..f6245fd --- /dev/null +++ b/dot-net-sdk/dto/Shard.cs @@ -0,0 +1,13 @@ +namespace eppo_sdk.dto; + + public class Shard + { + public string salt { get; } + public List ranges { get; set; } + + public Shard(string salt, IEnumerable ranges) + { + this.salt = salt; + this.ranges = new List(ranges); + } + } diff --git a/dot-net-sdk/dto/ShardRange.cs b/dot-net-sdk/dto/ShardRange.cs index 3dd6d47..d8f2661 100644 --- a/dot-net-sdk/dto/ShardRange.cs +++ b/dot-net-sdk/dto/ShardRange.cs @@ -2,11 +2,23 @@ namespace eppo_sdk.dto; public class ShardRange { - public int start { get; set; } - public int end { get; set; } + public int start { get; } + public int end { get; } + + public ShardRange(int start, int end) + { + if (start > end) + { + throw new ArgumentOutOfRangeException(nameof(start), "Start must be less than or equal to End."); + } + + this.start = start; + this.end = end; + } + public override string ToString() { return $"[start: {start} | end: {end}]"; } -} \ No newline at end of file +} diff --git a/dot-net-sdk/dto/Split.cs b/dot-net-sdk/dto/Split.cs new file mode 100644 index 0000000..d4ce07f --- /dev/null +++ b/dot-net-sdk/dto/Split.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +namespace eppo_sdk.dto; + +public class Split +{ + public string variationKey { get; } + public List shards { get; set; } + public IReadOnlyDictionary extraLogging { get; } + + public Split(string variationKey, IEnumerable shards, IDictionary extraLogging) + { + this.variationKey = variationKey; + this.shards = new List(shards); + this.extraLogging = extraLogging != null ? new Dictionary( extraLogging).AsReadOnly() : new Dictionary(); + } +} diff --git a/dot-net-sdk/dto/Variation.cs b/dot-net-sdk/dto/Variation.cs index db5f0d5..898a0df 100644 --- a/dot-net-sdk/dto/Variation.cs +++ b/dot-net-sdk/dto/Variation.cs @@ -1,7 +1,3 @@ namespace eppo_sdk.dto; -public class Variation -{ - public EppoValue typedValue { get; set; } - public ShardRange shardRange { get; set; } -} \ No newline at end of file +public record Variation(string Key, EppoValue Value); diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index d814d1f..8d3709b 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -1,4 +1,6 @@ +using System.Linq.Dynamic.Core.CustomTypeProviders; using eppo_sdk.dto; +using Namotion.Reflection; using Newtonsoft.Json; using static NUnit.Framework.Assert; @@ -6,26 +8,87 @@ namespace eppo_sdk_test; public class ValidateJsonConvertion { - [Test] - public void ShouldConvertCondition() - { - const string json = @"{ + [Test] + public void ShouldConvertCondition() + { + const string json = @"{ 'value': ['iOS','Android'], 'operator': 'ONE_OF', 'attribute': 'device' }"; - var condition = JsonConvert.DeserializeObject(json); - Multiple(() => - { - That(condition.operatorType, Is.EqualTo(OperatorType.ONE_OF)); - That(condition.attribute, Is.EqualTo("device")); - }); - } - - [Test] - public void ShouldConvertRules() + var condition = JsonConvert.DeserializeObject(json); + Multiple(() => { - const string json = @"[ + That(condition, Is.Not.Null); + That(condition?.Operator, Is.EqualTo(OperatorType.ONE_OF)); + That(condition?.Attribute, Is.EqualTo("device")); + That(condition?.Value.ArrayValue().Count, Is.EqualTo(2)); + }); + } + [Test] + public void ShouldConvertSplits() + { + const string json = @"[ + { + 'variationKey': 'on', + 'shards': [ + { + 'salt': 'some-salt', + 'ranges': [ + { + 'start': 0, + 'end': 2500 + }, + { + 'start': 2500, + 'end': 9999 + } + ] + }, + { + 'salt': 'some-salt-two', + 'ranges': [ + { + 'start': 9999, + 'end': 10000 + } + ] + } + ], + 'extraLogging': { + 'foo': 'bar', + 'bar': 'baz' + } + } + ]"; + var splits = JsonConvert.DeserializeObject>(json); + Assert.Multiple(() => + { + That(splits, Is.Not.Null); + That(splits?.Count, Is.EqualTo(1)); + That(splits?[0].variationKey, Is.EqualTo("on")); + That(splits?[0].shards.Count, Is.EqualTo(2)); + That(splits?[0].shards[0].salt, Is.EqualTo("some-salt")); + That(splits?[0].shards[0].ranges.Count, Is.EqualTo(2)); + That(splits?[0].shards[0].ranges[0].start, Is.EqualTo(0)); + That(splits?[0].shards[0].ranges[0].end, Is.EqualTo(2500)); + That(splits?[0].shards[0].ranges[1].start, Is.EqualTo(2500)); + That(splits?[0].shards[0].ranges[1].end, Is.EqualTo(9999)); + + That(splits?[0].shards[1].salt, Is.EqualTo("some-salt-two")); + + That(splits?[0].extraLogging, Is.EquivalentTo(new Dictionary + { + ["foo"] = "bar", + ["bar"] = "baz" + })); + }); + } + + [Test] + public void ShouldConvertRules() + { + const string json = @"[ { 'allocationKey': 'allocation-experiment-4', 'conditions': [{'value': ['iOS','Android'],'operator': 'ONE_OF','attribute': 'device'}, @@ -42,17 +105,123 @@ public void ShouldConvertRules() ]} ]"; - var rules = JsonConvert.DeserializeObject>(json); - Assert.That(rules.Count, Is.EqualTo(3)); - Assert.That(rules[0].conditions[0].operatorType, Is.EqualTo(OperatorType.ONE_OF)); - Assert.That(rules[0].conditions[0].value.ArrayValue(), Is.EqualTo(new List + var rules = JsonConvert.DeserializeObject>(json); + Assert.That(rules?.Count, Is.EqualTo(3)); + Assert.That(rules[0].conditions[0].Operator, Is.EqualTo(OperatorType.ONE_OF)); + Assert.That(rules[0].conditions[0].Value.ArrayValue(), Is.EqualTo(new List { "iOS", "Android" })); - Assert.That(rules[0].conditions[0].attribute, Is.EqualTo("device")); + Assert.That(rules[0].conditions[0].Attribute, Is.EqualTo("device")); + + Assert.That(rules[1].conditions[0].Operator, Is.EqualTo(OperatorType.NOT_ONE_OF)); + Assert.That(rules[1].conditions[0].Attribute, Is.EqualTo("country")); + } + + [Test] + public void ShouldConvertVariations() + { + const string json = @"{ + 'on': { + 'key': 'on', + 'value': true + }, + 'off': { + 'key': 'off', + 'value': false + } + }"; + Variation expectedVariation = new Variation("on", EppoValue.Bool(true)); + + var variations = JsonConvert.DeserializeObject>(json); + Multiple(() => + { + That(variations, Is.Not.Null); + That(variations?.Count, Is.EqualTo(2)); + That(variations?.ContainsKey("on") ?? false); + Variation? on = null; + That(variations?.TryGetValue("on", out on), Is.True); + That(on, Is.Not.Null); + That(on?.Value.BoolValue(), Is.True); + + }); + } + + [Test] + public void ShouldConvertAllocations() + { + const string json = /*lang=json*/ @"[ + { + 'key': 'on-for-age-50+', + 'rules': [ + { + 'conditions': [ + { + 'attribute': 'age', + 'operator': 'GTE', + 'value': 50 + } + ] + } + ], + 'splits': [ + { + 'variationKey': 'on', + 'shards': [ + { + 'salt': 'some-salt', + 'ranges': [ + { + 'start': 0, + 'end': 10000 + } + ] + } + ] + } + ], + 'doLog': false, + 'startAt': 5000, + 'endAt': 2147483647 + }, + { + 'key': 'off-for-all', + 'rules': [], + 'splits': [ + { + 'variationKey': 'off', + 'shards': [] + } + ], + 'doLog': true, + 'startAt': 5000, + 'endAt': 2147483647 + } + ] + "; + + var allocations = JsonConvert.DeserializeObject>(json); + Assert.Multiple(() => + { + That(allocations, Is.Not.Null); + That(allocations?.Count, Is.EqualTo(2)); + That(allocations?[0].key, Is.EqualTo("on-for-age-50+")); + That(allocations?[0].doLog, Is.EqualTo(false)); + That(allocations?[0].startAt, Is.EqualTo(5000)); + That(allocations?[0].endAt, Is.EqualTo(Int32.MaxValue)); + + That(allocations?[0].rules.Count, Is.EqualTo(1)); + That(allocations?[0].splits.Count, Is.EqualTo(1)); + + That(allocations?[1].key, Is.EqualTo("off-for-all")); + That(allocations?[1].doLog, Is.EqualTo(true)); + That(allocations?[1].startAt, Is.EqualTo(5000)); + That(allocations?[1].endAt, Is.EqualTo(Int32.MaxValue)); + + That(allocations?[1].rules.Count, Is.EqualTo(0)); + That(allocations?[1].splits.Count, Is.EqualTo(1)); + }); - Assert.That(rules[1].conditions[0].operatorType, Is.EqualTo(OperatorType.NOT_ONE_OF)); - Assert.That(rules[1].conditions[0].attribute, Is.EqualTo("country")); - } + } } \ No newline at end of file From 90716f7d9cfae43992cadce4ce29524736236da4 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 27 May 2024 22:37:21 -0600 Subject: [PATCH 03/27] make DTOs records --- dot-net-sdk/dto/Allocation.cs | 22 +++------------------- dot-net-sdk/dto/Condition.cs | 13 ++----------- dot-net-sdk/dto/Flag.cs | 18 +----------------- dot-net-sdk/dto/Rule.cs | 5 +---- dot-net-sdk/dto/Shard.cs | 14 +++----------- dot-net-sdk/dto/ShardRange.cs | 17 +---------------- dot-net-sdk/dto/Split.cs | 12 +----------- 7 files changed, 12 insertions(+), 89 deletions(-) diff --git a/dot-net-sdk/dto/Allocation.cs b/dot-net-sdk/dto/Allocation.cs index 82eeebd..2d64113 100644 --- a/dot-net-sdk/dto/Allocation.cs +++ b/dot-net-sdk/dto/Allocation.cs @@ -1,21 +1,5 @@ namespace eppo_sdk.dto; - public class Allocation - { - public string key { get; } - public List rules { get; set; } - public List splits { get; set; } - public bool doLog { get; set; } - public int? startAt { get; set; } - public int? endAt { get; set; } - - public Allocation(string key, IEnumerable rules, IEnumerable splits, bool doLog, int? startAt = null, int? endAt = null) - { - this.key = key; - this.rules = new List(rules); - this.splits = new List(splits); - this.doLog = doLog; - this.startAt = startAt; - this.endAt = endAt; - } - } +public record Allocation(string key, List rules, List splits, bool doLog, int? startAt, int? endAt) +{ +} diff --git a/dot-net-sdk/dto/Condition.cs b/dot-net-sdk/dto/Condition.cs index 3b574f8..496af1b 100644 --- a/dot-net-sdk/dto/Condition.cs +++ b/dot-net-sdk/dto/Condition.cs @@ -3,16 +3,7 @@ namespace eppo_sdk.dto; -public class Condition +public record Condition(string Attribute, OperatorType Operator, EppoValue Value) { - - public string Attribute { get; set; } - - public OperatorType Operator { get; set; } - public EppoValue Value { get; set; } - - public override string ToString() - { - return $"Operator: {Operator} | Attribute: {Attribute} | Value: {Value}"; - } + public override string ToString() => $"Operator: {Operator} | Attribute: {Attribute} | Value: {Value}"; } \ No newline at end of file diff --git a/dot-net-sdk/dto/Flag.cs b/dot-net-sdk/dto/Flag.cs index 8f76c69..a3196fd 100644 --- a/dot-net-sdk/dto/Flag.cs +++ b/dot-net-sdk/dto/Flag.cs @@ -1,21 +1,5 @@ namespace eppo_sdk.dto; -public class Flag +public record Flag(string key, bool enabled, List allocations, EppoValueType variationType, List variations, int totalShards) { - public string key { get; set; } - public bool enabled { get; set; } - public List allocations { get; set; } - public EppoValueType variationType { get; set; } - public List variations { get; set; } - public int totalShards { get; set; } - - public Flag(string key, bool enabled, IEnumerable allocations, EppoValueType variationType, IEnumerable variations, int totalShards) - { - this.key = key; - this.enabled = enabled; - this.allocations = new List(allocations); - this.variationType = variationType; - this.variations = new List(variations); - this.totalShards = totalShards; - } } \ No newline at end of file diff --git a/dot-net-sdk/dto/Rule.cs b/dot-net-sdk/dto/Rule.cs index c726e61..d218a2a 100644 --- a/dot-net-sdk/dto/Rule.cs +++ b/dot-net-sdk/dto/Rule.cs @@ -1,6 +1,3 @@ namespace eppo_sdk.dto; -public class Rule -{ - public List conditions { get; set; } -} \ No newline at end of file +public record Rule(List conditions); diff --git a/dot-net-sdk/dto/Shard.cs b/dot-net-sdk/dto/Shard.cs index f6245fd..90fc50f 100644 --- a/dot-net-sdk/dto/Shard.cs +++ b/dot-net-sdk/dto/Shard.cs @@ -1,13 +1,5 @@ namespace eppo_sdk.dto; - public class Shard - { - public string salt { get; } - public List ranges { get; set; } - - public Shard(string salt, IEnumerable ranges) - { - this.salt = salt; - this.ranges = new List(ranges); - } - } +public record Shard(string salt, List ranges) +{ +} diff --git a/dot-net-sdk/dto/ShardRange.cs b/dot-net-sdk/dto/ShardRange.cs index d8f2661..8dd0e1c 100644 --- a/dot-net-sdk/dto/ShardRange.cs +++ b/dot-net-sdk/dto/ShardRange.cs @@ -1,22 +1,7 @@ namespace eppo_sdk.dto; -public class ShardRange +public record ShardRange(int start, int end) { - public int start { get; } - public int end { get; } - - public ShardRange(int start, int end) - { - if (start > end) - { - throw new ArgumentOutOfRangeException(nameof(start), "Start must be less than or equal to End."); - } - - this.start = start; - this.end = end; - } - - public override string ToString() { return $"[start: {start} | end: {end}]"; diff --git a/dot-net-sdk/dto/Split.cs b/dot-net-sdk/dto/Split.cs index d4ce07f..c444245 100644 --- a/dot-net-sdk/dto/Split.cs +++ b/dot-net-sdk/dto/Split.cs @@ -1,16 +1,6 @@ using System.Collections.Generic; namespace eppo_sdk.dto; -public class Split +public record Split(string variationKey, List shards, IReadOnlyDictionary extraLogging) { - public string variationKey { get; } - public List shards { get; set; } - public IReadOnlyDictionary extraLogging { get; } - - public Split(string variationKey, IEnumerable shards, IDictionary extraLogging) - { - this.variationKey = variationKey; - this.shards = new List(shards); - this.extraLogging = extraLogging != null ? new Dictionary( extraLogging).AsReadOnly() : new Dictionary(); - } } From 6856eff2f482a63cb62ad00ef2fe30b8910ffbd9 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 27 May 2024 23:26:45 -0600 Subject: [PATCH 04/27] Point at UFC endpoint --- dot-net-sdk/constants/Constants.cs | 1 + dot-net-sdk/http/ExperimentConfigurationRequester.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dot-net-sdk/constants/Constants.cs b/dot-net-sdk/constants/Constants.cs index 6484051..417d5dc 100644 --- a/dot-net-sdk/constants/Constants.cs +++ b/dot-net-sdk/constants/Constants.cs @@ -15,4 +15,5 @@ public class Constants public const int MAX_CACHE_ENTRIES = 1000; public const string RAC_ENDPOINT = "/randomized_assignment/v3/config"; + public const string UFC_ENDPOINT = "/flag-config/v1/config"; } \ No newline at end of file diff --git a/dot-net-sdk/http/ExperimentConfigurationRequester.cs b/dot-net-sdk/http/ExperimentConfigurationRequester.cs index 0f76f06..f95c994 100644 --- a/dot-net-sdk/http/ExperimentConfigurationRequester.cs +++ b/dot-net-sdk/http/ExperimentConfigurationRequester.cs @@ -18,7 +18,7 @@ public ExperimentConfigurationRequester(EppoHttpClient eppoHttpClient) { ExperimentConfigurationResponse? config = null; try { - return this.eppoHttpClient.Get(Constants.RAC_ENDPOINT); + return this.eppoHttpClient.Get(Constants.UFC_ENDPOINT); } catch (UnauthorizedAccessException e) { From c22ae147dd9decd79a0493d6dda1c24730d06898 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 27 May 2024 23:26:52 -0600 Subject: [PATCH 05/27] Rule evaluation --- dot-net-sdk/validators/RuleValidator.cs | 207 +++++++++++++----------- 1 file changed, 114 insertions(+), 93 deletions(-) diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 02b6938..5bc1bc1 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -5,108 +5,129 @@ namespace eppo_sdk.validators; -public class RuleValidator -{ - public static Rule? FindMatchingRule(SubjectAttributes subjectAttributes, List rules) - { - return rules.Find(rule => MatchesRule(subjectAttributes, rule)); - } - - private static bool MatchesRule(SubjectAttributes subjectAttributes, Rule rule) - { - List conditionEvaluations = EvaluateRuleCondition(subjectAttributes, rule.conditions); - return !conditionEvaluations.Contains(false); - } - private static List EvaluateRuleCondition(SubjectAttributes subjectAttributes, List ruleConditions) - { - return - ruleConditions.ConvertAll(condition => EvaluateCondition(subjectAttributes, condition)); - } +public static class RuleValidator +{ + // public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, Dictionary subjectAttributes) + // { + // if (!flag.Enabled) return null; + + // var now = DateTime.UtcNow.ToUnixTimeSeconds(); + // foreach (var allocation in flag.Allocations) + // { + // if (allocation.StartAt.HasValue && allocation.StartAt.Value > now) + // { + // continue; + // } + // if (allocation.EndAt.HasValue && allocation.EndAt.Value < now) + // { + // continue; + // } + + // var subject = new Dictionary() { { "id", subjectKey } }; + // subject.Concat(subjectAttributes); + // if (MatchesAnyRule(allocation.Rules, subject)) + // { + // foreach (var split in allocation.Splits) + // { + // if (MatchesAllShards(split.Shards, subjectKey, flag.TotalShards)) + // { + // return new FlagEvaluation(flag.Variations[split.VariationKey], allocation.DoLog, allocation.Key); + // } + // } + // } + // } + + // return null; + // } + + + public static bool FindMatchingRule(SubjectAttributes subjectAttributes, List rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)) != default; + + private static bool MatchesRule(SubjectAttributes subjectAttributes, Rule rule) => rule.conditions.All(condition => EvaluateCondition(subjectAttributes, condition)); private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condition condition) { try { - if (subjectAttributes.ContainsKey(condition.attribute) && - subjectAttributes.TryGetValue(condition.attribute, out EppoValue outVal)) + if (subjectAttributes.TryGetValue(condition.Attribute, out var value)) { - var value = outVal!; // Assuming non-null for simplicity, handle nulls as necessary - - if (condition.operatorType == GTE) - { - if (value.isNumeric() && condition.value.isNumeric()) - { - return value.DoubleValue() >= condition.value.DoubleValue(); - } - - if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.value.StringValue(), out var conditionSemver)) - { - return valueSemver >= conditionSemver; - } - - return false; - } - else if (condition.operatorType == GT) + switch (condition.Operator) { - if (value.isNumeric() && condition.value.isNumeric()) - { - return value.DoubleValue() > condition.value.DoubleValue(); - } - - if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.value.StringValue(), out var conditionSemver)) - { - return valueSemver > conditionSemver; - } - - return false; - } - else if (condition.operatorType == LTE) - { - if (value.isNumeric() && condition.value.isNumeric()) - { - return value.DoubleValue() <= condition.value.DoubleValue(); - } - - if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.value.StringValue(), out var conditionSemver)) - { - return valueSemver <= conditionSemver; - } - - return false; - } - else if (condition.operatorType == LT) - { - if (value.isNumeric() && condition.value.isNumeric()) - { - return value.DoubleValue() < condition.value.DoubleValue(); - } - - if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.value.StringValue(), out var conditionSemver)) - { - return valueSemver < conditionSemver; - } - - return false; - } - else if (condition.operatorType == MATCHES) - { - return Regex.Match(value.StringValue(), condition.value.StringValue(), RegexOptions.IgnoreCase).Success; - } - else if (condition.operatorType == ONE_OF) - { - return Compare.IsOneOf(value.StringValue(), condition.value.ArrayValue()); - } - else if (condition.operatorType == NOT_ONE_OF) - { - return !Compare.IsOneOf(value.StringValue(), condition.value.ArrayValue()); + case GTE: + { + if (value.isNumeric() && condition.Value.isNumeric()) + { + return value.DoubleValue() >= condition.Value.DoubleValue(); + } + + if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && + NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + { + return valueSemver >= conditionSemver; + } + + return false; + } + case GT: + { + if (value.isNumeric() && condition.Value.isNumeric()) + { + return value.DoubleValue() > condition.Value.DoubleValue(); + } + + if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && + NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + { + return valueSemver > conditionSemver; + } + + return false; + } + case LTE: + { + if (value.isNumeric() && condition.Value.isNumeric()) + { + return value.DoubleValue() <= condition.Value.DoubleValue(); + } + + if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && + NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + { + return valueSemver <= conditionSemver; + } + + return false; + } + case LT: + { + if (value.isNumeric() && condition.Value.isNumeric()) + { + return value.DoubleValue() < condition.Value.DoubleValue(); + } + + if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && + NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + { + return valueSemver < conditionSemver; + } + + return false; + } + case MATCHES: + { + return Regex.Match(value.StringValue(), condition.Value.StringValue(), RegexOptions.IgnoreCase).Success; + } + case ONE_OF: + { + return Compare.IsOneOf(value.StringValue(), condition.Value.ArrayValue()); + } + case NOT_ONE_OF: + { + return !Compare.IsOneOf(value.StringValue(), condition.Value.ArrayValue()); + } } } - return false; // Return false if attribute is not found or other errors occur } catch (Exception) @@ -122,4 +143,4 @@ public static bool IsOneOf(string a, List arrayValues) { return arrayValues.ConvertAll(v => v.ToLower()).IndexOf(a.ToLower()) >= 0; } -} \ No newline at end of file +} From 16678d780964894f85078fdbd74983e6b3f36db8 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 28 May 2024 12:06:51 -0600 Subject: [PATCH 06/27] handy method --- dot-net-sdk/dto/EppoValue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dot-net-sdk/dto/EppoValue.cs b/dot-net-sdk/dto/EppoValue.cs index ed338b8..46bd453 100644 --- a/dot-net-sdk/dto/EppoValue.cs +++ b/dot-net-sdk/dto/EppoValue.cs @@ -35,6 +35,8 @@ public EppoValue(EppoValueType type) public static EppoValue Bool(bool value) => new(value ? "true" : "false", EppoValueType.BOOLEAN); + public static EppoValue String(string value) => new(value, EppoValueType.STRING); + public bool BoolValue() { return bool.Parse(value); From 9acc287add7b818c8b5a3f4d51cd2bb08fde6d4a Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Tue, 28 May 2024 14:22:56 -0600 Subject: [PATCH 07/27] Flag evaluation and testing WIP --- dot-net-sdk/dto/Flag.cs | 2 +- dot-net-sdk/dto/FlagEvaluation.cs | 3 + dot-net-sdk/validators/RuleValidator.cs | 81 ++++---- eppo-sdk-test/validators/RuleValidatorTest.cs | 178 +++++++++++++----- 4 files changed, 177 insertions(+), 87 deletions(-) create mode 100644 dot-net-sdk/dto/FlagEvaluation.cs diff --git a/dot-net-sdk/dto/Flag.cs b/dot-net-sdk/dto/Flag.cs index a3196fd..52e830f 100644 --- a/dot-net-sdk/dto/Flag.cs +++ b/dot-net-sdk/dto/Flag.cs @@ -1,5 +1,5 @@ namespace eppo_sdk.dto; -public record Flag(string key, bool enabled, List allocations, EppoValueType variationType, List variations, int totalShards) +public record Flag(string key, bool enabled, List allocations, EppoValueType variationType, Dictionary variations, int totalShards) { } \ No newline at end of file diff --git a/dot-net-sdk/dto/FlagEvaluation.cs b/dot-net-sdk/dto/FlagEvaluation.cs new file mode 100644 index 0000000..d206762 --- /dev/null +++ b/dot-net-sdk/dto/FlagEvaluation.cs @@ -0,0 +1,3 @@ +namespace eppo_sdk.dto; + +public record FlagEvaluation(Variation Variation, bool DoLog, string AllocationKey); diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 5bc1bc1..2d72a70 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -2,6 +2,7 @@ using eppo_sdk.dto; using static eppo_sdk.dto.OperatorType; using NuGet.Versioning; +using eppo_sdk.helpers; namespace eppo_sdk.validators; @@ -9,41 +10,51 @@ namespace eppo_sdk.validators; public static class RuleValidator { - // public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, Dictionary subjectAttributes) - // { - // if (!flag.Enabled) return null; - - // var now = DateTime.UtcNow.ToUnixTimeSeconds(); - // foreach (var allocation in flag.Allocations) - // { - // if (allocation.StartAt.HasValue && allocation.StartAt.Value > now) - // { - // continue; - // } - // if (allocation.EndAt.HasValue && allocation.EndAt.Value < now) - // { - // continue; - // } - - // var subject = new Dictionary() { { "id", subjectKey } }; - // subject.Concat(subjectAttributes); - // if (MatchesAnyRule(allocation.Rules, subject)) - // { - // foreach (var split in allocation.Splits) - // { - // if (MatchesAllShards(split.Shards, subjectKey, flag.TotalShards)) - // { - // return new FlagEvaluation(flag.Variations[split.VariationKey], allocation.DoLog, allocation.Key); - // } - // } - // } - // } - - // return null; - // } - - - public static bool FindMatchingRule(SubjectAttributes subjectAttributes, List rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)) != default; + public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, SubjectAttributes subjectAttributes) + { + if (!flag.enabled) return null; + + var now = DateTimeOffset.Now.ToUnixTimeSeconds(); + foreach (var allocation in flag.allocations) + { + if (allocation.startAt.HasValue && allocation.startAt.Value > now || allocation.endAt.HasValue && allocation.endAt.Value < now) + { + continue; + } + + subjectAttributes.Add("id", EppoValue.String(subjectKey)); + + if (MatchesAnyRule(allocation.rules, subjectAttributes)) + { + foreach (var split in allocation.splits) + { + if (MatchesAllShards(split.shards, subjectKey, flag.totalShards)) + { + return new FlagEvaluation(flag.variations[split.variationKey], allocation.doLog, allocation.key); + } + } + } + } + + return null; + } + + + + // Find the first shard that does not match. If it's null. then all shards match. + public static bool MatchesAllShards(IEnumerable shards, string subjectKey, int totalShards) => shards.First(shard => !MatchesShard(shard, subjectKey, totalShards)) == null; + + private static bool MatchesShard(Shard shard, string subjectKey, int totalShards) + { + var hashKey = shard.salt + "-" + subjectKey; + var subjectBucket = Sharder.GetShard(hashKey, totalShards); + + return shard.ranges.Any(range => Sharder.IsInRange(subjectBucket, range)); + } + + private static bool MatchesAnyRule(IEnumerable rules, SubjectAttributes subject) => rules.Any() && FindMatchingRule(subject, rules) != null; + + public static Rule? FindMatchingRule(SubjectAttributes subjectAttributes, IEnumerable rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)); private static bool MatchesRule(SubjectAttributes subjectAttributes, Rule rule) => rule.conditions.All(condition => EvaluateCondition(subjectAttributes, condition)); private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condition condition) diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index 4103e21..133904e 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -150,43 +150,144 @@ public void ShouldNotMatchAnyRuleWithNotOneOfRuleNotPassed() Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } + + + private const string SubjectKey = "subjectKey"; + private const int TotalShards = 10; + + // Assuming you have these classes defined elsewhere (adapt them to your implementation) + private List nonMatchingShards; + private List matchingSplits; + private Rule rockAndRollLegendRule; + private Variation musicVariation; + private Subject subject; + private Variation matchVariation; + + [SetUp] + public void Setup() + { + // Initialize your test data here (nonMatchingShards, matchingSplits, etc.) + } + + [Test] + public void NoMatchingShards_ReturnsFalse() + { + Assert.False(RuleEvaluator.MatchesAllShards(nonMatchingShards.ToArray(), SubjectKey, TotalShards)); + } + + [Test] + public void SomeMatchingShards_ReturnsFalse() + { + var allShards = matchingSplits.Concat(nonMatchingShards).ToList(); + Assert.False(RuleEvaluator.MatchesAllShards(allShards.ToArray(), SubjectKey, TotalShards)); + } + + [Test] + public void MatchesShards_ReturnsTrue() + { + Assert.True(RuleEvaluator.MatchesAllShards(matchingSplits.ToArray(), SubjectKey, TotalShards)); + } + + [Test] + public void FlagEvaluation_ReturnsMatchingVariation() + { + var allocations = new List() { + new Allocation( + "rock", + new List() { rockAndRollLegendRule }, + musicSplits, + false) + }; + var variations = new Dictionary() { + { "music", musicVariation } + }; + + var bigFlag = new Flag( + "HallOfFame", + true, + allocations, + VariationType.String, + variations, + TotalShards); + + var result = RuleEvaluator.EvaluateFlag(bigFlag, SubjectKey, subject); + + Assert.NotNull(result); + Assert.AreEqual(musicVariation.Key, result.Variation.Key); + Assert.AreEqual(musicVariation.Value, result.Variation.Value); + } + + [Test] + public void DisabledFlag_ReturnsNull() + { + var flag = new Flag("disabled", false, new List(), VariationType.Boolean, new Dictionary(), TotalShards); + Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, new Dictionary())); + } + + [Test] + public void FlagWithInactiveAllocations_ReturnsNull() + { + var now = DateTime.UtcNow.ToUnixTimeSeconds(); + var overAlloc = new Allocation("over", new List(), matchingSplits, false, endAt: now - 10000); + var futureAlloc = new Allocation("hasntStarted", new List(), matchingSplits, false, startAt: now + 60000); + + var flag = new Flag( + "inactive_allocs", + true, + new List() { overAlloc, futureAlloc }, + VariationType.Boolean, + new Dictionary() { { matchVariation.Key, matchVariation } }, + TotalShards); + + Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject)); + } + + [Test] + public void FlagWithoutAllocations_ReturnsNull() + { + var flag = new Flag("no_allocs", true, new List(), VariationType.Boolean, new Dictionary(), TotalShards); + Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject)); + } + + [Test] + public void MatchesVariationWithoutRules_ReturnsMatchingVariation() + { + var allocation1 = new Allocation("alloc1", new List(), matchingSplits, false); + var basicVariation = new Variation("foo", "bar"); + var flag = new Flag( + "matches", + true, + new List() { allocation1 }, + VariationType.String, + new Dictionary() { { "match", basicVariation } }, + TotalShards); + + var result = RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject); + + Assert.NotNull(result); + Assert.That(result.variation.value is.EqualTo("bar")); + private static void AddOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition - { - value = new EppoValue(new List + rule.conditions.Add(new Condition("oneOf", OperatorType.ONE_OF, new EppoValue(new List { "value1", "value2" - }), - attribute = "oneOf", - operatorType = OperatorType.ONE_OF - }); + }))); } private static void AddNotOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition - { - value = new EppoValue(new List + rule.conditions.Add(new Condition("oneOf", OperatorType.NOT_ONE_OF, new EppoValue(new List { "value1", "value2" - }), - attribute = "oneOf", - operatorType = OperatorType.NOT_ONE_OF - }); + }))); } private static void AddRegexConditionToRule(Rule rule) { - var condition = new Condition - { - value = new EppoValue("[a-z]+", EppoValueType.STRING), - attribute = "match", - operatorType = OperatorType.MATCHES - }; - rule.conditions.Add(condition); + rule.conditions.Add(new Condition("match", OperatorType.MATCHES, EppoValue.String("[a-z]+"))); } private static void AddPriceToSubjectAttribute(SubjectAttributes subjectAttributes) @@ -196,36 +297,14 @@ private static void AddPriceToSubjectAttribute(SubjectAttributes subjectAttribut private static void AddNumericConditionToRule(Rule rule) { - rule.conditions.Add(new Condition - { - value = new EppoValue("10", EppoValueType.NUMBER), - attribute = "price", - operatorType = OperatorType.GTE - }); - - rule.conditions.Add(new Condition - { - value = new EppoValue("20", EppoValueType.NUMBER), - attribute = "price", - operatorType = OperatorType.LTE - }); + rule.conditions.Add(new Condition("price", OperatorType.GTE, new EppoValue("10", EppoValueType.NUMBER))); + rule.conditions.Add(new Condition("price", OperatorType.LTE, new EppoValue("20", EppoValueType.NUMBER))); } private static void AddSemVerConditionToRule(Rule rule) { - rule.conditions.Add(new Condition - { - value = new EppoValue("1.2.3", EppoValueType.STRING), - attribute = "appVersion", - operatorType = OperatorType.GTE - }); - - rule.conditions.Add(new Condition - { - value = new EppoValue("2.2.0", EppoValueType.STRING), - attribute = "appVersion", - operatorType = OperatorType.LTE - }); + rule.conditions.Add(new Condition("appVersion", OperatorType.GTE, new EppoValue("1.2.3", EppoValueType.NUMBER))); + rule.conditions.Add(new Condition("appVersion", OperatorType.LTE, new EppoValue("2.2.0", EppoValueType.NUMBER))); } private static void AddNameToSubjectAttribute(SubjectAttributes subjectAttributes) @@ -235,9 +314,6 @@ private static void AddNameToSubjectAttribute(SubjectAttributes subjectAttribute private static Rule CreateRule(List conditions) { - return new Rule - { - conditions = conditions - }; + return new Rule(conditions); } } \ No newline at end of file From b526a0612b09c9ebe5a33aef2431be70025ca035 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 29 May 2024 08:42:17 -0600 Subject: [PATCH 08/27] test fix wip --- eppo-sdk-test/validators/RuleValidatorTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index 133904e..28245d7 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -265,7 +265,8 @@ public void MatchesVariationWithoutRules_ReturnsMatchingVariation() var result = RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject); Assert.NotNull(result); - Assert.That(result.variation.value is.EqualTo("bar")); + Assert.That(result.variation.value, Is.EqualTo("bar")); + } private static void AddOneOfCondition(Rule rule) { From 4b17946ba0ff65d6e1dbbb46015c0d5f3eff20f0 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 31 May 2024 10:23:33 -0600 Subject: [PATCH 09/27] merge main --- dot-net-sdk/EppoClient.cs | 45 +++--- dot-net-sdk/dto/Condition.cs | 13 +- dot-net-sdk/dto/EppoValue.cs | 60 -------- dot-net-sdk/dto/EppoValueDeserializer.cs | 43 ------ dot-net-sdk/dto/EppoValueType.cs | 3 +- dot-net-sdk/dto/ExperimentConfiguration.cs | 4 +- dot-net-sdk/dto/HasEppoValue.cs | 140 ++++++++++++++++++ dot-net-sdk/dto/SubjectAttributes.cs | 2 +- dot-net-sdk/dto/Variation.cs | 6 +- dot-net-sdk/validators/RuleValidator.cs | 44 +++--- eppo-sdk-test/EppoClientTest.cs | 12 +- eppo-sdk-test/ValidateJsonConvertion.cs | 6 +- eppo-sdk-test/validators/RuleValidatorTest.cs | 137 +++++++++-------- 13 files changed, 295 insertions(+), 220 deletions(-) delete mode 100644 dot-net-sdk/dto/EppoValue.cs delete mode 100644 dot-net-sdk/dto/EppoValueDeserializer.cs create mode 100644 dot-net-sdk/dto/HasEppoValue.cs diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index 39b928e..f83a5c3 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -6,6 +6,7 @@ using eppo_sdk.store; using eppo_sdk.tasks; using eppo_sdk.validators; +using Newtonsoft.Json.Linq; using NLog; namespace eppo_sdk; @@ -29,6 +30,11 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } + public JObject? GetJsonAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + { + return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.JsonValue(); + } + public bool? GetBoolAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) { return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.BoolValue(); @@ -40,7 +46,7 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC } - public int? GetIntegerAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public long? GetIntegerAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) { return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.IntegerValue(); } @@ -52,7 +58,7 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC } - private EppoValue? GetAssignment(string subjectKey, string flagKey, SubjectAttributes subjectAttributes) + private HasEppoValue? GetAssignment(string subjectKey, string flagKey, SubjectAttributes subjectAttributes) { InputValidator.ValidateNotBlank(subjectKey, "Invalid argument: subjectKey cannot be blank"); InputValidator.ValidateNotBlank(flagKey, "Invalid argument: flagKey cannot be blank"); @@ -93,23 +99,26 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC var assignedVariation = GetAssignedVariation(subjectKey, flagKey, configuration.subjectShards, allocation.variations); - try - { - _eppoClientConfig.AssignmentLogger - .LogAssignment(new AssignmentLogData( - flagKey, - rule.allocationKey, - assignedVariation.typedValue.StringValue(), - subjectKey, - subjectAttributes - )); - } - catch (Exception) + if (assignedVariation != null && !assignedVariation.IsNull()) { - // Ignore Exception + try + { + _eppoClientConfig.AssignmentLogger + .LogAssignment(new AssignmentLogData( + flagKey, + rule.allocationKey, + assignedVariation.StringValue() ?? "null", + subjectKey, + subjectAttributes + )); + } + catch (Exception) + { + // Ignore Exception + } } - return assignedVariation?.typedValue; + return assignedVariation; } private bool IsInExperimentSample(string subjectKey, string flagKey, int subjectShards, @@ -126,10 +135,10 @@ private Variation GetAssignedVariation(string subjectKey, string flagKey, int su return variations.Find(config => Sharder.IsInRange(shard, config.shardRange))!; } - public EppoValue GetSubjectVariationOverride(string subjectKey, ExperimentConfiguration experimentConfiguration) + public HasEppoValue GetSubjectVariationOverride(string subjectKey, ExperimentConfiguration experimentConfiguration) { var hexedSubjectKey = Sharder.GetHex(subjectKey); - return experimentConfiguration.typedOverrides.GetValueOrDefault(hexedSubjectKey, new EppoValue()); + return new HasEppoValue(experimentConfiguration.typedOverrides.GetValueOrDefault(hexedSubjectKey, null)); } public static EppoClient Init(EppoClientConfig eppoClientConfig) diff --git a/dot-net-sdk/dto/Condition.cs b/dot-net-sdk/dto/Condition.cs index 496af1b..1d73b26 100644 --- a/dot-net-sdk/dto/Condition.cs +++ b/dot-net-sdk/dto/Condition.cs @@ -3,7 +3,14 @@ namespace eppo_sdk.dto; -public record Condition(string Attribute, OperatorType Operator, EppoValue Value) +public class Condition : HasEppoValue { - public override string ToString() => $"Operator: {Operator} | Attribute: {Attribute} | Value: {Value}"; -} \ No newline at end of file + public string Attribute { get; set; } + + public OperatorType Operator { get; set; } + + public override string ToString() + { + return $"operator: {Operator} | Attribute: {Attribute} | value: {Value}"; + } +} diff --git a/dot-net-sdk/dto/EppoValue.cs b/dot-net-sdk/dto/EppoValue.cs deleted file mode 100644 index ef09ad0..0000000 --- a/dot-net-sdk/dto/EppoValue.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Globalization; -using Newtonsoft.Json; - -namespace eppo_sdk.dto; - -[JsonConverter(typeof(EppoValueDeserializer))] -public class EppoValue -{ - public string? value { get; set; } - - public EppoValueType type { get; set; } = EppoValueType.NULL; - - public List? array { get; set; } - - - public static EppoValue Bool(string? value) => new(value, EppoValueType.BOOLEAN); - public static EppoValue Bool(bool value) => new(value.ToString(), EppoValueType.BOOLEAN); - public static EppoValue Number(string value) => new(value, EppoValueType.NUMBER); - public static EppoValue String(string? value) => new(value, EppoValueType.STRING); - public static EppoValue Integer(string value) => new(value, EppoValueType.INTEGER); - public static EppoValue Null() => new(); - - public EppoValue() - { - } - - public EppoValue(string? value, EppoValueType type) - { - this.value = value; - this.type = type; - } - - public EppoValue(List array) - { - this.array = array; - this.type = EppoValueType.ARRAY_OF_STRING; - } - - public EppoValue(EppoValueType type) => this.type = type; - - public bool BoolValue() => bool.Parse(value); - - public double DoubleValue() => double.Parse(value, NumberStyles.Number); - - public int IntegerValue() => int.Parse(value, NumberStyles.Number); - - public bool IsNumeric() => double.TryParse(value, out _); - - public string StringValue() => value; - - public List ArrayValue() => array; - - public bool IsNull() => EppoValueType.NULL.Equals(type); - - public static bool IsNullValue(EppoValue? value) => - value == null /* null pointer */ || - value.IsNull() /* parsed as a null JSON token type */ || - value.value == null; /* Value type is set but value is null */ - -} diff --git a/dot-net-sdk/dto/EppoValueDeserializer.cs b/dot-net-sdk/dto/EppoValueDeserializer.cs deleted file mode 100644 index b5d6e3e..0000000 --- a/dot-net-sdk/dto/EppoValueDeserializer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using eppo_sdk.exception; -using Newtonsoft.Json; - -namespace eppo_sdk.dto; - -public class EppoValueDeserializer : JsonConverter -{ - public override void WriteJson(JsonWriter writer, EppoValue? value, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - public override EppoValue? ReadJson(JsonReader reader, Type objectType, EppoValue? existingValue, - bool hasExistingValue, - JsonSerializer serializer) - { - var value = reader.Value; - switch (reader.TokenType) - { - case JsonToken.String: - return EppoValue.String(value.ToString()); - case JsonToken.Integer: - return EppoValue.Integer(value.ToString()); - case JsonToken.Float: - return EppoValue.Number(value.ToString()); - case JsonToken.Boolean: - return EppoValue.Bool(value.ToString()); - case JsonToken.Null: - return EppoValue.Null(); - case JsonToken.StartArray: - var val = new List(); - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - val.Add(reader.Value.ToString()); - reader.Read(); - } - return new EppoValue(val); - default: - throw new UnsupportedEppoValueException("Unsupported Eppo Value Type: " + reader.TokenType.ToString()); - } - } -} \ No newline at end of file diff --git a/dot-net-sdk/dto/EppoValueType.cs b/dot-net-sdk/dto/EppoValueType.cs index c20b4bc..31b86a3 100644 --- a/dot-net-sdk/dto/EppoValueType.cs +++ b/dot-net-sdk/dto/EppoValueType.cs @@ -7,5 +7,6 @@ public enum EppoValueType STRING, BOOLEAN, NULL, - ARRAY_OF_STRING + ARRAY_OF_STRING, + JSON } \ No newline at end of file diff --git a/dot-net-sdk/dto/ExperimentConfiguration.cs b/dot-net-sdk/dto/ExperimentConfiguration.cs index ef1b201..567d18e 100644 --- a/dot-net-sdk/dto/ExperimentConfiguration.cs +++ b/dot-net-sdk/dto/ExperimentConfiguration.cs @@ -5,7 +5,7 @@ public class ExperimentConfiguration public string name { get; set; } public bool enabled { get; set; } public int subjectShards { get; set; } - public Dictionary typedOverrides { get; set; } + public Dictionary typedOverrides { get; set; } public Dictionary allocations { get; set; } public List rules { get; set; } @@ -14,4 +14,4 @@ public class ExperimentConfiguration allocations.TryGetValue(allocationKey, out var value); return value; } -} \ No newline at end of file +} diff --git a/dot-net-sdk/dto/HasEppoValue.cs b/dot-net-sdk/dto/HasEppoValue.cs new file mode 100644 index 0000000..bb80fc3 --- /dev/null +++ b/dot-net-sdk/dto/HasEppoValue.cs @@ -0,0 +1,140 @@ +using System.Globalization; +using System.Numerics; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NLog; + +namespace eppo_sdk.dto; + + +public class HasEppoValue +{ + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private bool _typed; + + private Object? _value; + public Object? Value + { + get { return _value; } + set + { + if (!_typed) + { + _type = InferTypeFromValue(value); + _value = value; + } + } + } + public Object? typedValue + { + get { return _value; } + set + { + _typed = true; + _type = InferTypeFromValue(value); + _value = value; + } + } + private EppoValueType _type; + public EppoValueType Type { get { return _type; } } + + public bool? BoolValue() => _value != null ? (bool)_value : null; + public double? DoubleValue() => Convert.ToDouble(_value); + public long? IntegerValue() => _value != null ? (long)_value : null; + public string? StringValue() => _value != null ? (string)_value : null; + public List? ArrayValue() + { + if (_value == null) + { + return null; + } + + if (_value is JArray array) + { + + return new List(array.ToObject()); + } + return (List)_value; + } + public JObject? JsonValue() => _value == null ? null : (JObject)_value; + + + + private static EppoValueType InferTypeFromValue(Object? value) + { + if (value == null) return EppoValueType.NULL; + + if (value is Array || value.GetType().IsArray || value is JArray || value is List || value is IEnumerable) + { + return EppoValueType.ARRAY_OF_STRING; + } + else if (value is bool || value is Boolean) + { + return EppoValueType.BOOLEAN; + } + else if (value is float || value is double || value is Double || value is float) + { + return EppoValueType.NUMBER; + + } + else if (value is int || value is long || value is BigInteger) + { + return EppoValueType.INTEGER; + + } + else if (value is string || value is String) + { + return EppoValueType.STRING; + } + else if (value is JObject) + { + return EppoValueType.JSON; + } + else + { + Type type = value!.GetType(); + Logger.Error($"Unexpected value of type {type}"); + Console.WriteLine($"Unexpected value of type {type}"); + return EppoValueType.NULL; + } + } + + public static HasEppoValue Bool(string? value) => new(value, EppoValueType.BOOLEAN); + public static HasEppoValue Bool(bool value) => new(value, EppoValueType.BOOLEAN); + public static HasEppoValue Number(string value) => new(value, EppoValueType.NUMBER); + public static HasEppoValue String(string? value) => new(value, EppoValueType.STRING); + public static HasEppoValue Integer(string value) => new(value, EppoValueType.INTEGER); + public static HasEppoValue Null() => new(); + + public HasEppoValue() + { + } + + public HasEppoValue(object? value, EppoValueType type) + { + this.Value = value; + } + + [JsonConstructor] + public HasEppoValue(object? value) + { + this.Value = value; + } + + public HasEppoValue(List array) + { + this.Value = array; + } + + public bool IsNumeric() => _type == EppoValueType.NUMBER || _type == EppoValueType.INTEGER; + + public bool IsNull() => EppoValueType.NULL.Equals(Type); + + public static bool IsNullValue(HasEppoValue? value) => + value == null /* null pointer */ || + value.IsNull() /* parsed as a null JSON token type */ || + value.Value == null; /* Value type is set but value is null */ + +} diff --git a/dot-net-sdk/dto/SubjectAttributes.cs b/dot-net-sdk/dto/SubjectAttributes.cs index ee98ba1..4bdfb38 100644 --- a/dot-net-sdk/dto/SubjectAttributes.cs +++ b/dot-net-sdk/dto/SubjectAttributes.cs @@ -1,6 +1,6 @@ namespace eppo_sdk.dto; -public class SubjectAttributes: Dictionary +public class SubjectAttributes: Dictionary { } \ No newline at end of file diff --git a/dot-net-sdk/dto/Variation.cs b/dot-net-sdk/dto/Variation.cs index 898a0df..2f69cdd 100644 --- a/dot-net-sdk/dto/Variation.cs +++ b/dot-net-sdk/dto/Variation.cs @@ -1,3 +1,7 @@ namespace eppo_sdk.dto; -public record Variation(string Key, EppoValue Value); +public class Variation : HasEppoValue +{ + public string Key {get; set;} + public ShardRange shardRange { get; set; } +} \ No newline at end of file diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index d9b6d7b..b441290 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -62,24 +62,26 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi try { // Operators other than `IS_NULL` need to assume non-null - if (condition.Operator == IS_NULL) { - bool isNull = !subjectAttributes.TryGetValue(condition.Attribute, out EppoValue? outVal) || EppoValue.IsNullValue(outVal); - return condition.Value.BoolValue() == isNull; + if (condition.Operator == IS_NULL) + { + bool isNull = !subjectAttributes.TryGetValue(condition.Attribute, out Object? outVal) || HasEppoValue.IsNullValue(new HasEppoValue(outVal)); + return condition.BoolValue() == isNull; } - else if (subjectAttributes.TryGetValue(condition.Attribute, out EppoValue? outVal)) + else if (subjectAttributes.TryGetValue(condition.Attribute, out Object? outVal)) { - EppoValue value = outVal!; - switch (condition.Operator) + var value = new HasEppoValue(outVal!); // Assuming non-null for simplicity, handle nulls as necessary + + switch (condition.Operator) { case GTE: { - if (value.IsNumeric() && condition.Value.IsNumeric()) + if (value.IsNumeric() && condition.IsNumeric()) { - return value.DoubleValue() >= condition.Value.DoubleValue(); + return value.DoubleValue() >= condition.DoubleValue(); } if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + NuGetVersion.TryParse(condition.StringValue(), out var conditionSemver)) { return valueSemver >= conditionSemver; } @@ -88,13 +90,13 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi } case GT: { - if (value.IsNumeric() && condition.Value.IsNumeric()) + if (value.IsNumeric() && condition.IsNumeric()) { - return value.DoubleValue() > condition.Value.DoubleValue(); + return value.DoubleValue() > condition.DoubleValue(); } if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + NuGetVersion.TryParse(condition.StringValue(), out var conditionSemver)) { return valueSemver > conditionSemver; } @@ -103,13 +105,13 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi } case LTE: { - if (value.IsNumeric() && condition.Value.IsNumeric()) + if (value.IsNumeric() && condition.IsNumeric()) { - return value.DoubleValue() <= condition.Value.DoubleValue(); + return value.DoubleValue() <= condition.DoubleValue(); } if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + NuGetVersion.TryParse(condition.StringValue(), out var conditionSemver)) { return valueSemver <= conditionSemver; } @@ -118,13 +120,13 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi } case LT: { - if (value.IsNumeric() && condition.Value.IsNumeric()) + if (value.IsNumeric() && condition.IsNumeric()) { - return value.DoubleValue() < condition.Value.DoubleValue(); + return value.DoubleValue() < condition.DoubleValue(); } if (NuGetVersion.TryParse(value.StringValue(), out var valueSemver) && - NuGetVersion.TryParse(condition.Value.StringValue(), out var conditionSemver)) + NuGetVersion.TryParse(condition.StringValue(), out var conditionSemver)) { return valueSemver < conditionSemver; } @@ -133,15 +135,15 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi } case MATCHES: { - return Regex.Match(value.StringValue(), condition.Value.StringValue(), RegexOptions.IgnoreCase).Success; + return Regex.Match(value.StringValue(), condition.StringValue(), RegexOptions.IgnoreCase).Success; } case ONE_OF: { - return Compare.IsOneOf(value.StringValue(), condition.Value.ArrayValue()); + return Compare.IsOneOf(value.StringValue(), condition.ArrayValue()); } case NOT_ONE_OF: { - return !Compare.IsOneOf(value.StringValue(), condition.Value.ArrayValue()); + return !Compare.IsOneOf(value.StringValue(), condition.ArrayValue()); } } } diff --git a/eppo-sdk-test/EppoClientTest.cs b/eppo-sdk-test/EppoClientTest.cs index 50153a1..b82aeb9 100644 --- a/eppo-sdk-test/EppoClientTest.cs +++ b/eppo-sdk-test/EppoClientTest.cs @@ -56,22 +56,22 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) switch (assignmentTestCase.valueType) { case "boolean": - var boolExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => x.BoolValue()); + var boolExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (bool?)x); Assert.That(GetBoolAssignments(assignmentTestCase), Is.EqualTo(boolExpectations)); break; case "number": - var numericExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => x.DoubleValue()); + var numericExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (double?)x); Assert.That(GetNumericAssignments(assignmentTestCase), Is.EqualTo(numericExpectations)); break; case "integer": - var intExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => x.IntegerValue()); + var intExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (long?)x); Assert.That(GetIntegerAssignments(assignmentTestCase), Is.EqualTo(intExpectations)); break; case "string": - var stringExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => x.StringValue()); + var stringExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (string?)x); Assert.That(GetStringAssignments(assignmentTestCase), Is.EqualTo(stringExpectations)); break; @@ -104,7 +104,7 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) client.GetNumericAssignment(subject, assignmentTestCase.experiment)); } - private static List GetIntegerAssignments(AssignmentTestCase assignmentTestCase) + private static List GetIntegerAssignments(AssignmentTestCase assignmentTestCase) { var client = EppoClient.GetInstance(); if (assignmentTestCase.subjectsWithAttributes != null) @@ -160,5 +160,5 @@ public class AssignmentTestCase public string valueType { get; set; } = "string"; public List? subjectsWithAttributes { get; set; } public List subjects { get; set; } - public List expectedAssignments { get; set; } + public List expectedAssignments { get; set; } } \ No newline at end of file diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index 8d3709b..b2050aa 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -1,6 +1,4 @@ -using System.Linq.Dynamic.Core.CustomTypeProviders; using eppo_sdk.dto; -using Namotion.Reflection; using Newtonsoft.Json; using static NUnit.Framework.Assert; @@ -22,7 +20,7 @@ public void ShouldConvertCondition() That(condition, Is.Not.Null); That(condition?.Operator, Is.EqualTo(OperatorType.ONE_OF)); That(condition?.Attribute, Is.EqualTo("device")); - That(condition?.Value.ArrayValue().Count, Is.EqualTo(2)); + That(condition?.ArrayValue().Count, Is.EqualTo(2)); }); } [Test] @@ -108,7 +106,7 @@ public void ShouldConvertRules() var rules = JsonConvert.DeserializeObject>(json); Assert.That(rules?.Count, Is.EqualTo(3)); Assert.That(rules[0].conditions[0].Operator, Is.EqualTo(OperatorType.ONE_OF)); - Assert.That(rules[0].conditions[0].Value.ArrayValue(), Is.EqualTo(new List + Assert.That(rules[0].conditions[0].ArrayValue(), Is.EqualTo(new List { "iOS", "Android" diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index b07ac4a..0ce1bcd 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -52,8 +52,8 @@ public void ShouldMatchAnyRuleWhenRuleMatches() var subjectAttributes = new SubjectAttributes { - { "price", EppoValue.Number("15") }, - { "appVersion", EppoValue.String("1.15.0") } + { "price", 15 }, + { "appVersion", "1.15.0" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); @@ -67,7 +67,7 @@ public void ShouldNotMatchAnyRuleWhenThrowInvalidSubjectAttribute() AddNumericConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "price", EppoValue.String("abcd") } }; + var subjectAttributes = new SubjectAttributes { { "price", "abcd" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -80,7 +80,7 @@ public void ShouldMatchAnyRuleWithRegexCondition() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", EppoValue.String("abcd")} }; + var subjectAttributes = new SubjectAttributes { { "match", "abcd"} }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -93,7 +93,7 @@ public void ShouldNotMatchAnyRuleWithRegexConditionIsUnmatched() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", EppoValue.String("123") } }; + var subjectAttributes = new SubjectAttributes { { "match", "123" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -106,7 +106,7 @@ public void ShouldMatchAnyRuleWithOneOfRule() AddOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", EppoValue.String("value2") } }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value2" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -119,7 +119,7 @@ public void ShouldNotMatchAnyRuleWithOneOfRule() AddOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", EppoValue.String("value3")} }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value3"} }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -132,7 +132,7 @@ public void ShouldMatchAnyRuleWithNotOneOfRule() AddNotOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", EppoValue.String("value3") } }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value3" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -145,7 +145,7 @@ public void ShouldNotMatchAnyRuleWithNotOneOfRuleNotPassed() AddNotOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", EppoValue.String("value1") } }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value1" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -158,7 +158,7 @@ public void ShouldMatchRuleIsNullTrueNullType() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", new EppoValue() } }; + var subjectAttributes = new SubjectAttributes { { "isnull", null } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -171,7 +171,7 @@ public void ShouldMatchRuleIsNullTrue() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", EppoValue.String(null) } }; + var subjectAttributes = new SubjectAttributes { { "isnull", null } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -198,7 +198,7 @@ public void ShouldMatchRuleIsNullFalse() AddIsNullCondition(rule, false); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", EppoValue.String("not null") } }; + var subjectAttributes = new SubjectAttributes { { "isnull", "not null" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -211,7 +211,7 @@ public void ShouldNotMatchRuleIsNullTrue() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", EppoValue.String("not null") } }; + var subjectAttributes = new SubjectAttributes { { "isnull", "not null" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -224,7 +224,7 @@ public void ShouldNotMatchRuleIsNullFalse() AddIsNullCondition(rule, false); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", new EppoValue() } }; + var subjectAttributes = new SubjectAttributes { { "isnull", null } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -232,36 +232,12 @@ private static void AddIsNullCondition(Rule rule, Boolean value) { rule.conditions.Add(new Condition { - value = EppoValue.Bool(value), - attribute = "isnull", - operatorType = OperatorType.IS_NULL + Value = value, + Attribute = "isnull", + Operator = OperatorType.IS_NULL }); } - [Test] - public void ShouldNotMatchRuleIsNullFalse() - { - var rules = new List(); - var rule = CreateRule(new List()); - AddIsNullCondition(rule, false); - rules.Add(rule); - - var subjectAttributes = new SubjectAttributes { { "isnull", new EppoValue() } }; - - Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); - } - private static void AddIsNullCondition(Rule rule, Boolean value) - { - rule.conditions.Add(new Condition - { - value = EppoValue.Bool(value), - attribute = "isnull", - operatorType = OperatorType.IS_NULL - }); - } - - - private const string SubjectKey = "subjectKey"; private const int TotalShards = 10; @@ -282,20 +258,20 @@ public void Setup() [Test] public void NoMatchingShards_ReturnsFalse() { - Assert.False(RuleEvaluator.MatchesAllShards(nonMatchingShards.ToArray(), SubjectKey, TotalShards)); + Assert.False(RuleValidator.MatchesAllShards(nonMatchingShards.ToArray(), SubjectKey, TotalShards)); } [Test] public void SomeMatchingShards_ReturnsFalse() { var allShards = matchingSplits.Concat(nonMatchingShards).ToList(); - Assert.False(RuleEvaluator.MatchesAllShards(allShards.ToArray(), SubjectKey, TotalShards)); + Assert.False(RuleValidator.MatchesAllShards(allShards.ToArray(), SubjectKey, TotalShards)); } [Test] public void MatchesShards_ReturnsTrue() { - Assert.True(RuleEvaluator.MatchesAllShards(matchingSplits.ToArray(), SubjectKey, TotalShards)); + Assert.True(RuleValidator.MatchesAllShards(matchingSplits.ToArray(), SubjectKey, TotalShards)); } [Test] @@ -320,7 +296,7 @@ public void FlagEvaluation_ReturnsMatchingVariation() variations, TotalShards); - var result = RuleEvaluator.EvaluateFlag(bigFlag, SubjectKey, subject); + var result = RuleValidator.EvaluateFlag(bigFlag, SubjectKey, subject); Assert.NotNull(result); Assert.AreEqual(musicVariation.Key, result.Variation.Key); @@ -331,7 +307,7 @@ public void FlagEvaluation_ReturnsMatchingVariation() public void DisabledFlag_ReturnsNull() { var flag = new Flag("disabled", false, new List(), VariationType.Boolean, new Dictionary(), TotalShards); - Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, new Dictionary())); + Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, new Dictionary())); } [Test] @@ -349,14 +325,14 @@ public void FlagWithInactiveAllocations_ReturnsNull() new Dictionary() { { matchVariation.Key, matchVariation } }, TotalShards); - Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject)); + Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, subject)); } [Test] public void FlagWithoutAllocations_ReturnsNull() { var flag = new Flag("no_allocs", true, new List(), VariationType.Boolean, new Dictionary(), TotalShards); - Assert.Null(RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject)); + Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, subject)); } [Test] @@ -372,55 +348,96 @@ public void MatchesVariationWithoutRules_ReturnsMatchingVariation() new Dictionary() { { "match", basicVariation } }, TotalShards); - var result = RuleEvaluator.EvaluateFlag(flag, SubjectKey, subject); + var result = RuleValidator.EvaluateFlag(flag, SubjectKey, subject); Assert.NotNull(result); Assert.That(result.variation.value, Is.EqualTo("bar")); } + + + private static void AddOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition("oneOf", OperatorType.ONE_OF, new EppoValue(new List + rule.conditions.Add(new Condition + { + Value =new List { "value1", "value2" - }))); + }, + Attribute = "oneOf", + Operator = OperatorType.ONE_OF + }); } private static void AddNotOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition("oneOf", OperatorType.NOT_ONE_OF, new EppoValue(new List + rule.conditions.Add(new Condition + { + Value = new List { "value1", "value2" - }))); + }, + Attribute = "oneOf", + Operator = OperatorType.NOT_ONE_OF + }); } private static void AddRegexConditionToRule(Rule rule) { - rule.conditions.Add(new Condition("match", OperatorType.MATCHES, EppoValue.String("[a-z]+"))); + var condition = new Condition + { + Value = "[a-z]+", + Attribute = "match", + Operator = OperatorType.MATCHES + }; + rule.conditions.Add(condition); } private static void AddPriceToSubjectAttribute(SubjectAttributes subjectAttributes) { - subjectAttributes.Add("price", EppoValue.String("30")); + subjectAttributes.Add("price", "30"); } private static void AddNumericConditionToRule(Rule rule) { - rule.conditions.Add(new Condition("price", OperatorType.GTE, new EppoValue("10", EppoValueType.NUMBER))); - rule.conditions.Add(new Condition("price", OperatorType.LTE, new EppoValue("20", EppoValueType.NUMBER))); + rule.conditions.Add(new Condition + { + Value = 10, + Attribute = "price", + Operator = OperatorType.GTE + }); + + rule.conditions.Add(new Condition + { + Value = 20, + Attribute = "price", + Operator = OperatorType.LTE + }); } private static void AddSemVerConditionToRule(Rule rule) { - rule.conditions.Add(new Condition("appVersion", OperatorType.GTE, new EppoValue("1.2.3", EppoValueType.NUMBER))); - rule.conditions.Add(new Condition("appVersion", OperatorType.LTE, new EppoValue("2.2.0", EppoValueType.NUMBER))); + rule.conditions.Add(new Condition + { + Value = "1.2.3", + Attribute = "appVersion", + Operator = OperatorType.GTE + }); + + rule.conditions.Add(new Condition + { + Value = "2.2.0", + Attribute = "appVersion", + Operator = OperatorType.LTE + }); } private static void AddNameToSubjectAttribute(SubjectAttributes subjectAttributes) { - subjectAttributes.Add("name", EppoValue.String("test")); + subjectAttributes.Add("name", "test"); } private static Rule CreateRule(List conditions) From 162907f27a3f4cb77d4bfdf99edc953fc02d2d04 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 31 May 2024 12:57:03 -0600 Subject: [PATCH 10/27] non null values --- dot-net-sdk/dto/HasEppoValue.cs | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/dot-net-sdk/dto/HasEppoValue.cs b/dot-net-sdk/dto/HasEppoValue.cs index bb80fc3..f791b38 100644 --- a/dot-net-sdk/dto/HasEppoValue.cs +++ b/dot-net-sdk/dto/HasEppoValue.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Numerics; +using eppo_sdk.exception; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NLog; @@ -11,7 +12,7 @@ public class HasEppoValue { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - + private bool _typed; private Object? _value; @@ -40,27 +41,29 @@ public Object? typedValue private EppoValueType _type; public EppoValueType Type { get { return _type; } } - public bool? BoolValue() => _value != null ? (bool)_value : null; - public double? DoubleValue() => Convert.ToDouble(_value); - public long? IntegerValue() => _value != null ? (long)_value : null; - public string? StringValue() => _value != null ? (string)_value : null; - public List? ArrayValue() + private T _nonNullValue(Func func) { if (_value == null) { - return null; - } - - if (_value is JArray array) - { - - return new List(array.ToObject()); + throw new UnsupportedEppoValueException($"Value of type {Type} is null or invalid"); } - return (List)_value; + return func(_value); } - public JObject? JsonValue() => _value == null ? null : (JObject)_value; + public bool BoolValue() => _nonNullValue((o) => (bool)o); + public double DoubleValue() => _nonNullValue(Convert.ToDouble); + public long IntegerValue() => _nonNullValue((o) => (long)o); + public string StringValue() => _nonNullValue((o) => (string)o); + public List ArrayValue() => _nonNullValue>((object o) => + { + if (o is JArray array) + { + return new List(array.ToObject()); + } + return (List)_value; + }); + public JObject JsonValue() => _nonNullValue((o) => (JObject)o); private static EppoValueType InferTypeFromValue(Object? value) { From 14f436aea054889056a1082eee2cab26bd8565bb Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 31 May 2024 12:59:51 -0600 Subject: [PATCH 11/27] more tests --- dot-net-sdk/dto/Allocation.cs | 2 +- dot-net-sdk/dto/Condition.cs | 7 + dot-net-sdk/dto/Split.cs | 2 +- dot-net-sdk/dto/Variation.cs | 5 + dot-net-sdk/validators/RuleValidator.cs | 19 +- eppo-sdk-test/ValidateJsonConvertion.cs | 4 +- eppo-sdk-test/validators/RuleValidatorTest.cs | 195 ++++++++++-------- 7 files changed, 133 insertions(+), 101 deletions(-) diff --git a/dot-net-sdk/dto/Allocation.cs b/dot-net-sdk/dto/Allocation.cs index 2d64113..9e20e7f 100644 --- a/dot-net-sdk/dto/Allocation.cs +++ b/dot-net-sdk/dto/Allocation.cs @@ -1,5 +1,5 @@ namespace eppo_sdk.dto; -public record Allocation(string key, List rules, List splits, bool doLog, int? startAt, int? endAt) +public record Allocation(string key, List rules, List splits, bool doLog, long? startAt, long? endAt) { } diff --git a/dot-net-sdk/dto/Condition.cs b/dot-net-sdk/dto/Condition.cs index 1d73b26..3fd4fc6 100644 --- a/dot-net-sdk/dto/Condition.cs +++ b/dot-net-sdk/dto/Condition.cs @@ -9,6 +9,13 @@ public class Condition : HasEppoValue public OperatorType Operator { get; set; } + public Condition(string attribute, OperatorType op, object? value) + { + Attribute = attribute; + Operator = op; + Value = value; + } + public override string ToString() { return $"operator: {Operator} | Attribute: {Attribute} | value: {Value}"; diff --git a/dot-net-sdk/dto/Split.cs b/dot-net-sdk/dto/Split.cs index c444245..15b05f4 100644 --- a/dot-net-sdk/dto/Split.cs +++ b/dot-net-sdk/dto/Split.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; namespace eppo_sdk.dto; -public record Split(string variationKey, List shards, IReadOnlyDictionary extraLogging) +public record Split(string variationKey, List shards, IReadOnlyDictionary? extraLogging) { } diff --git a/dot-net-sdk/dto/Variation.cs b/dot-net-sdk/dto/Variation.cs index 2f69cdd..71b23b1 100644 --- a/dot-net-sdk/dto/Variation.cs +++ b/dot-net-sdk/dto/Variation.cs @@ -4,4 +4,9 @@ public class Variation : HasEppoValue { public string Key {get; set;} public ShardRange shardRange { get; set; } + + public Variation(string key, object value) { + Key = key; + Value = value; + } } \ No newline at end of file diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index b441290..2b3ae42 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -3,12 +3,13 @@ using static eppo_sdk.dto.OperatorType; using NuGet.Versioning; using eppo_sdk.helpers; +using eppo_sdk.exception; namespace eppo_sdk.validators; -public static class RuleValidator +public static partial class RuleValidator { public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, SubjectAttributes subjectAttributes) { @@ -22,15 +23,20 @@ public static class RuleValidator continue; } - subjectAttributes.Add("id", EppoValue.String(subjectKey)); + subjectAttributes.Add("id", subjectKey); - if (MatchesAnyRule(allocation.rules, subjectAttributes)) + if (allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) { foreach (var split in allocation.splits) { if (MatchesAllShards(split.shards, subjectKey, flag.totalShards)) { - return new FlagEvaluation(flag.variations[split.variationKey], allocation.doLog, allocation.key); + if (flag.variations.TryGetValue(split.variationKey, out Variation variation) && variation != null) + { + return new FlagEvaluation(variation, allocation.doLog, allocation.key); + } + throw new ExperimentConfigurationNotFound($"Variation {split.variationKey} could not be found"); + } } } @@ -42,7 +48,7 @@ public static class RuleValidator // Find the first shard that does not match. If it's null. then all shards match. - public static bool MatchesAllShards(IEnumerable shards, string subjectKey, int totalShards) => shards.First(shard => !MatchesShard(shard, subjectKey, totalShards)) == null; + public static bool MatchesAllShards(IEnumerable shards, string subjectKey, int totalShards) => shards.FirstOrDefault(shard => !MatchesShard(shard, subjectKey, totalShards)) == null; private static bool MatchesShard(Shard shard, string subjectKey, int totalShards) { @@ -57,6 +63,7 @@ private static bool MatchesShard(Shard shard, string subjectKey, int totalShards public static Rule? FindMatchingRule(SubjectAttributes subjectAttributes, IEnumerable rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)); private static bool MatchesRule(SubjectAttributes subjectAttributes, Rule rule) => rule.conditions.All(condition => EvaluateCondition(subjectAttributes, condition)); + private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condition condition) { try @@ -71,7 +78,7 @@ private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condi { var value = new HasEppoValue(outVal!); // Assuming non-null for simplicity, handle nulls as necessary - switch (condition.Operator) + switch (condition.Operator) { case GTE: { diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index b2050aa..01ad8c2 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -130,7 +130,7 @@ public void ShouldConvertVariations() 'value': false } }"; - Variation expectedVariation = new Variation("on", EppoValue.Bool(true)); + Variation expectedVariation = new Variation("on", true); var variations = JsonConvert.DeserializeObject>(json); Multiple(() => @@ -141,7 +141,7 @@ public void ShouldConvertVariations() Variation? on = null; That(variations?.TryGetValue("on", out on), Is.True); That(on, Is.Not.Null); - That(on?.Value.BoolValue(), Is.True); + That(on?.BoolValue(), Is.True); }); } diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index 0ce1bcd..0dc6713 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -80,7 +80,7 @@ public void ShouldMatchAnyRuleWithRegexCondition() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", "abcd"} }; + var subjectAttributes = new SubjectAttributes { { "match", "abcd" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -93,7 +93,7 @@ public void ShouldNotMatchAnyRuleWithRegexConditionIsUnmatched() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", "123" } }; + var subjectAttributes = new SubjectAttributes { { "match", "123" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -119,7 +119,7 @@ public void ShouldNotMatchAnyRuleWithOneOfRule() AddOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value3"} }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value3" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -134,7 +134,7 @@ public void ShouldMatchAnyRuleWithNotOneOfRule() var subjectAttributes = new SubjectAttributes { { "oneOf", "value3" } }; - Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); + Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.EqualTo(rule)); } [Test] @@ -145,7 +145,7 @@ public void ShouldNotMatchAnyRuleWithNotOneOfRuleNotPassed() AddNotOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value1" } }; + var subjectAttributes = new SubjectAttributes { { "oneOf", "value1" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -185,7 +185,7 @@ public void ShouldMatchRuleIsNullNoAttribute() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { }; + var subjectAttributes = new SubjectAttributes { }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -230,98 +230,149 @@ public void ShouldNotMatchRuleIsNullFalse() } private static void AddIsNullCondition(Rule rule, Boolean value) { - rule.conditions.Add(new Condition - { - Value = value, - Attribute = "isnull", - Operator = OperatorType.IS_NULL - }); + rule.conditions.Add(new("isnull", OperatorType.IS_NULL, value)); } - private const string SubjectKey = "subjectKey"; + private const string SubjectKey = "subjectKey"; private const int TotalShards = 10; - // Assuming you have these classes defined elsewhere (adapt them to your implementation) - private List nonMatchingShards; - private List matchingSplits; - private Rule rockAndRollLegendRule; - private Variation musicVariation; - private Subject subject; - private Variation matchVariation; + private List nonMatchingSplits; + private List matchingSplits; + private List musicSplits; + private Rule rockAndRollLegendRule = new(new List + { + new("age",OperatorType.GTE,40), + new("occupation",OperatorType.MATCHES, "musician"), + new("albumCount", OperatorType.GTE, 50) + }); + private SubjectAttributes subject; + private Variation matchVariation = new("match", "foo"); [SetUp] public void Setup() { - // Initialize your test data here (nonMatchingShards, matchingSplits, etc.) + subject = new() + { + ["age"] = 42, + ["albumCount"] = 57, + ["occupation"] = "musician" + }; + var allShards = new List(); + var shardRangeAll = new List() + { + new ShardRange(0, TotalShards) + }; + + allShards.Add(new Shard("na", shardRangeAll)); + + matchingSplits = new List() + { + new Split("match", allShards, null) + }; + + musicSplits = new List() + { + new Split("music", new List() + { + new Shard("na", new List() + { + new ShardRange(2, 5) + }) + }, null) + }; + + nonMatchingSplits = new List() + { + new Split("match", new List() + { + new Shard("na", new List() + { + new ShardRange(0, 4), + new ShardRange(5, 9) + }), + new Shard("cl", new List() + { + + }) + }, null) + }; } [Test] public void NoMatchingShards_ReturnsFalse() { - Assert.False(RuleValidator.MatchesAllShards(nonMatchingShards.ToArray(), SubjectKey, TotalShards)); + Assert.That(RuleValidator.MatchesAllShards(nonMatchingSplits[0].shards, SubjectKey, TotalShards), Is.False); } [Test] public void SomeMatchingShards_ReturnsFalse() { - var allShards = matchingSplits.Concat(nonMatchingShards).ToList(); - Assert.False(RuleValidator.MatchesAllShards(allShards.ToArray(), SubjectKey, TotalShards)); + var allShards = matchingSplits[0].shards; + allShards.AddRange(nonMatchingSplits[0].shards); + Assert.That(RuleValidator.MatchesAllShards(allShards, SubjectKey, TotalShards), Is.False); } [Test] public void MatchesShards_ReturnsTrue() { - Assert.True(RuleValidator.MatchesAllShards(matchingSplits.ToArray(), SubjectKey, TotalShards)); + Assert.That(RuleValidator.MatchesAllShards(matchingSplits[0].shards, SubjectKey, TotalShards), Is.True); } [Test] public void FlagEvaluation_ReturnsMatchingVariation() { var allocations = new List() { - new Allocation( + new( "rock", new List() { rockAndRollLegendRule }, musicSplits, - false) + false, null, null) }; var variations = new Dictionary() { - { "music", musicVariation } + { "music", new("music", "rockandroll") }, + { "football", new("football", "football") }, + { "space", new("space", "space") } }; var bigFlag = new Flag( "HallOfFame", true, allocations, - VariationType.String, + EppoValueType.STRING, variations, TotalShards); var result = RuleValidator.EvaluateFlag(bigFlag, SubjectKey, subject); - Assert.NotNull(result); - Assert.AreEqual(musicVariation.Key, result.Variation.Key); - Assert.AreEqual(musicVariation.Value, result.Variation.Value); + Assert.Multiple(() => + { + Assert.NotNull(result); + Assert.That(result.Variation.Key, Is.EqualTo("music")); + Assert.That(result.Variation.Value, Is.EqualTo("rockandroll")); + } + ); } [Test] public void DisabledFlag_ReturnsNull() { - var flag = new Flag("disabled", false, new List(), VariationType.Boolean, new Dictionary(), TotalShards); - Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, new Dictionary())); + var flag = new Flag("disabled", false, new List(), EppoValueType.BOOLEAN, new Dictionary(), TotalShards); + Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, new SubjectAttributes())); } [Test] public void FlagWithInactiveAllocations_ReturnsNull() { - var now = DateTime.UtcNow.ToUnixTimeSeconds(); - var overAlloc = new Allocation("over", new List(), matchingSplits, false, endAt: now - 10000); - var futureAlloc = new Allocation("hasntStarted", new List(), matchingSplits, false, startAt: now + 60000); + + var now = DateTimeOffset.Now.ToUnixTimeSeconds(); + var overAlloc = new Allocation("over", new List(), matchingSplits, false, null, endAt: now - 10000); + var futureAlloc = new Allocation("hasntStarted", new List(), matchingSplits, false, startAt: now + 60000, null); var flag = new Flag( "inactive_allocs", true, new List() { overAlloc, futureAlloc }, - VariationType.Boolean, + EppoValueType.BOOLEAN, new Dictionary() { { matchVariation.Key, matchVariation } }, TotalShards); @@ -331,68 +382,51 @@ public void FlagWithInactiveAllocations_ReturnsNull() [Test] public void FlagWithoutAllocations_ReturnsNull() { - var flag = new Flag("no_allocs", true, new List(), VariationType.Boolean, new Dictionary(), TotalShards); + var flag = new Flag("no_allocs", true, new List(), EppoValueType.BOOLEAN, new Dictionary(), TotalShards); Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, subject)); } [Test] public void MatchesVariationWithoutRules_ReturnsMatchingVariation() { - var allocation1 = new Allocation("alloc1", new List(), matchingSplits, false); + var allocation1 = new Allocation("alloc1", new List(), matchingSplits, false, null, null); var basicVariation = new Variation("foo", "bar"); var flag = new Flag( "matches", true, new List() { allocation1 }, - VariationType.String, + EppoValueType.STRING, new Dictionary() { { "match", basicVariation } }, TotalShards); var result = RuleValidator.EvaluateFlag(flag, SubjectKey, subject); Assert.NotNull(result); - Assert.That(result.variation.value, Is.EqualTo("bar")); + Assert.That(result.Variation.Value, Is.EqualTo("bar")); } - - - private static void AddOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition - { - Value =new List + rule.conditions.Add(new("oneOf", OperatorType.ONE_OF, new List { "value1", "value2" - }, - Attribute = "oneOf", - Operator = OperatorType.ONE_OF - }); + })); } private static void AddNotOneOfCondition(Rule rule) { - rule.conditions.Add(new Condition - { - Value = new List + rule.conditions.Add(new Condition("oneOf", OperatorType.NOT_ONE_OF, new List { "value1", "value2" - }, - Attribute = "oneOf", - Operator = OperatorType.NOT_ONE_OF - }); + })); + } private static void AddRegexConditionToRule(Rule rule) { - var condition = new Condition - { - Value = "[a-z]+", - Attribute = "match", - Operator = OperatorType.MATCHES - }; + var condition = new Condition("match", OperatorType.MATCHES, "[a-z]+"); rule.conditions.Add(condition); } @@ -403,36 +437,15 @@ private static void AddPriceToSubjectAttribute(SubjectAttributes subjectAttribut private static void AddNumericConditionToRule(Rule rule) { - rule.conditions.Add(new Condition - { - Value = 10, - Attribute = "price", - Operator = OperatorType.GTE - }); + rule.conditions.Add(new Condition("price", OperatorType.GTE, 10)); - rule.conditions.Add(new Condition - { - Value = 20, - Attribute = "price", - Operator = OperatorType.LTE - }); + rule.conditions.Add(new Condition("price", OperatorType.LTE, 20)); } private static void AddSemVerConditionToRule(Rule rule) { - rule.conditions.Add(new Condition - { - Value = "1.2.3", - Attribute = "appVersion", - Operator = OperatorType.GTE - }); - - rule.conditions.Add(new Condition - { - Value = "2.2.0", - Attribute = "appVersion", - Operator = OperatorType.LTE - }); + rule.conditions.Add(new Condition("appVersion", OperatorType.GTE, "1.2.3")); + rule.conditions.Add(new Condition("appVersion", OperatorType.LTE, "2.2.0")); } private static void AddNameToSubjectAttribute(SubjectAttributes subjectAttributes) @@ -444,4 +457,4 @@ private static Rule CreateRule(List conditions) { return new Rule(conditions); } -} \ No newline at end of file +} From 1cc8747831d9ed744284dd12a029db6148a995c9 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 31 May 2024 14:32:30 -0600 Subject: [PATCH 12/27] pull UFC test data --- Makefile | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index d0cb23e..f06f320 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,22 @@ help: Makefile @sed -n 's/^##//p' $< -.PHONY: test-data + + testDataDir := eppo-sdk-test/files +tempDir := ${testDataDir}/temp +gitDataDir := ${tempDir}/sdk-test-data +branchName := main +githubRepoLink := https://github.com/Eppo-exp/sdk-test-data.git + +.PHONY: test-data test-data: rm -rf $(testDataDir) - mkdir -p $(testDataDir) - gsutil cp gs://sdk-test-data/rac-experiments-*.json $(testDataDir) - gsutil cp -r gs://sdk-test-data/assignment-v2 $(testDataDir) + mkdir -p $(tempDir) + git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir} + cp -r ${gitDataDir}/ufc ${testDataDir}/ + rm -rf ${tempDir} + .PHONY: build build: From aacf6536bb15f2036c3d57534b8941926d8b97c2 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 31 May 2024 14:33:48 -0600 Subject: [PATCH 13/27] update eppo client algo --- dot-net-sdk/EppoClient.cs | 84 ++++++++----------- dot-net-sdk/constants/Constants.cs | 1 - dot-net-sdk/dto/ExperimentConfiguration.cs | 17 ---- .../dto/ExperimentConfigurationResponse.cs | 2 +- 4 files changed, 34 insertions(+), 70 deletions(-) delete mode 100644 dot-net-sdk/dto/ExperimentConfiguration.cs diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index f83a5c3..c0fb571 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -30,35 +30,35 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } - public JObject? GetJsonAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public JObject? GetJsonAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) { - return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.JsonValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.JsonValue(); } - public bool? GetBoolAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public bool? GetBoolAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) { - return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.BoolValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.BoolValue(); } - public double? GetNumericAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public double? GetNumericAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) { - return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.DoubleValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.DoubleValue(); } - public long? GetIntegerAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public long? GetIntegerAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) { - return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.IntegerValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.IntegerValue(); } - public string? GetStringAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null) + public string? GetStringAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) { - return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.StringValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.StringValue(); } - private HasEppoValue? GetAssignment(string subjectKey, string flagKey, SubjectAttributes subjectAttributes) + private HasEppoValue? GetAssignment(string flagKey, string subjectKey, SubjectAttributes subjectAttributes) { InputValidator.ValidateNotBlank(subjectKey, "Invalid argument: subjectKey cannot be blank"); InputValidator.ValidateNotBlank(flagKey, "Invalid argument: flagKey cannot be blank"); @@ -70,12 +70,6 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC return null; } - var subjectVariationOverride = this.GetSubjectVariationOverride(subjectKey, configuration); - if (!subjectVariationOverride.IsNull()) - { - return subjectVariationOverride; - } - if (!configuration.enabled) { Logger.Info( @@ -83,64 +77,52 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC return null; } - var rule = RuleValidator.FindMatchingRule(subjectAttributes, configuration.rules); - if (rule == null) + var result = RuleValidator.EvaluateFlag(configuration, subjectKey, subjectAttributes); + if (result == null) { - Logger.Info("[Eppo SDK] No assigned variation. The subject attributes did not match any targeting rules"); return null; } - var allocation = configuration.GetAllocation(rule.allocationKey); - if (!IsInExperimentSample(subjectKey, flagKey, configuration.subjectShards, allocation!.percentExposure)) + var assignment = result.Variation; + + if (HasEppoValue.IsNullValue(assignment)) { - Logger.Info("[Eppo SDK] No assigned variation. The subject is not part of the sample population"); return null; } - - var assignedVariation = - GetAssignedVariation(subjectKey, flagKey, configuration.subjectShards, allocation.variations); - if (assignedVariation != null && !assignedVariation.IsNull()) + + try { - try - { - _eppoClientConfig.AssignmentLogger - .LogAssignment(new AssignmentLogData( - flagKey, - rule.allocationKey, - assignedVariation.StringValue() ?? "null", - subjectKey, - subjectAttributes - )); - } - catch (Exception) - { - // Ignore Exception - } + _eppoClientConfig.AssignmentLogger + .LogAssignment(new AssignmentLogData( + flagKey, + result.AllocationKey, + assignment.StringValue() ?? "null", + subjectKey, + subjectAttributes + )); + } + catch (Exception) + { + // Ignore Exception } - return assignedVariation; + return assignment; } - private bool IsInExperimentSample(string subjectKey, string flagKey, int subjectShards, + private bool IsInExperimentSample(string flagKey, string subjectKey, int subjectShards, float percentageExposure) { var shard = Sharder.GetShard($"exposure-{subjectKey}-{flagKey}", subjectShards); return shard <= percentageExposure * subjectShards; } - private Variation GetAssignedVariation(string subjectKey, string flagKey, int subjectShards, + private Variation GetAssignedVariation(string flagKey, string subjectKey, int subjectShards, List variations) { var shard = Sharder.GetShard($"assignment-{subjectKey}-{flagKey}", subjectShards); return variations.Find(config => Sharder.IsInRange(shard, config.shardRange))!; } - public HasEppoValue GetSubjectVariationOverride(string subjectKey, ExperimentConfiguration experimentConfiguration) - { - var hexedSubjectKey = Sharder.GetHex(subjectKey); - return new HasEppoValue(experimentConfiguration.typedOverrides.GetValueOrDefault(hexedSubjectKey, null)); - } - public static EppoClient Init(EppoClientConfig eppoClientConfig) { lock (Baton) diff --git a/dot-net-sdk/constants/Constants.cs b/dot-net-sdk/constants/Constants.cs index 417d5dc..4f7e38c 100644 --- a/dot-net-sdk/constants/Constants.cs +++ b/dot-net-sdk/constants/Constants.cs @@ -14,6 +14,5 @@ public class Constants public const int MAX_CACHE_ENTRIES = 1000; - public const string RAC_ENDPOINT = "/randomized_assignment/v3/config"; public const string UFC_ENDPOINT = "/flag-config/v1/config"; } \ No newline at end of file diff --git a/dot-net-sdk/dto/ExperimentConfiguration.cs b/dot-net-sdk/dto/ExperimentConfiguration.cs deleted file mode 100644 index 567d18e..0000000 --- a/dot-net-sdk/dto/ExperimentConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace eppo_sdk.dto; - -public class ExperimentConfiguration -{ - public string name { get; set; } - public bool enabled { get; set; } - public int subjectShards { get; set; } - public Dictionary typedOverrides { get; set; } - public Dictionary allocations { get; set; } - public List rules { get; set; } - - public Allocation? GetAllocation(string allocationKey) - { - allocations.TryGetValue(allocationKey, out var value); - return value; - } -} diff --git a/dot-net-sdk/dto/ExperimentConfigurationResponse.cs b/dot-net-sdk/dto/ExperimentConfigurationResponse.cs index 5b3ce71..e5c0021 100644 --- a/dot-net-sdk/dto/ExperimentConfigurationResponse.cs +++ b/dot-net-sdk/dto/ExperimentConfigurationResponse.cs @@ -1,5 +1,5 @@ namespace eppo_sdk.dto; public class ExperimentConfigurationResponse { - public Dictionary flags { get; set; } + public Dictionary flags { get; set; } } \ No newline at end of file From 0619433b8bcba5ee52ab35a6688ac02fdf45c670 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 09:44:24 -0600 Subject: [PATCH 14/27] types and tests --- dot-net-sdk/EppoClient.cs | 43 ++--- dot-net-sdk/dto/Allocation.cs | 2 +- dot-net-sdk/dto/AssignmentLogData.cs | 4 +- dot-net-sdk/dto/EppoValueType.cs | 2 +- dot-net-sdk/dto/HasEppoValue.cs | 8 +- dot-net-sdk/dto/SubjectAttributes.cs | 2 +- dot-net-sdk/http/EppoHttpClient.cs | 1 + dot-net-sdk/store/ConfigurationStore.cs | 6 +- dot-net-sdk/store/IConfigurationStore.cs | 4 +- dot-net-sdk/validators/RuleValidator.cs | 21 ++- eppo-sdk-test/EppoClientTest.cs | 168 ++++++++++-------- eppo-sdk-test/ValidateJsonConvertion.cs | 16 +- .../helpers/AssignmentLogDataTest.cs | 2 +- eppo-sdk-test/validators/RuleValidatorTest.cs | 49 ++--- 14 files changed, 170 insertions(+), 158 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index c0fb571..d475fa2 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -30,35 +30,35 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } - public JObject? GetJsonAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) + public JObject? GetJsonAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.JsonValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.JsonValue(); } - public bool? GetBoolAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) + public bool? GetBoolAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.BoolValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.BoolValue(); } - public double? GetNumericAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) + public double? GetNumericAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.DoubleValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.DoubleValue(); } - public long? GetIntegerAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) + public long? GetIntegerAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.IntegerValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.IntegerValue(); } - public string? GetStringAssignment(string flagKey, string subjectKey, SubjectAttributes? subjectAttributes = null) + public string? GetStringAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new SubjectAttributes())?.StringValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.StringValue(); } - private HasEppoValue? GetAssignment(string flagKey, string subjectKey, SubjectAttributes subjectAttributes) + private HasEppoValue? GetAssignment(string flagKey, string subjectKey, Subject subjectAttributes) { InputValidator.ValidateNotBlank(subjectKey, "Invalid argument: subjectKey cannot be blank"); InputValidator.ValidateNotBlank(flagKey, "Invalid argument: flagKey cannot be blank"); @@ -89,7 +89,7 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC { return null; } - + try { _eppoClientConfig.AssignmentLogger @@ -109,21 +109,8 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC return assignment; } - private bool IsInExperimentSample(string flagKey, string subjectKey, int subjectShards, - float percentageExposure) - { - var shard = Sharder.GetShard($"exposure-{subjectKey}-{flagKey}", subjectShards); - return shard <= percentageExposure * subjectShards; - } - - private Variation GetAssignedVariation(string flagKey, string subjectKey, int subjectShards, - List variations) - { - var shard = Sharder.GetShard($"assignment-{subjectKey}-{flagKey}", subjectShards); - return variations.Find(config => Sharder.IsInRange(shard, config.shardRange))!; - } - - public static EppoClient Init(EppoClientConfig eppoClientConfig) + public static EppoClient Init(EppoClientConfig eppoClientConfig, bool startPolling = true) + { lock (Baton) { @@ -157,7 +144,7 @@ public static EppoClient Init(EppoClientConfig eppoClientConfig) var fetchExperimentsTask = new FetchExperimentsTask(configurationStore, Constants.TIME_INTERVAL_IN_MILLIS, Constants.JITTER_INTERVAL_IN_MILLIS); - fetchExperimentsTask.Run(); + if (startPolling) fetchExperimentsTask.Run(); _client = new EppoClient(configurationStore, eppoClientConfig, fetchExperimentsTask); } diff --git a/dot-net-sdk/dto/Allocation.cs b/dot-net-sdk/dto/Allocation.cs index 9e20e7f..1695dcf 100644 --- a/dot-net-sdk/dto/Allocation.cs +++ b/dot-net-sdk/dto/Allocation.cs @@ -1,5 +1,5 @@ namespace eppo_sdk.dto; -public record Allocation(string key, List rules, List splits, bool doLog, long? startAt, long? endAt) +public record Allocation(string key, List rules, List splits, bool doLog, DateTime? startAt, DateTime? endAt) { } diff --git a/dot-net-sdk/dto/AssignmentLogData.cs b/dot-net-sdk/dto/AssignmentLogData.cs index 18f1c7a..8ec356c 100644 --- a/dot-net-sdk/dto/AssignmentLogData.cs +++ b/dot-net-sdk/dto/AssignmentLogData.cs @@ -8,14 +8,14 @@ public class AssignmentLogData public string variation; public DateTime timestamp; public string subject; - public SubjectAttributes subjectAttributes; + public Subject subjectAttributes; public AssignmentLogData( string featureFlag, string allocation, string variation, string subject, - SubjectAttributes subjectAttributes) + Subject subjectAttributes) { this.experiment = featureFlag + "-" + allocation; this.featureFlag = featureFlag; diff --git a/dot-net-sdk/dto/EppoValueType.cs b/dot-net-sdk/dto/EppoValueType.cs index 31b86a3..48ee946 100644 --- a/dot-net-sdk/dto/EppoValueType.cs +++ b/dot-net-sdk/dto/EppoValueType.cs @@ -2,7 +2,7 @@ namespace eppo_sdk.dto; public enum EppoValueType { - NUMBER, + NUMERIC, INTEGER, STRING, BOOLEAN, diff --git a/dot-net-sdk/dto/HasEppoValue.cs b/dot-net-sdk/dto/HasEppoValue.cs index f791b38..4396c16 100644 --- a/dot-net-sdk/dto/HasEppoValue.cs +++ b/dot-net-sdk/dto/HasEppoValue.cs @@ -51,7 +51,7 @@ private T _nonNullValue(Func func) } public bool BoolValue() => _nonNullValue((o) => (bool)o); public double DoubleValue() => _nonNullValue(Convert.ToDouble); - public long IntegerValue() => _nonNullValue((o) => (long)o); + public long IntegerValue() => _nonNullValue(Convert.ToInt64); public string StringValue() => _nonNullValue((o) => (string)o); public List ArrayValue() => _nonNullValue>((object o) => @@ -79,7 +79,7 @@ private static EppoValueType InferTypeFromValue(Object? value) } else if (value is float || value is double || value is Double || value is float) { - return EppoValueType.NUMBER; + return EppoValueType.NUMERIC; } else if (value is int || value is long || value is BigInteger) @@ -106,7 +106,7 @@ private static EppoValueType InferTypeFromValue(Object? value) public static HasEppoValue Bool(string? value) => new(value, EppoValueType.BOOLEAN); public static HasEppoValue Bool(bool value) => new(value, EppoValueType.BOOLEAN); - public static HasEppoValue Number(string value) => new(value, EppoValueType.NUMBER); + public static HasEppoValue Number(string value) => new(value, EppoValueType.NUMERIC); public static HasEppoValue String(string? value) => new(value, EppoValueType.STRING); public static HasEppoValue Integer(string value) => new(value, EppoValueType.INTEGER); public static HasEppoValue Null() => new(); @@ -131,7 +131,7 @@ public HasEppoValue(List array) this.Value = array; } - public bool IsNumeric() => _type == EppoValueType.NUMBER || _type == EppoValueType.INTEGER; + public bool IsNumeric() => _type == EppoValueType.NUMERIC || _type == EppoValueType.INTEGER; public bool IsNull() => EppoValueType.NULL.Equals(Type); diff --git a/dot-net-sdk/dto/SubjectAttributes.cs b/dot-net-sdk/dto/SubjectAttributes.cs index 4bdfb38..dab9ae9 100644 --- a/dot-net-sdk/dto/SubjectAttributes.cs +++ b/dot-net-sdk/dto/SubjectAttributes.cs @@ -1,6 +1,6 @@ namespace eppo_sdk.dto; -public class SubjectAttributes: Dictionary +public class Subject: Dictionary { } \ No newline at end of file diff --git a/dot-net-sdk/http/EppoHttpClient.cs b/dot-net-sdk/http/EppoHttpClient.cs index 8ffee74..c81013a 100644 --- a/dot-net-sdk/http/EppoHttpClient.cs +++ b/dot-net-sdk/http/EppoHttpClient.cs @@ -53,6 +53,7 @@ Dictionary headers { _defaultParams.ToList().ForEach(x => parameters.Add(x.Key, x.Value)); +var fullURL = _baseUrl + url; var request = new RestRequest { Timeout = _requestTimeOutMillis diff --git a/dot-net-sdk/store/ConfigurationStore.cs b/dot-net-sdk/store/ConfigurationStore.cs index 94326e1..160fa86 100644 --- a/dot-net-sdk/store/ConfigurationStore.cs +++ b/dot-net-sdk/store/ConfigurationStore.cs @@ -32,16 +32,16 @@ public static ConfigurationStore GetInstance(MemoryCache experimentConfiguration return _instance; } - public void SetExperimentConfiguration(string key, ExperimentConfiguration experimentConfiguration) + public void SetExperimentConfiguration(string key, Flag experimentConfiguration) { _experimentConfigurationCache.Set(key, experimentConfiguration, new MemoryCacheEntryOptions().SetSize(1)); } - public ExperimentConfiguration? GetExperimentConfiguration(string key) + public Flag? GetExperimentConfiguration(string key) { try { - if (_experimentConfigurationCache.TryGetValue(key, out ExperimentConfiguration? result)) + if (_experimentConfigurationCache.TryGetValue(key, out Flag? result)) { return result; } diff --git a/dot-net-sdk/store/IConfigurationStore.cs b/dot-net-sdk/store/IConfigurationStore.cs index d285bfc..f5d05aa 100644 --- a/dot-net-sdk/store/IConfigurationStore.cs +++ b/dot-net-sdk/store/IConfigurationStore.cs @@ -5,6 +5,6 @@ namespace eppo_sdk.store; public interface IConfigurationStore { void FetchExperimentConfiguration(); - ExperimentConfiguration? GetExperimentConfiguration(string key); - void SetExperimentConfiguration(string key, ExperimentConfiguration experimentConfiguration); + Flag? GetExperimentConfiguration(string key); + void SetExperimentConfiguration(string key, Flag experimentConfiguration); } \ No newline at end of file diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 2b3ae42..3aa0326 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -4,6 +4,8 @@ using NuGet.Versioning; using eppo_sdk.helpers; using eppo_sdk.exception; +using Microsoft.Extensions.Logging; +using NLog; namespace eppo_sdk.validators; @@ -11,11 +13,14 @@ namespace eppo_sdk.validators; public static partial class RuleValidator { - public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, SubjectAttributes subjectAttributes) + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, Subject subjectAttributes) { if (!flag.enabled) return null; - var now = DateTimeOffset.Now.ToUnixTimeSeconds(); + var now = DateTimeOffset.Now; foreach (var allocation in flag.allocations) { if (allocation.startAt.HasValue && allocation.startAt.Value > now || allocation.endAt.HasValue && allocation.endAt.Value < now) @@ -23,7 +28,9 @@ public static partial class RuleValidator continue; } - subjectAttributes.Add("id", subjectKey); + if (!subjectAttributes.TryAdd("id", subjectKey)) { + Logger.Warn($"`id` {subjectKey} already added to subject attributes"); + } if (allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) { @@ -58,13 +65,13 @@ private static bool MatchesShard(Shard shard, string subjectKey, int totalShards return shard.ranges.Any(range => Sharder.IsInRange(subjectBucket, range)); } - private static bool MatchesAnyRule(IEnumerable rules, SubjectAttributes subject) => rules.Any() && FindMatchingRule(subject, rules) != null; + private static bool MatchesAnyRule(IEnumerable rules, Subject subject) => rules.Any() && FindMatchingRule(subject, rules) != null; - public static Rule? FindMatchingRule(SubjectAttributes subjectAttributes, IEnumerable rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)); + public static Rule? FindMatchingRule(Subject subjectAttributes, IEnumerable rules) => rules.FirstOrDefault(rule => MatchesRule(subjectAttributes, rule)); - private static bool MatchesRule(SubjectAttributes subjectAttributes, Rule rule) => rule.conditions.All(condition => EvaluateCondition(subjectAttributes, condition)); + private static bool MatchesRule(Subject subjectAttributes, Rule rule) => rule.conditions.All(condition => EvaluateCondition(subjectAttributes, condition)); - private static bool EvaluateCondition(SubjectAttributes subjectAttributes, Condition condition) + private static bool EvaluateCondition(Subject subjectAttributes, Condition condition) { try { diff --git a/eppo-sdk-test/EppoClientTest.cs b/eppo-sdk-test/EppoClientTest.cs index b82aeb9..d20a31d 100644 --- a/eppo-sdk-test/EppoClientTest.cs +++ b/eppo-sdk-test/EppoClientTest.cs @@ -22,16 +22,16 @@ public void Setup() { BaseUrl = _mockServer?.Urls[0]! }; - EppoClient.Init(config); + EppoClient.Init(config, startPolling: true); } private void SetupMockServer() { _mockServer = WireMockServer.Start(); - var response = GetMockRandomizedAssignments(); + var response = GetMockFlagConfig(); Console.WriteLine($"MockServer started at: {_mockServer.Urls[0]}"); this._mockServer - .Given(Request.Create().UsingGet().WithPath(new RegexMatcher(".*randomized_assignment.*"))) + .Given(Request.Create().UsingGet().WithPath(new RegexMatcher("flag-config.*"))) .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK).WithBody(response).WithHeader("Content-Type", "application/json")); } @@ -41,10 +41,10 @@ public void TearDown() _mockServer?.Stop(); } - private static string GetMockRandomizedAssignments() + private static string GetMockFlagConfig() { var filePath = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent?.Parent?.Parent?.FullName, - "files/rac-experiments-v3.json"); + "files/ufc/flags-v1.json"); using var sr = new StreamReader(filePath); return sr.ReadToEnd(); } @@ -52,88 +52,107 @@ private static string GetMockRandomizedAssignments() [Test, TestCaseSource(nameof(GetTestAssignmentData))] public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) { + var client = EppoClient.GetInstance(); + - switch (assignmentTestCase.valueType) + switch (assignmentTestCase.VariationType) { - case "boolean": - var boolExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (bool?)x); - Assert.That(GetBoolAssignments(assignmentTestCase), Is.EqualTo(boolExpectations)); + case (EppoValueType.BOOLEAN): + var boolExpectations = assignmentTestCase.Subjects.ConvertAll(x => (bool?)x.Assignment); + var assignments = assignmentTestCase.Subjects.ConvertAll(subject => + client.GetBoolAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); - break; - case "number": - var numericExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (double?)x); - Assert.That(GetNumericAssignments(assignmentTestCase), Is.EqualTo(numericExpectations)); + Assert.That(assignments, Is.EqualTo(boolExpectations)); break; - case "integer": - var intExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (long?)x); - Assert.That(GetIntegerAssignments(assignmentTestCase), Is.EqualTo(intExpectations)); + case (EppoValueType.INTEGER): + var longExpectations = assignmentTestCase.Subjects.ConvertAll(x => (long?)x.Assignment); + var longAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); + + Assert.That(longAssignments, Is.EqualTo(longExpectations)); break; - case "string": - var stringExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (string?)x); - Assert.That(GetStringAssignments(assignmentTestCase), Is.EqualTo(stringExpectations)); + + case (EppoValueType.JSON): + // var longExpectations = assignmentTestCase.Subjects.ConvertAll(x => (long?)x.Assignment); + // var longAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + // client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); + + // Assert.That(longAssignments, Is.EqualTo(longExpectations)); break; - } - } + // case (EppoValueType.NUMERIC): + // var numExpectation = assignmentTestCase.Subjects.ConvertAll(x => (double?)x.Assignment); + // var numAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + // client.GetNumericAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); - private static List GetBoolAssignments(AssignmentTestCase assignmentTestCase) - { - var client = EppoClient.GetInstance(); - if (assignmentTestCase.subjectsWithAttributes != null) - { - return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetBoolAssignment(subject.subjectKey, assignmentTestCase.experiment, - subject.subjectAttributes)); - } + // Assert.That(numAssignments, Is.EqualTo(numExpectation)); - return assignmentTestCase.subjects.ConvertAll(subject => - client.GetBoolAssignment(subject, assignmentTestCase.experiment)); - } + // break; + // case "number": + // var numericExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (double?)x); + // Assert.That(GetNumericAssignments(assignmentTestCase), Is.EqualTo(numericExpectations)); - private static List GetNumericAssignments(AssignmentTestCase assignmentTestCase) - { - var client = EppoClient.GetInstance(); - if (assignmentTestCase.subjectsWithAttributes != null) - { - return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetNumericAssignment(subject.subjectKey, assignmentTestCase.experiment, - subject.subjectAttributes)); - } + // break; + // case "integer": + // var intExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (long?)x); + // Assert.That(GetIntegerAssignments(assignmentTestCase), Is.EqualTo(intExpectations)); - return assignmentTestCase.subjects.ConvertAll(subject => - client.GetNumericAssignment(subject, assignmentTestCase.experiment)); - } + // break; + // case "string": + // var stringExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (string?)x); + // Assert.That(GetStringAssignments(assignmentTestCase), Is.EqualTo(stringExpectations)); - private static List GetIntegerAssignments(AssignmentTestCase assignmentTestCase) - { - var client = EppoClient.GetInstance(); - if (assignmentTestCase.subjectsWithAttributes != null) - { - return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetIntegerAssignment(subject.subjectKey, assignmentTestCase.experiment, - subject.subjectAttributes)); + // break; } - - return assignmentTestCase.subjects.ConvertAll(subject => - client.GetIntegerAssignment(subject, assignmentTestCase.experiment)); + Console.WriteLine(assignmentTestCase); } - private static List GetStringAssignments(AssignmentTestCase assignmentTestCase) - { - var client = EppoClient.GetInstance(); - if (assignmentTestCase.subjectsWithAttributes != null) - { - return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetStringAssignment(subject.subjectKey, assignmentTestCase.experiment, - subject.subjectAttributes)); - } - return assignmentTestCase.subjects.ConvertAll(subject => - client.GetStringAssignment(subject, assignmentTestCase.experiment)); - } + // private static List GetNumericAssignments(AssignmentTestCase assignmentTestCase) + // { + // var client = EppoClient.GetInstance(); + // if (assignmentTestCase.subjectsWithAttributes != null) + // { + // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetNumericAssignment(subject.subjectKey, assignmentTestCase.experiment, + // subject.subjectAttributes)); + // } + + // return assignmentTestCase.subjects.ConvertAll(subject => + // client.GetNumericAssignment(subject, assignmentTestCase.experiment)); + // } + + // private static List GetIntegerAssignments(AssignmentTestCase assignmentTestCase) + // { + // var client = EppoClient.GetInstance(); + // if (assignmentTestCase.subjectsWithAttributes != null) + // { + // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetIntegerAssignment(subject.subjectKey, assignmentTestCase.experiment, + // subject.subjectAttributes)); + // } + + // return assignmentTestCase.subjects.ConvertAll(subject => + // client.GetIntegerAssignment(subject, assignmentTestCase.experiment)); + // } + + // private static List GetStringAssignments(AssignmentTestCase assignmentTestCase) + // { + // var client = EppoClient.GetInstance(); + // if (assignmentTestCase.subjectsWithAttributes != null) + // { + // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetStringAssignment(subject.subjectKey, assignmentTestCase.experiment, + // subject.subjectAttributes)); + // } + + // return assignmentTestCase.subjects.ConvertAll(subject => + // client.GetStringAssignment(subject, assignmentTestCase.experiment)); + // } private static List GetTestAssignmentData() { var dir = new DirectoryInfo(Environment.CurrentDirectory).Parent?.Parent?.Parent?.FullName; - return Directory.EnumerateFiles($"{dir}/files/assignment-v2", "*.json") + return Directory.EnumerateFiles($"{dir}/files/ufc/tests", "*.json") .Select(File.ReadAllText) .Select(JsonConvert.DeserializeObject).ToList(); } @@ -147,18 +166,15 @@ public void LogAssignment(AssignmentLogData assignmentLogData) } } -public class SubjectWithAttributes + +public class AssignmentTestCase { - public string subjectKey { get; set; } + public required string Flag { get; set; } + public EppoValueType VariationType { get; set; } = EppoValueType.STRING; + public required object DefaultValue; + + public required List Subjects { get; set; } - public SubjectAttributes subjectAttributes { get; set; } } -public class AssignmentTestCase -{ - public string experiment { get; set; } - public string valueType { get; set; } = "string"; - public List? subjectsWithAttributes { get; set; } - public List subjects { get; set; } - public List expectedAssignments { get; set; } -} \ No newline at end of file +public record SubjectTestRecord(string SubjectKey, Subject SubjectAttributes, object Assignment); diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index 01ad8c2..d63a838 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -180,8 +180,8 @@ public void ShouldConvertAllocations() } ], 'doLog': false, - 'startAt': 5000, - 'endAt': 2147483647 + 'startAt': '2022-10-31T09:00:00.594Z', + 'endAt': '2050-10-31T09:00:00.594Z', }, { 'key': 'off-for-all', @@ -193,8 +193,8 @@ public void ShouldConvertAllocations() } ], 'doLog': true, - 'startAt': 5000, - 'endAt': 2147483647 + 'startAt': '2022-10-31T09:00:00.594Z', + 'endAt': '2050-10-31T09:00:00.594Z', } ] "; @@ -206,16 +206,16 @@ public void ShouldConvertAllocations() That(allocations?.Count, Is.EqualTo(2)); That(allocations?[0].key, Is.EqualTo("on-for-age-50+")); That(allocations?[0].doLog, Is.EqualTo(false)); - That(allocations?[0].startAt, Is.EqualTo(5000)); - That(allocations?[0].endAt, Is.EqualTo(Int32.MaxValue)); + // That(allocations?[0].startAt, Is.EqualTo(5000)); + // That(allocations?[0].endAt, Is.EqualTo(Int32.MaxValue)); That(allocations?[0].rules.Count, Is.EqualTo(1)); That(allocations?[0].splits.Count, Is.EqualTo(1)); That(allocations?[1].key, Is.EqualTo("off-for-all")); That(allocations?[1].doLog, Is.EqualTo(true)); - That(allocations?[1].startAt, Is.EqualTo(5000)); - That(allocations?[1].endAt, Is.EqualTo(Int32.MaxValue)); + // That(allocations?[1].startAt, Is.EqualTo(5000)); + // That(allocations?[1].endAt, Is.EqualTo(Int32.MaxValue)); That(allocations?[1].rules.Count, Is.EqualTo(0)); That(allocations?[1].splits.Count, Is.EqualTo(1)); diff --git a/eppo-sdk-test/helpers/AssignmentLogDataTest.cs b/eppo-sdk-test/helpers/AssignmentLogDataTest.cs index e9653ee..412d46f 100644 --- a/eppo-sdk-test/helpers/AssignmentLogDataTest.cs +++ b/eppo-sdk-test/helpers/AssignmentLogDataTest.cs @@ -12,7 +12,7 @@ public void ShouldReturnAssignmentLogData() "allocation", "variation", "subject", - new SubjectAttributes()); + new Subject()); Assert.That(assignmentLogData.experiment, Is.EqualTo("feature-flag-allocation")); } } diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index 0dc6713..c9b7bd1 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -11,7 +11,7 @@ public void ShouldMatchAndyRuleWithEmptyCondition() var ruleWithEmptyConditions = CreateRule(new List()); var rules = new List { ruleWithEmptyConditions }; - var subjectAttributes = new SubjectAttributes(); + var subjectAttributes = new Subject(); AddNameToSubjectAttribute(subjectAttributes); Assert.That(ruleWithEmptyConditions, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); @@ -21,7 +21,7 @@ public void ShouldMatchAndyRuleWithEmptyCondition() public void ShouldMatchAnyRuleWithEmptyRules() { var rules = new List(); - var subjectAttributes = new SubjectAttributes(); + var subjectAttributes = new Subject(); AddNameToSubjectAttribute(subjectAttributes); Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); @@ -35,7 +35,7 @@ public void ShouldMatchAnyRuleWhenNoRuleMatches() AddNumericConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes(); + var subjectAttributes = new Subject(); AddPriceToSubjectAttribute(subjectAttributes); Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); @@ -50,7 +50,7 @@ public void ShouldMatchAnyRuleWhenRuleMatches() AddSemVerConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes + var subjectAttributes = new Subject { { "price", 15 }, { "appVersion", "1.15.0" } @@ -67,7 +67,7 @@ public void ShouldNotMatchAnyRuleWhenThrowInvalidSubjectAttribute() AddNumericConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "price", "abcd" } }; + var subjectAttributes = new Subject { { "price", "abcd" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -80,7 +80,7 @@ public void ShouldMatchAnyRuleWithRegexCondition() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", "abcd" } }; + var subjectAttributes = new Subject { { "match", "abcd" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -93,7 +93,7 @@ public void ShouldNotMatchAnyRuleWithRegexConditionIsUnmatched() AddRegexConditionToRule(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "match", "123" } }; + var subjectAttributes = new Subject { { "match", "123" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -106,7 +106,7 @@ public void ShouldMatchAnyRuleWithOneOfRule() AddOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value2" } }; + var subjectAttributes = new Subject { { "oneOf", "value2" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -119,7 +119,7 @@ public void ShouldNotMatchAnyRuleWithOneOfRule() AddOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value3" } }; + var subjectAttributes = new Subject { { "oneOf", "value3" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -132,7 +132,7 @@ public void ShouldMatchAnyRuleWithNotOneOfRule() AddNotOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value3" } }; + var subjectAttributes = new Subject { { "oneOf", "value3" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.EqualTo(rule)); } @@ -145,7 +145,7 @@ public void ShouldNotMatchAnyRuleWithNotOneOfRuleNotPassed() AddNotOneOfCondition(rule); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "oneOf", "value1" } }; + var subjectAttributes = new Subject { { "oneOf", "value1" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -158,7 +158,7 @@ public void ShouldMatchRuleIsNullTrueNullType() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", null } }; + var subjectAttributes = new Subject { { "isnull", null } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -171,7 +171,7 @@ public void ShouldMatchRuleIsNullTrue() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", null } }; + var subjectAttributes = new Subject { { "isnull", null } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -185,7 +185,7 @@ public void ShouldMatchRuleIsNullNoAttribute() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { }; + var subjectAttributes = new Subject { }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -198,7 +198,7 @@ public void ShouldMatchRuleIsNullFalse() AddIsNullCondition(rule, false); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", "not null" } }; + var subjectAttributes = new Subject { { "isnull", "not null" } }; Assert.That(rule, Is.EqualTo(RuleValidator.FindMatchingRule(subjectAttributes, rules))); } @@ -211,7 +211,7 @@ public void ShouldNotMatchRuleIsNullTrue() AddIsNullCondition(rule, true); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", "not null" } }; + var subjectAttributes = new Subject { { "isnull", "not null" } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -224,7 +224,7 @@ public void ShouldNotMatchRuleIsNullFalse() AddIsNullCondition(rule, false); rules.Add(rule); - var subjectAttributes = new SubjectAttributes { { "isnull", null } }; + var subjectAttributes = new Subject { { "isnull", null } }; Assert.That(RuleValidator.FindMatchingRule(subjectAttributes, rules), Is.Null); } @@ -245,7 +245,7 @@ private static void AddIsNullCondition(Rule rule, Boolean value) new("occupation",OperatorType.MATCHES, "musician"), new("albumCount", OperatorType.GTE, 50) }); - private SubjectAttributes subject; + private Subject subject; private Variation matchVariation = new("match", "foo"); [SetUp] @@ -357,16 +357,17 @@ public void FlagEvaluation_ReturnsMatchingVariation() public void DisabledFlag_ReturnsNull() { var flag = new Flag("disabled", false, new List(), EppoValueType.BOOLEAN, new Dictionary(), TotalShards); - Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, new SubjectAttributes())); + Assert.Null(RuleValidator.EvaluateFlag(flag, SubjectKey, new Subject())); } [Test] public void FlagWithInactiveAllocations_ReturnsNull() { - var now = DateTimeOffset.Now.ToUnixTimeSeconds(); - var overAlloc = new Allocation("over", new List(), matchingSplits, false, null, endAt: now - 10000); - var futureAlloc = new Allocation("hasntStarted", new List(), matchingSplits, false, startAt: now + 60000, null); + var now = DateTimeOffset.Now; + var overAlloc = new Allocation("over", new List(), matchingSplits, false, null, endAt: now.Subtract( new TimeSpan( 0,0,10000)).DateTime ); + + var futureAlloc = new Allocation("hasntStarted", new List(), matchingSplits, false, startAt: now.Add( new TimeSpan( 0,0,6000)).DateTime, null); var flag = new Flag( "inactive_allocs", @@ -430,7 +431,7 @@ private static void AddRegexConditionToRule(Rule rule) rule.conditions.Add(condition); } - private static void AddPriceToSubjectAttribute(SubjectAttributes subjectAttributes) + private static void AddPriceToSubjectAttribute(Subject subjectAttributes) { subjectAttributes.Add("price", "30"); } @@ -448,7 +449,7 @@ private static void AddSemVerConditionToRule(Rule rule) rule.conditions.Add(new Condition("appVersion", OperatorType.LTE, "2.2.0")); } - private static void AddNameToSubjectAttribute(SubjectAttributes subjectAttributes) + private static void AddNameToSubjectAttribute(Subject subjectAttributes) { subjectAttributes.Add("name", "test"); } From 98a280c88017bf85be52252fb34dd56556fa19f8 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 09:44:55 -0600 Subject: [PATCH 15/27] undebug --- dot-net-sdk/EppoClient.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index d475fa2..1b233d8 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -109,8 +109,7 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC return assignment; } - public static EppoClient Init(EppoClientConfig eppoClientConfig, bool startPolling = true) - + public static EppoClient Init(EppoClientConfig eppoClientConfig) { lock (Baton) { @@ -144,7 +143,7 @@ public static EppoClient Init(EppoClientConfig eppoClientConfig, bool startPolli var fetchExperimentsTask = new FetchExperimentsTask(configurationStore, Constants.TIME_INTERVAL_IN_MILLIS, Constants.JITTER_INTERVAL_IN_MILLIS); - if (startPolling) fetchExperimentsTask.Run(); + fetchExperimentsTask.Run(); _client = new EppoClient(configurationStore, eppoClientConfig, fetchExperimentsTask); } From 29a07f0d5aa393d1a43b47996830573710796980 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 10:36:58 -0600 Subject: [PATCH 16/27] default value --- dot-net-sdk/EppoClient.cs | 20 ++++++++++---------- dot-net-sdk/validators/RuleValidator.cs | 4 +--- eppo-sdk-test/EppoClientTest.cs | 24 +++++++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index 1b233d8..d0ea2bd 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -30,31 +30,31 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } - public JObject? GetJsonAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) + public JObject GetJsonAssignment(string flagKey, string subjectKey, Subject subjectAttributes, JObject defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.JsonValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.JsonValue() ?? defaultValue; } - public bool? GetBoolAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) + public bool GetBoolAssignment(string flagKey, string subjectKey, Subject subjectAttributes, bool defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.BoolValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.BoolValue() ?? defaultValue; } - public double? GetNumericAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) + public double GetNumericAssignment(string flagKey, string subjectKey, Subject subjectAttributes, double defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.DoubleValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.DoubleValue() ?? defaultValue; } - public long? GetIntegerAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) + public long GetIntegerAssignment(string flagKey, string subjectKey, Subject subjectAttributes, long defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.IntegerValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.IntegerValue() ?? defaultValue; } - public string? GetStringAssignment(string flagKey, string subjectKey, Subject? subjectAttributes = null) + public string GetStringAssignment(string flagKey, string subjectKey, Subject subjectAttributes, string defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.StringValue(); + return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.StringValue() ?? defaultValue; } diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 3aa0326..1516b2a 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -28,9 +28,7 @@ public static partial class RuleValidator continue; } - if (!subjectAttributes.TryAdd("id", subjectKey)) { - Logger.Warn($"`id` {subjectKey} already added to subject attributes"); - } + subjectAttributes["id"] = subjectKey; if (allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) { diff --git a/eppo-sdk-test/EppoClientTest.cs b/eppo-sdk-test/EppoClientTest.cs index d20a31d..d5c8aae 100644 --- a/eppo-sdk-test/EppoClientTest.cs +++ b/eppo-sdk-test/EppoClientTest.cs @@ -22,7 +22,7 @@ public void Setup() { BaseUrl = _mockServer?.Urls[0]! }; - EppoClient.Init(config, startPolling: true); + EppoClient.Init(config); } private void SetupMockServer() @@ -60,17 +60,17 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) case (EppoValueType.BOOLEAN): var boolExpectations = assignmentTestCase.Subjects.ConvertAll(x => (bool?)x.Assignment); var assignments = assignmentTestCase.Subjects.ConvertAll(subject => - client.GetBoolAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); + client.GetBoolAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (bool)assignmentTestCase.DefaultValue)); - Assert.That(assignments, Is.EqualTo(boolExpectations)); + Assert.That(assignments, Is.EqualTo(boolExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); break; case (EppoValueType.INTEGER): var longExpectations = assignmentTestCase.Subjects.ConvertAll(x => (long?)x.Assignment); var longAssignments = assignmentTestCase.Subjects.ConvertAll(subject => - client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); + client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (long)assignmentTestCase.DefaultValue)); - Assert.That(longAssignments, Is.EqualTo(longExpectations)); + Assert.That(longAssignments, Is.EqualTo(longExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); break; @@ -149,12 +149,17 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) // client.GetStringAssignment(subject, assignmentTestCase.experiment)); // } - private static List GetTestAssignmentData() + private static List GetTestAssignmentData() { var dir = new DirectoryInfo(Environment.CurrentDirectory).Parent?.Parent?.Parent?.FullName; - return Directory.EnumerateFiles($"{dir}/files/ufc/tests", "*.json") - .Select(File.ReadAllText) - .Select(JsonConvert.DeserializeObject).ToList(); + var files = Directory.EnumerateFiles($"{dir}/files/ufc/tests", "*.json"); + var testCases = new List(){}; + foreach (var file in files) { + var atc = JsonConvert.DeserializeObject(File.ReadAllText(file))!; + atc.TestCaseFile = file; + testCases.Add(atc); + } + return testCases; } } @@ -172,6 +177,7 @@ public class AssignmentTestCase public required string Flag { get; set; } public EppoValueType VariationType { get; set; } = EppoValueType.STRING; public required object DefaultValue; + public string? TestCaseFile; public required List Subjects { get; set; } From 73998fc699a5975ee1eb0ec0f49ce564ea1ea815 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 11:03:21 -0600 Subject: [PATCH 17/27] implement expected type checking --- dot-net-sdk/EppoClient.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index d0ea2bd..a003d9a 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -1,4 +1,5 @@ -using eppo_sdk.constants; +using System.Linq.Expressions; +using eppo_sdk.constants; using eppo_sdk.dto; using eppo_sdk.exception; using eppo_sdk.helpers; @@ -30,31 +31,42 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } + private HasEppoValue _typeCheckedAssignment(string flagKey, string subjectKey, Subject subjectAttributes, EppoValueType expectedValueType, object defaultValue) { + var result = GetAssignment(flagKey, subjectKey, subjectAttributes); + var eppoDefaultValue = new HasEppoValue(defaultValue); + if (HasEppoValue.IsNullValue(result)) return eppoDefaultValue; + var assignment = result!; + if (assignment.Type != expectedValueType) { + Logger.Warn($"Expected type {expectedValueType} does not match parsed type {assignment.Type}"); + return eppoDefaultValue; + } + return assignment; + } public JObject GetJsonAssignment(string flagKey, string subjectKey, Subject subjectAttributes, JObject defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.JsonValue() ?? defaultValue; + return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.JSON, defaultValue).JsonValue(); } public bool GetBoolAssignment(string flagKey, string subjectKey, Subject subjectAttributes, bool defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.BoolValue() ?? defaultValue; + return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.BOOLEAN, defaultValue).BoolValue(); } public double GetNumericAssignment(string flagKey, string subjectKey, Subject subjectAttributes, double defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.DoubleValue() ?? defaultValue; + return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.NUMERIC, defaultValue).DoubleValue(); } public long GetIntegerAssignment(string flagKey, string subjectKey, Subject subjectAttributes, long defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.IntegerValue() ?? defaultValue; + return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.INTEGER, defaultValue).IntegerValue(); } public string GetStringAssignment(string flagKey, string subjectKey, Subject subjectAttributes, string defaultValue) { - return GetAssignment(flagKey, subjectKey, subjectAttributes ?? new Subject())?.StringValue() ?? defaultValue; + return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.STRING, defaultValue).StringValue(); } From 77b50451a2add785a90ddc4e0448402cfb1c84c7 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 11:32:56 -0600 Subject: [PATCH 18/27] Allow rules list to be null --- dot-net-sdk/validators/RuleValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 1516b2a..12df98e 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -30,7 +30,7 @@ public static partial class RuleValidator subjectAttributes["id"] = subjectKey; - if (allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) + if (allocation?.rules == null || allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) { foreach (var split in allocation.splits) { From 0a38e3935126b22e7111fed2a9d5ea2c0e2dae68 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 11:33:30 -0600 Subject: [PATCH 19/27] Parse value when it is encoded JSON --- dot-net-sdk/dto/HasEppoValue.cs | 54 ++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/dot-net-sdk/dto/HasEppoValue.cs b/dot-net-sdk/dto/HasEppoValue.cs index 4396c16..9fa881b 100644 --- a/dot-net-sdk/dto/HasEppoValue.cs +++ b/dot-net-sdk/dto/HasEppoValue.cs @@ -13,29 +13,14 @@ public class HasEppoValue private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - private bool _typed; - private Object? _value; public Object? Value { get { return _value; } set { - if (!_typed) - { - _type = InferTypeFromValue(value); - _value = value; - } - } - } - public Object? typedValue - { - get { return _value; } - set - { - _typed = true; - _type = InferTypeFromValue(value); - _value = value; + _type = InferTypeFromValue(value, out object? typedValue); + _value = typedValue ?? value; } } private EppoValueType _type; @@ -65,8 +50,9 @@ public List ArrayValue() => _nonNullValue>((object o) => public JObject JsonValue() => _nonNullValue((o) => (JObject)o); - private static EppoValueType InferTypeFromValue(Object? value) + private static EppoValueType InferTypeFromValue(Object? value, out Object? typedValue) { + typedValue = null; if (value == null) return EppoValueType.NULL; if (value is Array || value.GetType().IsArray || value is JArray || value is List || value is IEnumerable) @@ -89,6 +75,12 @@ private static EppoValueType InferTypeFromValue(Object? value) } else if (value is string || value is String) { + // This string could be encoded JSON. + if (TryGetJObject((string)value, out var jObject)) + { + typedValue = jObject; + return EppoValueType.JSON; + } return EppoValueType.STRING; } else if (value is JObject) @@ -104,6 +96,32 @@ private static EppoValueType InferTypeFromValue(Object? value) } } + private static bool TryGetJObject(string jsonString, out JObject? jObject) + { + jObject = null; + if (string.IsNullOrWhiteSpace(jsonString)) + { + return false; + } + + try + { + // Attempt to parse the JSON string using JToken.Parse + var token = JToken.Parse(jsonString); + // Check if the parsed token is of type JObject (represents an object) + if (token is JObject @object) + { + jObject = @object; + return true; + } + return false; + } + catch (JsonReaderException) + { + return false; + } + } + public static HasEppoValue Bool(string? value) => new(value, EppoValueType.BOOLEAN); public static HasEppoValue Bool(bool value) => new(value, EppoValueType.BOOLEAN); public static HasEppoValue Number(string value) => new(value, EppoValueType.NUMERIC); From 9fe83fbe355e64dd14fa417da9ea631e41a8b403 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 11:34:32 -0600 Subject: [PATCH 20/27] update tests --- eppo-sdk-test/EppoClientTest.cs | 88 +++++++-------------------------- 1 file changed, 18 insertions(+), 70 deletions(-) diff --git a/eppo-sdk-test/EppoClientTest.cs b/eppo-sdk-test/EppoClientTest.cs index d5c8aae..da972a5 100644 --- a/eppo-sdk-test/EppoClientTest.cs +++ b/eppo-sdk-test/EppoClientTest.cs @@ -3,6 +3,7 @@ using eppo_sdk.dto; using eppo_sdk.logger; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using WireMock.Matchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -63,7 +64,6 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) client.GetBoolAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (bool)assignmentTestCase.DefaultValue)); Assert.That(assignments, Is.EqualTo(boolExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); - break; case (EppoValueType.INTEGER): var longExpectations = assignmentTestCase.Subjects.ConvertAll(x => (long?)x.Assignment); @@ -71,85 +71,33 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (long)assignmentTestCase.DefaultValue)); Assert.That(longAssignments, Is.EqualTo(longExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); - break; - case (EppoValueType.JSON): - // var longExpectations = assignmentTestCase.Subjects.ConvertAll(x => (long?)x.Assignment); - // var longAssignments = assignmentTestCase.Subjects.ConvertAll(subject => - // client.GetIntegerAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); - - // Assert.That(longAssignments, Is.EqualTo(longExpectations)); + var jsonExpectations = assignmentTestCase.Subjects.ConvertAll(x => (JObject)x.Assignment); + var jsonAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + client.GetJsonAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (JObject)assignmentTestCase.DefaultValue)); + Assert.That(jsonAssignments, Is.EqualTo(jsonExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); break; - // case (EppoValueType.NUMERIC): - // var numExpectation = assignmentTestCase.Subjects.ConvertAll(x => (double?)x.Assignment); - // var numAssignments = assignmentTestCase.Subjects.ConvertAll(subject => - // client.GetNumericAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes)); - - // Assert.That(numAssignments, Is.EqualTo(numExpectation)); + case (EppoValueType.NUMERIC): + var numExpectations = assignmentTestCase.Subjects.ConvertAll(x => (double?)x.Assignment); + var numAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + client.GetNumericAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (double)assignmentTestCase.DefaultValue)); - // break; - // case "number": - // var numericExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (double?)x); - // Assert.That(GetNumericAssignments(assignmentTestCase), Is.EqualTo(numericExpectations)); - - // break; - // case "integer": - // var intExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (long?)x); - // Assert.That(GetIntegerAssignments(assignmentTestCase), Is.EqualTo(intExpectations)); - - // break; - // case "string": - // var stringExpectations = assignmentTestCase.expectedAssignments.ConvertAll(x => (string?)x); - // Assert.That(GetStringAssignments(assignmentTestCase), Is.EqualTo(stringExpectations)); + Assert.That(numAssignments, Is.EqualTo(numExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); + break; + case (EppoValueType.STRING): + var stringExpectations = assignmentTestCase.Subjects.ConvertAll(x => (string)x.Assignment); + var stringAssignments = assignmentTestCase.Subjects.ConvertAll(subject => + client.GetStringAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (string)assignmentTestCase.DefaultValue)); - // break; + Assert.That(stringAssignments, Is.EqualTo(stringExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); + break; } - Console.WriteLine(assignmentTestCase); } - // private static List GetNumericAssignments(AssignmentTestCase assignmentTestCase) - // { - // var client = EppoClient.GetInstance(); - // if (assignmentTestCase.subjectsWithAttributes != null) - // { - // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetNumericAssignment(subject.subjectKey, assignmentTestCase.experiment, - // subject.subjectAttributes)); - // } - - // return assignmentTestCase.subjects.ConvertAll(subject => - // client.GetNumericAssignment(subject, assignmentTestCase.experiment)); - // } - - // private static List GetIntegerAssignments(AssignmentTestCase assignmentTestCase) - // { - // var client = EppoClient.GetInstance(); - // if (assignmentTestCase.subjectsWithAttributes != null) - // { - // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetIntegerAssignment(subject.subjectKey, assignmentTestCase.experiment, - // subject.subjectAttributes)); - // } - - // return assignmentTestCase.subjects.ConvertAll(subject => - // client.GetIntegerAssignment(subject, assignmentTestCase.experiment)); - // } - - // private static List GetStringAssignments(AssignmentTestCase assignmentTestCase) - // { - // var client = EppoClient.GetInstance(); - // if (assignmentTestCase.subjectsWithAttributes != null) - // { - // return assignmentTestCase.subjectsWithAttributes.ConvertAll(subject => client.GetStringAssignment(subject.subjectKey, assignmentTestCase.experiment, - // subject.subjectAttributes)); - // } - - // return assignmentTestCase.subjects.ConvertAll(subject => - // client.GetStringAssignment(subject, assignmentTestCase.experiment)); - // } - - private static List GetTestAssignmentData() + static List GetTestAssignmentData() { var dir = new DirectoryInfo(Environment.CurrentDirectory).Parent?.Parent?.Parent?.FullName; var files = Directory.EnumerateFiles($"{dir}/files/ufc/tests", "*.json"); From a8bf5575ad35fb5d2af1185821da0eea57e10f35 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 11:59:57 -0600 Subject: [PATCH 21/27] merge artifacts --- dot-net-sdk/dto/Condition.cs | 1 - dot-net-sdk/validators/RuleValidator.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dot-net-sdk/dto/Condition.cs b/dot-net-sdk/dto/Condition.cs index d4d1b83..3fd4fc6 100644 --- a/dot-net-sdk/dto/Condition.cs +++ b/dot-net-sdk/dto/Condition.cs @@ -3,7 +3,6 @@ namespace eppo_sdk.dto; -public class Condition : HasEppoValue public class Condition : HasEppoValue { public string Attribute { get; set; } diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 515de71..12df98e 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -82,7 +82,6 @@ private static bool EvaluateCondition(Subject subjectAttributes, Condition condi else if (subjectAttributes.TryGetValue(condition.Attribute, out Object? outVal)) { var value = new HasEppoValue(outVal!); // Assuming non-null for simplicity, handle nulls as necessary - var value = new HasEppoValue(outVal!); // Assuming non-null for simplicity, handle nulls as necessary switch (condition.Operator) { From eaa0cdffd5e39640df27f4f498377e8e6f5c7852 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 12:23:11 -0600 Subject: [PATCH 22/27] nits --- dot-net-sdk/EppoClient.cs | 4 +++- dot-net-sdk/constants/Constants.cs | 2 +- dot-net-sdk/dto/Flag.cs | 2 +- dot-net-sdk/dto/Variation.cs | 2 +- dot-net-sdk/http/EppoHttpClient.cs | 1 - eppo-sdk-test/ValidateJsonConvertion.cs | 8 ++++---- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index a003d9a..8050096 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -37,7 +37,7 @@ private HasEppoValue _typeCheckedAssignment(string flagKey, string subjectKey, S if (HasEppoValue.IsNullValue(result)) return eppoDefaultValue; var assignment = result!; if (assignment.Type != expectedValueType) { - Logger.Warn($"Expected type {expectedValueType} does not match parsed type {assignment.Type}"); + Logger.Warn($"[Eppo SDK] Expected type {expectedValueType} does not match parsed type {assignment.Type}"); return eppoDefaultValue; } return assignment; @@ -92,6 +92,7 @@ public string GetStringAssignment(string flagKey, string subjectKey, Subject sub var result = RuleValidator.EvaluateFlag(configuration, subjectKey, subjectAttributes); if (result == null) { + Logger.Info("[Eppo SDK] No assigned variation. The subject attributes did not match any targeting rules"); return null; } @@ -99,6 +100,7 @@ public string GetStringAssignment(string flagKey, string subjectKey, Subject sub if (HasEppoValue.IsNullValue(assignment)) { + Logger.Warn("[Eppo SDK] Assigned varition is null"); return null; } diff --git a/dot-net-sdk/constants/Constants.cs b/dot-net-sdk/constants/Constants.cs index 4f7e38c..33158d5 100644 --- a/dot-net-sdk/constants/Constants.cs +++ b/dot-net-sdk/constants/Constants.cs @@ -15,4 +15,4 @@ public class Constants public const int MAX_CACHE_ENTRIES = 1000; public const string UFC_ENDPOINT = "/flag-config/v1/config"; -} \ No newline at end of file +} diff --git a/dot-net-sdk/dto/Flag.cs b/dot-net-sdk/dto/Flag.cs index 52e830f..c73279e 100644 --- a/dot-net-sdk/dto/Flag.cs +++ b/dot-net-sdk/dto/Flag.cs @@ -2,4 +2,4 @@ namespace eppo_sdk.dto; public record Flag(string key, bool enabled, List allocations, EppoValueType variationType, Dictionary variations, int totalShards) { -} \ No newline at end of file +} diff --git a/dot-net-sdk/dto/Variation.cs b/dot-net-sdk/dto/Variation.cs index 71b23b1..478447a 100644 --- a/dot-net-sdk/dto/Variation.cs +++ b/dot-net-sdk/dto/Variation.cs @@ -9,4 +9,4 @@ public Variation(string key, object value) { Key = key; Value = value; } -} \ No newline at end of file +} diff --git a/dot-net-sdk/http/EppoHttpClient.cs b/dot-net-sdk/http/EppoHttpClient.cs index c81013a..8ffee74 100644 --- a/dot-net-sdk/http/EppoHttpClient.cs +++ b/dot-net-sdk/http/EppoHttpClient.cs @@ -53,7 +53,6 @@ Dictionary headers { _defaultParams.ToList().ForEach(x => parameters.Add(x.Key, x.Value)); -var fullURL = _baseUrl + url; var request = new RestRequest { Timeout = _requestTimeOutMillis diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index 57d215c..5c4795e 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -207,16 +207,16 @@ public void ShouldConvertAllocations() That(allocations?.Count, Is.EqualTo(2)); That(allocations?[0].key, Is.EqualTo("on-for-age-50+")); That(allocations?[0].doLog, Is.EqualTo(false)); - // That(allocations?[0].startAt, Is.EqualTo(5000)); - // That(allocations?[0].endAt, Is.EqualTo(Int32.MaxValue)); + That(allocations?[0].startAt, Is.EqualTo(DateTime.Parse("2022-10-31T09:00:00.594Z").ToUniversalTime())); + That(allocations?[0].endAt, Is.EqualTo(DateTime.Parse("2050-10-31T09:00:00.594Z").ToUniversalTime())); That(allocations?[0].rules.Count, Is.EqualTo(1)); That(allocations?[0].splits.Count, Is.EqualTo(1)); That(allocations?[1].key, Is.EqualTo("off-for-all")); That(allocations?[1].doLog, Is.EqualTo(true)); - // That(allocations?[1].startAt, Is.EqualTo(5000)); - // That(allocations?[1].endAt, Is.EqualTo(Int32.MaxValue)); + That(allocations?[1].startAt, Is.EqualTo(DateTime.Parse("2022-10-31T09:00:00.594Z").ToUniversalTime())); + That(allocations?[1].endAt, Is.EqualTo(DateTime.Parse("2050-10-31T09:00:00.594Z").ToUniversalTime())); That(allocations?[1].rules.Count, Is.EqualTo(0)); That(allocations?[1].splits.Count, Is.EqualTo(1)); From 7460cf89c6fc37b59f90f0e8194f504eed30cd78 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Mon, 3 Jun 2024 12:26:08 -0600 Subject: [PATCH 23/27] datetime hard --- dot-net-sdk/validators/RuleValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 12df98e..943354e 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -20,7 +20,7 @@ public static partial class RuleValidator { if (!flag.enabled) return null; - var now = DateTimeOffset.Now; + var now = DateTimeOffset.Now.ToUniversalTime(); foreach (var allocation in flag.allocations) { if (allocation.startAt.HasValue && allocation.startAt.Value > now || allocation.endAt.HasValue && allocation.endAt.Value < now) From 4b1b5c0a9a7c747126cc5ab5dbde4814362a004f Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 5 Jun 2024 09:55:55 -0600 Subject: [PATCH 24/27] case sensitive --- dot-net-sdk/validators/RuleValidator.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 943354e..ac32a62 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -147,7 +147,7 @@ private static bool EvaluateCondition(Subject subjectAttributes, Condition condi } case MATCHES: { - return Regex.Match(value.StringValue(), condition.StringValue(), RegexOptions.IgnoreCase).Success; + return Regex.Match(value.StringValue(), condition.StringValue()).Success; } case ONE_OF: { @@ -170,8 +170,5 @@ private static bool EvaluateCondition(Subject subjectAttributes, Condition condi internal class Compare { - public static bool IsOneOf(string a, List arrayValues) - { - return arrayValues.ConvertAll(v => v.ToLower()).IndexOf(a.ToLower()) >= 0; - } + public static bool IsOneOf(string a, List arrayValues) => arrayValues.IndexOf(a) >= 0; } From b0c234425d5d6aa712a1921446125d74395ba82c Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Wed, 5 Jun 2024 10:03:31 -0600 Subject: [PATCH 25/27] logger updates --- dot-net-sdk/dto/HasEppoValue.cs | 3 +-- dot-net-sdk/http/ExperimentConfigurationRequester.cs | 2 +- dot-net-sdk/validators/RuleValidator.cs | 5 ----- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/dot-net-sdk/dto/HasEppoValue.cs b/dot-net-sdk/dto/HasEppoValue.cs index 9fa881b..60911c2 100644 --- a/dot-net-sdk/dto/HasEppoValue.cs +++ b/dot-net-sdk/dto/HasEppoValue.cs @@ -90,8 +90,7 @@ private static EppoValueType InferTypeFromValue(Object? value, out Object? typed else { Type type = value!.GetType(); - Logger.Error($"Unexpected value of type {type}"); - Console.WriteLine($"Unexpected value of type {type}"); + Logger.Error($"[Eppo SDK] Unexpected value of type {type}"); return EppoValueType.NULL; } } diff --git a/dot-net-sdk/http/ExperimentConfigurationRequester.cs b/dot-net-sdk/http/ExperimentConfigurationRequester.cs index 60247dc..78e9e3b 100644 --- a/dot-net-sdk/http/ExperimentConfigurationRequester.cs +++ b/dot-net-sdk/http/ExperimentConfigurationRequester.cs @@ -21,7 +21,7 @@ public ExperimentConfigurationRequester(EppoHttpClient eppoHttpClient) { } catch (Exception e) { - logger.Warn($"Unable to Fetch Experiment Configuration: {e.Message}"); + logger.Warn($"[Eppo SDK] Unable to Fetch Experiment Configuration: {e.Message}"); } return null; diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index ac32a62..158b3a9 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -4,8 +4,6 @@ using NuGet.Versioning; using eppo_sdk.helpers; using eppo_sdk.exception; -using Microsoft.Extensions.Logging; -using NLog; namespace eppo_sdk.validators; @@ -13,9 +11,6 @@ namespace eppo_sdk.validators; public static partial class RuleValidator { - - private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); - public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, Subject subjectAttributes) { if (!flag.enabled) return null; From 7967223aca5d7ee3f7618f66d156b5e83e968497 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 7 Jun 2024 09:30:41 -0600 Subject: [PATCH 26/27] fix assignment logging --- dot-net-sdk/EppoClient.cs | 36 ++++++++----- dot-net-sdk/dto/AssignmentLogData.cs | 52 +++++++++++-------- dot-net-sdk/dto/Flag.cs | 8 ++- dot-net-sdk/dto/FlagEvaluation.cs | 18 ++++++- dot-net-sdk/dto/Split.cs | 2 +- dot-net-sdk/helpers/AppDetails.cs | 10 ++++ dot-net-sdk/validators/RuleValidator.cs | 19 ++++--- eppo-sdk-test/EppoClientTest.cs | 2 +- eppo-sdk-test/ValidateJsonConvertion.cs | 24 ++++----- .../helpers/AssignmentLogDataTest.cs | 7 ++- eppo-sdk-test/validators/RuleValidatorTest.cs | 8 +-- 11 files changed, 122 insertions(+), 64 deletions(-) diff --git a/dot-net-sdk/EppoClient.cs b/dot-net-sdk/EppoClient.cs index 8050096..26b94a7 100644 --- a/dot-net-sdk/EppoClient.cs +++ b/dot-net-sdk/EppoClient.cs @@ -31,12 +31,14 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC _fetchExperimentsTask = fetchExperimentsTask; } - private HasEppoValue _typeCheckedAssignment(string flagKey, string subjectKey, Subject subjectAttributes, EppoValueType expectedValueType, object defaultValue) { + private HasEppoValue _typeCheckedAssignment(string flagKey, string subjectKey, Subject subjectAttributes, EppoValueType expectedValueType, object defaultValue) + { var result = GetAssignment(flagKey, subjectKey, subjectAttributes); var eppoDefaultValue = new HasEppoValue(defaultValue); if (HasEppoValue.IsNullValue(result)) return eppoDefaultValue; var assignment = result!; - if (assignment.Type != expectedValueType) { + if (assignment.Type != expectedValueType) + { Logger.Warn($"[Eppo SDK] Expected type {expectedValueType} does not match parsed type {assignment.Type}"); return eppoDefaultValue; } @@ -47,7 +49,7 @@ public JObject GetJsonAssignment(string flagKey, string subjectKey, Subject subj return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.JSON, defaultValue).JsonValue(); } - public bool GetBoolAssignment(string flagKey, string subjectKey, Subject subjectAttributes, bool defaultValue) + public bool GetBooleanAssignment(string flagKey, string subjectKey, Subject subjectAttributes, bool defaultValue) { return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.BOOLEAN, defaultValue).BoolValue(); } @@ -104,22 +106,28 @@ public string GetStringAssignment(string flagKey, string subjectKey, Subject sub return null; } - try - { - _eppoClientConfig.AssignmentLogger - .LogAssignment(new AssignmentLogData( + AssignmentLogData assignmentEvent = new AssignmentLogData( flagKey, result.AllocationKey, - assignment.StringValue() ?? "null", + result.Variation.Key, subjectKey, - subjectAttributes - )); - } - catch (Exception) + subjectAttributes, + AppDetails.GetInstance().AsDict(), + result.ExtraLogging + ); + + if (result.DoLog) { - // Ignore Exception + try + { + _eppoClientConfig.AssignmentLogger + .LogAssignment(assignmentEvent); + } + catch (Exception) + { + // Ignore Exception + } } - return assignment; } diff --git a/dot-net-sdk/dto/AssignmentLogData.cs b/dot-net-sdk/dto/AssignmentLogData.cs index 8ec356c..d565de7 100644 --- a/dot-net-sdk/dto/AssignmentLogData.cs +++ b/dot-net-sdk/dto/AssignmentLogData.cs @@ -1,28 +1,38 @@ +using System.Reflection.Metadata.Ecma335; + namespace eppo_sdk.dto; -public class AssignmentLogData +public record AssignmentLogData { - public string experiment; - public string featureFlag; - public string allocation; - public string variation; - public DateTime timestamp; - public string subject; - public Subject subjectAttributes; + public string Experiment; + public string FeatureFlag; + public string Allocation; + public string Variation; + public DateTime Timestamp; + public string Subject; + public Subject SubjectAttributes; + + public IReadOnlyDictionary? ExtraLogging; + public IReadOnlyDictionary MetaData; - public AssignmentLogData( - string featureFlag, - string allocation, - string variation, - string subject, - Subject subjectAttributes) + public AssignmentLogData(string featureFlag, + string allocation, + string variation, + string subject, + Subject subjectAttributes, + IReadOnlyDictionary metaData, + IReadOnlyDictionary extraLoggging + + ) { - this.experiment = featureFlag + "-" + allocation; - this.featureFlag = featureFlag; - this.allocation = allocation; - this.variation = variation; - this.timestamp = new DateTime(); - this.subject = subject; - this.subjectAttributes = subjectAttributes; + this.Experiment = featureFlag + "-" + allocation; + this.FeatureFlag = featureFlag; + this.Allocation = allocation; + this.Variation = variation; + this.Timestamp = new DateTime(); + this.Subject = subject; + this.SubjectAttributes = subjectAttributes; + MetaData = metaData; + ExtraLogging = extraLoggging; } } diff --git a/dot-net-sdk/dto/Flag.cs b/dot-net-sdk/dto/Flag.cs index c73279e..75e2737 100644 --- a/dot-net-sdk/dto/Flag.cs +++ b/dot-net-sdk/dto/Flag.cs @@ -1,5 +1,11 @@ namespace eppo_sdk.dto; -public record Flag(string key, bool enabled, List allocations, EppoValueType variationType, Dictionary variations, int totalShards) +public record Flag( + string key, + bool enabled, + List Allocations, + EppoValueType variationType, + Dictionary variations, + int totalShards) { } diff --git a/dot-net-sdk/dto/FlagEvaluation.cs b/dot-net-sdk/dto/FlagEvaluation.cs index d206762..d6253a0 100644 --- a/dot-net-sdk/dto/FlagEvaluation.cs +++ b/dot-net-sdk/dto/FlagEvaluation.cs @@ -1,3 +1,19 @@ namespace eppo_sdk.dto; -public record FlagEvaluation(Variation Variation, bool DoLog, string AllocationKey); +public record FlagEvaluation +{ + public Variation Variation; + public bool DoLog; + public string AllocationKey; + public IReadOnlyDictionary ExtraLogging; + + public FlagEvaluation(Variation variation, bool doLog, string allocationKey, IReadOnlyDictionary? extraLogging) + { + Variation = variation; + DoLog = doLog; + AllocationKey = allocationKey; + ExtraLogging = extraLogging == null ? + new Dictionary() : + (IReadOnlyDictionary)extraLogging.ToDictionary(pair => pair.Key, pair => Convert.ToString(pair.Value)); + } +} diff --git a/dot-net-sdk/dto/Split.cs b/dot-net-sdk/dto/Split.cs index 15b05f4..8d149bf 100644 --- a/dot-net-sdk/dto/Split.cs +++ b/dot-net-sdk/dto/Split.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; namespace eppo_sdk.dto; -public record Split(string variationKey, List shards, IReadOnlyDictionary? extraLogging) +public record Split(string VariationKey, List Shards, IReadOnlyDictionary? ExtraLogging) { } diff --git a/dot-net-sdk/helpers/AppDetails.cs b/dot-net-sdk/helpers/AppDetails.cs index 7f50d7d..3bb278a 100644 --- a/dot-net-sdk/helpers/AppDetails.cs +++ b/dot-net-sdk/helpers/AppDetails.cs @@ -4,6 +4,7 @@ namespace eppo_sdk.helpers; public class AppDetails { + private const string SDK_LANG = "c#"; static AppDetails? _instance; private readonly string? _version; @@ -37,4 +38,13 @@ public string GetVersion() { return this._version!; } + + public IReadOnlyDictionary AsDict() + { + return new Dictionary() { + ["sdkLanguage"] = SDK_LANG, + ["sdkName"] = GetName(), + ["sdkVersion"] = GetVersion() + }; + } } \ No newline at end of file diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 158b3a9..6ca0bb7 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -11,31 +11,36 @@ namespace eppo_sdk.validators; public static partial class RuleValidator { + private const string SUBJECT_KEY_FIELD = "id"; + public static FlagEvaluation? EvaluateFlag(Flag flag, string subjectKey, Subject subjectAttributes) { if (!flag.enabled) return null; var now = DateTimeOffset.Now.ToUniversalTime(); - foreach (var allocation in flag.allocations) + foreach (var allocation in flag.Allocations) { if (allocation.startAt.HasValue && allocation.startAt.Value > now || allocation.endAt.HasValue && allocation.endAt.Value < now) { continue; } - subjectAttributes["id"] = subjectKey; + if (!subjectAttributes.ContainsKey(SUBJECT_KEY_FIELD)) + { + subjectAttributes[SUBJECT_KEY_FIELD] = subjectKey; + } - if (allocation?.rules == null || allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) + if (allocation.rules == null || allocation.rules.Count == 0 || MatchesAnyRule(allocation.rules, subjectAttributes)) { foreach (var split in allocation.splits) { - if (MatchesAllShards(split.shards, subjectKey, flag.totalShards)) + if (MatchesAllShards(split.Shards, subjectKey, flag.totalShards)) { - if (flag.variations.TryGetValue(split.variationKey, out Variation variation) && variation != null) + if (flag.variations.TryGetValue(split.VariationKey, out Variation? variation) && variation != null) { - return new FlagEvaluation(variation, allocation.doLog, allocation.key); + return new FlagEvaluation(variation, allocation.doLog, allocation.key, split.ExtraLogging); } - throw new ExperimentConfigurationNotFound($"Variation {split.variationKey} could not be found"); + throw new ExperimentConfigurationNotFound($"Variation {split.VariationKey} could not be found"); } } diff --git a/eppo-sdk-test/EppoClientTest.cs b/eppo-sdk-test/EppoClientTest.cs index da972a5..e5d79f9 100644 --- a/eppo-sdk-test/EppoClientTest.cs +++ b/eppo-sdk-test/EppoClientTest.cs @@ -61,7 +61,7 @@ public void ShouldValidateAssignments(AssignmentTestCase assignmentTestCase) case (EppoValueType.BOOLEAN): var boolExpectations = assignmentTestCase.Subjects.ConvertAll(x => (bool?)x.Assignment); var assignments = assignmentTestCase.Subjects.ConvertAll(subject => - client.GetBoolAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (bool)assignmentTestCase.DefaultValue)); + client.GetBooleanAssignment(assignmentTestCase.Flag, subject.SubjectKey, subject.SubjectAttributes, (bool)assignmentTestCase.DefaultValue)); Assert.That(assignments, Is.EqualTo(boolExpectations), $"Unexpected values for test file: {assignmentTestCase.TestCaseFile}"); break; diff --git a/eppo-sdk-test/ValidateJsonConvertion.cs b/eppo-sdk-test/ValidateJsonConvertion.cs index 5c4795e..2e47534 100644 --- a/eppo-sdk-test/ValidateJsonConvertion.cs +++ b/eppo-sdk-test/ValidateJsonConvertion.cs @@ -65,18 +65,18 @@ public void ShouldConvertSplits() { That(splits, Is.Not.Null); That(splits?.Count, Is.EqualTo(1)); - That(splits?[0].variationKey, Is.EqualTo("on")); - That(splits?[0].shards.Count, Is.EqualTo(2)); - That(splits?[0].shards[0].salt, Is.EqualTo("some-salt")); - That(splits?[0].shards[0].ranges.Count, Is.EqualTo(2)); - That(splits?[0].shards[0].ranges[0].start, Is.EqualTo(0)); - That(splits?[0].shards[0].ranges[0].end, Is.EqualTo(2500)); - That(splits?[0].shards[0].ranges[1].start, Is.EqualTo(2500)); - That(splits?[0].shards[0].ranges[1].end, Is.EqualTo(9999)); - - That(splits?[0].shards[1].salt, Is.EqualTo("some-salt-two")); - - That(splits?[0].extraLogging, Is.EquivalentTo(new Dictionary + That(splits?[0].VariationKey, Is.EqualTo("on")); + That(splits?[0].Shards.Count, Is.EqualTo(2)); + That(splits?[0].Shards[0].salt, Is.EqualTo("some-salt")); + That(splits?[0].Shards[0].ranges.Count, Is.EqualTo(2)); + That(splits?[0].Shards[0].ranges[0].start, Is.EqualTo(0)); + That(splits?[0].Shards[0].ranges[0].end, Is.EqualTo(2500)); + That(splits?[0].Shards[0].ranges[1].start, Is.EqualTo(2500)); + That(splits?[0].Shards[0].ranges[1].end, Is.EqualTo(9999)); + + That(splits?[0].Shards[1].salt, Is.EqualTo("some-salt-two")); + + That(splits?[0].ExtraLogging, Is.EquivalentTo(new Dictionary { ["foo"] = "bar", ["bar"] = "baz" diff --git a/eppo-sdk-test/helpers/AssignmentLogDataTest.cs b/eppo-sdk-test/helpers/AssignmentLogDataTest.cs index 412d46f..9a74955 100644 --- a/eppo-sdk-test/helpers/AssignmentLogDataTest.cs +++ b/eppo-sdk-test/helpers/AssignmentLogDataTest.cs @@ -1,4 +1,5 @@ using eppo_sdk.dto; +using eppo_sdk.helpers; namespace eppo_sdk_test.helpers; @@ -12,7 +13,9 @@ public void ShouldReturnAssignmentLogData() "allocation", "variation", "subject", - new Subject()); - Assert.That(assignmentLogData.experiment, Is.EqualTo("feature-flag-allocation")); + new Subject(), + AppDetails.GetInstance().AsDict(), + new Dictionary()); + Assert.That(assignmentLogData.Experiment, Is.EqualTo("feature-flag-allocation")); } } diff --git a/eppo-sdk-test/validators/RuleValidatorTest.cs b/eppo-sdk-test/validators/RuleValidatorTest.cs index c9b7bd1..984bd3b 100644 --- a/eppo-sdk-test/validators/RuleValidatorTest.cs +++ b/eppo-sdk-test/validators/RuleValidatorTest.cs @@ -301,21 +301,21 @@ public void Setup() [Test] public void NoMatchingShards_ReturnsFalse() { - Assert.That(RuleValidator.MatchesAllShards(nonMatchingSplits[0].shards, SubjectKey, TotalShards), Is.False); + Assert.That(RuleValidator.MatchesAllShards(nonMatchingSplits[0].Shards, SubjectKey, TotalShards), Is.False); } [Test] public void SomeMatchingShards_ReturnsFalse() { - var allShards = matchingSplits[0].shards; - allShards.AddRange(nonMatchingSplits[0].shards); + var allShards = matchingSplits[0].Shards; + allShards.AddRange(nonMatchingSplits[0].Shards); Assert.That(RuleValidator.MatchesAllShards(allShards, SubjectKey, TotalShards), Is.False); } [Test] public void MatchesShards_ReturnsTrue() { - Assert.That(RuleValidator.MatchesAllShards(matchingSplits[0].shards, SubjectKey, TotalShards), Is.True); + Assert.That(RuleValidator.MatchesAllShards(matchingSplits[0].Shards, SubjectKey, TotalShards), Is.True); } [Test] From 5a241c768daf5c833d460897288d7ff6d27365a6 Mon Sep 17 00:00:00 2001 From: Ty Potter Date: Fri, 7 Jun 2024 10:01:40 -0600 Subject: [PATCH 27/27] convert numbers to string based on convention --- dot-net-sdk/validators/RuleValidator.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/dot-net-sdk/validators/RuleValidator.cs b/dot-net-sdk/validators/RuleValidator.cs index 6ca0bb7..df203b7 100644 --- a/dot-net-sdk/validators/RuleValidator.cs +++ b/dot-net-sdk/validators/RuleValidator.cs @@ -4,6 +4,8 @@ using NuGet.Versioning; using eppo_sdk.helpers; using eppo_sdk.exception; +using Newtonsoft.Json; +using System.ComponentModel; namespace eppo_sdk.validators; @@ -151,11 +153,11 @@ private static bool EvaluateCondition(Subject subjectAttributes, Condition condi } case ONE_OF: { - return Compare.IsOneOf(value.StringValue(), condition.ArrayValue()); + return Compare.IsOneOf(value, condition.ArrayValue()); } case NOT_ONE_OF: { - return !Compare.IsOneOf(value.StringValue(), condition.ArrayValue()); + return !Compare.IsOneOf(value, condition.ArrayValue()); } } } @@ -170,5 +172,21 @@ private static bool EvaluateCondition(Subject subjectAttributes, Condition condi internal class Compare { - public static bool IsOneOf(string a, List arrayValues) => arrayValues.IndexOf(a) >= 0; + public static bool IsOneOf(HasEppoValue value, List arrayValues) + { + return arrayValues.IndexOf(ToString(value.Value)) >= 0; + } + private static string ToString(object? obj) { + // Simple casting to string except for tricksy floats. + if (obj is string v) { + return v; + } else if (obj is long i) { + return Convert.ToString(i); + } else if ((obj is double || obj is float) && Math.Truncate((double)obj) == (double)obj) { + // Example: 123456789.0 is cast to a more suitable format of int. + return Convert.ToString(Convert.ToInt32(obj)); + } + // Cross-SDK standard for encoding other possible value types such as bool, null and list + return JsonConvert.SerializeObject(obj); + } }