Skip to content

Commit 25fb6c7

Browse files
FF-3184 feat: add GetJSONBytesAssignment (#69)
* FF-3184 perf: move variation value parsing to precompute phase * FF-3184 feat: add GetJSONBytesAssignment --------- Co-authored-by: Leo Romanovsky <[email protected]>
1 parent 07f878f commit 25fb6c7

10 files changed

+126
-61
lines changed

eppoclient/client.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,25 @@ func (ec *EppoClient) GetJSONAssignment(flagKey string, subjectKey string, subje
9090
if err != nil || variation == nil {
9191
return defaultValue, err
9292
}
93-
return variation, err
93+
result, ok := variation.(jsonVariationValue)
94+
if !ok {
95+
ec.applicationLogger.Errorf("failed to cast %v to json. This should never happen. Please report bug to Eppo", variation)
96+
return defaultValue, fmt.Errorf("failed to cast %v to json. This should never happen. Please report bug to Eppo", variation)
97+
}
98+
return result.Parsed, err
99+
}
100+
101+
func (ec *EppoClient) GetJSONBytesAssignment(flagKey string, subjectKey string, subjectAttributes Attributes, defaultValue []byte) ([]byte, error) {
102+
variation, err := ec.getAssignment(ec.configurationStore.getConfiguration(), flagKey, subjectKey, subjectAttributes, jsonVariation)
103+
if err != nil || variation == nil {
104+
return defaultValue, err
105+
}
106+
result, ok := variation.(jsonVariationValue)
107+
if !ok {
108+
ec.applicationLogger.Errorf("failed to cast %v to json. This should never happen. Please report bug to Eppo", variation)
109+
return defaultValue, fmt.Errorf("failed to cast %v to json. This should never happen. Please report bug to Eppo", variation)
110+
}
111+
return result.Raw, err
94112
}
95113

96114
type BanditResult struct {

eppoclient/client_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,16 @@ func Test_LogAssignment(t *testing.T) {
5858
mockLogger.Mock.On("LogAssignment", mock.Anything).Return()
5959

6060
config := configResponse{
61-
Flags: map[string]flagConfiguration{
62-
"experiment-key-1": flagConfiguration{
61+
Flags: map[string]*flagConfiguration{
62+
"experiment-key-1": &flagConfiguration{
6363
Key: "experiment-key-1",
6464
Enabled: true,
6565
TotalShards: 10000,
6666
VariationType: stringVariation,
6767
Variations: map[string]variation{
6868
"control": variation{
6969
Key: "control",
70-
Value: "control",
70+
Value: []byte("\"control\""),
7171
},
7272
},
7373
Allocations: []allocation{
@@ -158,16 +158,16 @@ func Test_GetStringAssignmentHandlesLoggingPanic(t *testing.T) {
158158
var mockLogger = new(mockLogger)
159159
mockLogger.Mock.On("LogAssignment", mock.Anything).Panic("logging panic")
160160

161-
config := configResponse{Flags: map[string]flagConfiguration{
162-
"experiment-key-1": flagConfiguration{
161+
config := configResponse{Flags: map[string]*flagConfiguration{
162+
"experiment-key-1": &flagConfiguration{
163163
Key: "experiment-key-1",
164164
Enabled: true,
165165
TotalShards: 10000,
166166
VariationType: stringVariation,
167167
Variations: map[string]variation{
168168
"control": variation{
169169
Key: "control",
170-
Value: "control",
170+
Value: []byte("\"control\""),
171171
},
172172
},
173173
Allocations: []allocation{

eppoclient/configresponse.go

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,14 @@ import (
99
)
1010

1111
type configResponse struct {
12-
Flags map[string]flagConfiguration `json:"flags"`
13-
Bandits map[string][]banditVariation `json:"bandits,omitempty"`
12+
Flags map[string]*flagConfiguration `json:"flags"`
13+
Bandits map[string][]banditVariation `json:"bandits,omitempty"`
14+
}
15+
16+
func (response *configResponse) precompute() {
17+
for i := range response.Flags {
18+
response.Flags[i].precompute()
19+
}
1420
}
1521

1622
type flagConfiguration struct {
@@ -20,17 +26,39 @@ type flagConfiguration struct {
2026
Variations map[string]variation `json:"variations"`
2127
Allocations []allocation `json:"allocations"`
2228
TotalShards int64 `json:"totalShards"`
23-
}
24-
25-
func (flag *flagConfiguration) Precompute() {
29+
// Cached Variations parsed according to `VariationType`.
30+
//
31+
// Types are as follows:
32+
// - STRING -> string
33+
// - NUMERIC -> float64
34+
// - INTEGER -> int64
35+
// - BOOLEAN -> bool
36+
// - JSON -> jsonVariationValue
37+
ParsedVariations map[string]interface{} `json:"-"`
38+
}
39+
40+
func (flag *flagConfiguration) precompute() {
2641
for i := range flag.Allocations {
27-
flag.Allocations[i].Precompute()
42+
flag.Allocations[i].precompute()
43+
}
44+
45+
flag.ParsedVariations = make(map[string]interface{}, len(flag.Variations))
46+
for i := range flag.Variations {
47+
value, err := flag.VariationType.parseVariationValue(flag.Variations[i].Value)
48+
if err == nil {
49+
flag.ParsedVariations[i] = value
50+
}
2851
}
2952
}
3053

3154
type variation struct {
32-
Key string `json:"key"`
33-
Value interface{} `json:"value"`
55+
Key string `json:"key"`
56+
Value json.RawMessage `json:"value"`
57+
}
58+
59+
type jsonVariationValue struct {
60+
Raw []byte
61+
Parsed interface{}
3462
}
3563

3664
type variationType int
@@ -84,33 +112,52 @@ func (v *variationType) UnmarshalJSON(data []byte) error {
84112
return nil
85113
}
86114

87-
func (ty variationType) valueToAssignmentValue(value interface{}) (interface{}, error) {
115+
func (ty variationType) parseVariationValue(value json.RawMessage) (interface{}, error) {
88116
switch ty {
89117
case stringVariation:
90-
s := value.(string)
118+
var s string
119+
err := json.Unmarshal(value, &s)
120+
if err != nil {
121+
return nil, err
122+
}
91123
return s, nil
92124
case integerVariation:
93-
f64 := value.(float64)
94-
i64 := int64(f64)
95-
if f64 == float64(i64) {
96-
return i64, nil
97-
} else {
98-
return nil, fmt.Errorf("failed to convert number to integer")
125+
var i int64
126+
err := json.Unmarshal(value, &i)
127+
if err != nil {
128+
return nil, err
99129
}
130+
return i, nil
100131
case numericVariation:
101-
number := value.(float64)
102-
return number, nil
132+
var f float64
133+
err := json.Unmarshal(value, &f)
134+
if err != nil {
135+
return nil, err
136+
}
137+
return f, nil
103138
case booleanVariation:
104-
v := value.(bool)
105-
return v, nil
139+
var b bool
140+
err := json.Unmarshal(value, &b)
141+
if err != nil {
142+
return nil, err
143+
}
144+
return b, nil
106145
case jsonVariation:
107-
v := value.(string)
108-
var result interface{}
109-
err := json.Unmarshal([]byte(v), &result)
146+
var s string
147+
err := json.Unmarshal(value, &s)
110148
if err != nil {
111149
return nil, err
112150
}
113-
return result, nil
151+
152+
raw := []byte(s)
153+
154+
var parsed interface{}
155+
err = json.Unmarshal(raw, &parsed)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
return jsonVariationValue{raw, parsed}, nil
114161
default:
115162
return nil, fmt.Errorf("unexpected variation type: %v", ty)
116163
}
@@ -125,19 +172,19 @@ type allocation struct {
125172
DoLog *bool `json:"doLog"`
126173
}
127174

128-
func (a *allocation) Precompute() {
175+
func (a *allocation) precompute() {
129176
for i := range a.Rules {
130-
a.Rules[i].Precompute()
177+
a.Rules[i].precompute()
131178
}
132179
}
133180

134181
type rule struct {
135182
Conditions []condition `json:"conditions"`
136183
}
137184

138-
func (r *rule) Precompute() {
185+
func (r *rule) precompute() {
139186
for i := range r.Conditions {
140-
r.Conditions[i].Precompute()
187+
r.Conditions[i].precompute()
141188
}
142189
}
143190

@@ -152,7 +199,7 @@ type condition struct {
152199
SemVerValueValid bool
153200
}
154201

155-
func (c *condition) Precompute() {
202+
func (c *condition) precompute() {
156203
// Try to convert Value to a float64
157204
if num, err := toFloat64(c.Value); err == nil {
158205
c.NumericValue = num

eppoclient/configurationrequestor.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,6 @@ func (cr *configurationRequestor) fetchConfig() (configResponse, error) {
6565
return configResponse{}, err
6666
}
6767

68-
// Precompute flag values
69-
for _, flag := range response.Flags {
70-
flag.Precompute()
71-
}
72-
7368
return response, nil
7469
}
7570

eppoclient/configurationstore.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ type configuration struct {
1414
banditFlagAssociations map[string]map[string]banditVariation
1515
}
1616

17-
func (c *configuration) refreshBanditFlagAssociations() {
17+
func (c *configuration) precompute() {
1818
associations := make(map[string]map[string]banditVariation)
1919

20+
c.flags.precompute()
21+
2022
for _, banditVariations := range c.flags.Bandits {
2123
for _, bandit := range banditVariations {
2224
byVariation, ok := associations[bandit.FlagKey]
@@ -40,10 +42,10 @@ func (c configuration) getBanditVariant(flagKey, variation string) (result bandi
4042
return result, ok
4143
}
4244

43-
func (c configuration) getFlagConfiguration(key string) (flagConfiguration, error) {
45+
func (c configuration) getFlagConfiguration(key string) (*flagConfiguration, error) {
4446
flag, ok := c.flags.Flags[key]
4547
if !ok {
46-
return flag, ErrFlagConfigurationNotFound
48+
return nil, ErrFlagConfigurationNotFound
4749
}
4850

4951
return flag, nil
@@ -78,6 +80,6 @@ func (cs *configurationStore) getConfiguration() configuration {
7880
}
7981

8082
func (cs *configurationStore) setConfiguration(configuration configuration) {
81-
configuration.refreshBanditFlagAssociations()
83+
configuration.precompute()
8284
cs.configuration.Store(&configuration)
8385
}

eppoclient/configurationstore_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ func Test_GetConfiguration_unknownKey(t *testing.T) {
1313
result, err := config.getFlagConfiguration("unknown_exp")
1414

1515
assert.Error(t, err)
16-
assert.Equal(t, flagConfiguration{}, result)
16+
assert.Nil(t, result)
1717
}
1818

1919
func Test_GetConfiguration_knownKey(t *testing.T) {
2020
flags := configResponse{
21-
Flags: map[string]flagConfiguration{
22-
"experiment-key-1": flagConfiguration{
21+
Flags: map[string]*flagConfiguration{
22+
"experiment-key-1": &flagConfiguration{
2323
Key: "experiment-key-1",
2424
Enabled: false,
2525
VariationType: stringVariation,

eppoclient/eppoclient_e2e_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ func Test_e2e(t *testing.T) {
6060
case jsonVariation:
6161
value, _ := client.GetJSONAssignment(test.Flag, subject.SubjectKey, subject.SubjectAttributes, test.DefaultValue)
6262
assert.Equal(t, subject.Assignment, value)
63+
64+
// Convert DefaultValue to []byte for GetJSONBytesAssignment
65+
defaultValueBytes, _ := json.Marshal(test.DefaultValue)
66+
valueBytes, _ := client.GetJSONBytesAssignment(test.Flag, subject.SubjectKey, subject.SubjectAttributes, defaultValueBytes)
67+
68+
var parsedValueBytes interface{}
69+
_ = json.Unmarshal(valueBytes, &parsedValueBytes)
70+
assert.Equal(t, subject.Assignment, parsedValueBytes)
6371
case stringVariation:
6472
value, _ := client.GetStringAssignment(test.Flag, subject.SubjectKey, subject.SubjectAttributes, test.DefaultValue.(string))
6573
assert.Equal(t, subject.Assignment, value)

eppoclient/evalflags.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,18 @@ func (flag flagConfiguration) eval(subjectKey string, subjectAttributes Attribut
3434
return nil, nil, ErrSubjectAllocation
3535
}
3636

37-
variation, ok := flag.Variations[split.VariationKey]
37+
assignmentValue, ok := flag.ParsedVariations[split.VariationKey]
3838
if !ok {
3939
return nil, nil, fmt.Errorf("cannot find variation: %v", split.VariationKey)
4040
}
4141

42-
assignmentValue, err := flag.VariationType.valueToAssignmentValue(variation.Value)
43-
if err != nil {
44-
return nil, nil, err
45-
}
46-
4742
var assignmentEvent *AssignmentEvent
4843
if allocation.DoLog == nil || *allocation.DoLog {
4944
assignmentEvent = &AssignmentEvent{
5045
FeatureFlag: flag.Key,
5146
Allocation: allocation.Key,
5247
Experiment: flag.Key + "-" + allocation.Key,
53-
Variation: variation.Key,
48+
Variation: split.VariationKey,
5449
Subject: subjectKey,
5550
SubjectAttributes: subjectAttributes,
5651
Timestamp: now.UTC().Format(time.RFC3339),

eppoclient/rules_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ var textRule = rule{Conditions: []condition{
2424
var ruleWithEmptyConditions = rule{Conditions: []condition{}}
2525

2626
func init() {
27-
numericRule.Precompute()
28-
semverRule.Precompute()
29-
textRule.Precompute()
27+
numericRule.precompute()
28+
semverRule.precompute()
29+
textRule.precompute()
3030
}
3131

3232
func Test_TextRule_NoMatch(t *testing.T) {
@@ -292,7 +292,7 @@ func Test_isNotNull_attributePresent(t *testing.T) {
292292

293293
func Test_handles_all_numeric_types(t *testing.T) {
294294
condition := condition{Operator: "GT", Attribute: "powerLevel", Value: "9000"}
295-
condition.Precompute()
295+
condition.precompute()
296296

297297
// Floats
298298
assert.True(t, condition.matches(Attributes{"powerLevel": 9001.0}))
@@ -328,7 +328,7 @@ func Test_handles_all_numeric_types(t *testing.T) {
328328

329329
func Test_invalid_numeric_types(t *testing.T) {
330330
condition := condition{Operator: "GT", Attribute: "powerLevel", Value: "9000"}
331-
condition.Precompute()
331+
condition.precompute()
332332

333333
assert.False(t, condition.matches(Attributes{"powerLevel": "empty"}))
334334
assert.False(t, condition.matches(Attributes{"powerLevel": ""}))

eppoclient/ufc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestConditionPrecompute(t *testing.T) {
4646

4747
for _, tc := range tests {
4848
t.Run(tc.name, func(t *testing.T) {
49-
tc.condition.Precompute()
49+
tc.condition.precompute()
5050
assert.Equal(t, tc.expectedNumVal, tc.condition.NumericValue)
5151
assert.Equal(t, tc.expectedNumValValid, tc.condition.NumericValueValid)
5252
assert.Equal(t, tc.expectedSemVerVal, tc.condition.SemVerValue)

0 commit comments

Comments
 (0)