Skip to content

Commit 4bc89e0

Browse files
author
Michael Ng
authored
feat(config): Adds ConditionTree and moves around some modules for better organization. (#25)
1 parent 877c7b3 commit 4bc89e0

File tree

12 files changed

+563
-71
lines changed

12 files changed

+563
-71
lines changed

optimizely/config/datafileProjectConfig/datafile_project_config.go renamed to optimizely/config/datafileProjectConfig/config.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,38 +20,38 @@ import (
2020
"errors"
2121
"fmt"
2222

23+
"github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig/mappers"
2324
"github.com/optimizely/go-sdk/optimizely/entities"
2425
)
2526

26-
// DatafileParser parses a datafile into a DatafileProjectConfig
27-
type DatafileParser interface {
28-
Parse([]byte) (*DatafileProjectConfig, error)
29-
}
30-
3127
// DatafileProjectConfig is a project config backed by a datafile
3228
type DatafileProjectConfig struct {
33-
features map[string]entities.Feature
34-
parser DatafileParser
29+
audienceMap map[string]entities.Audience
30+
experimentMap map[string]entities.Experiment
31+
experimentKeyToIDMap map[string]string
32+
featureMap map[string]entities.Feature
3533
}
3634

3735
// NewDatafileProjectConfig initializes a new datafile from a json byte array using the default JSON datafile parser
3836
func NewDatafileProjectConfig(jsonDatafile []byte) *DatafileProjectConfig {
39-
parser := DatafileJSONParser{}
40-
return NewDatafileProjectConfigWithParser(parser, jsonDatafile)
41-
}
42-
43-
// NewDatafileProjectConfigWithParser initializes a new datafile from a json byte array using the given parser
44-
func NewDatafileProjectConfigWithParser(parser DatafileParser, jsonDatafile []byte) *DatafileProjectConfig {
45-
projectConfig, err := parser.Parse(jsonDatafile)
37+
datafile, err := Parse(jsonDatafile)
4638
if err != nil {
47-
// @TODO(mng): handle the error
39+
// @TODO(mng): handle error
4840
}
49-
return projectConfig
41+
42+
experiments, experimentKeyMap := mappers.MapExperiments(datafile.Experiments)
43+
config := &DatafileProjectConfig{
44+
audienceMap: mappers.MapAudiences(datafile.Audiences),
45+
experimentMap: experiments,
46+
experimentKeyToIDMap: experimentKeyMap,
47+
}
48+
49+
return config
5050
}
5151

5252
// GetFeatureByKey returns the feature with the given key
5353
func (config DatafileProjectConfig) GetFeatureByKey(featureKey string) (entities.Feature, error) {
54-
if feature, ok := config.features[featureKey]; ok {
54+
if feature, ok := config.featureMap[featureKey]; ok {
5555
return feature, nil
5656
}
5757

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package entities
18+
19+
// Audience represents an Audience object from the Optimizely datafile
20+
type Audience struct {
21+
ID string `json:"id"`
22+
Name string `json:"name"`
23+
Conditions interface{} `json:"condition"`
24+
}
25+
26+
// Experiment represents an Experiment object from the Optimizely datafile
27+
type Experiment struct {
28+
// @TODO(mng): include audienceConditions
29+
ID string `json:"id"`
30+
Key string `json:"key"`
31+
Status string `json:"status"`
32+
Variations []Variation `json:"variations"`
33+
TrafficAllocation []trafficAllocation `json:"trafficAllocation"`
34+
AudienceIds []string `json:"audienceIds"`
35+
ForcedVariations map[string]string `json:"forcedVariations"`
36+
}
37+
38+
// FeatureFlag represents a FeatureFlag object from the Optimizely datafile
39+
type FeatureFlag struct {
40+
ID string `json:"id"`
41+
RolloutID string `json:"rolloutId"`
42+
Key string `json:"key"`
43+
ExperimentIDs []string `json:"experimentIds"`
44+
Variables []string `json:"variables"`
45+
}
46+
47+
// trafficAllocation represents a traffic allocation range from the Optimizely datafile
48+
type trafficAllocation struct {
49+
EntityID string `json:"entityId"`
50+
EndOfRange int `json:"endOfRange"`
51+
}
52+
53+
// Variation represents an experiment variation from the Optimizely datafile
54+
type Variation struct {
55+
ID string `json:"id"`
56+
// @TODO(mng): include variables
57+
Key string `json:"key"`
58+
FeatureEnabled bool `json:"featureEnabled"`
59+
}
60+
61+
// Datafile represents the datafile we get from Optimizely
62+
type Datafile struct {
63+
AccountID string `json:"accountId"`
64+
AnonymizeIP bool `json:"anonymizeIP"`
65+
Audiences []Audience `json:"audiences"`
66+
BotFiltering bool `json:"botFiltering"`
67+
Experiments []Experiment `json:"experiments"`
68+
FeatureFlags []FeatureFlag `json:"featureFlags"`
69+
ProjectID string `json:"projectId"`
70+
Revision string `json:"revision"`
71+
Variables []string `json:"variables"`
72+
Version string `json:"version"`
73+
}

optimizely/config/datafileProjectConfig/json_parser.go

Lines changed: 6 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,18 @@ package datafileProjectConfig
1919
import (
2020
"encoding/json"
2121

22-
"github.com/optimizely/go-sdk/optimizely/entities"
22+
"github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig/entities"
2323
)
2424

25-
// FeatureFlag represents a FeatureFlag object from the Optimizely datafile
26-
type FeatureFlag struct {
27-
ID string `json:"id"`
28-
RolloutID string `json:"rolloutId"`
29-
Key string `json:"key"`
30-
ExperimentIDs []string `json:"experimentIds"`
31-
Variables []string `json:"variables"`
32-
}
33-
34-
// Datafile represents the datafile we get from Optimizely
35-
type Datafile struct {
36-
Version string `json:"version"`
37-
AnonymizeIP bool `json:"anonymizeIP"`
38-
ProjectID string `json:"projectId"`
39-
Variables []string `json:"variables"`
40-
FeatureFlags []FeatureFlag `json:"featureFlags"`
41-
BotFiltering bool `json:"botFiltering"`
42-
AccountID string `json:"accountId"`
43-
Revision string `json:"revision"`
44-
}
45-
46-
// DatafileJSONParser implements the DatafileParser interface and parses a JSON-based datafile into a DatafileProjectConfig
47-
type DatafileJSONParser struct {
48-
}
25+
// Parse parses the raw json datafile
26+
func Parse(jsonDatafile []byte) (*entities.Datafile, error) {
4927

50-
// Parse parses the json datafile
51-
func (parser DatafileJSONParser) Parse(jsonDatafile []byte) (*DatafileProjectConfig, error) {
52-
datafile := Datafile{}
53-
projectConfig := &DatafileProjectConfig{}
28+
datafile := &entities.Datafile{}
5429

5530
err := json.Unmarshal(jsonDatafile, &datafile)
5631
if err != nil {
57-
// @TODO(mng): return error
32+
return nil, err
5833
}
5934

60-
// convert the Datafile into a ProjectConfig
61-
projectConfig.features = mapFeatureFlags(datafile.FeatureFlags)
62-
return projectConfig, nil
63-
}
64-
65-
func mapFeatureFlags(featureFlags []FeatureFlag) map[string]entities.Feature {
66-
featureMap := make(map[string]entities.Feature)
67-
for _, featureFlag := range featureFlags {
68-
featureMap[featureFlag.Key] = entities.Feature{
69-
Key: featureFlag.Key,
70-
ID: featureFlag.ID,
71-
}
72-
}
73-
return featureMap
35+
return datafile, nil
7436
}

optimizely/config/datafileProjectConfig/json_parser_test.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"testing"
2222

23+
"github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig/entities"
2324
"github.com/stretchr/testify/assert"
2425
)
2526

@@ -28,6 +29,8 @@ func TestParseDatafilePasses(t *testing.T) {
2829
testFeatureID := "feature_id_123"
2930
datafileString := fmt.Sprintf(`{
3031
"projectId": "1337",
32+
"accountId": "1338",
33+
"version": "4",
3134
"featureFlags": [
3235
{
3336
"key": "%s",
@@ -37,15 +40,22 @@ func TestParseDatafilePasses(t *testing.T) {
3740
}`, testFeatureKey, testFeatureID)
3841

3942
rawDatafile := []byte(datafileString)
40-
parser := DatafileJSONParser{}
41-
projectConfig, err := parser.Parse(rawDatafile)
43+
parsedDatafile, err := Parse(rawDatafile)
4244
if err != nil {
4345
assert.Fail(t, err.Error())
4446
}
45-
feature, err := projectConfig.GetFeatureByKey("feature_test_1")
46-
if err != nil {
47-
assert.Fail(t, err.Error())
47+
48+
expectedDatafile := &entities.Datafile{
49+
AccountID: "1338",
50+
ProjectID: "1337",
51+
Version: "4",
52+
FeatureFlags: []entities.FeatureFlag{
53+
entities.FeatureFlag{
54+
Key: testFeatureKey,
55+
ID: testFeatureID,
56+
},
57+
},
4858
}
4959

50-
assert.Equal(t, "feature_id_123", feature.ID)
60+
assert.Equal(t, expectedDatafile, parsedDatafile)
5161
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/****************************************************************************
2+
* Copyright 2019, Optimizely, Inc. and contributors *
3+
* *
4+
* Licensed under the Apache License, Version 2.0 (the "License"); *
5+
* you may not use this file except in compliance with the License. *
6+
* You may obtain a copy of the License at *
7+
* *
8+
* http://www.apache.org/licenses/LICENSE-2.0 *
9+
* *
10+
* Unless required by applicable law or agreed to in writing, software *
11+
* distributed under the License is distributed on an "AS IS" BASIS, *
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13+
* See the License for the specific language governing permissions and *
14+
* limitations under the License. *
15+
***************************************************************************/
16+
17+
package mappers
18+
19+
import (
20+
"encoding/json"
21+
"reflect"
22+
23+
datafileEntities "github.com/optimizely/go-sdk/optimizely/config/datafileProjectConfig/entities"
24+
"github.com/optimizely/go-sdk/optimizely/entities"
25+
)
26+
27+
// MapAudiences maps the raw datafile audience entities to SDK Audience entities
28+
func MapAudiences(audiences []datafileEntities.Audience) map[string]entities.Audience {
29+
30+
audienceMap := make(map[string]entities.Audience)
31+
for _, audience := range audiences {
32+
conditionTree, err := buildConditionTree(audience.Conditions)
33+
if err != nil {
34+
// @TODO: handle error
35+
}
36+
audienceMap[audience.ID] = entities.Audience{
37+
ID: audience.ID,
38+
Name: audience.Name,
39+
ConditionTree: conditionTree,
40+
}
41+
}
42+
return audienceMap
43+
}
44+
45+
// Takes the conditions array from the audience in the datafile and turns it into a condition tree
46+
func buildConditionTree(conditions interface{}) (*entities.ConditionTreeNode, error) {
47+
48+
value := reflect.ValueOf(conditions)
49+
visited := make(map[interface{}]bool)
50+
var retErr error
51+
52+
conditionTree := &entities.ConditionTreeNode{}
53+
var populateConditions func(v reflect.Value, root *entities.ConditionTreeNode)
54+
populateConditions = func(v reflect.Value, root *entities.ConditionTreeNode) {
55+
56+
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
57+
if v.Kind() == reflect.Ptr {
58+
// Check for recursive data
59+
if visited[v.Interface()] {
60+
return
61+
}
62+
visited[v.Interface()] = true
63+
}
64+
v = v.Elem()
65+
}
66+
67+
switch v.Kind() {
68+
69+
case reflect.Slice, reflect.Array:
70+
for i := 0; i < v.Len(); i++ {
71+
n := &entities.ConditionTreeNode{}
72+
typedV := v.Index(i).Interface()
73+
switch typedV.(type) {
74+
case string:
75+
n.Operator = typedV.(string)
76+
root.Operator = n.Operator
77+
continue
78+
79+
case map[string]interface{}:
80+
jsonBody, err := json.Marshal(typedV)
81+
if err != nil {
82+
retErr = err
83+
return
84+
}
85+
condition := entities.Condition{}
86+
if err := json.Unmarshal(jsonBody, &condition); err != nil {
87+
retErr = err
88+
return
89+
}
90+
n.Condition = condition
91+
}
92+
93+
root.Nodes = append(root.Nodes, n)
94+
95+
populateConditions(v.Index(i), n)
96+
}
97+
}
98+
}
99+
100+
populateConditions(value, conditionTree)
101+
return conditionTree, retErr
102+
}

0 commit comments

Comments
 (0)