Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor json logic evaluator to pass custom operators as options #684

Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions core/pkg/eval/fractional_evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
18 changes: 16 additions & 2 deletions core/pkg/eval/fractional_evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,14 @@ 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([]string{"fractionalEvaluation"},
NewFractionalEvaluator(log).FractionalEvaluation,
),
)
je.store.Flags = tt.flags.Flags

value, variant, reason, err := resolve[string](
Expand Down Expand Up @@ -415,7 +422,14 @@ 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([]string{"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,
Expand Down
22 changes: 13 additions & 9 deletions core/pkg/eval/json_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,17 @@ const (
Disabled = "DISABLED"
)

func NewJSONEvaluator(logger *logger.Logger, s *store.Flags) *JSONEvaluator {
type JSONEvaluatorOption func(je *JSONEvaluator)

func WithEvaluator(aliases []string, evalFunc func(interface{}, interface{}) interface{}) JSONEvaluatorOption {
bacherfl marked this conversation as resolved.
Show resolved Hide resolved
return func(_ *JSONEvaluator) {
for _, alias := range aliases {
jsonlogic.AddOperator(alias, evalFunc)
}
}
}

func NewJSONEvaluator(logger *logger.Logger, s *store.Flags, opts ...JSONEvaluatorOption) *JSONEvaluator {
ev := JSONEvaluator{
Logger: logger.WithFields(
zap.String("component", "evaluator"),
Expand All @@ -58,16 +68,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
}
Expand Down
4 changes: 4 additions & 0 deletions core/pkg/eval/semver_evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 9 additions & 1 deletion core/pkg/eval/semver_evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
[]string{"sem_ver"},
NewSemVerComparisonEvaluator(log).SemVerEvaluation,
),
)
je.store.Flags = tt.flags.Flags

value, variant, reason, err := resolve[string](
Expand Down
6 changes: 5 additions & 1 deletion core/pkg/eval/string_comparison_evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down
21 changes: 19 additions & 2 deletions core/pkg/eval/string_comparison_evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,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(
[]string{"starts_with"},
NewStringComparisonEvaluator(log).StartsWithEvaluation,
),
)
je.store.Flags = tt.flags.Flags

value, variant, reason, err := resolve[string](
Expand Down Expand Up @@ -399,7 +407,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(
[]string{"ends_with"},
NewStringComparisonEvaluator(log).EndsWithEvaluation,
),
)

je.store.Flags = tt.flags.Flags

value, variant, reason, err := resolve[string](
Expand Down
27 changes: 26 additions & 1 deletion core/pkg/runtime/from_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,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")),
Expand Down Expand Up @@ -140,6 +141,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(
[]string{"fractionalEvaluation"},
eval.NewFractionalEvaluator(logger).FractionalEvaluation,
),
eval.WithEvaluator(
[]string{"starts_with"},
eval.NewStringComparisonEvaluator(logger).StartsWithEvaluation,
),
eval.WithEvaluator(
[]string{"ends_with"},
eval.NewStringComparisonEvaluator(logger).EndsWithEvaluation,
),
eval.WithEvaluator(
[]string{"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{}
Expand Down
10 changes: 10 additions & 0 deletions core/pkg/runtime/from_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}