From cb22d6ace60d94436085fafef5d2d94146716492 Mon Sep 17 00:00:00 2001 From: hermeswaldemarin Date: Wed, 29 Nov 2023 09:15:02 +0000 Subject: [PATCH] Adding Custom Fields Support --- sdk/Context.go | 178 ++++++++++++++++++++++++++++------- sdk/Context_test.go | 53 +++++++++++ sdk/jsonmodels/Experiment.go | 31 +++--- sdk/testAssets/context.json | 31 +++++- 4 files changed, 240 insertions(+), 53 deletions(-) diff --git a/sdk/Context.go b/sdk/Context.go index b34d588..00d54c2 100644 --- a/sdk/Context.go +++ b/sdk/Context.go @@ -7,6 +7,8 @@ import ( "github.com/absmartly/go-sdk/sdk/internal" "github.com/absmartly/go-sdk/sdk/jsonmodels" "reflect" + "sort" + "strconv" "strings" "sync" "sync/atomic" @@ -14,41 +16,42 @@ import ( ) type Context struct { - PublishDelay_ int64 - RefreshInterval_ int64 - EventHandler_ ContextEventHandler - EventLogger_ ContextEventLogger - DataProvider_ ContextDataProvider - VariableParser_ VariableParser - AudienceMatcher_ AudienceMatcher - Units_ map[string]string - Failed_ *atomic.Value - Ready_ *atomic.Value - DataLock *sync.RWMutex - Data_ jsonmodels.ContextData - Index_ map[string]ExperimentVariables - IndexVariables_ map[interface{}]interface{} - ContextLock_ *sync.RWMutex - HashedUnits_ map[interface{}]interface{} - Assigners_ map[interface{}]interface{} - AssignmentCache map[string]Assignment - EventLock_ *sync.Mutex - Exposures_ []jsonmodels.Exposure - Achievements_ []jsonmodels.GoalAchievement - Attributes_ []interface{} - Overrides_ map[interface{}]interface{} - Cassignments_ map[interface{}]interface{} - PendingCount_ *atomic.Value - Closing_ *atomic.Value - Closed_ *atomic.Value - Refreshing_ *atomic.Value - ReadyFuture_ *future.Future - ClosingFuture_ *future.Future - RefreshFuture_ *future.Future - TimeoutLock_ *sync.Mutex - Timeout_ *time.Timer - RefreshTimer_ *time.Timer - Clock_ internal.Clock + PublishDelay_ int64 + RefreshInterval_ int64 + EventHandler_ ContextEventHandler + EventLogger_ ContextEventLogger + DataProvider_ ContextDataProvider + VariableParser_ VariableParser + AudienceMatcher_ AudienceMatcher + Units_ map[string]string + Failed_ *atomic.Value + Ready_ *atomic.Value + DataLock *sync.RWMutex + Data_ jsonmodels.ContextData + Index_ map[string]ExperimentVariables + ContextCustomFields_ map[string]map[string]ContextCustomFieldValue + IndexVariables_ map[interface{}]interface{} + ContextLock_ *sync.RWMutex + HashedUnits_ map[interface{}]interface{} + Assigners_ map[interface{}]interface{} + AssignmentCache map[string]Assignment + EventLock_ *sync.Mutex + Exposures_ []jsonmodels.Exposure + Achievements_ []jsonmodels.GoalAchievement + Attributes_ []interface{} + Overrides_ map[interface{}]interface{} + Cassignments_ map[interface{}]interface{} + PendingCount_ *atomic.Value + Closing_ *atomic.Value + Closed_ *atomic.Value + Refreshing_ *atomic.Value + ReadyFuture_ *future.Future + ClosingFuture_ *future.Future + RefreshFuture_ *future.Future + TimeoutLock_ *sync.Mutex + Timeout_ *time.Timer + RefreshTimer_ *time.Timer + Clock_ internal.Clock } type ExperimentVariables struct { @@ -56,6 +59,12 @@ type ExperimentVariables struct { Variables []map[string]interface{} } +type ContextCustomFieldValue struct { + Name string + Type string + Value interface{} +} + type Assignment struct { Id int Iteration int @@ -422,6 +431,82 @@ func (c *Context) GetVariableKeys() (map[string]string, error) { return variableKeys, nil } +func removeDuplicateValues(intSlice []string) []string { + keys := make(map[string]bool) + var list []string + + for _, entry := range intSlice { + if _, value := keys[entry]; !value { + keys[entry] = true + list = append(list, entry) + } + } + return list +} + +func (c *Context) GetCustomFieldValueKeys() ([]string, error) { + var err = c.CheckReady(true) + if err != nil { + return nil, err + } + + var keys []string + + c.DataLock.Lock() + + for _, experiment := range c.Data_.Experiments { + var customFieldValues = experiment.CustomFieldValues + if customFieldValues != nil { + for _, customFieldValue := range customFieldValues { + keys = append(keys, customFieldValue.Name) + } + } + } + c.DataLock.Unlock() + + sort.Strings(keys) + + return removeDuplicateValues(keys), nil +} + +func (c *Context) GetCustomFieldValue(experimentName string, key string) interface{} { + var err = c.CheckReady(true) + if err != nil { + return nil + } + c.DataLock.Lock() + var customFieldValues = c.ContextCustomFields_[experimentName] + + var value interface{} = nil + if customFieldValues != nil { + field, ok := customFieldValues[key] + if ok { + value = field.Value + } + } + c.DataLock.Unlock() + + return value +} + +func (c *Context) GetCustomFieldValueType(experimentName string, key string) string { + var customFieldValues = c.ContextCustomFields_[experimentName] + + c.DataLock.Lock() + + var fieldType string + if customFieldValues != nil { + field, ok := customFieldValues[key] + if ok { + fieldType = field.Type + } + } + + c.DataLock.Unlock() + + return fieldType +} + func (c *Context) GetVariableValue(key string, defaultValue interface{}) (interface{}, error) { var err = c.CheckReady(true) if err != nil { @@ -752,9 +837,11 @@ func (c *Context) CheckReady(expectNotClosed bool) error { func (c *Context) SetData(data jsonmodels.ContextData) { var index = map[string]ExperimentVariables{} var indexVariables = map[interface{}]interface{}{} + var contextCustomFields = map[string]map[string]ContextCustomFieldValue{} for _, experiment := range data.Experiments { var experiemntVariables = ExperimentVariables{} + var experimentCustomFields = map[string]ContextCustomFieldValue{} experiemntVariables.Data = experiment experiemntVariables.Variables = make([]map[string]interface{}, 0) @@ -770,12 +857,33 @@ func (c *Context) SetData(data jsonmodels.ContextData) { } } + for _, customFieldValue := range experiment.CustomFieldValues { + var value = ContextCustomFieldValue{} + value.Type = customFieldValue.Type + value.Name = customFieldValue.Name + if len(customFieldValue.Value) > 0 { + var customValue = customFieldValue.Value + if strings.HasPrefix(customFieldValue.Type, "json") { + value.Value = c.VariableParser_.Parse(*c, experiment.Name, customFieldValue.Name, customValue) + } else if strings.HasPrefix(customFieldValue.Type, "boolean") { + value.Value, _ = strconv.ParseBool(customValue) + } else if strings.HasPrefix(customFieldValue.Type, "number") { + value.Value, _ = strconv.ParseInt(customValue, 10, 64) + } else { + value.Value = customValue + } + + experimentCustomFields[value.Name] = value + } + } + contextCustomFields[experiment.Name] = experimentCustomFields index[experiment.Name] = experiemntVariables } c.DataLock.Lock() c.Index_ = index + c.ContextCustomFields_ = contextCustomFields c.IndexVariables_ = indexVariables c.Data_ = data c.Ready_.Store(true) diff --git a/sdk/Context_test.go b/sdk/Context_test.go index b9b7846..b5d5c40 100644 --- a/sdk/Context_test.go +++ b/sdk/Context_test.go @@ -2,6 +2,7 @@ package sdk import ( context2 "context" + "encoding/json" "errors" "github.com/absmartly/go-sdk/sdk/future" "github.com/absmartly/go-sdk/sdk/internal" @@ -772,6 +773,58 @@ func TestGetVariableKeys(t *testing.T) { assertAny(int32(0), context.GetPendingCount(), t) } +func TestGetCustomFieldValueKeys(t *testing.T) { + setUp() + var config = CreateDefaultContextConfig() + config.Units_ = units + var context = CreateTestContext(config, dataFutureReady) + assertAny(true, context.IsReady(), t) + assertAny(false, context.IsFailed(), t) + + var res, _ = context.GetCustomFieldValueKeys() + assertAny([]string{"country", "languages", "overrides"}, res, t) +} + +func TestGetCustomFieldValue(t *testing.T) { + setUp() + var config = CreateDefaultContextConfig() + config.Units_ = units + var context = CreateTestContext(config, dataFutureReady) + assertAny(true, context.IsReady(), t) + assertAny(false, context.IsFailed(), t) + + assertAny(nil, context.GetCustomFieldValue("not_found", "not_found"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_ab", "not_found"), t) + + assertAny("US,PT,ES,DE,FR", context.GetCustomFieldValue("exp_test_ab", "country"), t) + assertAny("string", context.GetCustomFieldValueType("exp_test_ab", "country"), t) + + var js = "{\"123\":1,\"456\":0}" + overrides, _ := json.Marshal(context.GetCustomFieldValue("exp_test_ab", "overrides")) + var str = string(overrides) + assertAny(js, str, t) + assertAny("json", context.GetCustomFieldValueType("exp_test_ab", "overrides"), t) + + assertAny(nil, context.GetCustomFieldValue("exp_test_ab", "languages"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_ab", "languages"), t) + + assertAny(nil, context.GetCustomFieldValue("exp_test_abc", "overrides"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_abc", "overrides"), t) + + assertAny("en-US,en-GB,pt-PT,pt-BR,es-ES,es-MX", context.GetCustomFieldValue("exp_test_abc", "languages"), t) + assertAny("string", context.GetCustomFieldValueType("exp_test_abc", "languages"), t) + + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "country"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "country"), t) + + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "overrides"), t) + + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "languages"), t) + assertAny(nil, context.GetCustomFieldValue("exp_test_no_custom_fields", "languages"), t) + +} + func TestPeekTreatmentOverrideVariant(t *testing.T) { setUp() var config = CreateDefaultContextConfig() diff --git a/sdk/jsonmodels/Experiment.go b/sdk/jsonmodels/Experiment.go index 63d0ee5..ca4e401 100644 --- a/sdk/jsonmodels/Experiment.go +++ b/sdk/jsonmodels/Experiment.go @@ -1,19 +1,20 @@ package jsonmodels type Experiment struct { - Id int `json:"id"` - Name string `json:"name"` - UnitType string `json:"unitType"` - Iteration int `json:"iteration"` - SeedHi int `json:"seedHi"` - SeedLo int `json:"seedLo"` - Split []float64 `json:"split"` - TrafficSeedHi int `json:"trafficSeedHi"` - TrafficSeedLo int `json:"trafficSeedLo"` - TrafficSplit []float64 `json:"trafficSplit"` - FullOnVariant int `json:"fullOnVariant"` - Applications []ExperimentApplication `json:"applications"` - Variants []ExperimentVariant `json:"variants"` - AudienceStrict bool `json:"audienceStrict"` - Audience string `json:"audience"` + Id int `json:"id"` + Name string `json:"name"` + UnitType string `json:"unitType"` + Iteration int `json:"iteration"` + SeedHi int `json:"seedHi"` + SeedLo int `json:"seedLo"` + Split []float64 `json:"split"` + TrafficSeedHi int `json:"trafficSeedHi"` + TrafficSeedLo int `json:"trafficSeedLo"` + TrafficSplit []float64 `json:"trafficSplit"` + FullOnVariant int `json:"fullOnVariant"` + Applications []ExperimentApplication `json:"applications"` + Variants []ExperimentVariant `json:"variants"` + CustomFieldValues []CustomFieldValue `json:"customFieldValues"` + AudienceStrict bool `json:"audienceStrict"` + Audience string `json:"audience"` } diff --git a/sdk/testAssets/context.json b/sdk/testAssets/context.json index 60b67a4..6d7abba 100644 --- a/sdk/testAssets/context.json +++ b/sdk/testAssets/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,