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 #691

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions core/pkg/eval/fractional_evaluation.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@ 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
20 changes: 18 additions & 2 deletions core/pkg/eval/fractional_evaluation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 11 additions & 9 deletions core/pkg/eval/json_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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
}
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(
"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 @@ -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](
Expand Down Expand Up @@ -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](
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 @@ -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")),
Expand Down Expand Up @@ -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{}
Expand Down
9 changes: 9 additions & 0 deletions core/pkg/runtime/from_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"testing"

"github.com/open-feature/flagd/core/pkg/logger"
"github.com/open-feature/flagd/core/pkg/store"
"github.com/stretchr/testify/require"
)

func TestParseSource(t *testing.T) {
Expand Down Expand Up @@ -298,3 +300,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)
}