@@ -16,9 +16,14 @@ package remoteconfig
16
16
17
17
import (
18
18
"crypto/sha256"
19
+ "encoding/json"
20
+ "errors"
19
21
"fmt"
20
22
"log"
21
23
"math/big"
24
+ "regexp"
25
+ "strconv"
26
+ "strings"
22
27
)
23
28
24
29
type conditionEvaluator struct {
@@ -28,15 +33,46 @@ type conditionEvaluator struct {
28
33
29
34
const (
30
35
maxConditionRecursionDepth = 10
31
- randomizationID = "randomizationID"
32
36
rootNestingLevel = 0
33
- totalMicroPercentiles = 100_000_000
37
+ doublePrecision = 64
38
+ whiteSpace = " "
39
+ segmentSeparator = "."
40
+ maxPossibleSegments = 5
41
+ )
42
+
43
+ var (
44
+ errTooManySegments = errors .New ("number of segments exceeds maximum allowed length" )
45
+ errNegativeSegment = errors .New ("segment cannot be negative" )
46
+ errInvalidCustomSignal = errors .New ("missing operator, key, or target values for custom signal condition" )
34
47
)
35
48
36
49
const (
37
- lessThanOrEqual = "LESS_OR_EQUAL"
38
- greaterThan = "GREATER_THAN"
39
- between = "BETWEEN"
50
+ randomizationID = "randomizationID"
51
+ totalMicroPercentiles = 100_000_000
52
+ lessThanOrEqual = "LESS_OR_EQUAL"
53
+ greaterThan = "GREATER_THAN"
54
+ between = "BETWEEN"
55
+ )
56
+
57
+ const (
58
+ stringContains = "STRING_CONTAINS"
59
+ stringDoesNotContain = "STRING_DOES_NOT_CONTAIN"
60
+ stringExactlyMatches = "STRING_EXACTLY_MATCHES"
61
+ stringContainsRegex = "STRING_CONTAINS_REGEX"
62
+
63
+ numericLessThan = "NUMERIC_LESS_THAN"
64
+ numericLessThanEqual = "NUMERIC_LESS_EQUAL"
65
+ numericEqual = "NUMERIC_EQUAL"
66
+ numericNotEqual = "NUMERIC_NOT_EQUAL"
67
+ numericGreaterThan = "NUMERIC_GREATER_THAN"
68
+ numericGreaterEqual = "NUMERIC_GREATER_EQUAL"
69
+
70
+ semanticVersionLessThan = "SEMANTIC_VERSION_LESS_THAN"
71
+ semanticVersionLessEqual = "SEMANTIC_VERSION_LESS_EQUAL"
72
+ semanticVersionEqual = "SEMANTIC_VERSION_EQUAL"
73
+ semanticVersionNotEqual = "SEMANTIC_VERSION_NOT_EQUAL"
74
+ semanticVersionGreaterThan = "SEMANTIC_VERSION_GREATER_THAN"
75
+ semanticVersionGreaterEqual = "SEMANTIC_VERSION_GREATER_EQUAL"
40
76
)
41
77
42
78
func (ce * conditionEvaluator ) evaluateConditions () map [string ]bool {
@@ -61,6 +97,8 @@ func (ce *conditionEvaluator) evaluateCondition(condition *oneOfCondition, nesti
61
97
return ce .evaluateAndCondition (condition .AndCondition , nestingLevel + 1 )
62
98
} else if condition .Percent != nil {
63
99
return ce .evaluatePercentCondition (condition .Percent )
100
+ } else if condition .CustomSignal != nil {
101
+ return ce .evaluateCustomSignalCondition (condition .CustomSignal )
64
102
}
65
103
log .Println ("Unknown condition type encountered." )
66
104
return false
@@ -69,7 +107,6 @@ func (ce *conditionEvaluator) evaluateCondition(condition *oneOfCondition, nesti
69
107
func (ce * conditionEvaluator ) evaluateOrCondition (orCondition * orCondition , nestingLevel int ) bool {
70
108
for _ , condition := range orCondition .Conditions {
71
109
result := ce .evaluateCondition (& condition , nestingLevel + 1 )
72
- // short-circuit evaluation, return true if any of the conditions return true
73
110
if result {
74
111
return true
75
112
}
@@ -80,7 +117,6 @@ func (ce *conditionEvaluator) evaluateOrCondition(orCondition *orCondition, nest
80
117
func (ce * conditionEvaluator ) evaluateAndCondition (andCondition * andCondition , nestingLevel int ) bool {
81
118
for _ , condition := range andCondition .Conditions {
82
119
result := ce .evaluateCondition (& condition , nestingLevel + 1 )
83
- // short-circuit evaluation, return false if any of the conditions return false
84
120
if ! result {
85
121
return false
86
122
}
@@ -121,11 +157,196 @@ func computeInstanceMicroPercentile(seed string, randomizationID string) uint32
121
157
hash := sha256 .New ()
122
158
hash .Write ([]byte (stringToHash ))
123
159
// Calculate the final SHA-256 hash as a byte slice (32 bytes).
124
- hashBytes := hash .Sum (nil )
125
-
126
- hashBigInt := new (big.Int ).SetBytes (hashBytes )
127
- // Convert the hash bytes to a big.Int. The "0x" prefix is implicit in the conversion from hex to big.Int.
160
+ // Convert to a big.Int. The "0x" prefix is implicit in the conversion from hex to big.Int.
161
+ hashBigInt := new (big.Int ).SetBytes (hash .Sum (nil ))
128
162
instanceMicroPercentileBigInt := new (big.Int ).Mod (hashBigInt , big .NewInt (totalMicroPercentiles ))
129
- // Can safely convert to uint32 since the range of instanceMicroPercentile is 0 to 100_000_000; range of uint32 is 0 to 4_294_967_295.
163
+ // Safely convert to uint32 since the range of instanceMicroPercentile is 0 to 100_000_000; range of uint32 is 0 to 4_294_967_295.
130
164
return uint32 (instanceMicroPercentileBigInt .Int64 ())
131
165
}
166
+
167
+ func (ce * conditionEvaluator ) evaluateCustomSignalCondition (customSignalCondition * customSignalCondition ) bool {
168
+ if err := customSignalCondition .isValid (); err != nil {
169
+ log .Println (err )
170
+ return false
171
+ }
172
+ actualValue , ok := ce .evaluationContext [customSignalCondition .CustomSignalKey ]
173
+ if ! ok {
174
+ log .Printf ("Custom signal key: %s, missing from context\n " , customSignalCondition .CustomSignalKey )
175
+ return false
176
+ }
177
+ switch customSignalCondition .CustomSignalOperator {
178
+ case stringContains :
179
+ return compareStrings (customSignalCondition .TargetCustomSignalValues , actualValue , func (actualValue , target string ) bool { return strings .Contains (actualValue , target ) })
180
+ case stringDoesNotContain :
181
+ return ! compareStrings (customSignalCondition .TargetCustomSignalValues , actualValue , func (actualValue , target string ) bool { return strings .Contains (actualValue , target ) })
182
+ case stringExactlyMatches :
183
+ return compareStrings (customSignalCondition .TargetCustomSignalValues , actualValue , func (actualValue , target string ) bool {
184
+ return strings .Trim (actualValue , whiteSpace ) == strings .Trim (target , whiteSpace )
185
+ })
186
+ case stringContainsRegex :
187
+ return compareStrings (customSignalCondition .TargetCustomSignalValues , actualValue , func (actualValue , targetPattern string ) bool {
188
+ result , err := regexp .MatchString (targetPattern , actualValue )
189
+ if err != nil {
190
+ return false
191
+ }
192
+ return result
193
+ })
194
+
195
+ // For numeric operators only one target value is allowed
196
+ case numericLessThan :
197
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result < 0 })
198
+ case numericLessThanEqual :
199
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result <= 0 })
200
+ case numericEqual :
201
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result == 0 })
202
+ case numericNotEqual :
203
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result != 0 })
204
+ case numericGreaterThan :
205
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result > 0 })
206
+ case numericGreaterEqual :
207
+ return compareNumbers (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result >= 0 })
208
+
209
+ // For semantic operators only one target value is allowed.
210
+ case semanticVersionLessThan :
211
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result < 0 })
212
+ case semanticVersionLessEqual :
213
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result <= 0 })
214
+ case semanticVersionEqual :
215
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result == 0 })
216
+ case semanticVersionNotEqual :
217
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result != 0 })
218
+ case semanticVersionGreaterThan :
219
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result > 0 })
220
+ case semanticVersionGreaterEqual :
221
+ return compareSemanticVersion (customSignalCondition .TargetCustomSignalValues [0 ], actualValue , func (result int ) bool { return result >= 0 })
222
+ }
223
+ log .Printf ("Unknown custom signal operator: %s\n " , customSignalCondition .CustomSignalOperator )
224
+ return false
225
+ }
226
+
227
+ func (cs * customSignalCondition ) isValid () error {
228
+ if cs .CustomSignalOperator == "" || cs .CustomSignalKey == "" || len (cs .TargetCustomSignalValues ) == 0 {
229
+ return errInvalidCustomSignal
230
+ }
231
+ return nil
232
+ }
233
+
234
+ func compareStrings (targetCustomSignalValues []string , actualValue any , predicateFn func (actualValue , target string ) bool ) bool {
235
+ csValStr , ok := actualValue .(string )
236
+ if ! ok {
237
+ if jsonBytes , err := json .Marshal (actualValue ); err == nil {
238
+ csValStr = string (jsonBytes )
239
+ } else {
240
+ log .Printf ("Failed to parse custom signal value '%v' as a string : %v\n " , actualValue , err )
241
+ return false
242
+ }
243
+ }
244
+ for _ , target := range targetCustomSignalValues {
245
+ if predicateFn (csValStr , target ) {
246
+ return true
247
+ }
248
+ }
249
+ return false
250
+ }
251
+
252
+ func compareNumbers (targetCustomSignalValue string , actualValue any , predicateFn func (result int ) bool ) bool {
253
+ targetFloat , err := strconv .ParseFloat (strings .Trim (targetCustomSignalValue , whiteSpace ), doublePrecision )
254
+ if err != nil {
255
+ log .Printf ("Failed to convert target custom signal value '%v' from string to number: %v" , targetCustomSignalValue , err )
256
+ return false
257
+ }
258
+ var actualValFloat float64
259
+ switch actualValue := actualValue .(type ) {
260
+ case float32 :
261
+ actualValFloat = float64 (actualValue )
262
+ case float64 :
263
+ actualValFloat = actualValue
264
+ case int8 :
265
+ actualValFloat = float64 (actualValue )
266
+ case int :
267
+ actualValFloat = float64 (actualValue )
268
+ case int16 :
269
+ actualValFloat = float64 (actualValue )
270
+ case int32 :
271
+ actualValFloat = float64 (actualValue )
272
+ case int64 :
273
+ actualValFloat = float64 (actualValue )
274
+ case uint8 :
275
+ actualValFloat = float64 (actualValue )
276
+ case uint :
277
+ actualValFloat = float64 (actualValue )
278
+ case uint16 :
279
+ actualValFloat = float64 (actualValue )
280
+ case uint32 :
281
+ actualValFloat = float64 (actualValue )
282
+ case uint64 :
283
+ actualValFloat = float64 (actualValue )
284
+ case bool :
285
+ if actualValue {
286
+ actualValFloat = 1
287
+ } else {
288
+ actualValFloat = 0
289
+ }
290
+ case string :
291
+ actualValFloat , err = strconv .ParseFloat (strings .Trim (actualValue , whiteSpace ), doublePrecision )
292
+ if err != nil {
293
+ log .Printf ("Failed to convert custom signal value '%v' from string to number: %v" , actualValue , err )
294
+ return false
295
+ }
296
+ default :
297
+ log .Printf ("Cannot parse custom signal value '%v' of type %T as a number" , actualValue , actualValue )
298
+ return false
299
+ }
300
+ result := 0
301
+ if actualValFloat > targetFloat {
302
+ result = 1
303
+ } else if actualValFloat < targetFloat {
304
+ result = - 1
305
+ }
306
+ return predicateFn (result )
307
+ }
308
+
309
+ func compareSemanticVersion (targetValue string , actualValue any , predicateFn func (result int ) bool ) bool {
310
+ targetSemVer , err := transformVersionToSegments (strings .Trim (targetValue , whiteSpace ))
311
+ if err != nil {
312
+ log .Printf ("Error transforming target semantic version %q: %v\n " , targetValue , err )
313
+ return false
314
+ }
315
+ actualValueStr := fmt .Sprintf ("%v" , actualValue )
316
+ actualSemVer , err := transformVersionToSegments (strings .Trim (actualValueStr , whiteSpace ))
317
+ if err != nil {
318
+ log .Printf ("Error transforming custom signal value '%v' to semantic version: %v\n " , actualValue , err )
319
+ return false
320
+ }
321
+ for idx := 0 ; idx < maxPossibleSegments ; idx ++ {
322
+ if actualSemVer [idx ] > targetSemVer [idx ] {
323
+ return predicateFn (1 )
324
+ } else if actualSemVer [idx ] < targetSemVer [idx ] {
325
+ return predicateFn (- 1 )
326
+ }
327
+ }
328
+ return predicateFn (0 )
329
+ }
330
+
331
+ func transformVersionToSegments (version string ) ([]int , error ) {
332
+ // Trim any trailing or leading segment separators (.) and split.
333
+ trimmedVersion := strings .Trim (version , segmentSeparator )
334
+ segments := strings .Split (trimmedVersion , segmentSeparator )
335
+
336
+ if len (segments ) > maxPossibleSegments {
337
+ return nil , errTooManySegments
338
+ }
339
+ // Initialize with the maximum possible segment length for consistent comparison.
340
+ transformedVersion := make ([]int , maxPossibleSegments )
341
+ for idx , segmentStr := range segments {
342
+ segmentInt , err := strconv .Atoi (segmentStr )
343
+ if err != nil {
344
+ return nil , err
345
+ }
346
+ if segmentInt < 0 {
347
+ return nil , errNegativeSegment
348
+ }
349
+ transformedVersion [idx ] = segmentInt
350
+ }
351
+ return transformedVersion , nil
352
+ }
0 commit comments