Skip to content

Commit e806a61

Browse files
add Get*Assignment methods with types (FF-757) (#22)
* add Get*Assignment methods with types (FF-757) * consistent golang version 1.19; version 2.0.0 * simpler unit test
1 parent 960a804 commit e806a61

File tree

13 files changed

+176
-85
lines changed

13 files changed

+176
-85
lines changed

.github/workflows/golang-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
steps:
1818
- uses: actions/setup-go@v3
1919
with:
20-
go-version: '1.18'
20+
go-version: '1.19'
2121
- uses: actions/checkout@v3
2222
- name: golangci-lint
2323
uses: golangci/golangci-lint-action@v3

.github/workflows/test-sdk.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
- uses: actions/checkout@v3
1111
- uses: actions/setup-go@v3
1212
with:
13-
go-version: 1.18
13+
go-version: 1.19
1414
- name: Build
1515
run: go build -v ./...
1616
- name: 'Set up GCP SDK for downloading test data'

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2022 Get Eppo
3+
Copyright (c) 2023 Eppo Data
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ testDataDir := eppoclient/test-data/
1717
test-data:
1818
rm -rf $(testDataDir)
1919
mkdir -p $(testDataDir)
20-
gsutil cp gs://sdk-test-data/rac-experiments-v2.json $(testDataDir)
20+
gsutil cp gs://sdk-test-data/rac-experiments-v3.json $(testDataDir)
2121
gsutil cp -r gs://sdk-test-data/assignment-v2 $(testDataDir)
2222

2323
test: test-data

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# Eppo SDK for Golang
22

3-
Eppoclient is a client sdk for `eppo.cloud` randomization API.
3+
EppoClient is a client sdk for the `eppo.cloud` randomization API.
44
It is used to retrieve the experiments data and put it to in-memory cache, and then get assignments information.
55

66
## Getting Started
77

88
Refer to our [SDK documentation](https://docs.geteppo.com/feature-flags/sdks/server-sdks/go) for how to install and use the SDK.
99

1010
## Supported Go Versions
11-
This version of the SDK is compatible with Go v1.18 and above.
11+
This version of the SDK is compatible with Go v1.19 and above.
1212

1313
## Example
1414

@@ -28,7 +28,7 @@ This version of the SDK is compatible with Go v1.18 and above.
2828
}
2929
3030
func someBLFunc() {
31-
assignment, _ := eppoClient.GetAssignment("subject-1", "experiment_5", sbjAttrs)
31+
assignment, _ := eppoClient.GetStringAssignment("subject-1", "experiment_5", sbjAttrs)
3232
3333
if assignment == "control" {
3434
// do something

eppoclient/assignmentlogger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type IAssignmentLogger interface {
88

99
type AssignmentEvent struct {
1010
Experiment string
11-
Variation string
11+
Variation Value
1212
Subject string
1313
Timestamp string
1414
SubjectAttributes dictionary

eppoclient/client.go

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,27 @@ func newEppoClient(configRequestor iConfigRequestor, assignmentLogger IAssignmen
2828
return ec
2929
}
3030

31-
func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
31+
func (ec *EppoClient) GetBoolAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (bool, error) {
32+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, BoolType)
33+
return variation.boolValue, err
34+
}
35+
36+
func (ec *EppoClient) GetNumericAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (float64, error) {
37+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, NumericType)
38+
return variation.numericValue, err
39+
}
40+
41+
func (ec *EppoClient) GetStringAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
42+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, StringType)
43+
return variation.stringValue, err
44+
}
45+
46+
func (ec *EppoClient) GetJSONAssignment(subjectKey string, flagKey string, subjectAttributes dictionary) (string, error) {
47+
variation, err := ec.getAssignment(subjectKey, flagKey, subjectAttributes, StringType)
48+
return variation.stringValue, err
49+
}
50+
51+
func (ec *EppoClient) getAssignment(subjectKey string, flagKey string, subjectAttributes dictionary, valueType ValueType) (Value, error) {
3252
if subjectKey == "" {
3353
panic("no subject key provided")
3454
}
@@ -39,30 +59,29 @@ func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAt
3959

4060
config, err := ec.configRequestor.GetConfiguration(flagKey)
4161
if err != nil {
42-
return "", err
62+
return Null(), err
4363
}
4464

45-
override := getSubjectVariationOverride(config, subjectKey)
46-
47-
if override != "" {
65+
override := getSubjectVariationOverride(config, subjectKey, valueType)
66+
if override != Null() {
4867
return override, nil
4968
}
5069

5170
// Check if disabled
5271
if !config.Enabled {
53-
return "", errors.New("the experiment or flag is not enabled")
72+
return Null(), errors.New("the experiment or flag is not enabled")
5473
}
5574

5675
// Find matching rule
5776
rule, err := findMatchingRule(subjectAttributes, config.Rules)
5877
if err != nil {
59-
return "", err
78+
return Null(), err
6079
}
6180

6281
// Check if in sample population
6382
allocation := config.Allocations[rule.AllocationKey]
6483
if !isInExperimentSample(subjectKey, flagKey, config.SubjectShards, allocation.PercentExposure) {
65-
return "", errors.New("subject not part of the sample population")
84+
return Null(), errors.New("subject not part of the sample population")
6685
}
6786

6887
// Get assigned variation
@@ -77,7 +96,7 @@ func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAt
7796
}
7897
}
7998

80-
assignedVariation := variationShard.Value.StringValue()
99+
assignedVariation := variationShard.Value
81100

82101
assignmentEvent := AssignmentEvent{
83102
Experiment: flagKey,
@@ -108,15 +127,15 @@ func (ec *EppoClient) GetAssignment(subjectKey string, flagKey string, subjectAt
108127
return assignedVariation, nil
109128
}
110129

111-
func getSubjectVariationOverride(experimentConfig experimentConfiguration, subject string) string {
130+
func getSubjectVariationOverride(experimentConfig experimentConfiguration, subject string, valueType ValueType) Value {
112131
hash := md5.Sum([]byte(subject))
113132
hashOutput := hex.EncodeToString(hash[:])
114133

115134
if val, ok := experimentConfig.Overrides[hashOutput]; ok {
116-
return val.(string)
135+
return val
117136
}
118137

119-
return ""
138+
return Null()
120139
}
121140

122141
func isInExperimentSample(subjectKey string, flagKey string, subjectShards int, percentExposure float32) bool {

eppoclient/client_test.go

Lines changed: 76 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func Test_AssignBlankExperiment(t *testing.T) {
1717
client := newEppoClient(mockConfigRequestor, mockLogger)
1818

1919
assert.Panics(t, func() {
20-
_, err := client.GetAssignment("subject-1", "", dictionary{})
20+
_, err := client.GetStringAssignment("subject-1", "", dictionary{})
2121
if err != nil {
2222
log.Println(err)
2323
}
@@ -30,7 +30,7 @@ func Test_AssignBlankSubject(t *testing.T) {
3030
client := newEppoClient(mockConfigRequestor, mockLogger)
3131

3232
assert.Panics(t, func() {
33-
_, err := client.GetAssignment("", "experiment-1", dictionary{})
33+
_, err := client.GetStringAssignment("", "experiment-1", dictionary{})
3434
if err != nil {
3535
log.Println(err)
3636
}
@@ -40,7 +40,7 @@ func Test_AssignBlankSubject(t *testing.T) {
4040
func Test_SubjectNotInSample(t *testing.T) {
4141
var mockLogger = new(mockLogger)
4242
var mockConfigRequestor = new(mockConfigRequestor)
43-
overrides := make(dictionary)
43+
overrides := make(map[string]Value)
4444
var mockVariations = []Variation{
4545
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
4646
}
@@ -62,7 +62,7 @@ func Test_SubjectNotInSample(t *testing.T) {
6262

6363
client := newEppoClient(mockConfigRequestor, mockLogger)
6464

65-
assignment, err := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
65+
assignment, err := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
6666

6767
assert.Equal(t, "", assignment)
6868
assert.NotNil(t, err)
@@ -73,7 +73,7 @@ func Test_LogAssignment(t *testing.T) {
7373
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
7474

7575
var mockConfigRequestor = new(mockConfigRequestor)
76-
overrides := make(dictionary)
76+
overrides := make(map[string]Value)
7777

7878
var mockVariations = []Variation{
7979
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
@@ -95,20 +95,20 @@ func Test_LogAssignment(t *testing.T) {
9595

9696
client := newEppoClient(mockConfigRequestor, mockLogger)
9797

98-
assignment, err := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
98+
assignment, err := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
9999
expected := "control"
100100

101101
assert.Nil(t, err)
102102
assert.Equal(t, expected, assignment)
103103
mockLogger.AssertNumberOfCalls(t, "LogAssignment", 1)
104104
}
105105

106-
func Test_GetAssignmentHandlesLoggingPanic(t *testing.T) {
106+
func Test_GetStringAssignmentHandlesLoggingPanic(t *testing.T) {
107107
var mockLogger = new(mockLogger)
108108
mockLogger.Mock.On("LogAssignment", mock.Anything).Panic("logging panic")
109109

110110
var mockConfigRequestor = new(mockConfigRequestor)
111-
overrides := make(dictionary)
111+
overrides := make(map[string]Value)
112112

113113
var mockVariations = []Variation{
114114
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
@@ -130,7 +130,7 @@ func Test_GetAssignmentHandlesLoggingPanic(t *testing.T) {
130130

131131
client := newEppoClient(mockConfigRequestor, mockLogger)
132132

133-
assignment, err := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
133+
assignment, err := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
134134
expected := "control"
135135

136136
assert.Nil(t, err)
@@ -144,7 +144,7 @@ func Test_AssignSubjectWithAttributesAndRules(t *testing.T) {
144144
var matchesEmailCondition = condition{Operator: "MATCHES", Value: ".*@eppo.com", Attribute: "email"}
145145
var textRule = rule{AllocationKey: defaultAllocationKey, Conditions: []condition{matchesEmailCondition}}
146146
var mockConfigRequestor = new(mockConfigRequestor)
147-
var overrides = make(dictionary)
147+
overrides := make(map[string]Value)
148148
var mockVariations = []Variation{
149149
{Name: "control", Value: String("control"), ShardRange: shardRange{Start: 0, End: 10000}},
150150
}
@@ -181,42 +181,77 @@ func Test_AssignSubjectWithAttributesAndRules(t *testing.T) {
181181
client := newEppoClient(mockConfigRequestor, mockLogger)
182182

183183
for _, tt := range tests {
184-
assignment, _ := client.GetAssignment(tt.a, tt.b, tt.c)
184+
assignment, _ := client.GetStringAssignment(tt.a, tt.b, tt.c)
185185

186186
assert.Equal(t, tt.want, assignment)
187187
}
188188
}
189189

190190
func Test_WithSubjectInOverrides(t *testing.T) {
191-
var mockLogger = new(mockLogger)
192-
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
193-
194-
var mockConfigRequestor = new(mockConfigRequestor)
195-
var mockVariations = []Variation{
196-
{Name: "control", ShardRange: shardRange{Start: 0, End: 100}},
197-
}
198-
var overrides = make(dictionary)
199-
overrides["d6d7705392bc7af633328bea8c4c6904"] = "override-variation"
200-
var allocations = make(map[string]Allocation)
201-
allocations[defaultAllocationKey] = Allocation{
202-
PercentExposure: 1,
203-
Variations: mockVariations,
204-
}
205-
var mockResult = experimentConfiguration{
206-
Name: "recommendation_algo",
207-
Enabled: true,
208-
SubjectShards: 1000,
209-
Overrides: overrides,
210-
Rules: []rule{textRule},
191+
var tests = []struct {
192+
name string
193+
inputVariationOverrideValue Value
194+
inputValueType ValueType
195+
want Value
196+
}{
197+
{"string override", String("variation-value"), StringType, String("variation-value")},
198+
{"numeric override", Numeric(5), NumericType, Numeric(5)},
199+
{"boolean override", Bool(true), BoolType, Bool(true)},
211200
}
212201

213-
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
214-
215-
client := newEppoClient(mockConfigRequestor, mockLogger)
202+
for _, tt := range tests {
203+
t.Run(tt.name, func(t *testing.T) {
204+
205+
var mockLogger = new(mockLogger)
206+
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
207+
208+
var mockConfigRequestor = new(mockConfigRequestor)
209+
var mockVariations = []Variation{
210+
{Name: "control", ShardRange: shardRange{Start: 0, End: 100}},
211+
}
212+
overrides := make(map[string]Value)
213+
overrides["d6d7705392bc7af633328bea8c4c6904"] = tt.inputVariationOverrideValue
214+
var allocations = make(map[string]Allocation)
215+
allocations[defaultAllocationKey] = Allocation{
216+
PercentExposure: 1,
217+
Variations: mockVariations,
218+
}
219+
var mockResult = experimentConfiguration{
220+
Name: "recommendation_algo",
221+
Enabled: true,
222+
SubjectShards: 1000,
223+
Overrides: overrides,
224+
Rules: []rule{textRule},
225+
}
226+
227+
mockConfigRequestor.Mock.On("GetConfiguration", "experiment-key-1").Return(mockResult, nil)
228+
229+
client := newEppoClient(mockConfigRequestor, mockLogger)
230+
231+
switch tt.inputValueType {
232+
case StringType:
233+
assignment, _ := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
234+
235+
if assignment != tt.want.stringValue {
236+
t.Errorf("got %s, want %s", assignment, tt.want.stringValue)
237+
}
238+
case NumericType:
239+
assignment, _ := client.GetNumericAssignment("user-1", "experiment-key-1", dictionary{})
240+
241+
if assignment != tt.want.numericValue {
242+
t.Errorf("got %T, want %T", assignment, tt.want.numericValue)
243+
}
244+
case BoolType:
245+
assignment, _ := client.GetBoolAssignment("user-1", "experiment-key-1", dictionary{})
246+
247+
if assignment != tt.want.boolValue {
248+
t.Errorf("got %t, want %t", assignment, tt.want.boolValue)
249+
}
250+
251+
}
252+
})
253+
}
216254

217-
expected := "override-variation"
218-
assignment, _ := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
219-
assert.Equal(t, expected, assignment)
220255
}
221256

222257
func Test_WithSubjectInOverridesExpDisabled(t *testing.T) {
@@ -227,8 +262,8 @@ func Test_WithSubjectInOverridesExpDisabled(t *testing.T) {
227262
var mockVariations = []Variation{
228263
{Name: "control", ShardRange: shardRange{Start: 0, End: 100}},
229264
}
230-
var overrides = make(dictionary)
231-
overrides["d6d7705392bc7af633328bea8c4c6904"] = "override-variation"
265+
overrides := make(map[string]Value)
266+
overrides["d6d7705392bc7af633328bea8c4c6904"] = String("override-variation")
232267
var allocations = make(map[string]Allocation)
233268
allocations[defaultAllocationKey] = Allocation{
234269
PercentExposure: 1,
@@ -248,7 +283,7 @@ func Test_WithSubjectInOverridesExpDisabled(t *testing.T) {
248283
client := newEppoClient(mockConfigRequestor, mockLogger)
249284

250285
expected := "override-variation"
251-
assignment, err := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
286+
assignment, err := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
252287

253288
assert.Nil(t, err)
254289
assert.Equal(t, expected, assignment)
@@ -264,7 +299,7 @@ func Test_WithNullExpConfig(t *testing.T) {
264299
client := newEppoClient(mockConfigRequestor, mockLogger)
265300

266301
expected := ""
267-
assignment, err := client.GetAssignment("user-1", "experiment-key-1", dictionary{})
302+
assignment, err := client.GetStringAssignment("user-1", "experiment-key-1", dictionary{})
268303

269304
assert.NotNil(t, err)
270305
assert.Equal(t, expected, assignment)

0 commit comments

Comments
 (0)