Skip to content

Commit 65eba44

Browse files
committed
Part II - Implementation of Evaluate (custom signals condition)
* chore:add custom signal implementation and tests for string and numeric operators * chore:add implementation and tests for semantic version operators
1 parent 0a89096 commit 65eba44

File tree

4 files changed

+779
-44
lines changed

4 files changed

+779
-44
lines changed

remoteconfig/condition_evaluator.go

Lines changed: 233 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ package remoteconfig
1616

1717
import (
1818
"crypto/sha256"
19+
"encoding/json"
20+
"errors"
1921
"fmt"
2022
"log"
2123
"math/big"
24+
"regexp"
25+
"strconv"
26+
"strings"
2227
)
2328

2429
type conditionEvaluator struct {
@@ -28,15 +33,46 @@ type conditionEvaluator struct {
2833

2934
const (
3035
maxConditionRecursionDepth = 10
31-
randomizationID = "randomizationID"
3236
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")
3447
)
3548

3649
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"
4076
)
4177

4278
func (ce *conditionEvaluator) evaluateConditions() map[string]bool {
@@ -61,6 +97,8 @@ func (ce *conditionEvaluator) evaluateCondition(condition *oneOfCondition, nesti
6197
return ce.evaluateAndCondition(condition.AndCondition, nestingLevel+1)
6298
} else if condition.Percent != nil {
6399
return ce.evaluatePercentCondition(condition.Percent)
100+
} else if condition.CustomSignal != nil {
101+
return ce.evaluateCustomSignalCondition(condition.CustomSignal)
64102
}
65103
log.Println("Unknown condition type encountered.")
66104
return false
@@ -69,7 +107,6 @@ func (ce *conditionEvaluator) evaluateCondition(condition *oneOfCondition, nesti
69107
func (ce *conditionEvaluator) evaluateOrCondition(orCondition *orCondition, nestingLevel int) bool {
70108
for _, condition := range orCondition.Conditions {
71109
result := ce.evaluateCondition(&condition, nestingLevel+1)
72-
// short-circuit evaluation, return true if any of the conditions return true
73110
if result {
74111
return true
75112
}
@@ -80,7 +117,6 @@ func (ce *conditionEvaluator) evaluateOrCondition(orCondition *orCondition, nest
80117
func (ce *conditionEvaluator) evaluateAndCondition(andCondition *andCondition, nestingLevel int) bool {
81118
for _, condition := range andCondition.Conditions {
82119
result := ce.evaluateCondition(&condition, nestingLevel+1)
83-
// short-circuit evaluation, return false if any of the conditions return false
84120
if !result {
85121
return false
86122
}
@@ -121,11 +157,196 @@ func computeInstanceMicroPercentile(seed string, randomizationID string) uint32
121157
hash := sha256.New()
122158
hash.Write([]byte(stringToHash))
123159
// 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))
128162
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.
130164
return uint32(instanceMicroPercentileBigInt.Int64())
131165
}
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

Comments
 (0)