diff --git a/core/pkg/eval/fractional_evaluation.go b/core/pkg/eval/fractional_evaluation.go index 1b7b3e4ce..5b3192fed 100644 --- a/core/pkg/eval/fractional_evaluation.go +++ b/core/pkg/eval/fractional_evaluation.go @@ -5,18 +5,28 @@ import ( "fmt" "math" + "github.com/open-feature/flagd/core/pkg/logger" + "github.com/zeebo/xxh3" ) +type FractionalEvaluator struct { + Logger *logger.Logger +} + type fractionalEvaluationDistribution struct { variant string percentage int } -func (je *JSONEvaluator) fractionalEvaluation(values, data interface{}) interface{} { +func NewFractionalEvaluator(logger *logger.Logger) *FractionalEvaluator { + return &FractionalEvaluator{Logger: logger} +} + +func (fe *FractionalEvaluator) FractionalEvaluation(values, data interface{}) interface{} { valueToDistribute, feDistributions, err := parseFractionalEvaluationData(values, data) if err != nil { - je.Logger.Error(fmt.Sprintf("parse fractional evaluation data: %v", err)) + fe.Logger.Error(fmt.Sprintf("parse fractional evaluation data: %v", err)) return nil } diff --git a/core/pkg/eval/fractional_evaluation_test.go b/core/pkg/eval/fractional_evaluation_test.go index 57b136f13..f0f7a21a3 100644 --- a/core/pkg/eval/fractional_evaluation_test.go +++ b/core/pkg/eval/fractional_evaluation_test.go @@ -281,7 +281,15 @@ func TestFractionalEvaluation(t *testing.T) { const reqID = "default" for name, tt := range tests { t.Run(name, func(t *testing.T) { - je := NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + log := logger.NewLogger(nil, false) + je := NewJSONEvaluator( + log, + store.NewFlags(), + WithEvaluator( + "fractionalEvaluation", + NewFractionalEvaluator(log).FractionalEvaluation, + ), + ) je.store.Flags = tt.flags.Flags value, variant, reason, err := resolve[string]( @@ -414,7 +422,15 @@ func BenchmarkFractionalEvaluation(b *testing.B) { reqID := "test" for name, tt := range tests { b.Run(name, func(b *testing.B) { - je := JSONEvaluator{store: &store.Flags{Flags: tt.flags.Flags}} + log := logger.NewLogger(nil, false) + je := NewJSONEvaluator( + log, + &store.Flags{Flags: tt.flags.Flags}, + WithEvaluator( + "fractionalEvaluation", + NewFractionalEvaluator(log).FractionalEvaluation, + ), + ) for i := 0; i < b.N; i++ { value, variant, reason, err := resolve[string]( reqID, tt.flagKey, tt.context, je.evaluateVariant, je.store.Flags[tt.flagKey].Variants, diff --git a/core/pkg/eval/json_evaluator.go b/core/pkg/eval/json_evaluator.go index 9706aec3f..47706017f 100644 --- a/core/pkg/eval/json_evaluator.go +++ b/core/pkg/eval/json_evaluator.go @@ -45,7 +45,15 @@ const ( Disabled = "DISABLED" ) -func NewJSONEvaluator(logger *logger.Logger, s *store.Flags) *JSONEvaluator { +type JSONEvaluatorOption func(je *JSONEvaluator) + +func WithEvaluator(name string, evalFunc func(interface{}, interface{}) interface{}) JSONEvaluatorOption { + return func(_ *JSONEvaluator) { + jsonlogic.AddOperator(name, evalFunc) + } +} + +func NewJSONEvaluator(logger *logger.Logger, s *store.Flags, opts ...JSONEvaluatorOption) *JSONEvaluator { ev := JSONEvaluator{ Logger: logger.WithFields( zap.String("component", "evaluator"), @@ -54,16 +62,10 @@ func NewJSONEvaluator(logger *logger.Logger, s *store.Flags) *JSONEvaluator { store: s, jsonEvalTracer: otel.Tracer("jsonEvaluator"), } - jsonlogic.AddOperator("fractionalEvaluation", ev.fractionalEvaluation) - sce := StringComparisonEvaluator{ - Logger: ev.Logger, + for _, o := range opts { + o(&ev) } - jsonlogic.AddOperator("starts_with", sce.StartsWithEvaluation) - jsonlogic.AddOperator("ends_with", sce.EndsWithEvaluation) - - sve := SemVerComparisonEvaluator{Logger: ev.Logger} - jsonlogic.AddOperator("sem_ver", sve.SemVerEvaluation) return &ev } diff --git a/core/pkg/eval/semver_evaluation.go b/core/pkg/eval/semver_evaluation.go index b8c26d262..0a5735e33 100644 --- a/core/pkg/eval/semver_evaluation.go +++ b/core/pkg/eval/semver_evaluation.go @@ -54,6 +54,10 @@ type SemVerComparisonEvaluator struct { Logger *logger.Logger } +func NewSemVerComparisonEvaluator(log *logger.Logger) *SemVerComparisonEvaluator { + return &SemVerComparisonEvaluator{Logger: log} +} + // SemVerEvaluation checks if the given property matches a semantic versioning condition. // It returns 'true', if the value of the given property meets the condition, 'false' if not. // As an example, it can be used in the following way inside an 'if' evaluation: diff --git a/core/pkg/eval/semver_evaluation_test.go b/core/pkg/eval/semver_evaluation_test.go index 55770e4dc..75672039b 100644 --- a/core/pkg/eval/semver_evaluation_test.go +++ b/core/pkg/eval/semver_evaluation_test.go @@ -727,7 +727,15 @@ func TestJSONEvaluator_semVerEvaluation(t *testing.T) { const reqID = "default" for name, tt := range tests { t.Run(name, func(t *testing.T) { - je := NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + log := logger.NewLogger(nil, false) + je := NewJSONEvaluator( + log, + store.NewFlags(), + WithEvaluator( + "sem_ver", + NewSemVerComparisonEvaluator(log).SemVerEvaluation, + ), + ) je.store.Flags = tt.flags.Flags value, variant, reason, err := resolve[string]( diff --git a/core/pkg/eval/string_comparison_evaluation.go b/core/pkg/eval/string_comparison_evaluation.go index e655b0f18..7b99ce229 100644 --- a/core/pkg/eval/string_comparison_evaluation.go +++ b/core/pkg/eval/string_comparison_evaluation.go @@ -12,6 +12,10 @@ type StringComparisonEvaluator struct { Logger *logger.Logger } +func NewStringComparisonEvaluator(log *logger.Logger) *StringComparisonEvaluator { + return &StringComparisonEvaluator{Logger: log} +} + // 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: @@ -59,7 +63,7 @@ func (sce *StringComparisonEvaluator) StartsWithEvaluation(values, _ interface{} // // 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{} { +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)) diff --git a/core/pkg/eval/string_comparison_evaluation_test.go b/core/pkg/eval/string_comparison_evaluation_test.go index b9f00c8ab..a3994b40b 100644 --- a/core/pkg/eval/string_comparison_evaluation_test.go +++ b/core/pkg/eval/string_comparison_evaluation_test.go @@ -191,7 +191,15 @@ func TestJSONEvaluator_startsWithEvaluation(t *testing.T) { const reqID = "default" for name, tt := range tests { t.Run(name, func(t *testing.T) { - je := NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + log := logger.NewLogger(nil, false) + je := NewJSONEvaluator( + log, + store.NewFlags(), + WithEvaluator( + "starts_with", + NewStringComparisonEvaluator(log).StartsWithEvaluation, + ), + ) je.store.Flags = tt.flags.Flags value, variant, reason, err := resolve[string]( @@ -397,7 +405,16 @@ func TestJSONEvaluator_endsWithEvaluation(t *testing.T) { const reqID = "default" for name, tt := range tests { t.Run(name, func(t *testing.T) { - je := NewJSONEvaluator(logger.NewLogger(nil, false), store.NewFlags()) + log := logger.NewLogger(nil, false) + je := NewJSONEvaluator( + log, + store.NewFlags(), + WithEvaluator( + "ends_with", + NewStringComparisonEvaluator(log).EndsWithEvaluation, + ), + ) + je.store.Flags = tt.flags.Flags value, variant, reason, err := resolve[string]( diff --git a/core/pkg/runtime/from_config.go b/core/pkg/runtime/from_config.go index 28e177b90..21f7b7e84 100644 --- a/core/pkg/runtime/from_config.go +++ b/core/pkg/runtime/from_config.go @@ -106,7 +106,8 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, s.FlagSources = sources // derive evaluator - evaluator := eval.NewJSONEvaluator(logger, s) + evaluator := setupJSONEvaluator(logger, s) + // derive service connectService := flageval.NewConnectService( logger.WithFields(zap.String("component", "service")), @@ -138,6 +139,30 @@ func FromConfig(logger *logger.Logger, version string, config Config) (*Runtime, }, nil } +func setupJSONEvaluator(logger *logger.Logger, s *store.Flags) *eval.JSONEvaluator { + evaluator := eval.NewJSONEvaluator( + logger, + s, + eval.WithEvaluator( + "fractionalEvaluation", + eval.NewFractionalEvaluator(logger).FractionalEvaluation, + ), + eval.WithEvaluator( + "starts_with", + eval.NewStringComparisonEvaluator(logger).StartsWithEvaluation, + ), + eval.WithEvaluator( + "ends_with", + eval.NewStringComparisonEvaluator(logger).EndsWithEvaluation, + ), + eval.WithEvaluator( + "sem_ver", + eval.NewSemVerComparisonEvaluator(logger).SemVerEvaluation, + ), + ) + return evaluator +} + // syncProvidersFromConfig is a helper to build ISync implementations from SourceConfig func syncProvidersFromConfig(logger *logger.Logger, sources []SourceConfig) ([]sync.ISync, error) { syncImpls := []sync.ISync{} diff --git a/core/pkg/runtime/from_config_test.go b/core/pkg/runtime/from_config_test.go index c83b2eb1a..bb1381bdc 100644 --- a/core/pkg/runtime/from_config_test.go +++ b/core/pkg/runtime/from_config_test.go @@ -4,6 +4,9 @@ import ( "reflect" "testing" + "github.com/open-feature/flagd/core/pkg/store" + "github.com/stretchr/testify/require" + "github.com/open-feature/flagd/core/pkg/logger" ) @@ -298,3 +301,10 @@ func Test_syncProvidersFromConfig(t *testing.T) { }) } } + +func Test_setupJSONEvaluator(t *testing.T) { + lg := logger.NewLogger(nil, false) + + je := setupJSONEvaluator(lg, store.NewFlags()) + require.NotNil(t, je) +}