From 5570f23663b27233fc05c64bf2c0a3d63cb40ed4 Mon Sep 17 00:00:00 2001 From: hermeswaldemarin Date: Thu, 30 Nov 2023 16:03:41 +0000 Subject: [PATCH] Adding Custom Field Support --- .github/workflows/main.yml | 20 +++ .github/workflows/publish.yml | 30 +++++ src/ABSmartly.Sdk/ABSmartly.Sdk.csproj | 4 +- src/ABSmartly.Sdk/Context.cs | 126 ++++++++++++++++++ src/ABSmartly.Sdk/Models/CustomFieldValue.cs | 46 +++++++ src/ABSmartly.Sdk/Models/Experiment.cs | 1 + tests/ABSmartly.Sdk.Tests/ContextTests.cs | 45 +++++++ .../Resources/context.json | 31 ++++- 8 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/publish.yml create mode 100644 src/ABSmartly.Sdk/Models/CustomFieldValue.cs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0b7f9e5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,20 @@ +name: CI +on: [push] +jobs: + build: + runs-on: windows-2019 + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: '6.0.x' + dotnet-quality: 'preview' + env: + NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}} + + - name: Run Build + run: dotnet build src/ABSmartly.Sdk + + - name: Run Tests + run: dotnet test tests/ABSmartly.Sdk.Tests diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..5c99bf4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,30 @@ +name: Publish Nuget +on: + push: + branches: + - "main" + tags: + - v* +jobs: + build: + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-dotnet@v3 + with: + dotnet-version: '5.0.x' + dotnet-quality: 'preview' + env: + NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}} + + - name: Run Build + run: dotnet build src/ABSmartly.Sdk + env: + NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}} + + - name: Run Release + run: dotnet nuget push src/ABSmartly.Sdk/bin/Debug/ --api-key $NUGET_AUTH_TOKEN --source https://api.nuget.org/v3/index.json + env: + NUGET_AUTH_TOKEN: ${{secrets.NUGET_AUTH_TOKEN}} \ No newline at end of file diff --git a/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj b/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj index d346e40..990b45b 100644 --- a/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj +++ b/src/ABSmartly.Sdk/ABSmartly.Sdk.csproj @@ -6,7 +6,7 @@ true ABSmartly.Sdk true - 1.1.1 + 1.2.1 A/B Smartly DotNet SDK A/B Smartly The A/B Smartly DotNet SDK is a client SDK for A/B Smartly service @@ -19,11 +19,11 @@ net5.0;netstandard2.0 1.0.0.0 1.1.1.77 + true - diff --git a/src/ABSmartly.Sdk/Context.cs b/src/ABSmartly.Sdk/Context.cs index ad17eaa..ade647c 100644 --- a/src/ABSmartly.Sdk/Context.cs +++ b/src/ABSmartly.Sdk/Context.cs @@ -55,6 +55,8 @@ public class Context : IContext, IDisposable, IAsyncDisposable private bool _failed; private Dictionary _index; + private Dictionary> _contextCustomFields; + private DictionaryLockableAdapter _indexVariables; private volatile int _pendingCount; @@ -460,6 +462,84 @@ private ExperimentVariables GetExperiment(string experimentName) _dataLock.ExitReadLock(); } } + + public List GetCustomFieldKeys() + { + try + { + _dataLock.EnterReadLock(); + + var keys = new List(); + + foreach (var experiment in _data.Experiments) + { + var customFieldValues = experiment.CustomFieldValues; + if (customFieldValues != null) + { + foreach (var customFieldValue in customFieldValues) + { + keys.Add(customFieldValue.Name); + } + } + } + + return keys.OrderBy(q => q).Distinct().ToList(); + } + finally + { + _dataLock.ExitReadLock(); + } + } + + public Object GetCustomFieldValue(String environmentName, String key) + { + try + { + _dataLock.EnterReadLock(); + + _contextCustomFields.TryGetValue(environmentName, out var customFieldValues); + + if (customFieldValues != null) + { + customFieldValues.TryGetValue(key, out var field); + if (field != null) + { + return field.Value; + } + } + + return null; + } + finally + { + _dataLock.ExitReadLock(); + } + } + + public Object GetCustomFieldType(String environmentName, String key) + { + try + { + _dataLock.EnterReadLock(); + + _contextCustomFields.TryGetValue(environmentName, out var customFieldValues); + + if (customFieldValues != null) + { + customFieldValues.TryGetValue(key, out var field); + if (field != null) + { + return field.Type; + } + } + + return null; + } + finally + { + _dataLock.ExitReadLock(); + } + } private ExperimentVariables GetVariableExperiment(string key) { @@ -854,6 +934,7 @@ private void SetData(ContextData data) { var index = new Dictionary(); var indexVariables = new Dictionary(); + var contextCustomFields = new Dictionary>(); foreach (var experiment in data.Experiments) { @@ -878,6 +959,43 @@ private void SetData(ContextData data) } index[experiment.Name] = experimentVariables; + + if (experiment.CustomFieldValues == null) continue; + + var experimentCustomFields = new Dictionary(); + foreach (var customFieldValue in experiment.CustomFieldValues) + { + var value = new ContextCustomFieldValue + { + Type = customFieldValue.Type + }; + + if (customFieldValue.Value != null) + { + var customValue = customFieldValue.Value; + + if (customFieldValue.Type.StartsWith("json")) + { + value.Value = _variableParser.Parse(this, experiment.Name, customFieldValue.Name, customValue); + } + else if(customFieldValue.Type.StartsWith("boolean")) + { + value.Value = Convert.ToBoolean(customValue); + } + else if(customFieldValue.Type.StartsWith("number")) + { + value.Value = Convert.ToInt64(customValue); + } + else + { + value.Value = customValue; + } + } + + experimentCustomFields[customFieldValue.Name] = value; + } + + contextCustomFields[experiment.Name] = experimentCustomFields; } try @@ -885,6 +1003,7 @@ private void SetData(ContextData data) _dataLock.EnterWriteLock(); _index = index; + _contextCustomFields = contextCustomFields; _indexVariables = new DictionaryLockableAdapter(new LockableCollectionSlimLock(_dataLock), indexVariables); @@ -998,6 +1117,13 @@ public class ExperimentVariables public Experiment Data { get; set; } public List> Variables { get; set; } } + + public class ContextCustomFieldValue + { + public String Name { get; set; } + public String Type { get; set; } + public Object Value { get; set; } + } public class Assignment { diff --git a/src/ABSmartly.Sdk/Models/CustomFieldValue.cs b/src/ABSmartly.Sdk/Models/CustomFieldValue.cs new file mode 100644 index 0000000..e13208d --- /dev/null +++ b/src/ABSmartly.Sdk/Models/CustomFieldValue.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; + +namespace ABSmartly.Models; + +[DebuggerDisplay("{DebugView},nq")] +public class CustomFieldValue +{ + public string Name { get; set; } + public string Type { get; set; } + public string Value { get; set; } + + private string DebugView => $"ExperimentVariant{{name={Name}, type={Type}, value={Value}}}"; + + public override string ToString() + { + return DebugView; + } + + + #region Equality members + + protected bool Equals(CustomFieldValue other) + { + return Name == other.Name && + Type == other.Type&& + Value == other.Value; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((CustomFieldValue)obj); + } + + public override int GetHashCode() + { + unchecked + { + return ((Name?.GetHashCode() ?? 0) * 397) ^ (Type?.GetHashCode() ?? 0) ^ (Value?.GetHashCode() ?? 0); + } + } + + #endregion +} \ No newline at end of file diff --git a/src/ABSmartly.Sdk/Models/Experiment.cs b/src/ABSmartly.Sdk/Models/Experiment.cs index 54f9a29..bf0fc26 100644 --- a/src/ABSmartly.Sdk/Models/Experiment.cs +++ b/src/ABSmartly.Sdk/Models/Experiment.cs @@ -20,6 +20,7 @@ public class Experiment public int FullOnVariant { get; set; } public ExperimentApplication[] Applications { get; set; } public ExperimentVariant[] Variants { get; set; } + public CustomFieldValue[] CustomFieldValues { get; set; } public bool AudienceStrict { get; set; } public string Audience { get; set; } diff --git a/tests/ABSmartly.Sdk.Tests/ContextTests.cs b/tests/ABSmartly.Sdk.Tests/ContextTests.cs index b9af4ca..71487f7 100644 --- a/tests/ABSmartly.Sdk.Tests/ContextTests.cs +++ b/tests/ABSmartly.Sdk.Tests/ContextTests.cs @@ -770,6 +770,51 @@ public void TestGetVariableKeys() context.GetVariableKeys().Should().BeEquivalentTo(_variableExperiments); } + + [Test] + public void TestGetCustomFieldKeys() + { + var context = CreateContext(_data); + + context.GetCustomFieldKeys().Should().BeEquivalentTo(new List { "country", "languages", "overrides" }); + } + + [Test] + public void TestGetCustomFieldValues() + { + var context = CreateContext(_data); + + context.GetCustomFieldValue("not_found", "not_found").Should().BeNull(); + context.GetCustomFieldValue("exp_test_ab", "not_found").Should().BeNull(); + context.GetCustomFieldValue("exp_test_ab", "country").Should().BeEquivalentTo("US,PT,ES,DE,FR"); + context.GetCustomFieldType("exp_test_ab", "country").Should().BeEquivalentTo("string"); + + context.GetCustomFieldValue("exp_test_ab", "overrides").Should().BeEquivalentTo(new Dictionary + { + { "123", 1 }, + { "456", 0 } + } + ); + context.GetCustomFieldType("exp_test_ab", "overrides").Should().BeEquivalentTo("json"); + + context.GetCustomFieldValue("exp_test_ab", "languages").Should().BeNull(); + context.GetCustomFieldValue("exp_test_ab", "languages").Should().BeNull(); + + context.GetCustomFieldValue("exp_test_abc", "overrides").Should().BeNull(); + context.GetCustomFieldValue("exp_test_abc", "overrides").Should().BeNull(); + + context.GetCustomFieldValue("exp_test_abc", "languages").Should().BeEquivalentTo("en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX"); + context.GetCustomFieldType("exp_test_abc", "languages").Should().BeEquivalentTo("string"); + + context.GetCustomFieldValue("exp_test_no_custom_fields", "country").Should().BeNull(); + context.GetCustomFieldValue("exp_test_no_custom_fields", "country").Should().BeNull(); + + context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides").Should().BeNull(); + context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides").Should().BeNull(); + + context.GetCustomFieldValue("exp_test_no_custom_fields", "languages").Should().BeNull(); + context.GetCustomFieldValue("exp_test_no_custom_fields", "languages").Should().BeNull(); + } [Test] public void TestPeekTreatmentReturnsOverrideVariant() diff --git a/tests/ABSmartly.Sdk.Tests/Resources/context.json b/tests/ABSmartly.Sdk.Tests/Resources/context.json index c2f0066..c4340aa 100644 --- a/tests/ABSmartly.Sdk.Tests/Resources/context.json +++ b/tests/ABSmartly.Sdk.Tests/Resources/context.json @@ -33,7 +33,19 @@ "config":"{\"banner.border\":1,\"banner.size\":\"large\"}" } ], - "audience": null + "audience": null, + "customFieldValues": [ + { + "name": "country", + "value": "US,PT,ES,DE,FR", + "type": "string" + }, + { + "name": "overrides", + "value": "{\"123\":1,\"456\":0}", + "type": "json" + } + ] }, { "id":2, @@ -73,7 +85,19 @@ "config":"{\"button.color\":\"red\"}" } ], - "audience": "" + "audience": "", + "customFieldValues": [ + { + "name": "country", + "value": "US,PT,ES,DE,FR", + "type": "string" + }, + { + "name": "languages", + "value": "en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX", + "type": "string" + } + ] }, { "id":3, @@ -113,7 +137,8 @@ "config":"{\"card.width\":\"75%\"}" } ], - "audience": "{}" + "audience": "{}", + "customFieldValues": null }, { "id":4,