diff --git a/e2e/common_test.go b/e2e/common_test.go index 60875d2f..b4546df5 100644 --- a/e2e/common_test.go +++ b/e2e/common_test.go @@ -1,8 +1,8 @@ package e2e_test import ( - "github.com/open-feature/go-sdk/pkg/openfeature" - "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" ) // ctxFunction is a context based evaluation callback diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go index afb9a6ac..a696ccd3 100644 --- a/e2e/evaluation_fuzz_test.go +++ b/e2e/evaluation_fuzz_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/open-feature/go-sdk/pkg/openfeature" - "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" ) func setupFuzzClient(f *testing.F) *openfeature.Client { diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index c13028e7..3563cbb6 100644 --- a/e2e/evaluation_test.go +++ b/e2e/evaluation_test.go @@ -8,8 +8,8 @@ import ( "testing" "github.com/cucumber/godog" - "github.com/open-feature/go-sdk/pkg/openfeature" - "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" ) var client = openfeature.NewClient("evaluation tests") diff --git a/pkg/openfeature/api.go b/openfeature/api.go similarity index 98% rename from pkg/openfeature/api.go rename to openfeature/api.go index 00a999f2..c02fed30 100644 --- a/pkg/openfeature/api.go +++ b/openfeature/api.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/go-logr/logr" - "github.com/open-feature/go-sdk/pkg/openfeature/internal" + "github.com/open-feature/go-sdk/openfeature/internal" "golang.org/x/exp/maps" ) diff --git a/openfeature/client.go b/openfeature/client.go new file mode 100644 index 00000000..3391531a --- /dev/null +++ b/openfeature/client.go @@ -0,0 +1,765 @@ +package openfeature + +import ( + "context" + "errors" + "fmt" + "sync" + "unicode/utf8" + + "github.com/go-logr/logr" +) + +// IClient defines the behaviour required of an openfeature client +type IClient interface { + Metadata() ClientMetadata + AddHooks(hooks ...Hook) + AddHandler(eventType EventType, callback EventCallback) + RemoveHandler(eventType EventType, callback EventCallback) + SetEvaluationContext(evalCtx EvaluationContext) + EvaluationContext() EvaluationContext + BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) + StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) + FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) + IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) + ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) + BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) + StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) + FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) + IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) + ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) +} + +// ClientMetadata provides a client's metadata +type ClientMetadata struct { + name string +} + +// NewClientMetadata constructs ClientMetadata +// Allows for simplified hook test cases while maintaining immutability +func NewClientMetadata(name string) ClientMetadata { + return ClientMetadata{ + name: name, + } +} + +// Name returns the client's name +func (cm ClientMetadata) Name() string { + return cm.name +} + +// Client implements the behaviour required of an openfeature client +type Client struct { + mx sync.RWMutex + metadata ClientMetadata + hooks []Hook + evaluationContext EvaluationContext + logger func() logr.Logger +} + +// NewClient returns a new Client. Name is a unique identifier for this client +func NewClient(name string) *Client { + return &Client{ + metadata: ClientMetadata{name: name}, + hooks: []Hook{}, + evaluationContext: EvaluationContext{}, + logger: globalLogger, + } +} + +// WithLogger sets the logger of the client +func (c *Client) WithLogger(l logr.Logger) *Client { + c.mx.Lock() + defer c.mx.Unlock() + c.logger = func() logr.Logger { return l } + return c +} + +// Metadata returns the client's metadata +func (c *Client) Metadata() ClientMetadata { + c.mx.RLock() + defer c.mx.RUnlock() + return c.metadata +} + +// AddHooks appends to the client's collection of any previously added hooks +func (c *Client) AddHooks(hooks ...Hook) { + c.mx.Lock() + defer c.mx.Unlock() + c.hooks = append(c.hooks, hooks...) +} + +// AddHandler allows to add Client level event handler +func (c *Client) AddHandler(eventType EventType, callback EventCallback) { + addClientHandler(c.metadata.Name(), eventType, callback) +} + +// RemoveHandler allows to remove Client level event handler +func (c *Client) RemoveHandler(eventType EventType, callback EventCallback) { + removeClientHandler(c.metadata.Name(), eventType, callback) +} + +// SetEvaluationContext sets the client's evaluation context +func (c *Client) SetEvaluationContext(evalCtx EvaluationContext) { + c.mx.Lock() + defer c.mx.Unlock() + c.evaluationContext = evalCtx +} + +// EvaluationContext returns the client's evaluation context +func (c *Client) EvaluationContext() EvaluationContext { + c.mx.RLock() + defer c.mx.RUnlock() + return c.evaluationContext +} + +// Type represents the type of a flag +type Type int64 + +const ( + Boolean Type = iota + String + Float + Int + Object +) + +func (t Type) String() string { + return typeToString[t] +} + +var typeToString = map[Type]string{ + Boolean: "bool", + String: "string", + Float: "float", + Int: "int", + Object: "object", +} + +type EvaluationDetails struct { + FlagKey string + FlagType Type + ResolutionDetail +} + +type BooleanEvaluationDetails struct { + Value bool + EvaluationDetails +} + +type StringEvaluationDetails struct { + Value string + EvaluationDetails +} + +type FloatEvaluationDetails struct { + Value float64 + EvaluationDetails +} + +type IntEvaluationDetails struct { + Value int64 + EvaluationDetails +} + +type InterfaceEvaluationDetails struct { + Value interface{} + EvaluationDetails +} + +type ResolutionDetail struct { + Variant string + Reason Reason + ErrorCode ErrorCode + ErrorMessage string + FlagMetadata FlagMetadata +} + +// FlagMetadata is a structure which supports definition of arbitrary properties, with keys of type string, and values +// of type boolean, string, int64 or float64. This structure is populated by a provider for use by an Application +// Author (via the Evaluation API) or an Application Integrator (via hooks). +type FlagMetadata map[string]interface{} + +// GetString fetch string value from FlagMetadata. +// Returns an error if the key does not exist, or, the value is of the wrong type +func (f FlagMetadata) GetString(key string) (string, error) { + v, ok := f[key] + if !ok { + return "", fmt.Errorf("key %s does not exist in FlagMetadata", key) + } + switch t := v.(type) { + case string: + return v.(string), nil + default: + return "", fmt.Errorf("wrong type for key %s, expected string, got %T", key, t) + } +} + +// GetBool fetch bool value from FlagMetadata. +// Returns an error if the key does not exist, or, the value is of the wrong type +func (f FlagMetadata) GetBool(key string) (bool, error) { + v, ok := f[key] + if !ok { + return false, fmt.Errorf("key %s does not exist in FlagMetadata", key) + } + switch t := v.(type) { + case bool: + return v.(bool), nil + default: + return false, fmt.Errorf("wrong type for key %s, expected bool, got %T", key, t) + } +} + +// GetInt fetch int64 value from FlagMetadata. +// Returns an error if the key does not exist, or, the value is of the wrong type +func (f FlagMetadata) GetInt(key string) (int64, error) { + v, ok := f[key] + if !ok { + return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key) + } + switch t := v.(type) { + case int: + return int64(v.(int)), nil + case int8: + return int64(v.(int8)), nil + case int16: + return int64(v.(int16)), nil + case int32: + return int64(v.(int32)), nil + case int64: + return v.(int64), nil + default: + return 0, fmt.Errorf("wrong type for key %s, expected integer, got %T", key, t) + } +} + +// GetFloat fetch float64 value from FlagMetadata. +// Returns an error if the key does not exist, or, the value is of the wrong type +func (f FlagMetadata) GetFloat(key string) (float64, error) { + v, ok := f[key] + if !ok { + return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key) + } + switch t := v.(type) { + case float32: + return float64(v.(float32)), nil + case float64: + return v.(float64), nil + default: + return 0, fmt.Errorf("wrong type for key %s, expected float, got %T", key, t) + } +} + +// Option applies a change to EvaluationOptions +type Option func(*EvaluationOptions) + +// EvaluationOptions should contain a list of hooks to be executed for a flag evaluation +type EvaluationOptions struct { + hooks []Hook + hookHints HookHints +} + +// HookHints returns evaluation options' hook hints +func (e EvaluationOptions) HookHints() HookHints { + return e.hookHints +} + +// Hooks returns evaluation options' hooks +func (e EvaluationOptions) Hooks() []Hook { + return e.hooks +} + +// WithHooks applies provided hooks. +func WithHooks(hooks ...Hook) Option { + return func(options *EvaluationOptions) { + options.hooks = hooks + } +} + +// WithHookHints applies provided hook hints. +func WithHookHints(hookHints HookHints) Option { + return func(options *EvaluationOptions) { + options.hookHints = hookHints + } +} + +// BooleanValue performs a flag evaluation that returns a boolean. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) { + details, err := c.BooleanValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue, err + } + + return details.Value, nil +} + +// StringValue performs a flag evaluation that returns a string. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) { + details, err := c.StringValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue, err + } + + return details.Value, nil +} + +// FloatValue performs a flag evaluation that returns a float64. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) { + details, err := c.FloatValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue, err + } + + return details.Value, nil +} + +// IntValue performs a flag evaluation that returns an int64. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) { + details, err := c.IntValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue, err + } + + return details.Value, nil +} + +// ObjectValue performs a flag evaluation that returns an object. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) { + details, err := c.ObjectValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue, err + } + + return details.Value, nil +} + +// BooleanValueDetails performs a flag evaluation that returns an evaluation details struct. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) { + c.mx.RLock() + defer c.mx.RUnlock() + + evalOptions := &EvaluationOptions{} + for _, option := range options { + option(evalOptions) + } + + evalDetails, err := c.evaluate(ctx, flag, Boolean, defaultValue, evalCtx, *evalOptions) + if err != nil { + return BooleanEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + }, err + } + + value, ok := evalDetails.Value.(bool) + if !ok { + err := errors.New("evaluated value is not a boolean") + c.logger().Error( + err, "invalid flag resolution type", "expectedType", "boolean", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + boolEvalDetails := BooleanEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + } + boolEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode + boolEvalDetails.EvaluationDetails.ErrorMessage = err.Error() + + return boolEvalDetails, err + } + + return BooleanEvaluationDetails{ + Value: value, + EvaluationDetails: evalDetails.EvaluationDetails, + }, nil +} + +// StringValueDetails performs a flag evaluation that returns an evaluation details struct. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) { + c.mx.RLock() + defer c.mx.RUnlock() + + evalOptions := &EvaluationOptions{} + for _, option := range options { + option(evalOptions) + } + + evalDetails, err := c.evaluate(ctx, flag, String, defaultValue, evalCtx, *evalOptions) + if err != nil { + return StringEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + }, err + } + + value, ok := evalDetails.Value.(string) + if !ok { + err := errors.New("evaluated value is not a string") + c.logger().Error( + err, "invalid flag resolution type", "expectedType", "string", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + strEvalDetails := StringEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + } + strEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode + strEvalDetails.EvaluationDetails.ErrorMessage = err.Error() + + return strEvalDetails, err + } + + return StringEvaluationDetails{ + Value: value, + EvaluationDetails: evalDetails.EvaluationDetails, + }, nil +} + +// FloatValueDetails performs a flag evaluation that returns an evaluation details struct. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) { + c.mx.RLock() + defer c.mx.RUnlock() + + evalOptions := &EvaluationOptions{} + for _, option := range options { + option(evalOptions) + } + + evalDetails, err := c.evaluate(ctx, flag, Float, defaultValue, evalCtx, *evalOptions) + if err != nil { + return FloatEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + }, err + } + + value, ok := evalDetails.Value.(float64) + if !ok { + err := errors.New("evaluated value is not a float64") + c.logger().Error( + err, "invalid flag resolution type", "expectedType", "float64", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + floatEvalDetails := FloatEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + } + floatEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode + floatEvalDetails.EvaluationDetails.ErrorMessage = err.Error() + + return floatEvalDetails, err + } + + return FloatEvaluationDetails{ + Value: value, + EvaluationDetails: evalDetails.EvaluationDetails, + }, nil +} + +// IntValueDetails performs a flag evaluation that returns an evaluation details struct. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) { + c.mx.RLock() + defer c.mx.RUnlock() + + evalOptions := &EvaluationOptions{} + for _, option := range options { + option(evalOptions) + } + + evalDetails, err := c.evaluate(ctx, flag, Int, defaultValue, evalCtx, *evalOptions) + if err != nil { + return IntEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + }, err + } + + value, ok := evalDetails.Value.(int64) + if !ok { + err := errors.New("evaluated value is not an int64") + c.logger().Error( + err, "invalid flag resolution type", "expectedType", "int64", + "gotType", fmt.Sprintf("%T", evalDetails.Value), + ) + intEvalDetails := IntEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: evalDetails.EvaluationDetails, + } + intEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode + intEvalDetails.EvaluationDetails.ErrorMessage = err.Error() + + return intEvalDetails, err + } + + return IntEvaluationDetails{ + Value: value, + EvaluationDetails: evalDetails.EvaluationDetails, + }, nil +} + +// ObjectValueDetails performs a flag evaluation that returns an evaluation details struct. +// +// Parameters: +// - ctx is the standard go context struct used to manage requests (e.g. timeouts) +// - flag is the key that uniquely identifies a particular flag +// - defaultValue is returned if an error occurs +// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) +// - options are optional additional evaluation options e.g. WithHooks & WithHookHints +func (c *Client) ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) { + c.mx.RLock() + defer c.mx.RUnlock() + + evalOptions := &EvaluationOptions{} + for _, option := range options { + option(evalOptions) + } + + return c.evaluate(ctx, flag, Object, defaultValue, evalCtx, *evalOptions) +} + +func (c *Client) evaluate( + ctx context.Context, flag string, flagType Type, defaultValue interface{}, evalCtx EvaluationContext, options EvaluationOptions, +) (InterfaceEvaluationDetails, error) { + evalDetails := InterfaceEvaluationDetails{ + Value: defaultValue, + EvaluationDetails: EvaluationDetails{ + FlagKey: flag, + FlagType: flagType, + }, + } + + if !utf8.Valid([]byte(flag)) { + return evalDetails, NewParseErrorResolutionError("flag key is not a UTF-8 encoded string") + } + + // ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour + provider, globalHooks, globalCtx := forTransaction(c.metadata.name) + + evalCtx = mergeContexts(evalCtx, c.evaluationContext, globalCtx) // API (global) -> client -> invocation + apiClientInvocationProviderHooks := append(append(append(globalHooks, c.hooks...), options.hooks...), provider.Hooks()...) // API, Client, Invocation, Provider + providerInvocationClientApiHooks := append(append(append(provider.Hooks(), options.hooks...), c.hooks...), globalHooks...) // Provider, Invocation, Client, API + + var err error + hookCtx := HookContext{ + flagKey: flag, + flagType: flagType, + defaultValue: defaultValue, + clientMetadata: c.metadata, + providerMetadata: provider.Metadata(), + evaluationContext: evalCtx, + } + + defer func() { + c.finallyHooks(ctx, hookCtx, providerInvocationClientApiHooks, options) + }() + + evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options) + hookCtx.evaluationContext = evalCtx + if err != nil { + c.logger().Error( + err, "before hook", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), + ) + err = fmt.Errorf("before hook: %w", err) + c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) + return evalDetails, err + } + + flatCtx := flattenContext(evalCtx) + var resolution InterfaceResolutionDetail + switch flagType { + case Object: + resolution = provider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx) + case Boolean: + defValue := defaultValue.(bool) + res := provider.BooleanEvaluation(ctx, flag, defValue, flatCtx) + resolution.ProviderResolutionDetail = res.ProviderResolutionDetail + resolution.Value = res.Value + case String: + defValue := defaultValue.(string) + res := provider.StringEvaluation(ctx, flag, defValue, flatCtx) + resolution.ProviderResolutionDetail = res.ProviderResolutionDetail + resolution.Value = res.Value + case Float: + defValue := defaultValue.(float64) + res := provider.FloatEvaluation(ctx, flag, defValue, flatCtx) + resolution.ProviderResolutionDetail = res.ProviderResolutionDetail + resolution.Value = res.Value + case Int: + defValue := defaultValue.(int64) + res := provider.IntEvaluation(ctx, flag, defValue, flatCtx) + resolution.ProviderResolutionDetail = res.ProviderResolutionDetail + resolution.Value = res.Value + } + + err = resolution.Error() + if err != nil { + c.logger().Error( + err, "flag resolution", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err, + "errMessage", resolution.ResolutionError.message, + ) + err = fmt.Errorf("error code: %w", err) + c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) + evalDetails.ResolutionDetail = resolution.ResolutionDetail() + evalDetails.Reason = ErrorReason + return evalDetails, err + } + evalDetails.Value = resolution.Value + evalDetails.ResolutionDetail = resolution.ResolutionDetail() + + if err := c.afterHooks(ctx, hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil { + c.logger().Error( + err, "after hook", "flag", flag, "defaultValue", defaultValue, + "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), + ) + err = fmt.Errorf("after hook: %w", err) + c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) + return evalDetails, err + } + + return evalDetails, nil +} + +func flattenContext(evalCtx EvaluationContext) FlattenedContext { + flatCtx := FlattenedContext{} + if evalCtx.attributes != nil { + flatCtx = evalCtx.Attributes() + } + if evalCtx.targetingKey != "" { + flatCtx[TargetingKey] = evalCtx.targetingKey + } + return flatCtx +} + +func (c *Client) beforeHooks( + ctx context.Context, hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions, +) (EvaluationContext, error) { + for _, hook := range hooks { + resultEvalCtx, err := hook.Before(ctx, hookCtx, options.hookHints) + if resultEvalCtx != nil { + hookCtx.evaluationContext = *resultEvalCtx + } + if err != nil { + return mergeContexts(hookCtx.evaluationContext, evalCtx), err + } + } + + return mergeContexts(hookCtx.evaluationContext, evalCtx), nil +} + +func (c *Client) afterHooks( + ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions, +) error { + for _, hook := range hooks { + if err := hook.After(ctx, hookCtx, evalDetails, options.hookHints); err != nil { + return err + } + } + + return nil +} + +func (c *Client) errorHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) { + for _, hook := range hooks { + hook.Error(ctx, hookCtx, err, options.hookHints) + } +} + +func (c *Client) finallyHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, options EvaluationOptions) { + for _, hook := range hooks { + hook.Finally(ctx, hookCtx, options.hookHints) + } +} + +// merges attributes from the given EvaluationContexts with the nth EvaluationContext taking precedence in case +// of any conflicts with the (n+1)th EvaluationContext +func mergeContexts(evaluationContexts ...EvaluationContext) EvaluationContext { + if len(evaluationContexts) == 0 { + return EvaluationContext{} + } + + // create copy to prevent mutation of given EvaluationContext + mergedCtx := EvaluationContext{ + attributes: evaluationContexts[0].Attributes(), + targetingKey: evaluationContexts[0].targetingKey, + } + + for i := 1; i < len(evaluationContexts); i++ { + if mergedCtx.targetingKey == "" && evaluationContexts[i].targetingKey != "" { + mergedCtx.targetingKey = evaluationContexts[i].targetingKey + } + + for k, v := range evaluationContexts[i].attributes { + _, ok := mergedCtx.attributes[k] + if !ok { + mergedCtx.attributes[k] = v + } + } + } + + return mergedCtx +} diff --git a/pkg/openfeature/client_example_test.go b/openfeature/client_example_test.go similarity index 97% rename from pkg/openfeature/client_example_test.go rename to openfeature/client_example_test.go index 0fddc287..4c257f94 100644 --- a/pkg/openfeature/client_example_test.go +++ b/openfeature/client_example_test.go @@ -6,7 +6,7 @@ import ( "fmt" "log" - "github.com/open-feature/go-sdk/pkg/openfeature" + "github.com/open-feature/go-sdk/openfeature" ) func ExampleNewClient() { diff --git a/pkg/openfeature/client_test.go b/openfeature/client_test.go similarity index 100% rename from pkg/openfeature/client_test.go rename to openfeature/client_test.go diff --git a/openfeature/doc.go b/openfeature/doc.go new file mode 100644 index 00000000..df158d59 --- /dev/null +++ b/openfeature/doc.go @@ -0,0 +1,4 @@ +/* +Package openfeature provides global access to the OpenFeature API. +*/ +package openfeature diff --git a/openfeature/evaluation_context.go b/openfeature/evaluation_context.go new file mode 100644 index 00000000..19135553 --- /dev/null +++ b/openfeature/evaluation_context.go @@ -0,0 +1,55 @@ +package openfeature + +// EvaluationContext provides ambient information for the purposes of flag evaluation +// The use of the constructor, NewEvaluationContext, is enforced to set EvaluationContext's fields in order +// to enforce immutability. +// https://openfeature.dev/specification/sections/evaluation-context +type EvaluationContext struct { + targetingKey string // uniquely identifying the subject (end-user, or client service) of a flag evaluation + attributes map[string]interface{} +} + +// Attribute retrieves the attribute with the given key +func (e EvaluationContext) Attribute(key string) interface{} { + return e.attributes[key] +} + +// TargetingKey returns the key uniquely identifying the subject (end-user, or client service) of a flag evaluation +func (e EvaluationContext) TargetingKey() string { + return e.targetingKey +} + +// Attributes returns a copy of the EvaluationContext's attributes +func (e EvaluationContext) Attributes() map[string]interface{} { + // copy attributes to new map to prevent mutation (maps are passed by reference) + attrs := make(map[string]interface{}, len(e.attributes)) + for key, value := range e.attributes { + attrs[key] = value + } + + return attrs +} + +// NewEvaluationContext constructs an EvaluationContext +// +// targetingKey - uniquely identifying the subject (end-user, or client service) of a flag evaluation +// attributes - contextual data used in flag evaluation +func NewEvaluationContext(targetingKey string, attributes map[string]interface{}) EvaluationContext { + // copy attributes to new map to avoid reference being externally available, thereby enforcing immutability + attrs := make(map[string]interface{}, len(attributes)) + for key, value := range attributes { + attrs[key] = value + } + + return EvaluationContext{ + targetingKey: targetingKey, + attributes: attrs, + } +} + +// NewTargetlessEvaluationContext constructs an EvaluationContext with an empty targeting key +// +// attributes - contextual data used in flag evaluation +func NewTargetlessEvaluationContext(attributes map[string]interface{}) EvaluationContext { + return NewEvaluationContext("", attributes) +} diff --git a/pkg/openfeature/evaluation_context_test.go b/openfeature/evaluation_context_test.go similarity index 100% rename from pkg/openfeature/evaluation_context_test.go rename to openfeature/evaluation_context_test.go diff --git a/pkg/openfeature/event_executor.go b/openfeature/event_executor.go similarity index 100% rename from pkg/openfeature/event_executor.go rename to openfeature/event_executor.go diff --git a/pkg/openfeature/event_executor_test.go b/openfeature/event_executor_test.go similarity index 99% rename from pkg/openfeature/event_executor_test.go rename to openfeature/event_executor_test.go index a176551a..d787e4d4 100644 --- a/pkg/openfeature/event_executor_test.go +++ b/openfeature/event_executor_test.go @@ -7,7 +7,7 @@ import ( "time" "github.com/go-logr/logr" - "github.com/open-feature/go-sdk/pkg/openfeature/internal" + "github.com/open-feature/go-sdk/openfeature/internal" "golang.org/x/exp/slices" ) diff --git a/openfeature/hooks.go b/openfeature/hooks.go new file mode 100644 index 00000000..a0a475f7 --- /dev/null +++ b/openfeature/hooks.go @@ -0,0 +1,111 @@ +package openfeature + +import "context" + +// Hook allows application developers to add arbitrary behavior to the flag evaluation lifecycle. +// They operate similarly to middleware in many web frameworks. +// https://github.com/open-feature/spec/blob/main/specification/hooks.md +type Hook interface { + Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) + After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error + Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) + Finally(ctx context.Context, hookContext HookContext, hookHints HookHints) +} + +// HookHints contains a map of hints for hooks +type HookHints struct { + mapOfHints map[string]interface{} +} + +// NewHookHints constructs HookHints +func NewHookHints(mapOfHints map[string]interface{}) HookHints { + return HookHints{mapOfHints: mapOfHints} +} + +// Value returns the value at the given key in the underlying map. +// Maintains immutability of the map. +func (h HookHints) Value(key string) interface{} { + return h.mapOfHints[key] +} + +// HookContext defines the base level fields of a hook context +type HookContext struct { + flagKey string + flagType Type + defaultValue interface{} + clientMetadata ClientMetadata + providerMetadata Metadata + evaluationContext EvaluationContext +} + +// FlagKey returns the hook context's flag key +func (h HookContext) FlagKey() string { + return h.flagKey +} + +// FlagType returns the hook context's flag type +func (h HookContext) FlagType() Type { + return h.flagType +} + +// DefaultValue returns the hook context's default value +func (h HookContext) DefaultValue() interface{} { + return h.defaultValue +} + +// ClientMetadata returns the client's metadata +func (h HookContext) ClientMetadata() ClientMetadata { + return h.clientMetadata +} + +// ProviderMetadata returns the provider's metadata +func (h HookContext) ProviderMetadata() Metadata { + return h.providerMetadata +} + +// EvaluationContext returns the hook context's EvaluationContext +func (h HookContext) EvaluationContext() EvaluationContext { + return h.evaluationContext +} + +// NewHookContext constructs HookContext +// Allows for simplified hook test cases while maintaining immutability +func NewHookContext( + flagKey string, + flagType Type, + defaultValue interface{}, + clientMetadata ClientMetadata, + providerMetadata Metadata, + evaluationContext EvaluationContext, +) HookContext { + return HookContext{ + flagKey: flagKey, + flagType: flagType, + defaultValue: defaultValue, + clientMetadata: clientMetadata, + providerMetadata: providerMetadata, + evaluationContext: evaluationContext, + } +} + +// check at compile time that UnimplementedHook implements the Hook interface +var _ Hook = UnimplementedHook{} + +// UnimplementedHook implements all hook methods with empty functions +// Include UnimplementedHook in your hook struct to avoid defining empty functions +// e.g. +// +// type MyHook struct { +// UnimplementedHook +// } +type UnimplementedHook struct{} + +func (UnimplementedHook) Before(context.Context, HookContext, HookHints) (*EvaluationContext, error) { + return nil, nil +} + +func (UnimplementedHook) After(context.Context, HookContext, InterfaceEvaluationDetails, HookHints) error { + return nil +} +func (UnimplementedHook) Error(context.Context, HookContext, error, HookHints) {} +func (UnimplementedHook) Finally(context.Context, HookContext, HookHints) {} diff --git a/pkg/openfeature/hooks_mock_test.go b/openfeature/hooks_mock_test.go similarity index 100% rename from pkg/openfeature/hooks_mock_test.go rename to openfeature/hooks_mock_test.go diff --git a/pkg/openfeature/hooks_test.go b/openfeature/hooks_test.go similarity index 100% rename from pkg/openfeature/hooks_test.go rename to openfeature/hooks_test.go diff --git a/pkg/openfeature/internal/logger.go b/openfeature/internal/internal/logger.go similarity index 100% rename from pkg/openfeature/internal/logger.go rename to openfeature/internal/internal/logger.go diff --git a/openfeature/internal/logger.go b/openfeature/internal/logger.go new file mode 100644 index 00000000..1b355f96 --- /dev/null +++ b/openfeature/internal/logger.go @@ -0,0 +1,25 @@ +package internal + +import ( + "log" + + "github.com/go-logr/logr" +) + +// Logger is the sdk's default logr.LogSink implementation. +// Logs using this logger logs only on error, all other logs are no-ops +type Logger struct{} + +func (l Logger) Init(info logr.RuntimeInfo) {} + +func (l Logger) Enabled(level int) bool { return true } + +func (l Logger) Info(level int, msg string, keysAndValues ...interface{}) {} + +func (l Logger) Error(err error, msg string, keysAndValues ...interface{}) { + log.Println("openfeature:", err) +} + +func (l Logger) WithValues(keysAndValues ...interface{}) logr.LogSink { return l } + +func (l Logger) WithName(name string) logr.LogSink { return l } diff --git a/pkg/openfeature/memprovider/README.md b/openfeature/memprovider/README.md similarity index 100% rename from pkg/openfeature/memprovider/README.md rename to openfeature/memprovider/README.md diff --git a/openfeature/memprovider/in_memory_provider.go b/openfeature/memprovider/in_memory_provider.go new file mode 100644 index 00000000..3f833f24 --- /dev/null +++ b/openfeature/memprovider/in_memory_provider.go @@ -0,0 +1,201 @@ +package memprovider + +import ( + "context" + "fmt" + + "github.com/open-feature/go-sdk/openfeature" +) + +const ( + Enabled State = "ENABLED" + Disabled State = "DISABLED" +) + +type InMemoryProvider struct { + flags map[string]InMemoryFlag +} + +func NewInMemoryProvider(from map[string]InMemoryFlag) InMemoryProvider { + return InMemoryProvider{ + flags: from, + } +} + +func (i InMemoryProvider) Metadata() openfeature.Metadata { + return openfeature.Metadata{ + Name: "InMemoryProvider", + } +} + +func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + memoryFlag, details, ok := i.find(flag) + if !ok { + return openfeature.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: *details, + } + } + + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[bool](resolveFlag, defaultValue, &detail) + + return openfeature.BoolResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + memoryFlag, details, ok := i.find(flag) + if !ok { + return openfeature.StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: *details, + } + } + + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[string](resolveFlag, defaultValue, &detail) + + return openfeature.StringResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + memoryFlag, details, ok := i.find(flag) + if !ok { + return openfeature.FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: *details, + } + } + + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[float64](resolveFlag, defaultValue, &detail) + + return openfeature.FloatResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + memoryFlag, details, ok := i.find(flag) + if !ok { + return openfeature.IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: *details, + } + } + + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[int](resolveFlag, int(defaultValue), &detail) + + return openfeature.IntResolutionDetail{ + Value: int64(result), + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + memoryFlag, details, ok := i.find(flag) + if !ok { + return openfeature.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: *details, + } + } + + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + + var result interface{} + if resolveFlag != nil { + result = resolveFlag + } else { + result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") + } + + return openfeature.InterfaceResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) Hooks() []openfeature.Hook { + return []openfeature.Hook{} +} + +func (i InMemoryProvider) find(flag string) (*InMemoryFlag, *openfeature.ProviderResolutionDetail, bool) { + memoryFlag, ok := i.flags[flag] + if !ok { + return nil, + &openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, false + } + + return &memoryFlag, nil, true +} + +// helpers + +// genericResolve is a helper to extract type verified evaluation and fill openfeature.ProviderResolutionDetail +func genericResolve[T comparable](value interface{}, defaultValue T, detail *openfeature.ProviderResolutionDetail) T { + v, ok := value.(T) + + if ok { + return v + } + + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") + return defaultValue +} + +// Type Definitions for InMemoryProvider flag + +// State of the feature flag +type State string + +// ContextEvaluator is a callback to perform openfeature.EvaluationContext backed evaluations. +// This is a callback implemented by the flag definer. +type ContextEvaluator *func(this InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail) + +// InMemoryFlag is the feature flag representation accepted by InMemoryProvider +type InMemoryFlag struct { + Key string + State State + DefaultVariant string + Variants map[string]interface{} + ContextEvaluator ContextEvaluator +} + +func (flag *InMemoryFlag) Resolve(defaultValue interface{}, evalCtx openfeature.FlattenedContext) ( + interface{}, openfeature.ProviderResolutionDetail) { + + // check the state + if flag.State == Disabled { + return defaultValue, openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewGeneralResolutionError("flag is disabled"), + Reason: openfeature.DisabledReason, + } + } + + // first resolve from context callback + if flag.ContextEvaluator != nil { + return (*flag.ContextEvaluator)(*flag, evalCtx) + } + + // fallback to evaluation + + return flag.Variants[flag.DefaultVariant], openfeature.ProviderResolutionDetail{ + Reason: openfeature.StaticReason, + Variant: flag.DefaultVariant, + } +} diff --git a/pkg/openfeature/memprovider/in_memory_provider_test.go b/openfeature/memprovider/in_memory_provider_test.go similarity index 99% rename from pkg/openfeature/memprovider/in_memory_provider_test.go rename to openfeature/memprovider/in_memory_provider_test.go index d5d48b90..f0aec0ba 100644 --- a/pkg/openfeature/memprovider/in_memory_provider_test.go +++ b/openfeature/memprovider/in_memory_provider_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/open-feature/go-sdk/pkg/openfeature" + "github.com/open-feature/go-sdk/openfeature" ) func TestInMemoryProvider_boolean(t *testing.T) { diff --git a/openfeature/noop_provider.go b/openfeature/noop_provider.go new file mode 100644 index 00000000..12a72555 --- /dev/null +++ b/openfeature/noop_provider.go @@ -0,0 +1,72 @@ +package openfeature + +import "context" + +// NoopProvider implements the FeatureProvider interface and provides functions for evaluating flags +type NoopProvider struct { +} + +// Metadata returns the metadata of the provider +func (e NoopProvider) Metadata() Metadata { + return Metadata{Name: "NoopProvider"} +} + +// BooleanEvaluation returns a boolean flag. +func (e NoopProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail { + return BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: ProviderResolutionDetail{ + Variant: "default-variant", + Reason: DefaultReason, + }, + } +} + +// StringEvaluation returns a string flag. +func (e NoopProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail { + return StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: ProviderResolutionDetail{ + Variant: "default-variant", + Reason: DefaultReason, + }, + } +} + +// FloatEvaluation returns a float flag. +func (e NoopProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail { + return FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: ProviderResolutionDetail{ + Variant: "default-variant", + Reason: DefaultReason, + }, + } +} + +// IntEvaluation returns an int flag. +func (e NoopProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail { + return IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: ProviderResolutionDetail{ + Variant: "default-variant", + Reason: DefaultReason, + }, + } +} + +// ObjectEvaluation returns an object flag +func (e NoopProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail { + return InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: ProviderResolutionDetail{ + Variant: "default-variant", + Reason: DefaultReason, + }, + } +} + +// Hooks returns hooks +func (e NoopProvider) Hooks() []Hook { + return []Hook{} +} diff --git a/pkg/openfeature/noop_provider_test.go b/openfeature/noop_provider_test.go similarity index 90% rename from pkg/openfeature/noop_provider_test.go rename to openfeature/noop_provider_test.go index 63b6e629..82986e21 100644 --- a/pkg/openfeature/noop_provider_test.go +++ b/openfeature/noop_provider_test.go @@ -3,7 +3,7 @@ package openfeature_test import ( "testing" - "github.com/open-feature/go-sdk/pkg/openfeature" + "github.com/open-feature/go-sdk/openfeature" ) func TestNoopProvider_Metadata(t *testing.T) { diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go new file mode 100644 index 00000000..cbf5b8c7 --- /dev/null +++ b/openfeature/openfeature.go @@ -0,0 +1,115 @@ +package openfeature + +import ( + "github.com/go-logr/logr" +) + +// api is the global evaluationAPI. This is a singleton and there can only be one instance. +// Avoid direct access. +var api evaluationAPI + +// init initializes the OpenFeature evaluation API +func init() { + initSingleton() +} + +func initSingleton() { + api = newEvaluationAPI() +} + +// SetProvider sets the default provider. Provider initialization is asynchronous and status can be checked from +// provider status +func SetProvider(provider FeatureProvider) error { + return api.setProvider(provider) +} + +// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and +// status can be checked from provider status +func SetNamedProvider(clientName string, provider FeatureProvider) error { + return api.setNamedProvider(clientName, provider) +} + +// SetEvaluationContext sets the global evaluation context. +func SetEvaluationContext(evalCtx EvaluationContext) { + api.setEvaluationContext(evalCtx) +} + +// SetLogger sets the global Logger. +func SetLogger(l logr.Logger) { + api.setLogger(l) +} + +// ProviderMetadata returns the default provider's metadata +func ProviderMetadata() Metadata { + return api.getProvider().Metadata() +} + +// AddHooks appends to the collection of any previously added hooks +func AddHooks(hooks ...Hook) { + api.addHooks(hooks...) +} + +// AddHandler allows to add API level event handler +func AddHandler(eventType EventType, callback EventCallback) { + api.eventExecutor.registerApiHandler(eventType, callback) +} + +// addClientHandler is a helper for Client to add an event handler +func addClientHandler(name string, t EventType, c EventCallback) { + api.eventExecutor.registerClientHandler(name, t, c) +} + +// RemoveHandler allows to remove API level event handler +func RemoveHandler(eventType EventType, callback EventCallback) { + api.eventExecutor.removeApiHandler(eventType, callback) +} + +// removeClientHandler is a helper for Client to add an event handler +func removeClientHandler(name string, t EventType, c EventCallback) { + api.eventExecutor.removeClientHandler(name, t, c) +} + +// getAPIEventRegistry is a helper for testing +func getAPIEventRegistry() map[EventType][]EventCallback { + return api.eventExecutor.apiRegistry +} + +// getClientRegistry is a helper for testing +func getClientRegistry(client string) *scopedCallback { + if v, ok := api.eventExecutor.scopedRegistry[client]; ok { + return &v + } + + return nil +} + +// Shutdown active providers +func Shutdown() { + api.shutdown() +} + +// getProvider returns the default provider of the API. Intended to be used by tests +func getProvider() FeatureProvider { + return api.getProvider() +} + +// getNamedProviders returns the named provider map of the API. Intended to be used by tests +func getNamedProviders() map[string]FeatureProvider { + return api.getNamedProviders() +} + +// getHooks returns hooks of the API. Intended to be used by tests +func getHooks() []Hook { + return api.getHooks() +} + +// globalLogger return the global logger set at the API +func globalLogger() logr.Logger { + return api.getLogger() +} + +// forTransaction is a helper to retrieve transaction scoped operators by Client. +// Here, transaction means a flag evaluation. +func forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) { + return api.forTransaction(clientName) +} diff --git a/pkg/openfeature/openfeature_test.go b/openfeature/openfeature_test.go similarity index 99% rename from pkg/openfeature/openfeature_test.go rename to openfeature/openfeature_test.go index 3e88ecd1..c98b68b7 100644 --- a/pkg/openfeature/openfeature_test.go +++ b/openfeature/openfeature_test.go @@ -8,7 +8,7 @@ import ( "github.com/go-logr/logr" "github.com/golang/mock/gomock" - "github.com/open-feature/go-sdk/pkg/openfeature/internal" + "github.com/open-feature/go-sdk/openfeature/internal" ) // The `API`, and any state it maintains SHOULD exist as a global singleton, diff --git a/openfeature/provider.go b/openfeature/provider.go new file mode 100644 index 00000000..135ba85c --- /dev/null +++ b/openfeature/provider.go @@ -0,0 +1,192 @@ +package openfeature + +import ( + "context" + "errors" +) + +const ( + // DefaultReason - the resolved value was configured statically, or otherwise fell back to a pre-configured value. + DefaultReason Reason = "DEFAULT" + // TargetingMatchReason - the resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting. + TargetingMatchReason Reason = "TARGETING_MATCH" + // SplitReason - the resolved value was the result of pseudorandom assignment. + SplitReason Reason = "SPLIT" + // DisabledReason - the resolved value was the result of the flag being disabled in the management system. + DisabledReason Reason = "DISABLED" + // StaticReason - the resolved value is static (no dynamic evaluation) + StaticReason Reason = "STATIC" + // CachedReason - the resolved value was retrieved from cache + CachedReason Reason = "CACHED" + // UnknownReason - the reason for the resolved value could not be determined. + UnknownReason Reason = "UNKNOWN" + // ErrorReason - the resolved value was the result of an error. + ErrorReason Reason = "ERROR" + + NotReadyState State = "NOT_READY" + ReadyState State = "READY" + ErrorState State = "ERROR" + StaleState State = "STALE" + + ProviderReady EventType = "PROVIDER_READY" + ProviderConfigChange EventType = "PROVIDER_CONFIGURATION_CHANGED" + ProviderStale EventType = "PROVIDER_STALE" + ProviderError EventType = "PROVIDER_ERROR" + + TargetingKey string = "targetingKey" // evaluation context map key. The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation. +) + +// FlattenedContext contains metadata for a given flag evaluation in a flattened structure. +// TargetingKey ("targetingKey") is stored as a string value if provided in the evaluation context. +type FlattenedContext map[string]interface{} + +// Reason indicates the semantic reason for a returned flag value +type Reason string + +// FeatureProvider interface defines a set of functions that can be called in order to evaluate a flag. +// This should be implemented by flag management systems. +type FeatureProvider interface { + Metadata() Metadata + BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail + StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail + FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail + IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail + ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail + Hooks() []Hook +} + +// State represents the status of the provider +type State string + +// StateHandler is the contract for initialization & shutdown. +// FeatureProvider can opt in for this behavior by implementing the interface +type StateHandler interface { + Init(evaluationContext EvaluationContext) error + Shutdown() + Status() State +} + +// NoopStateHandler is a noop StateHandler implementation +// Status always set to ReadyState to comply with specification +type NoopStateHandler struct { +} + +func (s *NoopStateHandler) Init(e EvaluationContext) error { + // NOOP + return nil +} + +func (s *NoopStateHandler) Shutdown() { + // NOOP +} + +func (s *NoopStateHandler) Status() State { + return ReadyState +} + +// Eventing + +// EventHandler is the eventing contract enforced for FeatureProvider +type EventHandler interface { + EventChannel() <-chan Event +} + +// EventType emitted by a provider implementation +type EventType string + +// ProviderEventDetails is the event payload emitted by FeatureProvider +type ProviderEventDetails struct { + Message string + FlagChanges []string + EventMetadata map[string]interface{} +} + +// Event is an event emitted by a FeatureProvider. +type Event struct { + ProviderName string + EventType + ProviderEventDetails +} + +type EventDetails struct { + providerName string + ProviderEventDetails +} + +type EventCallback *func(details EventDetails) + +// NoopEventHandler is the out-of-the-box EventHandler which is noop +type NoopEventHandler struct { +} + +func (s NoopEventHandler) EventChannel() <-chan Event { + return make(chan Event, 1) +} + +// ProviderResolutionDetail is a structure which contains a subset of the fields defined in the EvaluationDetail, +// representing the result of the provider's flag resolution process +// see https://github.com/open-feature/spec/blob/main/specification/types.md#resolution-details +// N.B we could use generics but to support older versions of go for now we will have type specific resolution +// detail +type ProviderResolutionDetail struct { + ResolutionError ResolutionError + Reason Reason + Variant string + FlagMetadata FlagMetadata +} + +func (p ProviderResolutionDetail) ResolutionDetail() ResolutionDetail { + metadata := FlagMetadata{} + if p.FlagMetadata != nil { + metadata = p.FlagMetadata + } + return ResolutionDetail{ + Variant: p.Variant, + Reason: p.Reason, + ErrorCode: p.ResolutionError.code, + ErrorMessage: p.ResolutionError.message, + FlagMetadata: metadata, + } +} + +func (p ProviderResolutionDetail) Error() error { + if p.ResolutionError.code == "" { + return nil + } + return errors.New(p.ResolutionError.Error()) +} + +// BoolResolutionDetail provides a resolution detail with boolean type +type BoolResolutionDetail struct { + Value bool + ProviderResolutionDetail +} + +// StringResolutionDetail provides a resolution detail with string type +type StringResolutionDetail struct { + Value string + ProviderResolutionDetail +} + +// FloatResolutionDetail provides a resolution detail with float64 type +type FloatResolutionDetail struct { + Value float64 + ProviderResolutionDetail +} + +// IntResolutionDetail provides a resolution detail with int64 type +type IntResolutionDetail struct { + Value int64 + ProviderResolutionDetail +} + +// InterfaceResolutionDetail provides a resolution detail with interface{} type +type InterfaceResolutionDetail struct { + Value interface{} + ProviderResolutionDetail +} + +// Metadata provides provider name +type Metadata struct { + Name string +} diff --git a/pkg/openfeature/provider_mock_test.go b/openfeature/provider_mock_test.go similarity index 100% rename from pkg/openfeature/provider_mock_test.go rename to openfeature/provider_mock_test.go diff --git a/pkg/openfeature/provider_test.go b/openfeature/provider_test.go similarity index 100% rename from pkg/openfeature/provider_test.go rename to openfeature/provider_test.go diff --git a/openfeature/resolution_error.go b/openfeature/resolution_error.go new file mode 100644 index 00000000..b4d4ae00 --- /dev/null +++ b/openfeature/resolution_error.go @@ -0,0 +1,104 @@ +package openfeature + +import "fmt" + +type ErrorCode string + +const ( + // ProviderNotReadyCode - the value was resolved before the provider was ready. + ProviderNotReadyCode ErrorCode = "PROVIDER_NOT_READY" + // FlagNotFoundCode - the flag could not be found. + FlagNotFoundCode ErrorCode = "FLAG_NOT_FOUND" + // ParseErrorCode - an error was encountered parsing data, such as a flag configuration. + ParseErrorCode ErrorCode = "PARSE_ERROR" + // TypeMismatchCode - the type of the flag value does not match the expected type. + TypeMismatchCode ErrorCode = "TYPE_MISMATCH" + // TargetingKeyMissingCode - the provider requires a targeting key and one was not provided in the evaluation context. + TargetingKeyMissingCode ErrorCode = "TARGETING_KEY_MISSING" + // InvalidContextCode - the evaluation context does not meet provider requirements. + InvalidContextCode ErrorCode = "INVALID_CONTEXT" + // GeneralCode - the error was for a reason not enumerated above. + GeneralCode ErrorCode = "GENERAL" +) + +// ResolutionError is an enumerated error code with an optional message +type ResolutionError struct { + // fields are unexported, this means providers are forced to create structs of this type using one of the constructors below. + // this effectively emulates an enum + code ErrorCode + message string +} + +func (r ResolutionError) Error() string { + return fmt.Sprintf("%s: %s", r.code, r.message) +} + +// NewProviderNotReadyResolutionError constructs a resolution error with code PROVIDER_NOT_READY +// +// Explanation - The value was resolved before the provider was ready. +func NewProviderNotReadyResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: ProviderNotReadyCode, + message: msg, + } +} + +// NewFlagNotFoundResolutionError constructs a resolution error with code FLAG_NOT_FOUND +// +// Explanation - The flag could not be found. +func NewFlagNotFoundResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: FlagNotFoundCode, + message: msg, + } +} + +// NewParseErrorResolutionError constructs a resolution error with code PARSE_ERROR +// +// Explanation - An error was encountered parsing data, such as a flag configuration. +func NewParseErrorResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: ParseErrorCode, + message: msg, + } +} + +// NewTypeMismatchResolutionError constructs a resolution error with code TYPE_MISMATCH +// +// Explanation - The type of the flag value does not match the expected type. +func NewTypeMismatchResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: TypeMismatchCode, + message: msg, + } +} + +// NewTargetingKeyMissingResolutionError constructs a resolution error with code TARGETING_KEY_MISSING +// +// Explanation - The provider requires a targeting key and one was not provided in the evaluation context. +func NewTargetingKeyMissingResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: TargetingKeyMissingCode, + message: msg, + } +} + +// NewInvalidContextResolutionError constructs a resolution error with code INVALID_CONTEXT +// +// Explanation - The evaluation context does not meet provider requirements. +func NewInvalidContextResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: InvalidContextCode, + message: msg, + } +} + +// NewGeneralResolutionError constructs a resolution error with code GENERAL +// +// Explanation - The error was for a reason not enumerated above. +func NewGeneralResolutionError(msg string) ResolutionError { + return ResolutionError{ + code: GeneralCode, + message: msg, + } +} diff --git a/pkg/openfeature/util_test.go b/openfeature/util_test.go similarity index 100% rename from pkg/openfeature/util_test.go rename to openfeature/util_test.go diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go index 3391531a..9abe3c9e 100644 --- a/pkg/openfeature/client.go +++ b/pkg/openfeature/client.go @@ -1,765 +1,125 @@ package openfeature import ( - "context" - "errors" - "fmt" - "sync" - "unicode/utf8" - - "github.com/go-logr/logr" + "github.com/open-feature/go-sdk/openfeature" ) // IClient defines the behaviour required of an openfeature client -type IClient interface { - Metadata() ClientMetadata - AddHooks(hooks ...Hook) - AddHandler(eventType EventType, callback EventCallback) - RemoveHandler(eventType EventType, callback EventCallback) - SetEvaluationContext(evalCtx EvaluationContext) - EvaluationContext() EvaluationContext - BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) - StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) - FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) - IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) - ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) - BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) - StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) - FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) - IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) - ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.IClient, instead. +type IClient = openfeature.IClient // ClientMetadata provides a client's metadata -type ClientMetadata struct { - name string -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.ClientMetadata, +// instead. +type ClientMetadata = openfeature.ClientMetadata // NewClientMetadata constructs ClientMetadata // Allows for simplified hook test cases while maintaining immutability +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.NewClientMetadata, instead. func NewClientMetadata(name string) ClientMetadata { - return ClientMetadata{ - name: name, - } -} - -// Name returns the client's name -func (cm ClientMetadata) Name() string { - return cm.name + return openfeature.NewClientMetadata(name) } // Client implements the behaviour required of an openfeature client -type Client struct { - mx sync.RWMutex - metadata ClientMetadata - hooks []Hook - evaluationContext EvaluationContext - logger func() logr.Logger -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Client, instead. +type Client = openfeature.Client // NewClient returns a new Client. Name is a unique identifier for this client +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewClient, +// instead. func NewClient(name string) *Client { - return &Client{ - metadata: ClientMetadata{name: name}, - hooks: []Hook{}, - evaluationContext: EvaluationContext{}, - logger: globalLogger, - } -} - -// WithLogger sets the logger of the client -func (c *Client) WithLogger(l logr.Logger) *Client { - c.mx.Lock() - defer c.mx.Unlock() - c.logger = func() logr.Logger { return l } - return c -} - -// Metadata returns the client's metadata -func (c *Client) Metadata() ClientMetadata { - c.mx.RLock() - defer c.mx.RUnlock() - return c.metadata -} - -// AddHooks appends to the client's collection of any previously added hooks -func (c *Client) AddHooks(hooks ...Hook) { - c.mx.Lock() - defer c.mx.Unlock() - c.hooks = append(c.hooks, hooks...) -} - -// AddHandler allows to add Client level event handler -func (c *Client) AddHandler(eventType EventType, callback EventCallback) { - addClientHandler(c.metadata.Name(), eventType, callback) -} - -// RemoveHandler allows to remove Client level event handler -func (c *Client) RemoveHandler(eventType EventType, callback EventCallback) { - removeClientHandler(c.metadata.Name(), eventType, callback) -} - -// SetEvaluationContext sets the client's evaluation context -func (c *Client) SetEvaluationContext(evalCtx EvaluationContext) { - c.mx.Lock() - defer c.mx.Unlock() - c.evaluationContext = evalCtx -} - -// EvaluationContext returns the client's evaluation context -func (c *Client) EvaluationContext() EvaluationContext { - c.mx.RLock() - defer c.mx.RUnlock() - return c.evaluationContext + return openfeature.NewClient(name) } // Type represents the type of a flag -type Type int64 +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Type, instead. +type Type = openfeature.Type const ( - Boolean Type = iota - String - Float - Int - Object + // Deprecated: use github.com/open-feature/go-sdk/openfeature.Boolean, + // instead. + Boolean = openfeature.Boolean + // Deprecated: use github.com/open-feature/go-sdk/openfeature.String, + // instead. + String = openfeature.String + // Deprecated: use github.com/open-feature/go-sdk/openfeature.Float, + // instead. + Float = openfeature.Float + // Deprecated: use github.com/open-feature/go-sdk/openfeature.Int, + // instead. + Int = openfeature.Int + // Deprecated: use github.com/open-feature/go-sdk/openfeature.Object, + // instead. + Object = openfeature.Object ) -func (t Type) String() string { - return typeToString[t] -} - -var typeToString = map[Type]string{ - Boolean: "bool", - String: "string", - Float: "float", - Int: "int", - Object: "object", -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.EvaluationDetails, instead. +type EvaluationDetails = openfeature.EvaluationDetails -type EvaluationDetails struct { - FlagKey string - FlagType Type - ResolutionDetail -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.BooleanEvaluationDetails, +// instead. +type BooleanEvaluationDetails = openfeature.BooleanEvaluationDetails -type BooleanEvaluationDetails struct { - Value bool - EvaluationDetails -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.StringEvaluationDetails, instead. +type StringEvaluationDetails = openfeature.StringEvaluationDetails -type StringEvaluationDetails struct { - Value string - EvaluationDetails -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.FloatEvaluationDetails, instead. +type FloatEvaluationDetails = openfeature.FloatEvaluationDetails -type FloatEvaluationDetails struct { - Value float64 - EvaluationDetails -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.IntEvaluationDetails, instead. +type IntEvaluationDetails = openfeature.IntEvaluationDetails -type IntEvaluationDetails struct { - Value int64 - EvaluationDetails -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.InterfaceEvaluationDetails, +// instead. +type InterfaceEvaluationDetails = openfeature.InterfaceEvaluationDetails -type InterfaceEvaluationDetails struct { - Value interface{} - EvaluationDetails -} - -type ResolutionDetail struct { - Variant string - Reason Reason - ErrorCode ErrorCode - ErrorMessage string - FlagMetadata FlagMetadata -} +// Deprecated: use github.com/open-feature/go-sdk/openfeature.ResolutionDetail, instead. +type ResolutionDetail = openfeature.ResolutionDetail // FlagMetadata is a structure which supports definition of arbitrary properties, with keys of type string, and values // of type boolean, string, int64 or float64. This structure is populated by a provider for use by an Application // Author (via the Evaluation API) or an Application Integrator (via hooks). -type FlagMetadata map[string]interface{} - -// GetString fetch string value from FlagMetadata. -// Returns an error if the key does not exist, or, the value is of the wrong type -func (f FlagMetadata) GetString(key string) (string, error) { - v, ok := f[key] - if !ok { - return "", fmt.Errorf("key %s does not exist in FlagMetadata", key) - } - switch t := v.(type) { - case string: - return v.(string), nil - default: - return "", fmt.Errorf("wrong type for key %s, expected string, got %T", key, t) - } -} - -// GetBool fetch bool value from FlagMetadata. -// Returns an error if the key does not exist, or, the value is of the wrong type -func (f FlagMetadata) GetBool(key string) (bool, error) { - v, ok := f[key] - if !ok { - return false, fmt.Errorf("key %s does not exist in FlagMetadata", key) - } - switch t := v.(type) { - case bool: - return v.(bool), nil - default: - return false, fmt.Errorf("wrong type for key %s, expected bool, got %T", key, t) - } -} - -// GetInt fetch int64 value from FlagMetadata. -// Returns an error if the key does not exist, or, the value is of the wrong type -func (f FlagMetadata) GetInt(key string) (int64, error) { - v, ok := f[key] - if !ok { - return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key) - } - switch t := v.(type) { - case int: - return int64(v.(int)), nil - case int8: - return int64(v.(int8)), nil - case int16: - return int64(v.(int16)), nil - case int32: - return int64(v.(int32)), nil - case int64: - return v.(int64), nil - default: - return 0, fmt.Errorf("wrong type for key %s, expected integer, got %T", key, t) - } -} - -// GetFloat fetch float64 value from FlagMetadata. -// Returns an error if the key does not exist, or, the value is of the wrong type -func (f FlagMetadata) GetFloat(key string) (float64, error) { - v, ok := f[key] - if !ok { - return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key) - } - switch t := v.(type) { - case float32: - return float64(v.(float32)), nil - case float64: - return v.(float64), nil - default: - return 0, fmt.Errorf("wrong type for key %s, expected float, got %T", key, t) - } -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.FlagMetadata, +// instead. +type FlagMetadata = openfeature.FlagMetadata // Option applies a change to EvaluationOptions -type Option func(*EvaluationOptions) +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Option, instead. +type Option = openfeature.Option // EvaluationOptions should contain a list of hooks to be executed for a flag evaluation -type EvaluationOptions struct { - hooks []Hook - hookHints HookHints -} - -// HookHints returns evaluation options' hook hints -func (e EvaluationOptions) HookHints() HookHints { - return e.hookHints -} - -// Hooks returns evaluation options' hooks -func (e EvaluationOptions) Hooks() []Hook { - return e.hooks -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.EvaluationOptions, instead. +type EvaluationOptions = openfeature.EvaluationOptions // WithHooks applies provided hooks. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.WithHooks, +// instead. func WithHooks(hooks ...Hook) Option { - return func(options *EvaluationOptions) { - options.hooks = hooks - } + return openfeature.WithHooks(hooks...) } // WithHookHints applies provided hook hints. -func WithHookHints(hookHints HookHints) Option { - return func(options *EvaluationOptions) { - options.hookHints = hookHints - } -} - -// BooleanValue performs a flag evaluation that returns a boolean. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) { - details, err := c.BooleanValueDetails(ctx, flag, defaultValue, evalCtx, options...) - if err != nil { - return defaultValue, err - } - - return details.Value, nil -} - -// StringValue performs a flag evaluation that returns a string. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) { - details, err := c.StringValueDetails(ctx, flag, defaultValue, evalCtx, options...) - if err != nil { - return defaultValue, err - } - - return details.Value, nil -} - -// FloatValue performs a flag evaluation that returns a float64. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) { - details, err := c.FloatValueDetails(ctx, flag, defaultValue, evalCtx, options...) - if err != nil { - return defaultValue, err - } - - return details.Value, nil -} - -// IntValue performs a flag evaluation that returns an int64. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) { - details, err := c.IntValueDetails(ctx, flag, defaultValue, evalCtx, options...) - if err != nil { - return defaultValue, err - } - - return details.Value, nil -} - -// ObjectValue performs a flag evaluation that returns an object. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) { - details, err := c.ObjectValueDetails(ctx, flag, defaultValue, evalCtx, options...) - if err != nil { - return defaultValue, err - } - - return details.Value, nil -} - -// BooleanValueDetails performs a flag evaluation that returns an evaluation details struct. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - - evalOptions := &EvaluationOptions{} - for _, option := range options { - option(evalOptions) - } - - evalDetails, err := c.evaluate(ctx, flag, Boolean, defaultValue, evalCtx, *evalOptions) - if err != nil { - return BooleanEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - }, err - } - - value, ok := evalDetails.Value.(bool) - if !ok { - err := errors.New("evaluated value is not a boolean") - c.logger().Error( - err, "invalid flag resolution type", "expectedType", "boolean", - "gotType", fmt.Sprintf("%T", evalDetails.Value), - ) - boolEvalDetails := BooleanEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - boolEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode - boolEvalDetails.EvaluationDetails.ErrorMessage = err.Error() - - return boolEvalDetails, err - } - - return BooleanEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil -} - -// StringValueDetails performs a flag evaluation that returns an evaluation details struct. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - - evalOptions := &EvaluationOptions{} - for _, option := range options { - option(evalOptions) - } - - evalDetails, err := c.evaluate(ctx, flag, String, defaultValue, evalCtx, *evalOptions) - if err != nil { - return StringEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - }, err - } - - value, ok := evalDetails.Value.(string) - if !ok { - err := errors.New("evaluated value is not a string") - c.logger().Error( - err, "invalid flag resolution type", "expectedType", "string", - "gotType", fmt.Sprintf("%T", evalDetails.Value), - ) - strEvalDetails := StringEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - strEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode - strEvalDetails.EvaluationDetails.ErrorMessage = err.Error() - - return strEvalDetails, err - } - - return StringEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil -} - -// FloatValueDetails performs a flag evaluation that returns an evaluation details struct. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - - evalOptions := &EvaluationOptions{} - for _, option := range options { - option(evalOptions) - } - - evalDetails, err := c.evaluate(ctx, flag, Float, defaultValue, evalCtx, *evalOptions) - if err != nil { - return FloatEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - }, err - } - - value, ok := evalDetails.Value.(float64) - if !ok { - err := errors.New("evaluated value is not a float64") - c.logger().Error( - err, "invalid flag resolution type", "expectedType", "float64", - "gotType", fmt.Sprintf("%T", evalDetails.Value), - ) - floatEvalDetails := FloatEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - floatEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode - floatEvalDetails.EvaluationDetails.ErrorMessage = err.Error() - - return floatEvalDetails, err - } - - return FloatEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil -} - -// IntValueDetails performs a flag evaluation that returns an evaluation details struct. // -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - - evalOptions := &EvaluationOptions{} - for _, option := range options { - option(evalOptions) - } - - evalDetails, err := c.evaluate(ctx, flag, Int, defaultValue, evalCtx, *evalOptions) - if err != nil { - return IntEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - }, err - } - - value, ok := evalDetails.Value.(int64) - if !ok { - err := errors.New("evaluated value is not an int64") - c.logger().Error( - err, "invalid flag resolution type", "expectedType", "int64", - "gotType", fmt.Sprintf("%T", evalDetails.Value), - ) - intEvalDetails := IntEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - intEvalDetails.EvaluationDetails.ErrorCode = TypeMismatchCode - intEvalDetails.EvaluationDetails.ErrorMessage = err.Error() - - return intEvalDetails, err - } - - return IntEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil -} - -// ObjectValueDetails performs a flag evaluation that returns an evaluation details struct. -// -// Parameters: -// - ctx is the standard go context struct used to manage requests (e.g. timeouts) -// - flag is the key that uniquely identifies a particular flag -// - defaultValue is returned if an error occurs -// - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx) -// - options are optional additional evaluation options e.g. WithHooks & WithHookHints -func (c *Client) ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - - evalOptions := &EvaluationOptions{} - for _, option := range options { - option(evalOptions) - } - - return c.evaluate(ctx, flag, Object, defaultValue, evalCtx, *evalOptions) -} - -func (c *Client) evaluate( - ctx context.Context, flag string, flagType Type, defaultValue interface{}, evalCtx EvaluationContext, options EvaluationOptions, -) (InterfaceEvaluationDetails, error) { - evalDetails := InterfaceEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: EvaluationDetails{ - FlagKey: flag, - FlagType: flagType, - }, - } - - if !utf8.Valid([]byte(flag)) { - return evalDetails, NewParseErrorResolutionError("flag key is not a UTF-8 encoded string") - } - - // ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour - provider, globalHooks, globalCtx := forTransaction(c.metadata.name) - - evalCtx = mergeContexts(evalCtx, c.evaluationContext, globalCtx) // API (global) -> client -> invocation - apiClientInvocationProviderHooks := append(append(append(globalHooks, c.hooks...), options.hooks...), provider.Hooks()...) // API, Client, Invocation, Provider - providerInvocationClientApiHooks := append(append(append(provider.Hooks(), options.hooks...), c.hooks...), globalHooks...) // Provider, Invocation, Client, API - - var err error - hookCtx := HookContext{ - flagKey: flag, - flagType: flagType, - defaultValue: defaultValue, - clientMetadata: c.metadata, - providerMetadata: provider.Metadata(), - evaluationContext: evalCtx, - } - - defer func() { - c.finallyHooks(ctx, hookCtx, providerInvocationClientApiHooks, options) - }() - - evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options) - hookCtx.evaluationContext = evalCtx - if err != nil { - c.logger().Error( - err, "before hook", "flag", flag, "defaultValue", defaultValue, - "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), - ) - err = fmt.Errorf("before hook: %w", err) - c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) - return evalDetails, err - } - - flatCtx := flattenContext(evalCtx) - var resolution InterfaceResolutionDetail - switch flagType { - case Object: - resolution = provider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx) - case Boolean: - defValue := defaultValue.(bool) - res := provider.BooleanEvaluation(ctx, flag, defValue, flatCtx) - resolution.ProviderResolutionDetail = res.ProviderResolutionDetail - resolution.Value = res.Value - case String: - defValue := defaultValue.(string) - res := provider.StringEvaluation(ctx, flag, defValue, flatCtx) - resolution.ProviderResolutionDetail = res.ProviderResolutionDetail - resolution.Value = res.Value - case Float: - defValue := defaultValue.(float64) - res := provider.FloatEvaluation(ctx, flag, defValue, flatCtx) - resolution.ProviderResolutionDetail = res.ProviderResolutionDetail - resolution.Value = res.Value - case Int: - defValue := defaultValue.(int64) - res := provider.IntEvaluation(ctx, flag, defValue, flatCtx) - resolution.ProviderResolutionDetail = res.ProviderResolutionDetail - resolution.Value = res.Value - } - - err = resolution.Error() - if err != nil { - c.logger().Error( - err, "flag resolution", "flag", flag, "defaultValue", defaultValue, - "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err, - "errMessage", resolution.ResolutionError.message, - ) - err = fmt.Errorf("error code: %w", err) - c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) - evalDetails.ResolutionDetail = resolution.ResolutionDetail() - evalDetails.Reason = ErrorReason - return evalDetails, err - } - evalDetails.Value = resolution.Value - evalDetails.ResolutionDetail = resolution.ResolutionDetail() - - if err := c.afterHooks(ctx, hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil { - c.logger().Error( - err, "after hook", "flag", flag, "defaultValue", defaultValue, - "evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), - ) - err = fmt.Errorf("after hook: %w", err) - c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, err, options) - return evalDetails, err - } - - return evalDetails, nil -} - -func flattenContext(evalCtx EvaluationContext) FlattenedContext { - flatCtx := FlattenedContext{} - if evalCtx.attributes != nil { - flatCtx = evalCtx.Attributes() - } - if evalCtx.targetingKey != "" { - flatCtx[TargetingKey] = evalCtx.targetingKey - } - return flatCtx -} - -func (c *Client) beforeHooks( - ctx context.Context, hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions, -) (EvaluationContext, error) { - for _, hook := range hooks { - resultEvalCtx, err := hook.Before(ctx, hookCtx, options.hookHints) - if resultEvalCtx != nil { - hookCtx.evaluationContext = *resultEvalCtx - } - if err != nil { - return mergeContexts(hookCtx.evaluationContext, evalCtx), err - } - } - - return mergeContexts(hookCtx.evaluationContext, evalCtx), nil -} - -func (c *Client) afterHooks( - ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions, -) error { - for _, hook := range hooks { - if err := hook.After(ctx, hookCtx, evalDetails, options.hookHints); err != nil { - return err - } - } - - return nil -} - -func (c *Client) errorHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) { - for _, hook := range hooks { - hook.Error(ctx, hookCtx, err, options.hookHints) - } -} - -func (c *Client) finallyHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, options EvaluationOptions) { - for _, hook := range hooks { - hook.Finally(ctx, hookCtx, options.hookHints) - } -} - -// merges attributes from the given EvaluationContexts with the nth EvaluationContext taking precedence in case -// of any conflicts with the (n+1)th EvaluationContext -func mergeContexts(evaluationContexts ...EvaluationContext) EvaluationContext { - if len(evaluationContexts) == 0 { - return EvaluationContext{} - } - - // create copy to prevent mutation of given EvaluationContext - mergedCtx := EvaluationContext{ - attributes: evaluationContexts[0].Attributes(), - targetingKey: evaluationContexts[0].targetingKey, - } - - for i := 1; i < len(evaluationContexts); i++ { - if mergedCtx.targetingKey == "" && evaluationContexts[i].targetingKey != "" { - mergedCtx.targetingKey = evaluationContexts[i].targetingKey - } - - for k, v := range evaluationContexts[i].attributes { - _, ok := mergedCtx.attributes[k] - if !ok { - mergedCtx.attributes[k] = v - } - } - } - - return mergedCtx +// Deprecated: use github.com/open-feature/go-sdk/openfeature.WithHookHints, +// instead. +func WithHookHints(hookHints HookHints) Option { + return openfeature.WithHookHints(hookHints) } diff --git a/pkg/openfeature/doc.go b/pkg/openfeature/doc.go index df158d59..aa0cf38a 100644 --- a/pkg/openfeature/doc.go +++ b/pkg/openfeature/doc.go @@ -1,4 +1,6 @@ /* Package openfeature provides global access to the OpenFeature API. + +Deprecated: use github.com/open-feature/go-sdk/openfeature, instead. */ package openfeature diff --git a/pkg/openfeature/evaluation_context.go b/pkg/openfeature/evaluation_context.go index 19135553..4ed6265e 100644 --- a/pkg/openfeature/evaluation_context.go +++ b/pkg/openfeature/evaluation_context.go @@ -1,55 +1,34 @@ package openfeature +import "github.com/open-feature/go-sdk/openfeature" + // EvaluationContext provides ambient information for the purposes of flag evaluation // The use of the constructor, NewEvaluationContext, is enforced to set EvaluationContext's fields in order // to enforce immutability. // https://openfeature.dev/specification/sections/evaluation-context -type EvaluationContext struct { - targetingKey string // uniquely identifying the subject (end-user, or client service) of a flag evaluation - attributes map[string]interface{} -} - -// Attribute retrieves the attribute with the given key -func (e EvaluationContext) Attribute(key string) interface{} { - return e.attributes[key] -} - -// TargetingKey returns the key uniquely identifying the subject (end-user, or client service) of a flag evaluation -func (e EvaluationContext) TargetingKey() string { - return e.targetingKey -} - -// Attributes returns a copy of the EvaluationContext's attributes -func (e EvaluationContext) Attributes() map[string]interface{} { - // copy attributes to new map to prevent mutation (maps are passed by reference) - attrs := make(map[string]interface{}, len(e.attributes)) - for key, value := range e.attributes { - attrs[key] = value - } - - return attrs -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.EvaluationContext, instead. +type EvaluationContext = openfeature.EvaluationContext // NewEvaluationContext constructs an EvaluationContext // // targetingKey - uniquely identifying the subject (end-user, or client service) of a flag evaluation // attributes - contextual data used in flag evaluation +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.NewEvaluationContext, instead. func NewEvaluationContext(targetingKey string, attributes map[string]interface{}) EvaluationContext { - // copy attributes to new map to avoid reference being externally available, thereby enforcing immutability - attrs := make(map[string]interface{}, len(attributes)) - for key, value := range attributes { - attrs[key] = value - } - - return EvaluationContext{ - targetingKey: targetingKey, - attributes: attrs, - } + return openfeature.NewEvaluationContext(targetingKey, attributes) } // NewTargetlessEvaluationContext constructs an EvaluationContext with an empty targeting key // // attributes - contextual data used in flag evaluation +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.NewTargetlessEvaluationContext, +// instead. func NewTargetlessEvaluationContext(attributes map[string]interface{}) EvaluationContext { - return NewEvaluationContext("", attributes) + return openfeature.NewTargetlessEvaluationContext(attributes) } diff --git a/pkg/openfeature/hooks.go b/pkg/openfeature/hooks.go index a0a475f7..22698394 100644 --- a/pkg/openfeature/hooks.go +++ b/pkg/openfeature/hooks.go @@ -1,75 +1,41 @@ package openfeature -import "context" +import ( + "github.com/open-feature/go-sdk/openfeature" +) // Hook allows application developers to add arbitrary behavior to the flag evaluation lifecycle. // They operate similarly to middleware in many web frameworks. // https://github.com/open-feature/spec/blob/main/specification/hooks.md -type Hook interface { - Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) - After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error - Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) - Finally(ctx context.Context, hookContext HookContext, hookHints HookHints) -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Hook, instead. +type Hook = openfeature.Hook // HookHints contains a map of hints for hooks -type HookHints struct { - mapOfHints map[string]interface{} -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.HookHints, +// instead. +type HookHints = openfeature.HookHints // NewHookHints constructs HookHints +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewHookHints, +// instead. func NewHookHints(mapOfHints map[string]interface{}) HookHints { - return HookHints{mapOfHints: mapOfHints} -} - -// Value returns the value at the given key in the underlying map. -// Maintains immutability of the map. -func (h HookHints) Value(key string) interface{} { - return h.mapOfHints[key] + return openfeature.NewHookHints(mapOfHints) } // HookContext defines the base level fields of a hook context -type HookContext struct { - flagKey string - flagType Type - defaultValue interface{} - clientMetadata ClientMetadata - providerMetadata Metadata - evaluationContext EvaluationContext -} - -// FlagKey returns the hook context's flag key -func (h HookContext) FlagKey() string { - return h.flagKey -} - -// FlagType returns the hook context's flag type -func (h HookContext) FlagType() Type { - return h.flagType -} - -// DefaultValue returns the hook context's default value -func (h HookContext) DefaultValue() interface{} { - return h.defaultValue -} - -// ClientMetadata returns the client's metadata -func (h HookContext) ClientMetadata() ClientMetadata { - return h.clientMetadata -} - -// ProviderMetadata returns the provider's metadata -func (h HookContext) ProviderMetadata() Metadata { - return h.providerMetadata -} - -// EvaluationContext returns the hook context's EvaluationContext -func (h HookContext) EvaluationContext() EvaluationContext { - return h.evaluationContext -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.HookContext, +// instead. +type HookContext = openfeature.HookContext // NewHookContext constructs HookContext // Allows for simplified hook test cases while maintaining immutability +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewHookContext, +// instead. func NewHookContext( flagKey string, flagType Type, @@ -78,34 +44,17 @@ func NewHookContext( providerMetadata Metadata, evaluationContext EvaluationContext, ) HookContext { - return HookContext{ - flagKey: flagKey, - flagType: flagType, - defaultValue: defaultValue, - clientMetadata: clientMetadata, - providerMetadata: providerMetadata, - evaluationContext: evaluationContext, - } + return openfeature.NewHookContext(flagKey, flagType, defaultValue, clientMetadata, providerMetadata, evaluationContext) } -// check at compile time that UnimplementedHook implements the Hook interface -var _ Hook = UnimplementedHook{} - // UnimplementedHook implements all hook methods with empty functions // Include UnimplementedHook in your hook struct to avoid defining empty functions // e.g. // -// type MyHook struct { +// type MyHook = openfeature.MyHook // UnimplementedHook // } -type UnimplementedHook struct{} - -func (UnimplementedHook) Before(context.Context, HookContext, HookHints) (*EvaluationContext, error) { - return nil, nil -} - -func (UnimplementedHook) After(context.Context, HookContext, InterfaceEvaluationDetails, HookHints) error { - return nil -} -func (UnimplementedHook) Error(context.Context, HookContext, error, HookHints) {} -func (UnimplementedHook) Finally(context.Context, HookContext, HookHints) {} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.UnimplementedHook, instead. +type UnimplementedHook = openfeature.UnimplementedHook diff --git a/pkg/openfeature/memprovider/in_memory_provider.go b/pkg/openfeature/memprovider/in_memory_provider.go index 979bfd2a..9cab3a64 100644 --- a/pkg/openfeature/memprovider/in_memory_provider.go +++ b/pkg/openfeature/memprovider/in_memory_provider.go @@ -1,201 +1,51 @@ package memprovider import ( - "context" - "fmt" - - "github.com/open-feature/go-sdk/pkg/openfeature" + "github.com/open-feature/go-sdk/openfeature/memprovider" ) const ( - Enabled State = "ENABLED" - Disabled State = "DISABLED" + // Deprecated: use + // github.com/open-feature/go-sdk/openfeature/memprovider.Enabled, + // instead. + Enabled = memprovider.Enabled + // Deprecated: use + // github.com/open-feature/go-sdk/openfeature/memprovider.Disabled, + // instead. + Disabled = memprovider.Disabled ) -type InMemoryProvider struct { - flags map[string]InMemoryFlag -} +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature/memprovider.InMemoryProvider, +// instead. +type InMemoryProvider = memprovider.InMemoryProvider +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature/memprovider.NewInMemoryProvider, +// instead. func NewInMemoryProvider(from map[string]InMemoryFlag) InMemoryProvider { - return InMemoryProvider{ - flags: from, - } -} - -func (i InMemoryProvider) Metadata() openfeature.Metadata { - return openfeature.Metadata{ - Name: "InMemoryProvider", - } -} - -func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - memoryFlag, details, ok := i.find(flag) - if !ok { - return openfeature.BoolResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: *details, - } - } - - resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) - result := genericResolve[bool](resolveFlag, defaultValue, &detail) - - return openfeature.BoolResolutionDetail{ - Value: result, - ProviderResolutionDetail: detail, - } -} - -func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - memoryFlag, details, ok := i.find(flag) - if !ok { - return openfeature.StringResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: *details, - } - } - - resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) - result := genericResolve[string](resolveFlag, defaultValue, &detail) - - return openfeature.StringResolutionDetail{ - Value: result, - ProviderResolutionDetail: detail, - } -} - -func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - memoryFlag, details, ok := i.find(flag) - if !ok { - return openfeature.FloatResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: *details, - } - } - - resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) - result := genericResolve[float64](resolveFlag, defaultValue, &detail) - - return openfeature.FloatResolutionDetail{ - Value: result, - ProviderResolutionDetail: detail, - } -} - -func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - memoryFlag, details, ok := i.find(flag) - if !ok { - return openfeature.IntResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: *details, - } - } - - resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) - result := genericResolve[int](resolveFlag, int(defaultValue), &detail) - - return openfeature.IntResolutionDetail{ - Value: int64(result), - ProviderResolutionDetail: detail, - } -} - -func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - memoryFlag, details, ok := i.find(flag) - if !ok { - return openfeature.InterfaceResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: *details, - } - } - - resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) - - var result interface{} - if resolveFlag != nil { - result = resolveFlag - } else { - result = defaultValue - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - } - - return openfeature.InterfaceResolutionDetail{ - Value: result, - ProviderResolutionDetail: detail, - } -} - -func (i InMemoryProvider) Hooks() []openfeature.Hook { - return []openfeature.Hook{} -} - -func (i InMemoryProvider) find(flag string) (*InMemoryFlag, *openfeature.ProviderResolutionDetail, bool) { - memoryFlag, ok := i.flags[flag] - if !ok { - return nil, - &openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, false - } - - return &memoryFlag, nil, true -} - -// helpers - -// genericResolve is a helper to extract type verified evaluation and fill openfeature.ProviderResolutionDetail -func genericResolve[T comparable](value interface{}, defaultValue T, detail *openfeature.ProviderResolutionDetail) T { - v, ok := value.(T) - - if ok { - return v - } - - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - return defaultValue + return memprovider.NewInMemoryProvider(from) } // Type Definitions for InMemoryProvider flag // State of the feature flag -type State string +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature/memprovider.State, instead. +type State = memprovider.State // ContextEvaluator is a callback to perform openfeature.EvaluationContext backed evaluations. // This is a callback implemented by the flag definer. -type ContextEvaluator *func(this InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail) +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature/memprovider.ContextEvaluator, +// instead. +type ContextEvaluator = memprovider.ContextEvaluator // InMemoryFlag is the feature flag representation accepted by InMemoryProvider -type InMemoryFlag struct { - Key string - State State - DefaultVariant string - Variants map[string]interface{} - ContextEvaluator ContextEvaluator -} - -func (flag *InMemoryFlag) Resolve(defaultValue interface{}, evalCtx openfeature.FlattenedContext) ( - interface{}, openfeature.ProviderResolutionDetail) { - - // check the state - if flag.State == Disabled { - return defaultValue, openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewGeneralResolutionError("flag is disabled"), - Reason: openfeature.DisabledReason, - } - } - - // first resolve from context callback - if flag.ContextEvaluator != nil { - return (*flag.ContextEvaluator)(*flag, evalCtx) - } - - // fallback to evaluation - - return flag.Variants[flag.DefaultVariant], openfeature.ProviderResolutionDetail{ - Reason: openfeature.StaticReason, - Variant: flag.DefaultVariant, - } -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature/memprovider.InMemoryFlag, +// instead. +type InMemoryFlag = memprovider.InMemoryFlag diff --git a/pkg/openfeature/noop_provider.go b/pkg/openfeature/noop_provider.go index 12a72555..97a1832b 100644 --- a/pkg/openfeature/noop_provider.go +++ b/pkg/openfeature/noop_provider.go @@ -1,72 +1,10 @@ package openfeature -import "context" - -// NoopProvider implements the FeatureProvider interface and provides functions for evaluating flags -type NoopProvider struct { -} - -// Metadata returns the metadata of the provider -func (e NoopProvider) Metadata() Metadata { - return Metadata{Name: "NoopProvider"} -} - -// BooleanEvaluation returns a boolean flag. -func (e NoopProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail { - return BoolResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: ProviderResolutionDetail{ - Variant: "default-variant", - Reason: DefaultReason, - }, - } -} - -// StringEvaluation returns a string flag. -func (e NoopProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail { - return StringResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: ProviderResolutionDetail{ - Variant: "default-variant", - Reason: DefaultReason, - }, - } -} - -// FloatEvaluation returns a float flag. -func (e NoopProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail { - return FloatResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: ProviderResolutionDetail{ - Variant: "default-variant", - Reason: DefaultReason, - }, - } -} - -// IntEvaluation returns an int flag. -func (e NoopProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail { - return IntResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: ProviderResolutionDetail{ - Variant: "default-variant", - Reason: DefaultReason, - }, - } -} - -// ObjectEvaluation returns an object flag -func (e NoopProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail { - return InterfaceResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: ProviderResolutionDetail{ - Variant: "default-variant", - Reason: DefaultReason, - }, - } -} - -// Hooks returns hooks -func (e NoopProvider) Hooks() []Hook { - return []Hook{} -} +import "github.com/open-feature/go-sdk/openfeature" + +// NoopProvider implements the FeatureProvider interface and provides functions +// for evaluating flags +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NoopProvider, +// instead. +type NoopProvider = openfeature.NoopProvider diff --git a/pkg/openfeature/openfeature.go b/pkg/openfeature/openfeature.go index cbf5b8c7..81252ebd 100644 --- a/pkg/openfeature/openfeature.go +++ b/pkg/openfeature/openfeature.go @@ -2,114 +2,80 @@ package openfeature import ( "github.com/go-logr/logr" + "github.com/open-feature/go-sdk/openfeature" ) -// api is the global evaluationAPI. This is a singleton and there can only be one instance. -// Avoid direct access. -var api evaluationAPI - -// init initializes the OpenFeature evaluation API -func init() { - initSingleton() -} - -func initSingleton() { - api = newEvaluationAPI() -} - -// SetProvider sets the default provider. Provider initialization is asynchronous and status can be checked from -// provider status +// SetProvider sets the default provider. Provider initialization is +// asynchronous and status can be checked from provider status +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.SetProvider, +// instead. func SetProvider(provider FeatureProvider) error { - return api.setProvider(provider) + return openfeature.SetProvider(provider) } -// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and -// status can be checked from provider status +// SetNamedProvider sets a provider mapped to the given Client name. Provider +// initialization is asynchronous and status can be checked from provider +// status +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.SetNamedProvider, +// instead. func SetNamedProvider(clientName string, provider FeatureProvider) error { - return api.setNamedProvider(clientName, provider) + return openfeature.SetNamedProvider(clientName, provider) } // SetEvaluationContext sets the global evaluation context. +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.SetEvaluationContext, instead. func SetEvaluationContext(evalCtx EvaluationContext) { - api.setEvaluationContext(evalCtx) + openfeature.SetEvaluationContext(evalCtx) } // SetLogger sets the global Logger. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.SetLogger, +// instead. func SetLogger(l logr.Logger) { - api.setLogger(l) + openfeature.SetLogger(l) } // ProviderMetadata returns the default provider's metadata +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.ProviderMetadata, +// instead. func ProviderMetadata() Metadata { - return api.getProvider().Metadata() + return openfeature.ProviderMetadata() } // AddHooks appends to the collection of any previously added hooks +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.AddHooks, +// instead. func AddHooks(hooks ...Hook) { - api.addHooks(hooks...) + openfeature.AddHooks(hooks...) } // AddHandler allows to add API level event handler +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.AddHandler, +// instead. func AddHandler(eventType EventType, callback EventCallback) { - api.eventExecutor.registerApiHandler(eventType, callback) -} - -// addClientHandler is a helper for Client to add an event handler -func addClientHandler(name string, t EventType, c EventCallback) { - api.eventExecutor.registerClientHandler(name, t, c) + openfeature.AddHandler(eventType, callback) } // RemoveHandler allows to remove API level event handler +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.RemoveHandler, +// instead. func RemoveHandler(eventType EventType, callback EventCallback) { - api.eventExecutor.removeApiHandler(eventType, callback) -} - -// removeClientHandler is a helper for Client to add an event handler -func removeClientHandler(name string, t EventType, c EventCallback) { - api.eventExecutor.removeClientHandler(name, t, c) -} - -// getAPIEventRegistry is a helper for testing -func getAPIEventRegistry() map[EventType][]EventCallback { - return api.eventExecutor.apiRegistry -} - -// getClientRegistry is a helper for testing -func getClientRegistry(client string) *scopedCallback { - if v, ok := api.eventExecutor.scopedRegistry[client]; ok { - return &v - } - - return nil + openfeature.RemoveHandler(eventType, callback) } // Shutdown active providers +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Shutdown, +// instead. func Shutdown() { - api.shutdown() -} - -// getProvider returns the default provider of the API. Intended to be used by tests -func getProvider() FeatureProvider { - return api.getProvider() -} - -// getNamedProviders returns the named provider map of the API. Intended to be used by tests -func getNamedProviders() map[string]FeatureProvider { - return api.getNamedProviders() -} - -// getHooks returns hooks of the API. Intended to be used by tests -func getHooks() []Hook { - return api.getHooks() -} - -// globalLogger return the global logger set at the API -func globalLogger() logr.Logger { - return api.getLogger() -} - -// forTransaction is a helper to retrieve transaction scoped operators by Client. -// Here, transaction means a flag evaluation. -func forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) { - return api.forTransaction(clientName) + openfeature.Shutdown() } diff --git a/pkg/openfeature/provider.go b/pkg/openfeature/provider.go index 135ba85c..d82d1409 100644 --- a/pkg/openfeature/provider.go +++ b/pkg/openfeature/provider.go @@ -1,192 +1,187 @@ package openfeature import ( - "context" - "errors" + "github.com/open-feature/go-sdk/openfeature" ) const ( // DefaultReason - the resolved value was configured statically, or otherwise fell back to a pre-configured value. - DefaultReason Reason = "DEFAULT" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.DefaultReason, instead. + DefaultReason = openfeature.DefaultReason // TargetingMatchReason - the resolved value was the result of a dynamic evaluation, such as a rule or specific user-targeting. - TargetingMatchReason Reason = "TARGETING_MATCH" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.TargetingMatchReason, instead. + TargetingMatchReason = openfeature.TargetingMatchReason // SplitReason - the resolved value was the result of pseudorandom assignment. - SplitReason Reason = "SPLIT" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.SplitReason, instead. + SplitReason = openfeature.SplitReason // DisabledReason - the resolved value was the result of the flag being disabled in the management system. - DisabledReason Reason = "DISABLED" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.DisabledReason, instead. + DisabledReason = openfeature.DisabledReason // StaticReason - the resolved value is static (no dynamic evaluation) - StaticReason Reason = "STATIC" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.StaticReason, instead. + StaticReason = openfeature.StaticReason // CachedReason - the resolved value was retrieved from cache - CachedReason Reason = "CACHED" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.CachedReason, instead. + CachedReason = openfeature.CachedReason // UnknownReason - the reason for the resolved value could not be determined. - UnknownReason Reason = "UNKNOWN" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + UnknownReason = openfeature.UnknownReason // ErrorReason - the resolved value was the result of an error. - ErrorReason Reason = "ERROR" - - NotReadyState State = "NOT_READY" - ReadyState State = "READY" - ErrorState State = "ERROR" - StaleState State = "STALE" - - ProviderReady EventType = "PROVIDER_READY" - ProviderConfigChange EventType = "PROVIDER_CONFIGURATION_CHANGED" - ProviderStale EventType = "PROVIDER_STALE" - ProviderError EventType = "PROVIDER_ERROR" - - TargetingKey string = "targetingKey" // evaluation context map key. The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation. + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ErrorReason, instead. + ErrorReason = openfeature.ErrorReason + + // Deprecated: use github.com/open-feature/go-sdk/openfeature.NotReadyState, instead. + NotReadyState = openfeature.NotReadyState + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ReadyState, instead. + ReadyState = openfeature.ReadyState + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ErrorState, instead. + ErrorState = openfeature.ErrorState + // Deprecated: use github.com/open-feature/go-sdk/openfeature.StaleState, instead. + StaleState = openfeature.StaleState + + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ProviderReady, instead. + ProviderReady = openfeature.ProviderReady + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ProviderConfigChange, instead. + ProviderConfigChange = openfeature.ProviderConfigChange + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ProviderStale, instead. + ProviderStale = openfeature.ProviderStale + // Deprecated: use github.com/open-feature/go-sdk/openfeature.ProviderError, instead. + ProviderError = openfeature.ProviderError + + // Deprecated: use github.com/open-feature/go-sdk/openfeature.TargetingKey, instead. + TargetingKey = openfeature.TargetingKey // evaluation context map key. The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation. ) -// FlattenedContext contains metadata for a given flag evaluation in a flattened structure. -// TargetingKey ("targetingKey") is stored as a string value if provided in the evaluation context. -type FlattenedContext map[string]interface{} +// FlattenedContext contains metadata for a given flag evaluation in a +// flattened structure. TargetingKey ("targetingKey") is stored as a string +// value if provided in the evaluation context. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.FlattenedContext, +// instead. +type FlattenedContext = openfeature.FlattenedContext // Reason indicates the semantic reason for a returned flag value -type Reason string - -// FeatureProvider interface defines a set of functions that can be called in order to evaluate a flag. -// This should be implemented by flag management systems. -type FeatureProvider interface { - Metadata() Metadata - BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail - StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail - FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail - IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail - ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail - Hooks() []Hook -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Reason, instead. +type Reason = openfeature.Reason + +// FeatureProvider interface defines a set of functions that can be called in +// order to evaluate a flag. This should be implemented by flag management +// systems. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.FeatureProvider, +// instead. +type FeatureProvider = openfeature.FeatureProvider // State represents the status of the provider -type State string +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.State, instead. +type State = openfeature.State // StateHandler is the contract for initialization & shutdown. // FeatureProvider can opt in for this behavior by implementing the interface -type StateHandler interface { - Init(evaluationContext EvaluationContext) error - Shutdown() - Status() State -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.StateHandler, +// instead. +type StateHandler = openfeature.StateHandler // NoopStateHandler is a noop StateHandler implementation // Status always set to ReadyState to comply with specification -type NoopStateHandler struct { -} - -func (s *NoopStateHandler) Init(e EvaluationContext) error { - // NOOP - return nil -} - -func (s *NoopStateHandler) Shutdown() { - // NOOP -} - -func (s *NoopStateHandler) Status() State { - return ReadyState -} - -// Eventing +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NoopStateHandler, +// instead. +type NoopStateHandler = openfeature.NoopStateHandler // EventHandler is the eventing contract enforced for FeatureProvider -type EventHandler interface { - EventChannel() <-chan Event -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventHandler, +// instead. +type EventHandler = openfeature.EventHandler // EventType emitted by a provider implementation -type EventType string +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventType, +// instead. +type EventType = openfeature.EventType // ProviderEventDetails is the event payload emitted by FeatureProvider -type ProviderEventDetails struct { - Message string - FlagChanges []string - EventMetadata map[string]interface{} -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.ProviderEventDetails, instead. +type ProviderEventDetails = openfeature.ProviderEventDetails // Event is an event emitted by a FeatureProvider. -type Event struct { - ProviderName string - EventType - ProviderEventDetails -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Event, instead. +type Event = openfeature.Event -type EventDetails struct { - providerName string - ProviderEventDetails -} +// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventDetails, +// instead. +type EventDetails = openfeature.EventDetails -type EventCallback *func(details EventDetails) +// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventCallback, +// instead. +type EventCallback = openfeature.EventCallback // NoopEventHandler is the out-of-the-box EventHandler which is noop -type NoopEventHandler struct { -} - -func (s NoopEventHandler) EventChannel() <-chan Event { - return make(chan Event, 1) -} - -// ProviderResolutionDetail is a structure which contains a subset of the fields defined in the EvaluationDetail, -// representing the result of the provider's flag resolution process -// see https://github.com/open-feature/spec/blob/main/specification/types.md#resolution-details -// N.B we could use generics but to support older versions of go for now we will have type specific resolution -// detail -type ProviderResolutionDetail struct { - ResolutionError ResolutionError - Reason Reason - Variant string - FlagMetadata FlagMetadata -} - -func (p ProviderResolutionDetail) ResolutionDetail() ResolutionDetail { - metadata := FlagMetadata{} - if p.FlagMetadata != nil { - metadata = p.FlagMetadata - } - return ResolutionDetail{ - Variant: p.Variant, - Reason: p.Reason, - ErrorCode: p.ResolutionError.code, - ErrorMessage: p.ResolutionError.message, - FlagMetadata: metadata, - } -} - -func (p ProviderResolutionDetail) Error() error { - if p.ResolutionError.code == "" { - return nil - } - return errors.New(p.ResolutionError.Error()) -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NoopEventHandler, +// instead. +type NoopEventHandler = openfeature.NoopEventHandler + +// ProviderResolutionDetail is a structure which contains a subset of the +// fields defined in the EvaluationDetail, representing the result of the +// provider's flag resolution process see +// https://github.com/open-feature/spec/blob/main/specification/types.md#resolution-details +// N.B we could use generics but to support older versions of go for now we +// will have type specific resolution detail +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.ProviderResolutionDetail, +// instead. +type ProviderResolutionDetail = openfeature.ProviderResolutionDetail // BoolResolutionDetail provides a resolution detail with boolean type -type BoolResolutionDetail struct { - Value bool - ProviderResolutionDetail -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.BoolResolutionDetail, instead. +type BoolResolutionDetail = openfeature.BoolResolutionDetail // StringResolutionDetail provides a resolution detail with string type -type StringResolutionDetail struct { - Value string - ProviderResolutionDetail -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.StringResolutionDetail, instead. +type StringResolutionDetail = openfeature.StringResolutionDetail // FloatResolutionDetail provides a resolution detail with float64 type -type FloatResolutionDetail struct { - Value float64 - ProviderResolutionDetail -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.FloatResolutionDetail, instead. +type FloatResolutionDetail = openfeature.FloatResolutionDetail // IntResolutionDetail provides a resolution detail with int64 type -type IntResolutionDetail struct { - Value int64 - ProviderResolutionDetail -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.IntResolutionDetail, instead. +type IntResolutionDetail = openfeature.IntResolutionDetail // InterfaceResolutionDetail provides a resolution detail with interface{} type -type InterfaceResolutionDetail struct { - Value interface{} - ProviderResolutionDetail -} +// +// Deprecated: use +// github.com/open-feature/go-sdk/openfeature.InterfaceResolutionDetail, +// instead. +type InterfaceResolutionDetail = openfeature.InterfaceResolutionDetail // Metadata provides provider name -type Metadata struct { - Name string -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.Metadata, +// instead. +type Metadata = openfeature.Metadata diff --git a/pkg/openfeature/resolution_error.go b/pkg/openfeature/resolution_error.go index b4d4ae00..fb118d93 100644 --- a/pkg/openfeature/resolution_error.go +++ b/pkg/openfeature/resolution_error.go @@ -1,104 +1,105 @@ package openfeature -import "fmt" +import "github.com/open-feature/go-sdk/openfeature" -type ErrorCode string +// Deprecated: use github.com/open-feature/go-sdk/openfeature.ErrorCode, instead. +type ErrorCode = openfeature.ErrorCode const ( // ProviderNotReadyCode - the value was resolved before the provider was ready. - ProviderNotReadyCode ErrorCode = "PROVIDER_NOT_READY" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + ProviderNotReadyCode = openfeature.ProviderNotReadyCode // FlagNotFoundCode - the flag could not be found. - FlagNotFoundCode ErrorCode = "FLAG_NOT_FOUND" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + FlagNotFoundCode = openfeature.FlagNotFoundCode // ParseErrorCode - an error was encountered parsing data, such as a flag configuration. - ParseErrorCode ErrorCode = "PARSE_ERROR" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + ParseErrorCode = openfeature.ParseErrorCode // TypeMismatchCode - the type of the flag value does not match the expected type. - TypeMismatchCode ErrorCode = "TYPE_MISMATCH" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + TypeMismatchCode = openfeature.TypeMismatchCode // TargetingKeyMissingCode - the provider requires a targeting key and one was not provided in the evaluation context. - TargetingKeyMissingCode ErrorCode = "TARGETING_KEY_MISSING" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + TargetingKeyMissingCode = openfeature.TargetingKeyMissingCode // InvalidContextCode - the evaluation context does not meet provider requirements. - InvalidContextCode ErrorCode = "INVALID_CONTEXT" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + InvalidContextCode = openfeature.InvalidContextCode // GeneralCode - the error was for a reason not enumerated above. - GeneralCode ErrorCode = "GENERAL" + // + // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. + GeneralCode = openfeature.GeneralCode ) // ResolutionError is an enumerated error code with an optional message -type ResolutionError struct { - // fields are unexported, this means providers are forced to create structs of this type using one of the constructors below. - // this effectively emulates an enum - code ErrorCode - message string -} - -func (r ResolutionError) Error() string { - return fmt.Sprintf("%s: %s", r.code, r.message) -} +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.ResolutionError, instead. +type ResolutionError = openfeature.ResolutionError // NewProviderNotReadyResolutionError constructs a resolution error with code PROVIDER_NOT_READY // // Explanation - The value was resolved before the provider was ready. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewProviderNotReadyResolutionError, instead. func NewProviderNotReadyResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: ProviderNotReadyCode, - message: msg, - } + return openfeature.NewProviderNotReadyResolutionError(msg) } // NewFlagNotFoundResolutionError constructs a resolution error with code FLAG_NOT_FOUND // // Explanation - The flag could not be found. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewFlagNotFoundResolutionError, instead. func NewFlagNotFoundResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: FlagNotFoundCode, - message: msg, - } + return openfeature.NewFlagNotFoundResolutionError(msg) } // NewParseErrorResolutionError constructs a resolution error with code PARSE_ERROR // // Explanation - An error was encountered parsing data, such as a flag configuration. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewParseErrorResolutionError, instead. func NewParseErrorResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: ParseErrorCode, - message: msg, - } + return openfeature.NewParseErrorResolutionError(msg) } // NewTypeMismatchResolutionError constructs a resolution error with code TYPE_MISMATCH // // Explanation - The type of the flag value does not match the expected type. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewTypeMismatchResolutionError, instead. func NewTypeMismatchResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: TypeMismatchCode, - message: msg, - } + return openfeature.NewTypeMismatchResolutionError(msg) } // NewTargetingKeyMissingResolutionError constructs a resolution error with code TARGETING_KEY_MISSING // // Explanation - The provider requires a targeting key and one was not provided in the evaluation context. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewTargetingKeyMissingResolutionError, instead. func NewTargetingKeyMissingResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: TargetingKeyMissingCode, - message: msg, - } + return openfeature.NewTargetingKeyMissingResolutionError(msg) } // NewInvalidContextResolutionError constructs a resolution error with code INVALID_CONTEXT // // Explanation - The evaluation context does not meet provider requirements. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewInvalidContextResolutionError, instead. func NewInvalidContextResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: InvalidContextCode, - message: msg, - } + return openfeature.NewInvalidContextResolutionError(msg) } // NewGeneralResolutionError constructs a resolution error with code GENERAL // // Explanation - The error was for a reason not enumerated above. +// +// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewGeneralResolutionError, instead. func NewGeneralResolutionError(msg string) ResolutionError { - return ResolutionError{ - code: GeneralCode, - message: msg, - } + return openfeature.NewGeneralResolutionError(msg) }