Skip to content

Commit

Permalink
feat: wip support yaml evaluator
Browse files Browse the repository at this point in the history
Signed-off-by: Suraj Banakar <surajrbanakar@gmail.com>
  • Loading branch information
vadasambar committed Nov 2, 2022
1 parent 187f0f9 commit 19a66fb
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 1 deletion.
2 changes: 1 addition & 1 deletion cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func init() {
flags.StringP(
syncProviderFlagName, "y", "filepath", "Set a sync provider e.g. filepath or remote",
)
flags.StringP(evaluatorFlagName, "e", "json", "Set an evaluator e.g. json")
flags.StringP(evaluatorFlagName, "e", "json", "Set an evaluator e.g. json, yaml")
flags.StringP(serverCertPathFlagName, "c", "", "Server side tls certificate path")
flags.StringP(serverKeyPathFlagName, "k", "", "Server side tls key path")
flags.StringToStringP(providerArgsFlagName,
Expand Down
235 changes: 235 additions & 0 deletions pkg/eval/yaml_evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package eval

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"

"github.com/diegoholiveira/jsonlogic/v3"
"github.com/open-feature/flagd/pkg/model"
schema "github.com/open-feature/schemas/json"
log "github.com/sirupsen/logrus"
"github.com/xeipuuv/gojsonschema"
"google.golang.org/protobuf/types/known/structpb"
)

type YAMLEvaluator struct {
state Flags
Logger *log.Entry
}

type constraints interface {
bool | string | map[string]any | float64
}

const (
Disabled = "DISABLED"
)

func (je *YAMLEvaluator) GetState() (string, error) {
data, err := yaml.Marshal(&je.state)
if err != nil {
return "", err
}
return string(data), nil
}

func (je *YAMLEvaluator) SetState(source string, state string) ([]StateChangeNotification, error) {
schemaLoader := gojsonschema.NewStringLoader(schema.FlagdDefinitions)
flagStringLoader := gojsonschema.NewStringLoader(state)
result, err := gojsonschema.Validate(schemaLoader, flagStringLoader)

if err != nil {
return nil, err
} else if !result.Valid() {
err := errors.New("invalid JSON file")
return nil, err
}

state, err = je.transposeEvaluators(state)
if err != nil {
return nil, fmt.Errorf("transpose evaluators: %w", err)
}

var newFlags Flags
err = json.Unmarshal([]byte(state), &newFlags)
if err != nil {
return nil, fmt.Errorf("unmarshal new state: %w", err)
}
if err := validateDefaultVariants(newFlags); err != nil {
return nil, err
}

s, notifications := je.state.Merge(source, newFlags)
je.state = s

return notifications, nil
}

func resolve[T constraints](key string, context *structpb.Struct,
variantEval func(string, *structpb.Struct) (string, string, error),
variants map[string]any) (
value T,
variant string,
reason string,
err error,
) {
variant, reason, err = variantEval(key, context)
if err != nil {
return value, variant, reason, err
}

var ok bool
value, ok = variants[variant].(T)
if !ok {
return value, variant, model.ErrorReason, errors.New(model.TypeMismatchErrorCode)
}

return value, variant, reason, nil
}

func (je *YAMLEvaluator) ResolveBooleanValue(flagKey string, context *structpb.Struct) (
value bool,
variant string,
reason string,
err error,
) {
return resolve[bool](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}

func (je *YAMLEvaluator) ResolveStringValue(flagKey string, context *structpb.Struct) (
value string,
variant string,
reason string,
err error,
) {
return resolve[string](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}

func (je *YAMLEvaluator) ResolveFloatValue(flagKey string, context *structpb.Struct) (
value float64,
variant string,
reason string,
err error,
) {
value, variant, reason, err = resolve[float64](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
return
}

func (je *YAMLEvaluator) ResolveIntValue(flagKey string, context *structpb.Struct) (
value int64,
variant string,
reason string,
err error,
) {
var val float64
val, variant, reason, err = resolve[float64](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
value = int64(val)
return
}

func (je *YAMLEvaluator) ResolveObjectValue(flagKey string, context *structpb.Struct) (
value map[string]any,
variant string,
reason string,
err error,
) {
return resolve[map[string]any](flagKey, context, je.evaluateVariant, je.state.Flags[flagKey].Variants)
}

// runs the rules (if defined) to determine the variant, otherwise falling through to the default
func (je *YAMLEvaluator) evaluateVariant(
flagKey string,
context *structpb.Struct,
) (variant string, reason string, err error) {
flag, ok := je.state.Flags[flagKey]
if !ok {
// flag not found
return "", model.ErrorReason, errors.New(model.FlagNotFoundErrorCode)
}

if flag.State == Disabled {
return "", model.ErrorReason, errors.New(model.FlagDisabledErrorCode)
}

// get the targeting logic, if any
targeting := flag.Targeting

if targeting != nil {
targetingBytes, err := targeting.MarshalJSON()
if err != nil {
je.Logger.Errorf("Error parsing rules for flag %s, %s", flagKey, err)
return "", model.ErrorReason, err
}

b, err := json.Marshal(context)
if err != nil {
je.Logger.Errorf("error parsing context for flag %s, %s, %v", flagKey, err, context)

return "", model.ErrorReason, errors.New(model.ErrorReason)
}
var result bytes.Buffer
// evaluate json-logic rules to determine the variant
err = jsonlogic.Apply(bytes.NewReader(targetingBytes), bytes.NewReader(b), &result)
if err != nil {
je.Logger.Errorf("Error applying rules %s", err)
return "", model.ErrorReason, err
}
// strip whitespace and quotes from the variant
variant = strings.ReplaceAll(strings.TrimSpace(result.String()), "\"", "")
}

// if this is a valid variant, return it
if _, ok := je.state.Flags[flagKey].Variants[variant]; ok {
return variant, model.TargetingMatchReason, nil
}

// if it's not a valid variant, use the default value
return je.state.Flags[flagKey].DefaultVariant, model.DefaultReason, nil
}

// validateDefaultVariants returns an error if any of the default variants aren't valid
func validateDefaultVariants(flags Flags) error {
for name, flag := range flags.Flags {
if _, ok := flag.Variants[flag.DefaultVariant]; !ok {
return fmt.Errorf(
"default variant '%s' isn't a valid variant of flag '%s'", flag.DefaultVariant, name,
)
}
}

return nil
}

func (je *YAMLEvaluator) transposeEvaluators(state string) (string, error) {
var evaluators Evaluators
if err := json.Unmarshal([]byte(state), &evaluators); err != nil {
return "", fmt.Errorf("unmarshal: %w", err)
}

for evalName, evalRaw := range evaluators.Evaluators {
// replace any occurrences of "evaluator": "evalName"
regex, err := regexp.Compile(fmt.Sprintf(`"\$ref":(\s)*"%s"`, evalName))
if err != nil {
return "", fmt.Errorf("compile regex: %w", err)
}

marshalledEval, err := evalRaw.MarshalJSON()
if err != nil {
return "", fmt.Errorf("marshal evaluator: %w", err)
}

evalValue := string(marshalledEval)
if len(evalValue) < 3 {
return "", errors.New("evaluator object is empty")
}
evalValue = evalValue[1 : len(evalValue)-2] // remove first { and last }

state = regex.ReplaceAllString(state, evalValue)
}

return state, nil
}
7 changes: 7 additions & 0 deletions pkg/runtime/from_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ func (r *Runtime) setEvaluatorFromConfig() error {
"component": "evaluator",
}),
}
case "yaml":
r.Evaluator = &eval.YAMLEvaluator{
Logger: log.WithFields(log.Fields{
"evaluator": "yaml",
"component": "evaluator",
}),
}
default:
return errors.New("no evaluator set")
}
Expand Down

0 comments on commit 19a66fb

Please sign in to comment.