From 991726ced66913de916ec47ed5dd1837b6daf203 Mon Sep 17 00:00:00 2001 From: Paddy Date: Mon, 20 Nov 2023 11:20:45 -0800 Subject: [PATCH] feat: Move all files from pkg/openfeature to openfeature. (#232) Move all files from pkg/openfeature to openfeature. Move the package from being github.com/open-feature/go-sdk/pkg/openfeature, which has an unnecessary pkg in the import path, to github.com/open-feature/go-sdk/openfeature, without the unnecessary "pkg" in the import path. The existing github.com/open-feature/go-sdk/pkg/openfeature package is now a compatibility shell; exported types, constants, variables, and functions are now aliased to the new github.com/open-feature/go-sdk/openfeature equivalents in a way that will pass equality checks, and types are interchangeable between both packages. No logic or tests live in the package anymore, only the exported identifiers, which call through to the new package for behavior. The memprovider package, similarly, has moved. Signed-off-by: Paddy Carver --- e2e/common_test.go | 4 +- e2e/evaluation_fuzz_test.go | 4 +- e2e/evaluation_test.go | 4 +- {pkg/openfeature => openfeature}/api.go | 2 +- openfeature/client.go | 765 +++++++++++++++++ .../client_example_test.go | 2 +- .../client_test.go | 0 openfeature/doc.go | 4 + openfeature/evaluation_context.go | 55 ++ .../evaluation_context_test.go | 0 .../event_executor.go | 0 .../event_executor_test.go | 2 +- openfeature/hooks.go | 111 +++ .../hooks_mock_test.go | 0 .../openfeature => openfeature}/hooks_test.go | 0 .../internal}/internal/logger.go | 0 openfeature/internal/logger.go | 25 + .../memprovider/README.md | 0 openfeature/memprovider/in_memory_provider.go | 201 +++++ .../memprovider/in_memory_provider_test.go | 2 +- openfeature/noop_provider.go | 72 ++ .../noop_provider_test.go | 2 +- openfeature/openfeature.go | 115 +++ .../openfeature_test.go | 2 +- openfeature/provider.go | 192 +++++ .../provider_mock_test.go | 0 .../provider_test.go | 0 openfeature/resolution_error.go | 104 +++ {pkg/openfeature => openfeature}/util_test.go | 0 pkg/openfeature/client.go | 796 ++---------------- pkg/openfeature/doc.go | 2 + pkg/openfeature/evaluation_context.go | 51 +- pkg/openfeature/hooks.go | 105 +-- .../memprovider/in_memory_provider.go | 212 +---- pkg/openfeature/noop_provider.go | 78 +- pkg/openfeature/openfeature.go | 118 +-- pkg/openfeature/provider.go | 279 +++--- pkg/openfeature/resolution_error.go | 95 +-- 38 files changed, 2044 insertions(+), 1360 deletions(-) rename {pkg/openfeature => openfeature}/api.go (98%) create mode 100644 openfeature/client.go rename {pkg/openfeature => openfeature}/client_example_test.go (97%) rename {pkg/openfeature => openfeature}/client_test.go (100%) create mode 100644 openfeature/doc.go create mode 100644 openfeature/evaluation_context.go rename {pkg/openfeature => openfeature}/evaluation_context_test.go (100%) rename {pkg/openfeature => openfeature}/event_executor.go (100%) rename {pkg/openfeature => openfeature}/event_executor_test.go (99%) create mode 100644 openfeature/hooks.go rename {pkg/openfeature => openfeature}/hooks_mock_test.go (100%) rename {pkg/openfeature => openfeature}/hooks_test.go (100%) rename {pkg/openfeature => openfeature/internal}/internal/logger.go (100%) create mode 100644 openfeature/internal/logger.go rename {pkg/openfeature => openfeature}/memprovider/README.md (100%) create mode 100644 openfeature/memprovider/in_memory_provider.go rename {pkg/openfeature => openfeature}/memprovider/in_memory_provider_test.go (99%) create mode 100644 openfeature/noop_provider.go rename {pkg/openfeature => openfeature}/noop_provider_test.go (90%) create mode 100644 openfeature/openfeature.go rename {pkg/openfeature => openfeature}/openfeature_test.go (99%) create mode 100644 openfeature/provider.go rename {pkg/openfeature => openfeature}/provider_mock_test.go (100%) rename {pkg/openfeature => openfeature}/provider_test.go (100%) create mode 100644 openfeature/resolution_error.go rename {pkg/openfeature => openfeature}/util_test.go (100%) 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) }