diff --git a/go.mod b/go.mod index 48ba4deb..29f24e70 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.25.0 require ( github.com/cucumber/godog v0.15.1 - github.com/go-logr/logr v1.4.3 github.com/stretchr/testify v1.11.1 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 diff --git a/go.sum b/go.sum index 2462beb1..18c31893 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= diff --git a/openfeature/client.go b/openfeature/client.go index 81c05db5..285a941d 100644 --- a/openfeature/client.go +++ b/openfeature/client.go @@ -7,8 +7,6 @@ import ( "slices" "sync" "unicode/utf8" - - "github.com/go-logr/logr" ) // ClientMetadata provides a client's metadata @@ -38,7 +36,7 @@ func (cm ClientMetadata) Domain() string { // Client implements the behaviour required of an openfeature client type Client struct { - api evaluationImpl + providerBinding providerBindingFn clientEventing clientEvent metadata ClientMetadata hooks []Hook @@ -51,22 +49,9 @@ type Client struct { // interface guard to ensure that Client implements IClient var _ IClient = (*Client)(nil) -// NewClient returns a new Client. Name is a unique identifier for this client -// This helper exists for historical reasons. It is recommended to interact with IEvaluation to derive IClient instances. +// NewClient returns a new [Client] bound to the provider registered for the given domain. func NewClient(domain string) *Client { - apiRef := api() - return newClient(domain, apiRef, apiRef.eventExecutor) -} - -func newClient(domain string, apiRef evaluationImpl, eventRef clientEvent) *Client { - return &Client{ - domain: domain, - api: apiRef, - clientEventing: eventRef, - metadata: ClientMetadata{domain: domain}, - hooks: []Hook{}, - evaluationContext: EvaluationContext{}, - } + return api().NewClient(WithDomain(domain)) } // State returns the state of the associated provider @@ -74,15 +59,6 @@ func (c *Client) State() State { return c.clientEventing.State(c.domain) } -// WithLogger sets the logger of the client -// -// Deprecated: use [github.com/open-feature/go-sdk/openfeature/hooks.LoggingHook] instead. -func (c *Client) WithLogger(l logr.Logger) *Client { - c.mx.Lock() - defer c.mx.Unlock() - return c -} - // Metadata returns the client's metadata func (c *Client) Metadata() ClientMetadata { c.mx.RLock() @@ -666,7 +642,7 @@ func (c *Client) Track(ctx context.Context, trackingEventName string, evalCtx Ev // - client // - invocation (highest precedence) func (c *Client) forTracking(ctx context.Context, evalCtx EvaluationContext) (Tracker, EvaluationContext) { - provider, _, globalEvalCtx := c.api.ForEvaluation(c.metadata.domain) + provider, _, globalEvalCtx := c.providerBinding(c.metadata.domain) evalCtx = mergeContexts(evalCtx, c.evaluationContext, TransactionContext(ctx), globalEvalCtx) trackingProvider, ok := provider.(Tracker) if !ok { @@ -691,7 +667,7 @@ func (c *Client) evaluate( } // ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour - provider, globalHooks, globalEvalCtx := c.api.ForEvaluation(c.metadata.domain) + provider, globalHooks, globalEvalCtx := c.providerBinding(c.metadata.domain) evalCtx = mergeContexts(evalCtx, c.evaluationContext, TransactionContext(ctx), globalEvalCtx) // API (global) -> transaction -> client -> invocation hooks := slices.Concat(globalHooks, c.hooks, options.hooks, provider.Hooks()) // API, Client, Invocation, Provider diff --git a/openfeature/client_test.go b/openfeature/client_test.go index a12df38a..cbd4b714 100644 --- a/openfeature/client_test.go +++ b/openfeature/client_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "sync/atomic" "testing" "time" @@ -12,27 +13,34 @@ import ( type clientMocks struct { clientHandlerAPI *MockclientEvent - evaluationAPI *MockevaluationImpl + providerBinding providerBindingFn providerAPI *MockFeatureProvider } func hydratedMocksForClientTests(t *testing.T, expectedEvaluations int) clientMocks { ctrl := gomock.NewController(t) mockClientAPI := NewMockclientEvent(ctrl) - mockEvaluationAPI := NewMockevaluationImpl(ctrl) mockProvider := NewMockFeatureProvider(ctrl) mockClientAPI.EXPECT().State(gomock.Any()).AnyTimes().Return(ReadyState) mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - mockEvaluationAPI.EXPECT().ForEvaluation(gomock.Any()).Times(expectedEvaluations).DoAndReturn(func(_ string) (*MockFeatureProvider, []Hook, EvaluationContext) { + var callCount atomic.Int64 + mockBindFn := func(string) (FeatureProvider, []Hook, EvaluationContext) { + callCount.Add(1) return mockProvider, nil, EvaluationContext{} + } + + t.Cleanup(func() { + if got := callCount.Load(); got != int64(expectedEvaluations) { + t.Errorf("expected %d resolver calls, got %d", expectedEvaluations, got) + } }) return clientMocks{ clientHandlerAPI: mockClientAPI, - evaluationAPI: mockEvaluationAPI, + providerBinding: mockBindFn, providerAPI: mockProvider, } } @@ -163,7 +171,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { t.Run("BooleanValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ Value: booleanValue, @@ -184,7 +192,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { t.Run("StringValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(StringResolutionDetail{ Value: stringValue, @@ -211,7 +219,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { t.Run("FloatValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(FloatResolutionDetail{ Value: floatValue, @@ -238,7 +246,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { t.Run("IntValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(IntResolutionDetail{ Value: intValue, @@ -265,7 +273,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { t.Run("ObjectValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(InterfaceResolutionDetail{ Value: objectValue, @@ -300,7 +308,7 @@ func TestRequirement_1_4_4(t *testing.T) { flagKey := "foo" t.Run("BooleanValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ Value: booleanValue, @@ -323,7 +331,7 @@ func TestRequirement_1_4_4(t *testing.T) { t.Run("StringValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(StringResolutionDetail{ Value: stringValue, @@ -346,7 +354,7 @@ func TestRequirement_1_4_4(t *testing.T) { t.Run("FloatValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(FloatResolutionDetail{ Value: floatValue, @@ -369,7 +377,7 @@ func TestRequirement_1_4_4(t *testing.T) { t.Run("IntValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(IntResolutionDetail{ Value: intValue, @@ -392,7 +400,7 @@ func TestRequirement_1_4_4(t *testing.T) { t.Run("ObjectValueDetails", func(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(InterfaceResolutionDetail{ Value: objectValue, @@ -419,7 +427,7 @@ func TestRequirement_1_4_4(t *testing.T) { func TestRequirement_1_4_7(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ @@ -447,7 +455,7 @@ func TestRequirement_1_4_7(t *testing.T) { func TestRequirement_1_4_8(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ Value: false, @@ -485,7 +493,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 2) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) defaultValue := true mocks.providerAPI.EXPECT().BooleanEvaluation(t.Context(), flagKey, defaultValue, flatCtx). @@ -519,7 +527,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 2) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) defaultValue := "default" mocks.providerAPI.EXPECT().StringEvaluation(t.Context(), flagKey, defaultValue, flatCtx). @@ -552,7 +560,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Float", func(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 2) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) defaultValue := 3.14159 mocks.providerAPI.EXPECT().FloatEvaluation(t.Context(), flagKey, defaultValue, flatCtx). @@ -585,7 +593,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Int", func(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 2) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) var defaultValue int64 = 3 mocks.providerAPI.EXPECT().IntEvaluation(t.Context(), flagKey, defaultValue, flatCtx). Return(IntResolutionDetail{ @@ -617,7 +625,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Object", func(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 2) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) type obj struct { foo string } @@ -664,7 +672,7 @@ func TestRequirement_1_4_12(t *testing.T) { errMessage := "error forced by test" mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ Value: true, @@ -700,7 +708,7 @@ func TestRequirement_1_4_13(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) defaultValue := true mocks.providerAPI.EXPECT().BooleanEvaluation(t.Context(), flagKey, defaultValue, flatCtx). Return(BoolResolutionDetail{ @@ -726,7 +734,7 @@ func TestRequirement_1_4_13(t *testing.T) { t.Cleanup(resetSingleton) mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) defaultValue := true metadata := FlagMetadata{ "bing": "bong", @@ -882,13 +890,11 @@ func TestTrack(t *testing.T) { t.Run(name, func(t *testing.T) { // arrange mocks := hydratedMocksForClientTests(t, 0) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) - provider := test.provider(test, mocks.providerAPI) - - mocks.evaluationAPI.EXPECT().ForEvaluation("test-client").AnyTimes().DoAndReturn(func(_ string) (FeatureProvider, []Hook, EvaluationContext) { + resolver := func(_ string) (FeatureProvider, []Hook, EvaluationContext) { return provider, nil, test.inCtx.api - }) + } + client := newClient("test-client", resolver, mocks.clientHandlerAPI) client.evaluationContext = test.inCtx.client ctx := WithTransactionContext(t.Context(), test.inCtx.txn) @@ -983,7 +989,7 @@ func TestBeforeHookNilContext(t *testing.T) { hookNilContext := UnimplementedHook{} mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) attributes := map[string]any{"should": "persist"} evalCtx := EvaluationContext{attributes: attributes} @@ -1003,7 +1009,7 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) { generalErrorCode := GeneralCode mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(BoolResolutionDetail{ Value: true, @@ -1035,7 +1041,7 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) { var value any = nil mocks := hydratedMocksForClientTests(t, 1) - client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) + client := newClient("test-client", mocks.providerBinding, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(InterfaceResolutionDetail{ Value: value, @@ -1455,3 +1461,14 @@ func TestRequirement_5_3_5(t *testing.T) { return NewClient(t.Name()).State() == FatalState }, time.Second, 100*time.Millisecond, "expected client to report FATAL state") } + +func newClient(domain string, providerBinding providerBindingFn, eventRef clientEvent) *Client { + return &Client{ + domain: domain, + providerBinding: providerBinding, + clientEventing: eventRef, + metadata: ClientMetadata{domain: domain}, + hooks: []Hook{}, + evaluationContext: EvaluationContext{}, + } +} diff --git a/openfeature/event_executor_test.go b/openfeature/event_executor_test.go index d2380fa1..eccc2176 100644 --- a/openfeature/event_executor_test.go +++ b/openfeature/event_executor_test.go @@ -682,7 +682,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { rsp <- e } - client := api().GetNamedClient(clientAssociation) + client := api().NewClient(WithDomain(clientAssociation)) client.AddHandler(ProviderReady, &callback) select { @@ -718,7 +718,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { rsp <- e } - client := api().GetNamedClient("someClient") + client := api().NewClient(WithDomain("someClient")) client.AddHandler(ProviderReady, &callback) select { @@ -756,7 +756,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { rsp <- e } - client := api().GetNamedClient("someClient") + client := api().NewClient(WithDomain("someClient")) client.AddHandler(ProviderReady, &callback) select { diff --git a/openfeature/interfaces.go b/openfeature/interfaces.go index 06fc720e..60198190 100644 --- a/openfeature/interfaces.go +++ b/openfeature/interfaces.go @@ -2,11 +2,11 @@ package openfeature import ( "context" - - "github.com/go-logr/logr" ) // IEvaluation defines the OpenFeature API contract +// +// Deprecated: IEvaluation will be removed in later versions. Use [EvaluationAPI] instead. type IEvaluation interface { SetProvider(provider FeatureProvider) error SetProviderAndWait(provider FeatureProvider) error @@ -61,18 +61,7 @@ type IEventing interface { RemoveHandler(eventType EventType, callback EventCallback) } -// evaluationImpl is an internal reference interface extending IEvaluation -type evaluationImpl interface { - IEvaluation - GetProvider() FeatureProvider - GetNamedProviders() map[string]FeatureProvider - GetHooks() []Hook - - // Deprecated: use [github.com/open-feature/go-sdk/openfeature/hooks.LoggingHook] instead. - SetLogger(l logr.Logger) - - ForEvaluation(clientName string) (FeatureProvider, []Hook, EvaluationContext) -} +type providerBindingFn func(domain string) (FeatureProvider, []Hook, EvaluationContext) // clientEvent is an internal reference for OpenFeature Client events type clientEvent interface { diff --git a/openfeature/interfaces_mock.go b/openfeature/interfaces_mock.go index 8cf48a90..71f1dfed 100644 --- a/openfeature/interfaces_mock.go +++ b/openfeature/interfaces_mock.go @@ -15,7 +15,6 @@ import ( context "context" reflect "reflect" - logr "github.com/go-logr/logr" gomock "go.uber.org/mock/gomock" ) @@ -748,332 +747,6 @@ func (mr *MockIEventingMockRecorder) RemoveHandler(eventType, callback any) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*MockIEventing)(nil).RemoveHandler), eventType, callback) } -// MockevaluationImpl is a mock of evaluationImpl interface. -type MockevaluationImpl struct { - ctrl *gomock.Controller - recorder *MockevaluationImplMockRecorder - isgomock struct{} -} - -// MockevaluationImplMockRecorder is the mock recorder for MockevaluationImpl. -type MockevaluationImplMockRecorder struct { - mock *MockevaluationImpl -} - -// NewMockevaluationImpl creates a new mock instance. -func NewMockevaluationImpl(ctrl *gomock.Controller) *MockevaluationImpl { - mock := &MockevaluationImpl{ctrl: ctrl} - mock.recorder = &MockevaluationImplMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockevaluationImpl) EXPECT() *MockevaluationImplMockRecorder { - return m.recorder -} - -// AddHandler mocks base method. -func (m *MockevaluationImpl) AddHandler(eventType EventType, callback EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddHandler", eventType, callback) -} - -// AddHandler indicates an expected call of AddHandler. -func (mr *MockevaluationImplMockRecorder) AddHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*MockevaluationImpl)(nil).AddHandler), eventType, callback) -} - -// AddHooks mocks base method. -func (m *MockevaluationImpl) AddHooks(hooks ...Hook) { - m.ctrl.T.Helper() - varargs := []any{} - for _, a := range hooks { - varargs = append(varargs, a) - } - m.ctrl.Call(m, "AddHooks", varargs...) -} - -// AddHooks indicates an expected call of AddHooks. -func (mr *MockevaluationImplMockRecorder) AddHooks(hooks ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHooks", reflect.TypeOf((*MockevaluationImpl)(nil).AddHooks), hooks...) -} - -// ForEvaluation mocks base method. -func (m *MockevaluationImpl) ForEvaluation(clientName string) (FeatureProvider, []Hook, EvaluationContext) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ForEvaluation", clientName) - ret0, _ := ret[0].(FeatureProvider) - ret1, _ := ret[1].([]Hook) - ret2, _ := ret[2].(EvaluationContext) - return ret0, ret1, ret2 -} - -// ForEvaluation indicates an expected call of ForEvaluation. -func (mr *MockevaluationImplMockRecorder) ForEvaluation(clientName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ForEvaluation", reflect.TypeOf((*MockevaluationImpl)(nil).ForEvaluation), clientName) -} - -// GetClient mocks base method. -func (m *MockevaluationImpl) GetClient() IClient { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetClient") - ret0, _ := ret[0].(IClient) - return ret0 -} - -// GetClient indicates an expected call of GetClient. -func (mr *MockevaluationImplMockRecorder) GetClient() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockevaluationImpl)(nil).GetClient)) -} - -// GetHooks mocks base method. -func (m *MockevaluationImpl) GetHooks() []Hook { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetHooks") - ret0, _ := ret[0].([]Hook) - return ret0 -} - -// GetHooks indicates an expected call of GetHooks. -func (mr *MockevaluationImplMockRecorder) GetHooks() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHooks", reflect.TypeOf((*MockevaluationImpl)(nil).GetHooks)) -} - -// GetNamedClient mocks base method. -func (m *MockevaluationImpl) GetNamedClient(clientName string) IClient { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNamedClient", clientName) - ret0, _ := ret[0].(IClient) - return ret0 -} - -// GetNamedClient indicates an expected call of GetNamedClient. -func (mr *MockevaluationImplMockRecorder) GetNamedClient(clientName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamedClient", reflect.TypeOf((*MockevaluationImpl)(nil).GetNamedClient), clientName) -} - -// GetNamedProviderMetadata mocks base method. -func (m *MockevaluationImpl) GetNamedProviderMetadata(name string) Metadata { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNamedProviderMetadata", name) - ret0, _ := ret[0].(Metadata) - return ret0 -} - -// GetNamedProviderMetadata indicates an expected call of GetNamedProviderMetadata. -func (mr *MockevaluationImplMockRecorder) GetNamedProviderMetadata(name any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamedProviderMetadata", reflect.TypeOf((*MockevaluationImpl)(nil).GetNamedProviderMetadata), name) -} - -// GetNamedProviders mocks base method. -func (m *MockevaluationImpl) GetNamedProviders() map[string]FeatureProvider { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNamedProviders") - ret0, _ := ret[0].(map[string]FeatureProvider) - return ret0 -} - -// GetNamedProviders indicates an expected call of GetNamedProviders. -func (mr *MockevaluationImplMockRecorder) GetNamedProviders() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamedProviders", reflect.TypeOf((*MockevaluationImpl)(nil).GetNamedProviders)) -} - -// GetProvider mocks base method. -func (m *MockevaluationImpl) GetProvider() FeatureProvider { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProvider") - ret0, _ := ret[0].(FeatureProvider) - return ret0 -} - -// GetProvider indicates an expected call of GetProvider. -func (mr *MockevaluationImplMockRecorder) GetProvider() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProvider", reflect.TypeOf((*MockevaluationImpl)(nil).GetProvider)) -} - -// GetProviderMetadata mocks base method. -func (m *MockevaluationImpl) GetProviderMetadata() Metadata { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProviderMetadata") - ret0, _ := ret[0].(Metadata) - return ret0 -} - -// GetProviderMetadata indicates an expected call of GetProviderMetadata. -func (mr *MockevaluationImplMockRecorder) GetProviderMetadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderMetadata", reflect.TypeOf((*MockevaluationImpl)(nil).GetProviderMetadata)) -} - -// RemoveHandler mocks base method. -func (m *MockevaluationImpl) RemoveHandler(eventType EventType, callback EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RemoveHandler", eventType, callback) -} - -// RemoveHandler indicates an expected call of RemoveHandler. -func (mr *MockevaluationImplMockRecorder) RemoveHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*MockevaluationImpl)(nil).RemoveHandler), eventType, callback) -} - -// SetEvaluationContext mocks base method. -func (m *MockevaluationImpl) SetEvaluationContext(evalCtx EvaluationContext) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetEvaluationContext", evalCtx) -} - -// SetEvaluationContext indicates an expected call of SetEvaluationContext. -func (mr *MockevaluationImplMockRecorder) SetEvaluationContext(evalCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEvaluationContext", reflect.TypeOf((*MockevaluationImpl)(nil).SetEvaluationContext), evalCtx) -} - -// SetLogger mocks base method. -func (m *MockevaluationImpl) SetLogger(l logr.Logger) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLogger", l) -} - -// SetLogger indicates an expected call of SetLogger. -func (mr *MockevaluationImplMockRecorder) SetLogger(l any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogger", reflect.TypeOf((*MockevaluationImpl)(nil).SetLogger), l) -} - -// SetNamedProvider mocks base method. -func (m *MockevaluationImpl) SetNamedProvider(clientName string, provider FeatureProvider, async bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetNamedProvider", clientName, provider, async) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetNamedProvider indicates an expected call of SetNamedProvider. -func (mr *MockevaluationImplMockRecorder) SetNamedProvider(clientName, provider, async any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProvider", reflect.TypeOf((*MockevaluationImpl)(nil).SetNamedProvider), clientName, provider, async) -} - -// SetNamedProviderWithContext mocks base method. -func (m *MockevaluationImpl) SetNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetNamedProviderWithContext", ctx, clientName, provider, async) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetNamedProviderWithContext indicates an expected call of SetNamedProviderWithContext. -func (mr *MockevaluationImplMockRecorder) SetNamedProviderWithContext(ctx, clientName, provider, async any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProviderWithContext", reflect.TypeOf((*MockevaluationImpl)(nil).SetNamedProviderWithContext), ctx, clientName, provider, async) -} - -// SetNamedProviderWithContextAndWait mocks base method. -func (m *MockevaluationImpl) SetNamedProviderWithContextAndWait(ctx context.Context, clientName string, provider FeatureProvider) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetNamedProviderWithContextAndWait", ctx, clientName, provider) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetNamedProviderWithContextAndWait indicates an expected call of SetNamedProviderWithContextAndWait. -func (mr *MockevaluationImplMockRecorder) SetNamedProviderWithContextAndWait(ctx, clientName, provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProviderWithContextAndWait", reflect.TypeOf((*MockevaluationImpl)(nil).SetNamedProviderWithContextAndWait), ctx, clientName, provider) -} - -// SetProvider mocks base method. -func (m *MockevaluationImpl) SetProvider(provider FeatureProvider) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProvider", provider) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetProvider indicates an expected call of SetProvider. -func (mr *MockevaluationImplMockRecorder) SetProvider(provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProvider", reflect.TypeOf((*MockevaluationImpl)(nil).SetProvider), provider) -} - -// SetProviderAndWait mocks base method. -func (m *MockevaluationImpl) SetProviderAndWait(provider FeatureProvider) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProviderAndWait", provider) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetProviderAndWait indicates an expected call of SetProviderAndWait. -func (mr *MockevaluationImplMockRecorder) SetProviderAndWait(provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProviderAndWait", reflect.TypeOf((*MockevaluationImpl)(nil).SetProviderAndWait), provider) -} - -// SetProviderWithContext mocks base method. -func (m *MockevaluationImpl) SetProviderWithContext(ctx context.Context, provider FeatureProvider) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProviderWithContext", ctx, provider) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetProviderWithContext indicates an expected call of SetProviderWithContext. -func (mr *MockevaluationImplMockRecorder) SetProviderWithContext(ctx, provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProviderWithContext", reflect.TypeOf((*MockevaluationImpl)(nil).SetProviderWithContext), ctx, provider) -} - -// SetProviderWithContextAndWait mocks base method. -func (m *MockevaluationImpl) SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetProviderWithContextAndWait", ctx, provider) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetProviderWithContextAndWait indicates an expected call of SetProviderWithContextAndWait. -func (mr *MockevaluationImplMockRecorder) SetProviderWithContextAndWait(ctx, provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProviderWithContextAndWait", reflect.TypeOf((*MockevaluationImpl)(nil).SetProviderWithContextAndWait), ctx, provider) -} - -// Shutdown mocks base method. -func (m *MockevaluationImpl) Shutdown() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Shutdown") -} - -// Shutdown indicates an expected call of Shutdown. -func (mr *MockevaluationImplMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockevaluationImpl)(nil).Shutdown)) -} - -// ShutdownWithContext mocks base method. -func (m *MockevaluationImpl) ShutdownWithContext(ctx context.Context) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ShutdownWithContext", ctx) - ret0, _ := ret[0].(error) - return ret0 -} - -// ShutdownWithContext indicates an expected call of ShutdownWithContext. -func (mr *MockevaluationImplMockRecorder) ShutdownWithContext(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownWithContext", reflect.TypeOf((*MockevaluationImpl)(nil).ShutdownWithContext), ctx) -} - // MockclientEvent is a mock of clientEvent interface. type MockclientEvent struct { ctrl *gomock.Controller diff --git a/openfeature/internal/factory/factory.go b/openfeature/internal/factory/factory.go index 7b7330d2..3a8d9b67 100644 --- a/openfeature/internal/factory/factory.go +++ b/openfeature/internal/factory/factory.go @@ -5,5 +5,10 @@ package factory // NewAPI is set by openfeature.init and read by openfeature/isolated.NewAPI. // Returns the openfeature evaluation API as any to avoid an import cycle; -// callers must type-assert to [openfeature.IEvaluation]. +// callers must type-assert to [*openfeature.EvaluationAPI]. var NewAPI func() any + +// CurrentAPI is set by openfeature.init and read by isolated tests via factory.CurrentAPI. +// Returns the current singleton evaluation API as any to avoid an import cycle; +// callers must type-assert to [*openfeature.EvaluationAPI]. +var CurrentAPI func() any diff --git a/openfeature/isolated/isolated.go b/openfeature/isolated/isolated.go index 228c4598..504b88c0 100644 --- a/openfeature/isolated/isolated.go +++ b/openfeature/isolated/isolated.go @@ -10,16 +10,9 @@ import ( "github.com/open-feature/go-sdk/openfeature/internal/factory" ) -// NewAPI returns a new, independent [openfeature.IEvaluation]. -// -// Usage: -// -// api := isolated.NewAPI() -// defer api.Shutdown() -// api.SetProvider(myProvider) -// client := api.GetClient() +// NewAPI returns a new, independent [openfeature.EvaluationAPI]. // // Experimental. -func NewAPI() openfeature.IEvaluation { - return factory.NewAPI().(openfeature.IEvaluation) +func NewAPI() *openfeature.EvaluationAPI { + return factory.NewAPI().(*openfeature.EvaluationAPI) } diff --git a/openfeature/isolated/isolated_example_test.go b/openfeature/isolated/isolated_example_test.go new file mode 100644 index 00000000..86bd1b6a --- /dev/null +++ b/openfeature/isolated/isolated_example_test.go @@ -0,0 +1,37 @@ +package isolated_test + +import ( + "context" + "fmt" + + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/isolated" +) + +func ExampleNewAPI() { + ctx := context.Background() + + // Create an isolated API instance independent of the global singleton. + api := isolated.NewAPI() + defer func() { _ = api.Shutdown(ctx) }() + + // Set a default provider (waits for initialization). + if err := api.SetProviderAndWait(ctx, openfeature.NoopProvider{}); err != nil { + fmt.Println("SetProviderAndWait failed:", err) + return + } + + // Set a domain-scoped provider with WithDomain. + if err := api.SetProviderAndWait(ctx, openfeature.NoopProvider{}, openfeature.WithDomain("my-domain")); err != nil { + fmt.Println("SetProviderAndWait + WithDomain failed:", err) + return + } + + // Obtain a client bound to the domain-scoped provider. + client := api.NewClient(openfeature.WithDomain("my-domain")) + + // Evaluate a boolean flag. + value := client.Boolean(ctx, "my-flag", true, openfeature.EvaluationContext{}) + fmt.Printf("flag value: %v", value) + // Output: flag value: true +} diff --git a/openfeature/isolated/isolated_test.go b/openfeature/isolated/isolated_test.go index cc580fbd..52c16bf2 100644 --- a/openfeature/isolated/isolated_test.go +++ b/openfeature/isolated/isolated_test.go @@ -1,9 +1,10 @@ package isolated_test import ( + "context" "testing" - "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/internal/factory" "github.com/open-feature/go-sdk/openfeature/isolated" ) @@ -13,8 +14,9 @@ func TestNewAPI_ReturnsDistinctInstances(t *testing.T) { a := isolated.NewAPI() b := isolated.NewAPI() t.Cleanup(func() { - a.Shutdown() - b.Shutdown() + ctx := context.Background() //nolint:usetesting + _ = a.Shutdown(ctx) + _ = b.Shutdown(ctx) }) if a == nil || b == nil { @@ -29,10 +31,12 @@ func TestNewAPI_ReturnsDistinctInstances(t *testing.T) { // return the global singleton. func TestNewAPI_NotSameAsSingleton(t *testing.T) { a := isolated.NewAPI() - t.Cleanup(func() { a.Shutdown() }) + t.Cleanup(func() { + ctx := context.Background() //nolint:usetesting + _ = a.Shutdown(ctx) + }) - //nolint:staticcheck // test needs the singleton reference for comparison - if a == openfeature.GetApiInstance() { + if a == factory.CurrentAPI() { t.Error("isolated.NewAPI() returned the global singleton") } } diff --git a/openfeature/isolated_api_test.go b/openfeature/isolated_api_test.go index 85d81406..f14ba057 100644 --- a/openfeature/isolated_api_test.go +++ b/openfeature/isolated_api_test.go @@ -6,30 +6,24 @@ import ( "testing" "time" + "github.com/open-feature/go-sdk/openfeature/internal/factory" "go.uber.org/goleak" "go.uber.org/mock/gomock" ) -// newAPIForTest constructs a fresh *evaluationAPI directly via the -// package-private helpers so tests don't depend on the public factory in -// openfeature/isolated (which would create an import cycle for tests in this -// package). -func newAPIForTest() *evaluationAPI { - return newEvaluationAPI(newEventExecutor()) -} - // Requirement 1.8.1: The API MUST provide a factory function that creates new, independent API instances. func TestRequirement_1_8_1(t *testing.T) { - instance := newAPIForTest() + instance := newAPI() if instance == nil { - t.Fatal("newAPIForTest() returned nil") + t.Fatal("newAPI() returned nil") } } -// Requirement 1.8.2: Isolated instances MUST conform to the same API contract as the singleton (IEvaluation). +// Requirement 1.8.2: Isolated instances MUST conform to the same API contract as the singleton. func TestRequirement_1_8_2(t *testing.T) { - // compile-time check: *EvaluationAPI must satisfy IEvaluation - var _ IEvaluation = newAPIForTest() + if _, ok := factory.NewAPI().(*EvaluationAPI); !ok { + t.Error("factory.NewAPI() did not return *EvaluationAPI") + } } // Requirement 1.8.1 (independence): State set on an isolated instance MUST NOT affect the singleton. @@ -40,13 +34,13 @@ func TestIsolatedAPI_IndependentFromSingleton(t *testing.T) { instanceProvider := NewMockFeatureProvider(ctrl) instanceProvider.EXPECT().Metadata().Return(Metadata{Name: "instance-provider"}).AnyTimes() - instance := newAPIForTest() - if err := instance.SetProviderAndWait(instanceProvider); err != nil { + instance := newAPI() + if err := instance.SetProviderAndWait(t.Context(), instanceProvider); err != nil { t.Fatalf("SetProviderAndWait on isolated instance: %v", err) } // Singleton should still have the default NoopProvider - if api().GetProviderMetadata().Name == "instance-provider" { + if api().getProviderMetadata().Name == "instance-provider" { t.Error("provider set on isolated instance leaked into the singleton") } } @@ -63,8 +57,8 @@ func TestIsolatedAPI_SingletonDoesNotAffectInstance(t *testing.T) { t.Fatalf("SetProviderAndWait on singleton: %v", err) } - instance := newAPIForTest() - if instance.GetProviderMetadata().Name == "singleton-provider" { + instance := newAPI() + if instance.getProviderMetadata().Name == "singleton-provider" { t.Error("singleton provider leaked into newly created isolated instance") } } @@ -81,15 +75,15 @@ func TestRequirement_1_8_4_CrossInstanceBinding(t *testing.T) { sharedProvider := NewMockFeatureProvider(ctrl) sharedProvider.EXPECT().Metadata().Return(Metadata{Name: "shared-provider"}).AnyTimes() - instance1 := newAPIForTest() - instance2 := newAPIForTest() + instance1 := newAPI() + instance2 := newAPI() - if err := instance1.SetProvider(sharedProvider); err != nil { + if err := instance1.SetProvider(t.Context(), sharedProvider); err != nil { t.Fatalf("SetProvider on instance1: %v", err) } // Registering the same provider on a different instance must return an error. - if err := instance2.SetProvider(sharedProvider); err == nil { + if err := instance2.SetProvider(t.Context(), sharedProvider); err == nil { t.Error("expected error when binding a provider already bound to another instance, got nil") } } @@ -109,20 +103,20 @@ func TestRequirement_1_8_4_ProviderReleasedAfterReplacement(t *testing.T) { replacementProvider := NewMockFeatureProvider(ctrl) replacementProvider.EXPECT().Metadata().Return(Metadata{Name: "replacement-provider"}).AnyTimes() - instance1 := newAPIForTest() - instance2 := newAPIForTest() + instance1 := newAPI() + instance2 := newAPI() - if err := instance1.SetProvider(sharedProvider); err != nil { + if err := instance1.SetProvider(t.Context(), sharedProvider); err != nil { t.Fatalf("SetProvider on instance1: %v", err) } // Replace the shared provider on instance1, which releases the binding. - if err := instance1.SetProvider(replacementProvider); err != nil { + if err := instance1.SetProvider(t.Context(), replacementProvider); err != nil { t.Fatalf("SetProvider (replacement) on instance1: %v", err) } // Now the shared provider should be free to bind to instance2. - if err := instance2.SetProvider(sharedProvider); err != nil { + if err := instance2.SetProvider(t.Context(), sharedProvider); err != nil { t.Errorf("expected no error binding released provider to instance2, got: %v", err) } } @@ -139,13 +133,13 @@ func TestRequirement_1_8_4_SameInstanceMultipleDomains(t *testing.T) { provider := NewMockFeatureProvider(ctrl) provider.EXPECT().Metadata().Return(Metadata{Name: "multi-domain-provider"}).AnyTimes() - instance := newAPIForTest() + instance := newAPI() - if err := instance.SetProvider(provider); err != nil { + if err := instance.SetProvider(t.Context(), provider); err != nil { t.Fatalf("SetProvider: %v", err) } // Registering the same provider as a named provider on the same instance must succeed. - if err := instance.SetNamedProvider("domain-a", provider, true); err != nil { + if err := instance.SetProvider(t.Context(), provider, WithDomain("domain-a")); err != nil { t.Errorf("expected no error binding same provider to a second domain on same instance, got: %v", err) } } @@ -162,40 +156,42 @@ func TestRequirement_1_8_4_ReleasedOnShutdown(t *testing.T) { provider := NewMockFeatureProvider(ctrl) provider.EXPECT().Metadata().Return(Metadata{Name: "shutdown-provider"}).AnyTimes() - instance1 := newAPIForTest() - instance2 := newAPIForTest() + instance1 := newAPI() + instance2 := newAPI() - if err := instance1.SetProvider(provider); err != nil { + if err := instance1.SetProvider(t.Context(), provider); err != nil { t.Fatalf("SetProvider on instance1: %v", err) } - instance1.Shutdown() + if err := instance1.Shutdown(t.Context()); err != nil { + t.Errorf("Shutdown: %v", err) + } // After shutdown, the provider should be free to bind to another instance. - if err := instance2.SetProvider(provider); err != nil { + if err := instance2.SetProvider(t.Context(), provider); err != nil { t.Errorf("expected no error binding provider after instance1 shutdown, got: %v", err) } } -// GetClient / GetNamedClient on an isolated instance must return clients bound to that instance. -func TestIsolatedAPI_GetClientBoundToInstance(t *testing.T) { +// NewClient on an isolated instance must return clients bound to that instance. +func TestIsolatedAPI_NewClientBoundToInstance(t *testing.T) { ctrl := gomock.NewController(t) provider := NewMockFeatureProvider(ctrl) provider.EXPECT().Metadata().Return(Metadata{Name: "instance-provider"}).AnyTimes() - instance := newAPIForTest() - if err := instance.SetProvider(provider); err != nil { + instance := newAPI() + if err := instance.SetProvider(t.Context(), provider); err != nil { t.Fatalf("SetProvider: %v", err) } - client := instance.GetClient() + client := instance.NewClient() if client == nil { - t.Fatal("GetClient() returned nil") + t.Fatal("NewClient() returned nil") } - namedClient := instance.GetNamedClient("my-domain") + namedClient := instance.NewClient(WithDomain("my-domain")) if namedClient == nil { - t.Fatal("GetNamedClient() returned nil") + t.Fatal("NewClient() returned nil") } } @@ -203,7 +199,7 @@ func TestIsolatedAPI_GetClientBoundToInstance(t *testing.T) { func TestIsolatedAPI_HooksIndependence(t *testing.T) { t.Cleanup(resetSingleton) - instance := newAPIForTest() + instance := newAPI() hook := UnimplementedHook{} instance.AddHooks(hook) @@ -224,7 +220,7 @@ func TestIsolatedAPI_SingletonHooksDoNotAffectInstance(t *testing.T) { AddHooks(UnimplementedHook{}) - instance := newAPIForTest() + instance := newAPI() instance.mu.RLock() instanceHookCount := len(instance.hks) instance.mu.RUnlock() @@ -238,7 +234,7 @@ func TestIsolatedAPI_SingletonHooksDoNotAffectInstance(t *testing.T) { func TestIsolatedAPI_EvalContextIndependence(t *testing.T) { t.Cleanup(resetSingleton) - instance := newAPIForTest() + instance := newAPI() instance.SetEvaluationContext(EvaluationContext{ attributes: map[string]any{"tenant": "isolated"}, }) @@ -261,7 +257,7 @@ func TestIsolatedAPI_SingletonEvalContextDoesNotAffectInstance(t *testing.T) { attributes: map[string]any{"tenant": "singleton"}, }) - instance := newAPIForTest() + instance := newAPI() instance.mu.RLock() instanceCtx := instance.evalCtx instance.mu.RUnlock() @@ -285,8 +281,8 @@ func TestIsolatedAPI_EventsIndependence(t *testing.T) { } AddHandler(ProviderReady, &singletonHandler) - instance := newAPIForTest() - if err := instance.SetProviderAndWait(provider); err != nil { + instance := newAPI() + if err := instance.SetProviderAndWait(t.Context(), provider); err != nil { t.Fatalf("SetProviderAndWait on isolated instance: %v", err) } @@ -306,8 +302,8 @@ func TestIsolatedAPI_EventsBetweenInstances(t *testing.T) { provider2 := NewMockFeatureProvider(ctrl) provider2.EXPECT().Metadata().Return(Metadata{Name: "instance2-provider"}).AnyTimes() - instance1 := newAPIForTest() - instance2 := newAPIForTest() + instance1 := newAPI() + instance2 := newAPI() var mu sync.Mutex var instance1Events []string @@ -320,7 +316,7 @@ func TestIsolatedAPI_EventsBetweenInstances(t *testing.T) { instance1.AddHandler(ProviderReady, &cb) // Setting a provider on instance2 should NOT trigger instance1's handler. - if err := instance2.SetProviderAndWait(provider2); err != nil { + if err := instance2.SetProviderAndWait(t.Context(), provider2); err != nil { t.Fatalf("SetProviderAndWait on instance2: %v", err) } @@ -341,14 +337,14 @@ func TestIsolatedAPI_ShutdownStopsEventExecutor(t *testing.T) { defer goleak.VerifyNone(t) - instance := newAPIForTest() + instance := newAPI() defaultProvider := struct { FeatureProvider EventHandler }{NoopProvider{}, &ProviderEventing{c: make(chan Event, 1)}} - if err := instance.SetProvider(defaultProvider); err != nil { + if err := instance.SetProvider(t.Context(), defaultProvider); err != nil { t.Fatalf("SetProvider on isolated instance: %v", err) } @@ -357,14 +353,16 @@ func TestIsolatedAPI_ShutdownStopsEventExecutor(t *testing.T) { EventHandler }{NoopProvider{}, &ProviderEventing{c: make(chan Event, 1)}} - if err := instance.SetNamedProvider("test-domain", namedProvider, true); err != nil { + if err := instance.SetProvider(t.Context(), namedProvider, WithDomain("test-domain")); err != nil { t.Fatalf("SetNamedProvider on isolated instance: %v", err) } // Allow the executor goroutines to start handling events. time.Sleep(50 * time.Millisecond) - instance.Shutdown() + if err := instance.Shutdown(t.Context()); err != nil { + t.Errorf("Shutdown: %v", err) + } // goleak will verify no goroutines leak from the isolated instance's // per-instance event executor. @@ -377,20 +375,20 @@ func TestIsolatedAPI_ShutdownWithContextStopsEventExecutor(t *testing.T) { defer goleak.VerifyNone(t) - instance := newAPIForTest() + instance := newAPI() eventingProvider := struct { FeatureProvider EventHandler }{NoopProvider{}, &ProviderEventing{c: make(chan Event, 1)}} - if err := instance.SetProvider(eventingProvider); err != nil { + if err := instance.SetProvider(t.Context(), eventingProvider); err != nil { t.Fatalf("SetProvider on isolated instance: %v", err) } time.Sleep(50 * time.Millisecond) - if err := instance.ShutdownWithContext(t.Context()); err != nil { + if err := instance.Shutdown(t.Context()); err != nil { t.Fatalf("ShutdownWithContext on isolated instance: %v", err) } } diff --git a/openfeature/main_test.go b/openfeature/main_test.go index 8e19fdd3..b77662c9 100644 --- a/openfeature/main_test.go +++ b/openfeature/main_test.go @@ -6,7 +6,6 @@ import ( "os" "testing" - "github.com/open-feature/go-sdk/openfeature/internal/factory" "go.uber.org/goleak" ) @@ -37,19 +36,19 @@ func startLeakTest(t *testing.T) { // fresh, isolated instances for the duration of the test (or subtest) and // returns the new API for further configuration. The previous globals are // restored and the executor is shut down via t.Cleanup. -func installIsolatedAPI(t *testing.T) *evaluationAPI { +func installIsolatedAPI(t *testing.T) *EvaluationAPI { t.Helper() - testAPI := factory.NewAPI().(*evaluationAPI) + testAPI := newAPI() originalAPI := apiInstance.Swap(testAPI) t.Cleanup(func() { // ShutdownWithContext (and similar) can reinitialize the global event // executor via resetSingleton; shut that replacement down too so it // doesn't leak. - _ = testAPI.ShutdownWithContext(context.Background()) //nolint:usetesting + _ = testAPI.Shutdown(context.Background()) //nolint:usetesting if current := apiInstance.Swap(originalAPI); current != testAPI { - _ = current.ShutdownWithContext(context.Background()) //nolint:usetesting + _ = current.Shutdown(context.Background()) //nolint:usetesting } }) diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go index 0bd3a948..fa6ab5dd 100644 --- a/openfeature/openfeature.go +++ b/openfeature/openfeature.go @@ -4,21 +4,22 @@ import ( "context" "sync/atomic" - "github.com/go-logr/logr" "github.com/open-feature/go-sdk/openfeature/internal/factory" ) // api is the global evaluationImpl implementation. This is a singleton and there can only be one instance. var ( - apiInstance atomic.Pointer[evaluationAPI] + apiInstance atomic.Pointer[EvaluationAPI] ) // init initializes the OpenFeature evaluation API func init() { // register the isolated-instance constructor; see openfeature/internal/factory. factory.NewAPI = func() any { - exec := newEventExecutor() - return newEvaluationAPI(exec) + return newAPI() + } + factory.CurrentAPI = func() any { + return api() } resetSingleton() } @@ -30,44 +31,40 @@ func resetSingleton() { // resetSingletonWithContext stops (if running) the event executor and starts a new one. func resetSingletonWithContext(ctx context.Context) error { - nextAPI := factory.NewAPI().(*evaluationAPI) + nextAPI := newAPI() oldAPI := apiInstance.Swap(nextAPI) if oldAPI != nil { - return oldAPI.ShutdownWithContext(ctx) + return oldAPI.Shutdown(ctx) } return nil } -// GetApiInstance returns the current singleton IEvaluation instance. -// -// Deprecated: use [NewDefaultClient] or [NewClient] directly instead -// -//nolint:staticcheck // Renaming this now would be a breaking change. -func GetApiInstance() IEvaluation { - return api() +// api returns the current singleton [EvaluationAPI]. +func api() *EvaluationAPI { + return apiInstance.Load() } -func api() *evaluationAPI { - return apiInstance.Load() +// newAPI creates a fresh *EvaluationAPI. +func newAPI() *EvaluationAPI { + exec := newEventExecutor() + return newEvaluationAPI(exec) } -// NewDefaultClient returns a [Client] for the default domain. The default domain [Client] is the [IClient] instance that -// wraps around an unnamed [FeatureProvider] +// NewDefaultClient returns a [Client] bound to the default (unnamed) [FeatureProvider]. func NewDefaultClient() *Client { - apiRef := api() - return newClient("", apiRef, apiRef.eventExecutor) + return api().NewClient() } // SetProvider sets the default [FeatureProvider]. Provider initialization is asynchronous and status can be checked from // provider status func SetProvider(provider FeatureProvider) error { - return api().SetProvider(provider) + return api().SetProvider(context.Background(), provider) } // SetProviderAndWait sets the default [FeatureProvider] and waits for its initialization. // Returns an error if initialization causes an error func SetProviderAndWait(provider FeatureProvider) error { - return api().SetProviderAndWait(provider) + return api().SetProviderAndWait(context.Background(), provider) } // SetProviderWithContext sets the default [FeatureProvider] with context-aware initialization. @@ -80,7 +77,7 @@ func SetProviderAndWait(provider FeatureProvider) error { // For providers that don't implement ContextAwareStateHandler, this behaves // identically to SetProvider() but with timeout protection. func SetProviderWithContext(ctx context.Context, provider FeatureProvider) error { - return api().SetProviderWithContext(ctx, provider) + return api().SetProvider(ctx, provider) } // SetProviderWithContextAndWait sets the default [FeatureProvider] with context-aware initialization and waits for completion. @@ -91,24 +88,24 @@ func SetProviderWithContext(ctx context.Context, provider FeatureProvider) error // application startup to wait for the provider before continuing. // Recommended timeout values: 1-5s for local providers, 10-30s for network-based providers. func SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error { - return api().SetProviderWithContextAndWait(ctx, provider) + return api().SetProviderAndWait(ctx, provider) } // ProviderMetadata returns the default [FeatureProvider] metadata func ProviderMetadata() Metadata { - return api().GetProviderMetadata() + return api().getProviderMetadata() } // SetNamedProvider sets a [FeatureProvider] mapped to the given [Client] domain. Provider initialization is asynchronous // and status can be checked from provider status func SetNamedProvider(domain string, provider FeatureProvider) error { - return api().SetNamedProvider(domain, provider, true) + return api().SetProvider(context.Background(), provider, WithDomain(domain)) } // SetNamedProviderAndWait sets a provider mapped to the given [Client] domain and waits for its initialization. // Returns an error if initialization cause error func SetNamedProviderAndWait(domain string, provider FeatureProvider) error { - return api().SetNamedProvider(domain, provider, false) + return api().SetProviderAndWait(context.Background(), provider, WithDomain(domain)) } // SetNamedProviderWithContext sets a [FeatureProvider] mapped to the given [Client] domain with context-aware initialization. @@ -119,7 +116,7 @@ func SetNamedProviderAndWait(domain string, provider FeatureProvider) error { // Named providers allow different domains to use different feature flag providers, // enabling multi-tenant applications or microservice architectures. func SetNamedProviderWithContext(ctx context.Context, domain string, provider FeatureProvider) error { - return api().SetNamedProviderWithContext(ctx, domain, provider, true) + return api().SetProvider(ctx, provider, WithDomain(domain)) } // SetNamedProviderWithContextAndWait sets a provider mapped to the given [Client] domain with context-aware initialization and waits for completion. @@ -129,12 +126,12 @@ func SetNamedProviderWithContext(ctx context.Context, domain string, provider Fe // Use this for synchronous named provider setup where you need to ensure // the provider is ready before proceeding. func SetNamedProviderWithContextAndWait(ctx context.Context, domain string, provider FeatureProvider) error { - return api().SetNamedProviderWithContextAndWait(ctx, domain, provider) + return api().SetProviderAndWait(ctx, provider, WithDomain(domain)) } // NamedProviderMetadata returns the named provider's Metadata func NamedProviderMetadata(name string) Metadata { - return api().GetNamedProviderMetadata(name) + return api().getDomainProviderMetadata(name) } // SetEvaluationContext sets the global [EvaluationContext]. @@ -142,12 +139,6 @@ func SetEvaluationContext(evalCtx EvaluationContext) { api().SetEvaluationContext(evalCtx) } -// SetLogger sets the global Logger. -// -// Deprecated: use [github.com/open-feature/go-sdk/openfeature/hooks.LoggingHook] instead. -func SetLogger(l logr.Logger) { -} - // AddHooks appends to the collection of any previously added hooks func AddHooks(hooks ...Hook) { api().AddHooks(hooks...) diff --git a/openfeature/openfeature_api.go b/openfeature/openfeature_api.go index 37a59d67..dd8d7e3b 100644 --- a/openfeature/openfeature_api.go +++ b/openfeature/openfeature_api.go @@ -8,8 +8,6 @@ import ( "reflect" "slices" "sync" - - "github.com/go-logr/logr" ) // providerBindingEntry holds a strong reference to the provider and the API instance it is bound to. @@ -17,7 +15,7 @@ import ( // that the uintptr key in providerBindings cannot be reused by a new allocation. type providerBindingEntry struct { provider FeatureProvider - api *evaluationAPI + api *EvaluationAPI } // providerBindings is a global registry mapping provider pointer identity (uintptr) to the API @@ -30,8 +28,10 @@ type providerBindingEntry struct { // // Lock ordering: always acquire evaluationAPI.mu before providerBindingsMu to avoid deadlocks. var ( - providerBindings = make(map[uintptr]*providerBindingEntry) - providerBindingsMu sync.Mutex + providerBindings = make(map[uintptr]*providerBindingEntry) + providerBindingsMu sync.Mutex + errNilProvider = errors.New("provider cannot be set to nil") + errNilDefaultProvider = errors.New("default provider cannot be set to nil") ) // providerBindingKey returns a stable, hashable identity for provider suitable for use as a map key, @@ -43,7 +43,7 @@ var ( // same "zerobase" address for all zero-size allocations, making such pointers indistinguishable. func providerBindingKey(provider FeatureProvider) (uintptr, bool) { rv := reflect.ValueOf(provider) - if rv.Kind() != reflect.Ptr || rv.IsNil() { + if rv.Kind() != reflect.Pointer || rv.IsNil() { return 0, false } if rv.Type().Elem().Size() == 0 { @@ -54,7 +54,7 @@ func providerBindingKey(provider FeatureProvider) (uintptr, bool) { // bindProvider registers provider as bound to apiInst. Returns an error if the provider is already // bound to a different API instance (spec 1.8.4). Must be called with apiInst.mu write-locked. -func bindProvider(provider FeatureProvider, apiInst *evaluationAPI) error { +func bindProvider(provider FeatureProvider, apiInst *EvaluationAPI) error { key, ok := providerBindingKey(provider) if !ok { return nil @@ -72,7 +72,7 @@ func bindProvider(provider FeatureProvider, apiInst *evaluationAPI) error { // unbindProvider removes provider from the global binding registry for apiInst. // Safe to call with apiInst.mu held (lock order: evaluationAPI.mu → providerBindingsMu). -func unbindProvider(provider FeatureProvider, apiInst *evaluationAPI) { +func unbindProvider(provider FeatureProvider, apiInst *EvaluationAPI) { key, ok := providerBindingKey(provider) if !ok { return @@ -86,10 +86,31 @@ func unbindProvider(provider FeatureProvider, apiInst *evaluationAPI) { } } -// evaluationAPI wraps OpenFeature evaluation API functionalities -type evaluationAPI struct { +// APIOption configures API-level operations such as domain selection. +type APIOption interface { + apply(*apiOptions) +} + +type ( + apiOptionFunc func(*apiOptions) + apiOptions struct { + domain string + } +) + +func (f apiOptionFunc) apply(o *apiOptions) { f(o) } + +// WithDomain returns an APIOption that scopes API-level operations (e.g. SetProvider, NewClient) +// to the given domain. When used with NewClient, the returned Client is bound to the +// provider registered for that domain. An empty domain is treated as the default (unscoped). +func WithDomain(domain string) APIOption { + return apiOptionFunc(func(o *apiOptions) { o.domain = domain }) +} + +// EvaluationAPI wraps OpenFeature evaluation API functionalities +type EvaluationAPI struct { defaultProvider FeatureProvider - namedProviders map[string]FeatureProvider + domainProviders map[string]FeatureProvider hks []Hook evalCtx EvaluationContext eventExecutor *eventExecutor @@ -97,10 +118,10 @@ type evaluationAPI struct { } // newEvaluationAPI is a helper to generate an API. Used internally -func newEvaluationAPI(eventExecutor *eventExecutor) *evaluationAPI { - return &evaluationAPI{ +func newEvaluationAPI(eventExecutor *eventExecutor) *EvaluationAPI { + return &EvaluationAPI{ defaultProvider: NoopProvider{}, - namedProviders: map[string]FeatureProvider{}, + domainProviders: map[string]FeatureProvider{}, hks: []Hook{}, evalCtx: EvaluationContext{}, mu: sync.RWMutex{}, @@ -108,68 +129,56 @@ func newEvaluationAPI(eventExecutor *eventExecutor) *evaluationAPI { } } -func (a *evaluationAPI) SetProvider(provider FeatureProvider) error { - return a.SetProviderWithContext(context.Background(), provider) -} - -func (a *evaluationAPI) SetProviderAndWait(provider FeatureProvider) error { - return a.SetProviderWithContextAndWait(context.Background(), provider) -} - -// SetProviderWithContext sets the default FeatureProvider with context-aware initialization. -func (a *evaluationAPI) SetProviderWithContext(ctx context.Context, provider FeatureProvider) error { - if provider == nil { - return errors.New("default provider cannot be set to nil") +// SetProvider sets a FeatureProvider with context-aware initialization. +// If WithDomain is provided, the provider is bound to the given domain. +func (a *EvaluationAPI) SetProvider(ctx context.Context, provider FeatureProvider, opts ...APIOption) error { + o := apiOptions{} + for _, opt := range opts { + opt.apply(&o) + } + if o.domain != "" { + _, err := a.setDomainProvider(ctx, o.domain, provider) + return err } - _, err := a.setProviderWithContext(ctx, provider) + _, err := a.setProvider(ctx, provider) return err } -// SetProviderWithContextAndWait sets the default FeatureProvider with context-aware initialization and waits for completion. -func (a *evaluationAPI) SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error { - if provider == nil { - return errors.New("default provider cannot be set to nil") +// SetProviderAndWait sets a FeatureProvider with context-aware initialization and waits for completion. +// If WithDomain is provided, the provider is bound to the given domain. +func (a *EvaluationAPI) SetProviderAndWait(ctx context.Context, provider FeatureProvider, opts ...APIOption) error { + o := apiOptions{} + for _, opt := range opts { + opt.apply(&o) } - initCh, err := a.setProviderWithContext(ctx, provider) - if err != nil { - return err - } - return <-initCh -} -// SetNamedProvider sets a provider with client name. Returns an error if FeatureProvider is nil -func (a *evaluationAPI) SetNamedProvider(clientName string, provider FeatureProvider, async bool) error { - return a.SetNamedProviderWithContext(context.Background(), clientName, provider, async) -} + var ( + initCh <-chan error + err error + ) -// SetNamedProviderWithContext sets a provider with client name using context-aware initialization. -func (a *evaluationAPI) SetNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error { - if provider == nil { - return errors.New("provider cannot be set to nil") + if o.domain != "" { + initCh, err = a.setDomainProvider(ctx, o.domain, provider) + } else { + initCh, err = a.setProvider(ctx, provider) } - initCh, err := a.setNamedProviderWithContext(ctx, clientName, provider) if err != nil { return err } - - if async { - return nil + select { + case err := <-initCh: + return err + case <-ctx.Done(): + return ctx.Err() } - - return <-initCh -} - -// SetNamedProviderWithContextAndWait sets a provider with client name using context-aware initialization and waits for completion. -func (a *evaluationAPI) SetNamedProviderWithContextAndWait(ctx context.Context, clientName string, provider FeatureProvider) error { - return a.SetNamedProviderWithContext(ctx, clientName, provider, false) } // unbindIfUnreferenced removes oldProvider from the global binding registry if it is no longer // referenced (as default or named provider) by this API instance. Must be called with a.mu held. // All comparisons use pointer identity (via providerBindingKey) to avoid panics from unhashable // FeatureProvider implementations that contain maps or slices. -func (a *evaluationAPI) unbindIfUnreferenced(oldProvider FeatureProvider) { +func (a *EvaluationAPI) unbindIfUnreferenced(oldProvider FeatureProvider) { oldKey, tracked := providerBindingKey(oldProvider) if !tracked { return @@ -179,7 +188,7 @@ func (a *evaluationAPI) unbindIfUnreferenced(oldProvider FeatureProvider) { return } // Is oldProvider still registered as a named provider? - for _, p := range a.namedProviders { + for _, p := range a.domainProviders { if k, ok := providerBindingKey(p); ok && k == oldKey { return } @@ -187,9 +196,12 @@ func (a *evaluationAPI) unbindIfUnreferenced(oldProvider FeatureProvider) { unbindProvider(oldProvider, a) } -// setProviderWithContext sets the default FeatureProvider of the EvaluationAPI with context-aware initialization. +// setProvider sets the default FeatureProvider of the EvaluationAPI with context-aware initialization. // Returns an error immediately if the provider is already bound to a different API instance (spec 1.8.4). -func (a *evaluationAPI) setProviderWithContext(ctx context.Context, provider FeatureProvider) (<-chan error, error) { +func (a *EvaluationAPI) setProvider(ctx context.Context, provider FeatureProvider) (<-chan error, error) { + if provider == nil { + return nil, errNilDefaultProvider + } a.mu.Lock() defer a.mu.Unlock() @@ -212,7 +224,11 @@ func (a *evaluationAPI) setProviderWithContext(ctx context.Context, provider Fea return a.initNew(ctx, "", provider), nil } -func (a *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider) (<-chan error, error) { +func (a *EvaluationAPI) setDomainProvider(ctx context.Context, domain string, provider FeatureProvider) (<-chan error, error) { + if provider == nil { + return nil, errNilProvider + } + a.mu.Lock() defer a.mu.Unlock() @@ -221,10 +237,10 @@ func (a *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clientN } // Initialize new named provider and Shutdown the old one - oldProvider := a.namedProviders[clientName] - a.namedProviders[clientName] = provider + oldProvider := a.domainProviders[domain] + a.domainProviders[domain] = provider - a.eventExecutor.registerNamedEventingProvider(clientName, provider) + a.eventExecutor.registerNamedEventingProvider(domain, provider) a.shutdownOld(ctx, oldProvider) @@ -233,31 +249,31 @@ func (a *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clientN a.unbindIfUnreferenced(oldProvider) } - return a.initNew(ctx, clientName, provider), nil + return a.initNew(ctx, domain, provider), nil } -func (a *evaluationAPI) initNew(ctx context.Context, clientName string, newProvider FeatureProvider) <-chan error { +func (a *EvaluationAPI) initNew(ctx context.Context, domain string, newProvider FeatureProvider) <-chan error { errCh := make(chan error, 1) // Initialize new provider async. The caller may wait on the channel. - go func(executor *eventExecutor, evalCtx EvaluationContext, ctx context.Context, provider FeatureProvider, clientName string) { + go func(executor *eventExecutor, evalCtx EvaluationContext, ctx context.Context, provider FeatureProvider, domain string) { event, err := initializerWithContext(ctx, provider, evalCtx) executor.triggerEvent(event, provider) if err != nil { - if clientName == "" { + if domain == "" { err = fmt.Errorf("failed to initialize default provider %q: %w", provider.Metadata().Name, err) } else { - err = fmt.Errorf("failed to initialize named provider %q for domain %q: %w", provider.Metadata().Name, clientName, err) + err = fmt.Errorf("failed to initialize named provider %q for domain %q: %w", provider.Metadata().Name, domain, err) } } errCh <- err - }(a.eventExecutor, a.evalCtx, ctx, newProvider, clientName) + }(a.eventExecutor, a.evalCtx, ctx, newProvider, domain) return errCh } -func (a *evaluationAPI) shutdownOld(ctx context.Context, oldProvider FeatureProvider) { +func (a *EvaluationAPI) shutdownOld(ctx context.Context, oldProvider FeatureProvider) { v, ok := oldProvider.(StateHandler) // oldProvider can be nil or without state handling capability @@ -265,7 +281,7 @@ func (a *evaluationAPI) shutdownOld(ctx context.Context, oldProvider FeatureProv return } - namedProviders := slices.Collect(maps.Values(a.namedProviders)) + namedProviders := slices.Collect(maps.Values(a.domainProviders)) // check for multiple bindings if oldProvider == a.defaultProvider || slices.Contains(namedProviders, oldProvider) { @@ -285,63 +301,68 @@ func (a *evaluationAPI) shutdownOld(ctx context.Context, oldProvider FeatureProv } // GetProviderMetadata returns the default FeatureProvider's metadata -func (a *evaluationAPI) GetProviderMetadata() Metadata { +func (a *EvaluationAPI) getProviderMetadata() Metadata { a.mu.RLock() defer a.mu.RUnlock() return a.defaultProvider.Metadata() } -// GetNamedProviderMetadata returns the default FeatureProvider's metadata -func (a *evaluationAPI) GetNamedProviderMetadata(name string) Metadata { +// getDomainProviderMetadata returns the default FeatureProvider's metadata +func (a *EvaluationAPI) getDomainProviderMetadata(domain string) Metadata { a.mu.RLock() defer a.mu.RUnlock() - provider, ok := a.namedProviders[name] + provider, ok := a.domainProviders[domain] if !ok { - return ProviderMetadata() + return a.defaultProvider.Metadata() } return provider.Metadata() } -// GetNamedProviders returns named providers map. -func (a *evaluationAPI) GetNamedProviders() map[string]FeatureProvider { +// getDomainProviders returns a map of domain-scoped providers keyed by domain name. +func (a *EvaluationAPI) getDomainProviders() map[string]FeatureProvider { a.mu.RLock() defer a.mu.RUnlock() - return a.namedProviders + return a.domainProviders } -// GetClient returns a IClient bound to the default provider -func (a *evaluationAPI) GetClient() IClient { - return newClient("", a, a.eventExecutor) -} - -// GetNamedClient returns a IClient bound to the given named provider -func (a *evaluationAPI) GetNamedClient(clientName string) IClient { - return newClient(clientName, a, a.eventExecutor) +// NewClient returns a [Client] bound to the default provider, +// or to a domain-scoped provider when [WithDomain] is supplied. +func (a *EvaluationAPI) NewClient(opts ...APIOption) *Client { + o := apiOptions{} + for _, opt := range opts { + opt.apply(&o) + } + return &Client{ + domain: o.domain, + providerBinding: a.resolveBinding, + clientEventing: a.eventExecutor, + metadata: NewClientMetadata(o.domain), + hooks: []Hook{}, + evaluationContext: EvaluationContext{}, + } } -func (a *evaluationAPI) SetEvaluationContext(evalCtx EvaluationContext) { +// SetEvaluationContext sets the global [EvaluationContext]. +func (a *EvaluationAPI) SetEvaluationContext(evalCtx EvaluationContext) { a.mu.Lock() defer a.mu.Unlock() a.evalCtx = evalCtx } -// Deprecated: use [github.com/open-feature/go-sdk/openfeature/hooks.LoggingHook] instead. -func (a *evaluationAPI) SetLogger(l logr.Logger) { -} - -func (a *evaluationAPI) AddHooks(hooks ...Hook) { +// AddHooks appends to the API-level hook collection. +func (a *EvaluationAPI) AddHooks(hooks ...Hook) { a.mu.Lock() defer a.mu.Unlock() a.hks = append(a.hks, hooks...) } -func (a *evaluationAPI) GetHooks() []Hook { +func (a *EvaluationAPI) getHooks() []Hook { a.mu.RLock() defer a.mu.RUnlock() @@ -349,25 +370,19 @@ func (a *evaluationAPI) GetHooks() []Hook { } // AddHandler allows to add API level event handler -func (a *evaluationAPI) AddHandler(eventType EventType, callback EventCallback) { +func (a *EvaluationAPI) AddHandler(eventType EventType, callback EventCallback) { a.eventExecutor.AddHandler(eventType, callback) } // RemoveHandler allows to remove API level event handler -func (a *evaluationAPI) RemoveHandler(eventType EventType, callback EventCallback) { +func (a *EvaluationAPI) RemoveHandler(eventType EventType, callback EventCallback) { a.eventExecutor.RemoveHandler(eventType, callback) } -func (a *evaluationAPI) Shutdown() { - // Use the context-aware shutdown with background context and ignore errors - // to maintain backward compatibility (Shutdown doesn't return an error) - _ = a.ShutdownWithContext(context.Background()) -} - -// ShutdownWithContext calls context-aware shutdown on all registered providers. +// Shutdown calls context-aware shutdown on all registered providers. // If providers implement ContextAwareStateHandler, ShutdownWithContext will be called with the provided context. // Returns an error if any provider shutdown fails or if context is cancelled during shutdown. -func (a *evaluationAPI) ShutdownWithContext(ctx context.Context) error { +func (a *EvaluationAPI) Shutdown(ctx context.Context) error { a.mu.Lock() defer a.mu.Unlock() var errs []error @@ -384,7 +399,7 @@ func (a *evaluationAPI) ShutdownWithContext(ctx context.Context) error { } // Shutdown all named providers - for name, provider := range a.namedProviders { + for name, provider := range a.domainProviders { if contextHandler, ok := provider.(ContextAwareStateHandler); ok { if err := contextHandler.ShutdownWithContext(ctx); err != nil { errs = append(errs, fmt.Errorf("named provider %q shutdown failed: %w", name, err)) @@ -404,7 +419,7 @@ func (a *evaluationAPI) ShutdownWithContext(ctx context.Context) error { // unbindAllProvidersLocked releases all provider bindings. Must be called with a.mu held (any level). // Acquires providerBindingsMu once for the entire operation to avoid repeated lock acquisitions // and to prevent panics from using unhashable FeatureProvider values as map keys. -func (a *evaluationAPI) unbindAllProvidersLocked() { +func (a *EvaluationAPI) unbindAllProvidersLocked() { providerBindingsMu.Lock() defer providerBindingsMu.Unlock() @@ -417,20 +432,20 @@ func (a *evaluationAPI) unbindAllProvidersLocked() { } deleteIfOwned(a.defaultProvider) - for _, p := range a.namedProviders { + for _, p := range a.domainProviders { deleteIfOwned(p) } } -// ForEvaluation is a helper to retrieve transaction scoped operators. -// Returns the default FeatureProvider if no provider mapping exist for the given client name. -func (a *evaluationAPI) ForEvaluation(clientName string) (FeatureProvider, []Hook, EvaluationContext) { +// resolveBinding looks up the provider, hooks, and evaluation context for the given domain. +// Returns the default FeatureProvider if no provider mapping exists for the given domain. +func (a *EvaluationAPI) resolveBinding(domain string) (FeatureProvider, []Hook, EvaluationContext) { a.mu.RLock() defer a.mu.RUnlock() var provider FeatureProvider - provider = a.namedProviders[clientName] + provider = a.domainProviders[domain] if provider == nil { provider = a.defaultProvider } @@ -438,8 +453,8 @@ func (a *evaluationAPI) ForEvaluation(clientName string) (FeatureProvider, []Hoo return provider, a.hks, a.evalCtx } -// GetProvider returns the default FeatureProvider -func (a *evaluationAPI) GetProvider() FeatureProvider { +// getProvider returns the default FeatureProvider +func (a *EvaluationAPI) getProvider() FeatureProvider { a.mu.RLock() defer a.mu.RUnlock() diff --git a/openfeature/openfeature_api_test.go b/openfeature/openfeature_api_test.go new file mode 100644 index 00000000..fa155555 --- /dev/null +++ b/openfeature/openfeature_api_test.go @@ -0,0 +1,92 @@ +package openfeature_test + +import ( + "reflect" + "slices" + "testing" + + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/isolated" +) + +// TestEvaluationAPINoUnexpectedExports ensures the [EvaluationAPI] public +// surface only exposes the intended methods. A failure here means the external +// consumer contract was broken — methods were added, removed, or renamed. +func TestEvaluationAPINoUnexpectedExports(t *testing.T) { + wantMethods := []string{ + "AddHandler", + "AddHooks", + "NewClient", + "RemoveHandler", + "SetEvaluationContext", + "SetProvider", + "SetProviderAndWait", + "Shutdown", + } + + apiType := reflect.TypeFor[*openfeature.EvaluationAPI]() + gotMethods := make([]string, apiType.NumMethod()) + for i := range apiType.NumMethod() { + gotMethods[i] = apiType.Method(i).Name + } + slices.Sort(gotMethods) + + if !slices.Equal(wantMethods, gotMethods) { + t.Errorf("EvaluationAPI public methods mismatch:\nwant: %v\ngot: %v", wantMethods, gotMethods) + } +} + +// TestEvaluationAPIBreakingPreventer calls every [EvaluationAPI] public method +// with valid arguments. This guards the external consumer contract at both +// compile time (must compile) and runtime (no panics). A signature change or +// broken behavior fails this test. +func TestEvaluationAPIBreakingPreventer(t *testing.T) { + api := isolated.NewAPI() + ctx := t.Context() + + // SetProvider — default provider (async init) + if err := api.SetProvider(ctx, openfeature.NoopProvider{}); err != nil { + t.Errorf("SetProvider: unexpected error: %v", err) + } + + // SetProviderWithDomain — named provider via WithDomain option (async init) + if err := api.SetProvider(ctx, openfeature.NoopProvider{}, openfeature.WithDomain("domain-a")); err != nil { + t.Errorf("SetProvider + WithDomain: unexpected error: %v", err) + } + + // SetProviderAndWait — default provider (sync init) + if err := api.SetProviderAndWait(ctx, openfeature.NoopProvider{}); err != nil { + t.Errorf("SetProviderAndWait: unexpected error: %v", err) + } + + // SetProviderAndWaitWithDomain — named provider via WithDomain option (sync init) + if err := api.SetProviderAndWait(ctx, openfeature.NoopProvider{}, openfeature.WithDomain("domain-b")); err != nil { + t.Errorf("SetProviderAndWait + WithDomain: unexpected error: %v", err) + } + + // NewClient — default client (no domain) + if client := api.NewClient(); client == nil { + t.Error("NewClient: expected non-nil client") + } + + // NewClientWithDomain — named client via WithDomain option + if client := api.NewClient(openfeature.WithDomain("domain-a")); client == nil { + t.Error("NewClient + WithDomain: expected non-nil client") + } + + // SetEvaluationContext — global evaluation context + api.SetEvaluationContext(openfeature.NewEvaluationContext("my-key", nil)) + + // AddHooks — append API-level hooks + api.AddHooks(openfeature.UnimplementedHook{}) + + // AddHandler / RemoveHandler — API-level eventing + handler := func(details openfeature.EventDetails) {} + api.AddHandler(openfeature.ProviderReady, &handler) + api.RemoveHandler(openfeature.ProviderReady, &handler) + + // Shutdown — clean up the isolated instance + if err := api.Shutdown(ctx); err != nil { + t.Errorf("Shutdown: unexpected error: %v", err) + } +} diff --git a/openfeature/openfeature_test.go b/openfeature/openfeature_test.go index cf5e7d03..7dbbd6ce 100644 --- a/openfeature/openfeature_test.go +++ b/openfeature/openfeature_test.go @@ -21,13 +21,13 @@ func TestRequirement_1_1_1(t *testing.T) { ofAPI := api() // set through instance level - err := ofAPI.SetProvider(mockProvider) + err := ofAPI.SetProvider(t.Context(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } // validate through global level - if api().GetProvider() != mockProvider { + if api().getProvider() != mockProvider { t.Error("func SetProvider hasn't set the provider to the singleton") } } @@ -67,7 +67,7 @@ func TestRequirement_1_1_2_2(t *testing.T) { expectChannelReceive(t, initSem, "initialization not invoked with provider registration") - if !reflect.DeepEqual(provider, api().GetProvider()) { + if !reflect.DeepEqual(provider, api().getProvider()) { t.Errorf("provider not updated to the one set") } }) @@ -86,7 +86,7 @@ func TestRequirement_1_1_2_2(t *testing.T) { expectChannelReceive(t, initSem, "initialization not invoked with provider registration") - if !reflect.DeepEqual(provider, api().GetNamedProviders()[client]) { + if !reflect.DeepEqual(provider, api().getDomainProviders()[client]) { t.Errorf("provider not updated to the one set") } }) @@ -385,7 +385,7 @@ func TestRequirement_1_1_3(t *testing.T) { t.Errorf("error setting up provider %v", err) } - namedProviders := api().GetNamedProviders() + namedProviders := api().getDomainProviders() // Validate binding @@ -403,14 +403,14 @@ func TestRequirement_1_1_3(t *testing.T) { // Validate provider retrieval by client evaluation. This uses forTransaction("clientName") - provider, _, _ := api().ForEvaluation("clientA") + provider, _, _ := api().resolveBinding("clientA") if provider.Metadata().Name != "providerA" { t.Errorf("expected %s, but got %s", "providerA", providerA.Metadata().Name) } - provider, _, _ = api().ForEvaluation("clientB") + provider, _, _ = api().resolveBinding("clientB") if provider.Metadata().Name != "providerB" { - t.Errorf("expected %s, but got %s", "providerB", providerA.Metadata().Name) + t.Errorf("expected %s, but got %s", "providerB", provider.Metadata().Name) } // Validate overriding: If the client-domain already has a bound provider, it is overwritten with the new mapping. @@ -423,16 +423,16 @@ func TestRequirement_1_1_3(t *testing.T) { t.Errorf("error setting up provider %v", err) } - namedProviders = api().GetNamedProviders() + namedProviders = api().getDomainProviders() if namedProviders["clientB"] != providerB2 { t.Errorf("named provider overriding failed") } // Validate provider retrieval by client evaluation. This uses forTransaction("clientName") - provider, _, _ = api().ForEvaluation("clientB") + provider, _, _ = api().resolveBinding("clientB") if provider.Metadata().Name != "providerB2" { - t.Errorf("expected %s, but got %s", "providerB2", providerA.Metadata().Name) + t.Errorf("expected %s, but got %s", "providerB2", provider.Metadata().Name) } } @@ -448,7 +448,7 @@ func TestRequirement_1_1_4(t *testing.T) { AddHooks(mockHook) AddHooks(mockHook, mockHook) - if len(api().GetHooks()) != 3 { + if len(api().getHooks()) != 3 { t.Error("func AddHooks didn't append the list of hooks to the existing collection of hooks") } } @@ -495,14 +495,14 @@ func TestRequirement_1_1_6(t *testing.T) { }) t.Run("client from api level - no domain", func(t *testing.T) { - client := api().GetClient() + client := api().NewClient() if client == nil { t.Errorf("expected an IClient instance, but got invalid") } }) t.Run("client from api level - with domain", func(t *testing.T) { - client := api().GetNamedClient("test-client") + client := api().NewClient(WithDomain("test-client")) if client == nil { t.Errorf("expected an IClient instance, but got invalid") } @@ -714,7 +714,7 @@ func TestDefaultClientUsage(t *testing.T) { } // Validate provider retrieval by client evaluation - provider, _, _ := api().ForEvaluation("ClientName") + provider, _, _ := api().resolveBinding("ClientName") if provider.Metadata().Name != "defaultClientReplacement" { t.Errorf("expected %s, but got %s", "defaultClientReplacement", provider.Metadata().Name)