Skip to content

Commit

Permalink
feat: add starts_with and ends_with json evaluators (#658)
Browse files Browse the repository at this point in the history
Signed-off-by: Florian Bacher <florian.bacher@dynatrace.com>
  • Loading branch information
bacherfl authored May 23, 2023
1 parent 07407a3 commit f932b8f
Show file tree
Hide file tree
Showing 5 changed files with 793 additions and 0 deletions.
6 changes: 6 additions & 0 deletions core/pkg/eval/json_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ func NewJSONEvaluator(logger *logger.Logger, s *store.Flags) *JSONEvaluator {
jsonEvalTracer: otel.Tracer("jsonEvaluator"),
}
jsonlogic.AddOperator("fractionalEvaluation", ev.fractionalEvaluation)

sce := StringComparisonEvaluator{
Logger: ev.Logger,
}
jsonlogic.AddOperator("starts_with", sce.StartsWithEvaluation)
jsonlogic.AddOperator("ends_with", sce.EndsWithEvaluation)
return &ev
}

Expand Down
116 changes: 116 additions & 0 deletions core/pkg/eval/string_comparison_evaluation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package eval

import (
"errors"
"fmt"
"strings"

"github.com/open-feature/flagd/core/pkg/logger"
)

type StringComparisonEvaluator struct {
Logger *logger.Logger
}

// StartsWithEvaluation checks if the given property starts with a certain prefix.
// It returns 'true', if the value of the given property starts with the prefix, 'false' if not.
// As an example, it can be used in the following way inside an 'if' evaluation:
//
// {
// "if": [
// {
// "starts_with": [{"var": "email"}, "user@faas"]
// },
// "red", null
// ]
// }
//
// This rule can be applied to the following data object, where the evaluation will resolve to 'true':
//
// { "email": "user@faas.com" }
//
// Note that the 'starts_with' evaluation rule must contain exactly two items, which both resolve to a
// string value
func (sce *StringComparisonEvaluator) StartsWithEvaluation(values, _ interface{}) interface{} {
propertyValue, target, err := parseStringComparisonEvaluationData(values)
if err != nil {
sce.Logger.Error(fmt.Sprintf("parse starts_with evaluation data: %v", err))
return nil
}
return strings.HasPrefix(propertyValue, target)
}

// EndsWithEvaluation checks if the given property ends with a certain prefix.
// It returns 'true', if the value of the given property starts with the prefix, 'false' if not.
// As an example, it can be used in the following way inside an 'if' evaluation:
//
// {
// "if": [
// {
// "ends_with": [{"var": "email"}, "faas.com"]
// },
// "red", null
// ]
// }
//
// This rule can be applied to the following data object, where the evaluation will resolve to 'true':
//
// { "email": "user@faas.com" }
//
// Note that the 'ends_with' evaluation rule must contain exactly two items, which both resolve to a
// string value
func (sce *StringComparisonEvaluator) EndsWithEvaluation(values, _ interface{}) interface{} {
propertyValue, target, err := parseStringComparisonEvaluationData(values)
if err != nil {
sce.Logger.Error(fmt.Sprintf("parse ends_with evaluation data: %v", err))
return nil
}
return strings.HasSuffix(propertyValue, target)
}

// parseStringComparisonEvaluationData tries to parse the input for the starts_with/ends_with evaluation.
// this evaluator requires an array containing exactly two strings.
// Note that, when used with jsonLogic, those two items can also have been objects in the original 'values' object,
// which have been resolved to string values by jsonLogic before this function is called.
// As an example, the following values object:
//
// {
// "if": [
// {
// "starts_with": [{"var": "email"}, "user@faas"]
// },
// "red", null
// ]
// }
//
// with the following data object:
//
// { "email": "user@faas.com" }
//
// will have been resolved to
//
// ["user@faas.com", "user@faas"]
//
// at the time this function is reached.
func parseStringComparisonEvaluationData(values interface{}) (string, string, error) {
parsed, ok := values.([]interface{})
if !ok {
return "", "", errors.New("[start/end]s_with evaluation is not an array")
}

if len(parsed) != 2 {
return "", "", errors.New("[start/end]s_with evaluation must contain a value and a comparison target")
}

property, ok := parsed[0].(string)
if !ok {
return "", "", errors.New("[start/end]s_with evaluation: property did not resolve to a string value")
}

targetValue, ok := parsed[1].(string)
if !ok {
return "", "", errors.New("[start/end]s_with evaluation: target value did not resolve to a string value")
}

return property, targetValue, nil
}
Loading

1 comment on commit f932b8f

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Go Benchmark'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.30.

Benchmark suite Current: f932b8f Previous: 2e06d58 Ratio
BenchmarkResolveBooleanValue/test_staticBoolFlag 2387 ns/op 304 B/op 7 allocs/op 1545 ns/op 304 B/op 7 allocs/op 1.54

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.