diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..9110808c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,243 @@ +# AGENTS.md - OpenFeature Go SDK + +This document provides guidelines for agents working on this codebase. + +## Build Commands + +```bash +# Run unit tests (short mode, skips slow e2e tests) +make test +# Equivalent: go test --short -tags testtools -cover -timeout 1m ./... + +# Run end-to-end tests (includes e2e tests with race detection) +make e2e-test +# Equivalent: git submodule update --init --recursive && go test -tags testtools -race -cover -timeout 1m ./e2e/... + +# Run linter +make lint +# Equivalent: golangci-lint run ./... + +# Auto-fix linting issues +make fix +# Equivalent: golangci-lint run ./... --fix + +# Generate mocks (requires mockgen) +make mockgen + +# Generate API documentation +make docs +``` + +### Running a Single Test + +```bash +# Run specific test in current package +go test -tags testtools -run TestMyFunction ./... + +# Run test in specific file/package +go test -tags testtools -run TestMyFunction ./path/to/package/... + +# Run with verbose output +go test -tags testtools -v -run TestMyFunction ./... +``` + +## Developer Certificate of Origin (DCO) + +This project requires all commits to include a Signed-off-by line to certify adherence to the [Developer Certificate of Origin](https://developercertificate.org/). + +### Signing Off Commits + +Add the `-s` flag to your commit command: + +```bash +git commit -s -m "your commit message" +``` + +This adds a line like `Signed-off-by: Name ` to your commit message. + +### Amending to Add DCO + +If you forgot to sign off a commit: + +```bash +git commit --amend -s +``` + +### Verifying DCO Status + +Check if your commits are signed: + +```bash +git log --pretty=format:%H,%s | head -5 +``` + +Or use the DCO bot to check PRs (the bot will flag unsigned commits). + +## Conventional Commits + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for commit messages. + +### Format + +``` +[optional scope]: + +[optional body] + +[optional footer(s)] +``` + +### Types + +- **feat**: A new feature +- **fix**: A bug fix +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, etc) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing tests or correcting existing tests +- **chore**: Changes to the build process or auxiliary tools + +### Examples + +``` +feat(provider): add new flag evaluation method + +fix(api): resolve nil pointer in evaluation context + +docs: update installation instructions +``` + +## Code Style Guidelines + +### Formatting & Imports + +- Use `gofumpt` for formatting (stricter than gofmt) +- Use `gci` for import sorting (stdlib first, then external) +- Run `make fix` before committing to auto-format + +### Linting + +This project uses golangci-lint with the following linters: + +- **staticcheck**: All checks enabled (SAxxxx rules) +- **errcheck**: Error checking enabled +- **govet**: All checks except fieldalignment and shadow +- **usetesting**: Enforces testing best practices +- **nolintlint**: Requires specific linter annotations (no unused nolint directives) +- **modernize**: Suggests modern Go idioms and language features +- **copyloopvar**: Detects loop variable copying bugs +- **intrange**: Suggests using `for range` for integer loops + +Excluded rules: + +- G101 (hardcoded credentials detection - too noisy) +- G404 (weak random number generation - used in tests) + +### Error Handling + +- Use `fmt.Errorf` with `%w` for error wrapping +- Return errors with descriptive messages (lowercase, no punctuation) +- Use sentinel errors from this package when applicable +- Check errors explicitly: `if err != nil { ... }` + +### Context Usage + +- Context is the first parameter in functions that need it +- Use `context.Background()` for top-level entry points +- Pass context through call chains (don't store in structs) +- Support cancellation where operations may be slow + +### Mutex Patterns + +- Use `sync.RWMutex` for read-heavy workloads +- `RLock/RUnlock` for reads, `Lock/Unlock` for writes +- Always defer unlock after locking +- Never hold locks across function calls or goroutines + +### Documentation + +- Document all exported types and functions with // comments +- Use proper Go doc comments starting with type/function name +- Example: `// Client implements the behaviour required of an openfeature client` +- Include usage examples in example test files (\*\_example_test.go) + +### Go Examples + +- Go examples are a critical part of documentation and testing +- Place in `*_example_test.go` files next to the code they document +- Naming convention: `Example_` for method examples, `Example` for type examples +- Example function signature: `func ExampleClient_Boolean()` +- Use `// Output:` comment to specify expected output (verified by `go test`) +- Use `fmt.Println` for output to verify example behavior +- External examples use package `openfeature_test` to test public API usage +- Examples serve dual purpose: test correctness and generate godoc examples +- Run examples with: `go test -tags testtools -run Example ./...` + +### Testing + +- Use standard `testing` package with `testtools` build tag +- Use `t.Cleanup()` for cleanup operations +- Use table-driven tests where appropriate +- Place tests in `*_test.go` files next to implementation +- E2E tests are in `/e2e` directory with `testtools` tag + +### Modern Go Guidelines + +#### Standard Library Packages + +- **`slices` package**: Use modern slice operations instead of manual implementations +- **`maps` package**: Use modern map operations + +#### Modern Language Features + +- **For loop variable scoping**: Variables declared in `for` loops are created anew for each iteration +- **Ranging over integers**: Use `for i := range n` instead of `for i := 0; i < n; i++` +- **Generics**: Use type parameters for reusable, type-safe functions and types +- **Type inference**: Leverage automatic type inference with `:=` when possible + +#### Modernization Tools + +- **`go fix`**: Automatically modernize code to use current Go features + + ```bash + go fix ./... + ``` + +- **`modernize` analyzer**: Integrated in golangci-lint (enabled by default) + - Run `make fix` to apply modernize suggestions automatically + +#### Best Practices + +- **Slice initialization**: Prefer `var s []T` over `s := []T{}` or `make([]T, 0)` for nil slices + + ```go + // Good + var results []string + results = append(results, "item") + + // Avoid + results := []string{} + ``` + +- **Preallocation**: Preallocate slice capacity when size is known + + ```go + // Good + items := make([]Item, 0, len(source)) + for _, s := range source { + items = append(items, Item(s)) + } + ``` + +- **Error handling**: Use `fmt.Errorf` with `%w` for wrapping errors +- **Context**: Pass context as first parameter, support cancellation +- **Concurrency**: Use `sync/errgroup` for managing goroutines with error handling +- **Testing**: Use table-driven tests with `t.Cleanup()` for resource cleanup + +### General + +- Go version: 1.25 +- Module: `go.openfeature.dev/openfeature/v2` +- Mock generation: `make mockgen` +- Run `make test && make lint` before committing diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..36b65faf --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,683 @@ +# OpenFeature Go SDK - Migration Guide + +## v1 to v2 + +This guide helps you upgrade from OpenFeature Go SDK v1.x to v2.0. The v2 release includes significant improvements to the API design, with several **breaking changes** that require code updates. + +## Table of Contents + +1. [Summary of Changes](#summary-of-changes) +2. [Breaking Changes](#breaking-changes) +3. [Migration Steps](#migration-steps) + +--- + +## Summary of Changes + +OpenFeature Go SDK v2 introduces a modern, context-aware API design with the following key improvements: + +- **Context-aware provider lifecycle**: All provider methods now require `context.Context` parameter +- **Simplified hook interface**: Hook signatures updated with better context handling +- **Type-safe evaluation**: Improved type system using Go generics and type constraints +- **Consistent naming**: Removed deprecated methods and standardized interface names +- **Better error handling**: Improved error propagation and handling patterns +- **Removed logger dependency**: No longer depends on `go-logr`, use slog or your preferred logging + +--- + +## Breaking Changes + +### 1. **IClient Interface Refactored** + +**v1:** + +```go +type IClient interface { + Metadata() ClientMetadata + AddHooks(hooks ...Hook) + 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) + // ... other *Value methods + BooleanValueDetails(...) (BooleanEvaluationDetails, error) + StringValueDetails(...) (StringEvaluationDetails, error) + // ... other *ValueDetails methods + Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool + String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string + // ... other shorthand methods + State() State + IEventing + Tracker +} +``` + +**v2:** + +- `IClient` is now private (`iClient`) and split into composable interfaces +- Value methods that return `(value, error)` (e.g., `BooleanValue`, `StringValue`) **removed**. Use `Boolean`, `String`, etc. (non-error) or `BooleanValueDetails`, `StringValueDetails`, etc. (with metadata) instead +- `State()` method removed; use `Eventing` methods directly + +**Migration Path:** + +```go +// v1: Using *Value methods (returns value with error) +value, err := client.StringValue(ctx, "flag-key", "default", evalCtx) + +// v2: Use non-error variants - String, Boolean, Int, Float, Object +// These return only the value (with default on error) +value := client.String(ctx, "flag-key", "default", evalCtx) + +// Or use *ValueDetails for evaluation metadata +details, err := client.StringValueDetails(ctx, "flag-key", "default", evalCtx) +if err != nil { + return "default" +} +value := details.Value +``` + +--- + +### 2. **Context-Aware Provider Lifecycle** + +**v1:** + +```go +type StateHandler interface { + Init(evaluationContext EvaluationContext) error + Shutdown() +} + +type ContextAwareStateHandler interface { + StateHandler + InitWithContext(ctx context.Context, evaluationContext EvaluationContext) error + ShutdownWithContext(ctx context.Context) error +} +``` + +**v2:** + +```go +type StateHandler interface { + Init(ctx context.Context) error + Shutdown(ctx context.Context) error +} +``` + +**Migration Path - Provider Implementation:** + +```go +// v1: Old implementation +type MyProvider struct { +} + +func (p *MyProvider) Init(evalCtx EvaluationContext) error { + // Initialize without context awareness + return nil +} + +func (p *MyProvider) Shutdown() { + // Cleanup without graceful timeout +} + +// v2: Updated implementation +type MyProvider struct { +} + +func (p *MyProvider) Init(ctx context.Context) error { + evalCtx := openfeature.EvaluationContextFromContext(ctx) + // Use passed context directly, or create a new timeout context if needed + return nil// or err +} + +func (p *MyProvider) Shutdown(ctx context.Context) error { + // Implement graceful close + return nil// or err +} +``` + +--- + +### 3. **Provider Setup API Changes** + +**v1:** + +```go +// Async setup (non-blocking) +api.SetProvider(provider) +api.SetProviderWithContext(ctx, provider) +api.SetNamedProvider(domain, provider) +api.SetNamedProviderWithContext(ctx, domain, provider) + +// Sync setup (blocking) +api.SetProviderAndWait(provider) +api.SetProviderAndWaitWithContext(ctx, provider) +api.SetNamedProviderAndWait(domain, provider) +api.SetNamedProviderWithContextAndWait(ctx, domain, provider) +``` + +**v2:** + +```go +// All methods now require context and use options +api.SetProvider(ctx, provider) // async +api.SetProviderAndWait(ctx, provider) // sync +api.SetProvider(ctx, provider, openfeature.WithDomain(domain)) // async with domain +api.SetProviderAndWait(ctx, provider, openfeature.WithDomain(domain)) // sync with domain +``` + +**Migration Path:** + +```go +// v1: Async without context +api.SetProvider(provider) + +// v2: Explicit context required (use context.TODO() if no timeout needed) +api.SetProvider(context.TODO(), provider) + +// v1: Async with context +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() +api.SetProviderWithContext(ctx, provider) + +// v2: Same pattern - context is now required +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() +api.SetProvider(ctx, provider) + +// v1: Named provider (domain-specific) +api.SetNamedProvider("user-service", userProvider) +api.SetNamedProviderWithContext(ctx, "user-service", userProvider) + +// v2: Use WithDomain option +api.SetProvider(context.TODO(), userProvider, openfeature.WithDomain("user-service")) +api.SetProvider(ctx, userProvider, openfeature.WithDomain("user-service")) + +// v1: Sync operations +api.SetProviderAndWait(provider) +api.SetProviderAndWaitWithContext(ctx, provider) +api.SetNamedProviderAndWait("user-service", userProvider) +api.SetNamedProviderWithContextAndWait(ctx, "user-service", userProvider) + +// v2: Sync with context and domain option +api.SetProviderAndWait(context.TODO(), provider) +api.SetProviderAndWait(ctx, provider) +api.SetProviderAndWait(context.TODO(), userProvider, openfeature.WithDomain("user-service")) +api.SetProviderAndWait(ctx, userProvider, openfeature.WithDomain("user-service")) +``` + +--- + +### 4. **Shutdown API Changes** + +**v1:** + +```go +// Non-context aware +api.Shutdown() + +// Context-aware +err := api.ShutdownWithContext(ctx) +``` + +**v2:** + +```go +// All shutdown requires context +err := api.Shutdown(ctx) +``` + +**Migration Path:** + +```go +// v1 +api.Shutdown() + +// v2: Use context for shutdown timeout +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +err := api.Shutdown(ctx) +``` + +--- + +### 5. **Hook Interface Changes** + +**v1:** + +```go +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, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) +} +``` + +**v2:** + +```go +type Hook interface { + Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) + After(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) error + Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) + Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) +} +``` + +**Key Differences:** + +- `Before` hook now returns `(context.Context, error)` instead of `(*EvaluationContext, error)` + - EvaluationContext is now passed through context using `ContextWithEvaluationContext()` and retrieved via `EvaluationContextFromContext()` + - This allows hooks to modify both context and evaluation context transparently +- `InterfaceEvaluationDetails` renamed to `HookEvaluationDetails` in `After` and `Finally` methods +- Hooks can now influence the evaluation context by modifying the context passed to subsequent hooks + +**Migration Path:** + +```go +// v1: Hook implementation +func (h *MyHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) { + evalCtx := hookContext.EvaluationContext() + evalCtx.SetString("user_id", "123") + return &evalCtx, nil +} + + +// v2: Updated hook implementation +func (h *MyHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) { + evalCtx := hookContext.EvaluationContext() + evalCtx.SetString("user_id", "123") + + // Attach evaluation context to the context for downstream use + return ContextWithEvaluationContext(ctx, evalCtx), nil +} + +``` + +--- + +### 6. **Type Naming Changes** + +**v1:** + +```go +type InterfaceEvaluationDetails = GenericEvaluationDetails[any] +type InterfaceResolutionDetail = GenericResolutionDetail[any] +``` + +**v2:** + +```go +type ObjectEvaluationDetails = EvaluationDetails[any] +type ObjectResolutionDetail = GenericResolutionDetail[any] +``` + +**Migration Path:** + +```go +// v1 +var details openfeature.InterfaceEvaluationDetails + +// v2 +var details openfeature.ObjectEvaluationDetails +``` + +--- + +### 7. **Deprecated Methods Removed** + +**Removed Methods:** + +1. `ClientMetadata.Name()` - Use `ClientMetadata.Domain()` instead +2. `Client.WithLogger()` - Use hooks or external logging instead (e.g., slog) +3. All `*Value()` methods - Use `*ValueDetails()` instead +4. `API.SetLogger()` - Use hooks or external logging instead + +**Migration Path:** + +```go +// v1: Deprecated +metadata.Name() + +// v2: Use Domain() +metadata.Domain() + +// v1: Logger on client +client.WithLogger(logger) + +// v2: Use LoggingHook from openfeature/hooks package +import "go.openfeature.dev/openfeature/v2/hooks" + +loggingHook := hooks.NewLoggingHook() // or implement your own +openfeature.AddHooks(loggingHook) +``` + +--- + +### 8. **Package Restructuring** + +**v1:** + +``` +openfeature/ + memprovider/ # In-memory provider + multi/ # Multi-provider strategies +``` + +**v2:** + +``` +openfeature/ + providers/ + inmemory/ # Renamed from memprovider + multi/ # Moved under providers +``` + +**Migration Path:** + +```go +// v1 +import "github.com/open-feature/go-sdk/openfeature/memprovider" +provider := memprovider.NewInMemoryProvider(flags) + +// v2 +import "go.openfeature.dev/openfeature/v2/providers/inmemory" +provider := inmemory.NewProvider(flags) +``` + +--- + +### 9. **EventCallback Type Change** + +**v1:** + +```go +type EventCallback *func(details EventDetails) +``` + +**v2:** + +```go +type EventCallback func(details EventDetails) +``` + +**Migration Path:** + +```go +// v1: Pointer to function +callback := &func(details EventDetails) { + // Handle event +} +api.OnProviderReady(callback) + +// v2: Direct function +api.OnProviderReady(func(details EventDetails) { + // Handle event +}) +``` + +--- + +--- + +### 10. **Interface Visibility Changes** + +**v1:** + +```go +type IClient interface { ... } // Public +type IEvaluation interface { ... } // Public +``` + +**v2:** + +```go +type iClient interface { ... } // Private (internal) +type Evaluator interface { ... } // Public, focused interface +type DetailEvaluator interface { ...} // Public, focused interface +``` + +Internal interfaces are now private. Use the provided public interfaces instead. + +--- + +### 11. **Client Creation and Metadata API** + +**v1:** + +```go +// Client creation with positional domain argument +client := openfeature.NewClient("domain-name") +client := openfeature.NewDefaultClient() // Default domain client + +// Named provider metadata +metadata := api.NamedProviderMetadata("domain-name") +``` + +**v2:** + +```go +// Client creation using WithDomain option +client := openfeature.NewClient(openfeature.WithDomain("domain-name")) +client := openfeature.NewClient() // Default domain client (no argument needed) + +// Unified provider metadata +metadata := api.ProviderMetadata(openfeature.WithDomain("domain-name")) +``` + +**Key Differences:** + +- `NewClient(domainName)` → `NewClient(openfeature.WithDomain(domainName))` - domain now passed as option +- `NewDefaultClient()` → `NewClient()` - simplified to single method +- `NamedProviderMetadata(domain)` → `ProviderMetadata(openfeature.WithDomain(domain))` - unified with default metadata API +- Domain is now specified as a `CallOption` rather than a positional string argument + +**Migration Path:** + +```go +// v1: Domain as positional argument +client := openfeature.NewClient("user-service") +defaultClient := openfeature.NewDefaultClient() +metadata := api.NamedProviderMetadata("user-service") + +// v2: Domain as option +client := openfeature.NewClient(openfeature.WithDomain("user-service")) +defaultClient := openfeature.NewClient() +metadata := api.ProviderMetadata(openfeature.WithDomain("user-service")) +``` + +--- + +## Migration Steps + +### Automated Migration with gopatch (Optional) + +To help automate the migration, you can use the [uber-go/gopatch](https://github.com/uber-go/gopatch) tool to apply common transformations across your codebase. + +#### Install gopatch + +```sh +go install github.com/uber-go/gopatch@latest +``` + +#### Apply the patch file + +The `openfeature_v1_to_v2.patch` file in this repository contains transformations for the most common breaking changes. Apply it to your codebase: + +```sh +gopatch -p ./openfeature_v1_to_v2.patch ./... +``` + +#### What the patch covers + +The patch file automatically handles: + +- **Evaluation methods**: Converts `StringValue()`, `BooleanValue()`, `IntValue()`, `FloatValue()`, and `ObjectValue()` to their non-error counterparts `String()`, `Boolean()`, `Int()`, `Float()`, and `Object()` +- **Provider setup**: Updates all provider setup calls to require explicit context: + - `SetProvider(provider)` → `SetProvider(ctx, provider)` + - `SetProviderAndWait(provider)` → `SetProviderAndWait(ctx, provider)` + - `SetNamedProvider()` and related methods +- **Shutdown calls**: Migrates `Shutdown()` and `ShutdownWithContext()` to context-aware `Shutdown(ctx)` +- **Package imports**: Updates provider imports from `memprovider` to `providers/inmemory` +- **Type names**: Renames `InterfaceEvaluationDetails` to `ObjectEvaluationDetails` + +#### After running gopatch + +After running the patch, review the changes and manually update: + +- **Hook implementations** - The patch cannot fully automate hook migrations. See Step 2 below for manual updates needed to `Before` method signatures. +- **Custom provider implementations** - Update `StateHandler` implementations as shown in Step 1 below. +- **Error handling** - Review error handling logic for `*Value()` methods which may need adjustment. +- Do `go mod tidy` and install missing dependencies + +--- + +### Step 0: Install + +```sh +go get "go.openfeature.dev/openfeature/v2" + +``` + +### Step 1: Update Provider Implementation + +If you have custom providers, update the `StateHandler` implementation: + +```go +// OLD +func (p *MyProvider) Init(evalCtx EvaluationContext) error { + // ... +} + +func (p *MyProvider) Shutdown() { + // ... +} + +// NEW +func (p *MyProvider) Init(ctx context.Context) error { + // ... + return nil +} + +func (p *MyProvider) Shutdown(ctx context.Context) error { + // ... + return nil +} +``` + +### Step 2: Update Hook Implementations + +If you have custom hooks, update the `Before` method signature: + +```go +// OLD +func (h *MyHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) { + evalCtx := hookContext.EvaluationContext() + return &evalCtx, nil +} + +// NEW +func (h *MyHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) { + evalCtx := hookContext.EvaluationContext() + // Attach evaluation context to context for downstream use + return ContextWithEvaluationContext(ctx, evalCtx), nil +} +``` + +Also rename `InterfaceEvaluationDetails` to `HookEvaluationDetails` in your `After` and `Finally` hook methods, and use `EvaluationContextFromContext()` to retrieve the evaluation context from the context if needed. + +### Step 3: Update Provider Setup Calls + +All provider setup now requires explicit `context.Context`: + +```go +// OLD +api.SetProvider(myProvider) +api.SetProviderAndWait(myProvider) +api.SetNamedProvider("my-client", myProvider, true) + +// NEW +api.SetProvider(context.Background(), myProvider) +api.SetProviderAndWait(context.Background(), myProvider) +api.SetProvider(context.Background(), myProvider, openfeature.WithDomain("my-client")) +``` + +### Step 4: Update Shutdown Calls + +```go +// OLD +api.Shutdown() + +// NEW +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +api.Shutdown(ctx) +``` + +### Step 5: Update Evaluation Calls + +Replace usage of `*Value` methods with non-error variants (`Boolean`, `String`, `Int`, `Float`, `Object`) for simple cases, or use `*ValueDetails` for detailed evaluation metadata: + +```go +// OLD +value, err := client.BooleanValue(ctx, "flag", false, evalCtx) +if err != nil { + // handle error +} +// use value + +// NEW: Simple case - use non-error variant +value := client.Boolean(ctx, "flag", false, evalCtx) +// use value + +// NEW: Need evaluation metadata - use *ValueDetails +details, err := client.BooleanValueDetails(ctx, "flag", false, evalCtx) +if err != nil { + // handle error +} +// use details.Value, details.Reason, details.Variant, etc. +``` + +### Step 6: Update Imports + +```go +// OLD +import "github.com/open-feature/go-sdk/openfeature/memprovider" + +// NEW +import "go.openfeature.dev/openfeature/v2/providers/inmemory" +``` + +### Step 7: Remove Logger Configuration + +```go +// OLD +api.SetLogger(myLogger) +client.WithLogger(myLogger) + +// NEW - Use hooks instead +import "go.openfeature.dev/openfeature/v2/hooks" +loggingHook := hooks.NewLoggingHook() +api.AddHooks(loggingHook) +``` + +### Step 8: Update new client and set provider calls + +```go +// OLD +api.SetNamedProvider(ctx, "user-service", userProvider) +api.SetNamedProviderAndWait(ctx, "billing-service", billingProvider) +userClient := openfeature.NewClient("user-service") +billingClient := openfeature.NewClient("billing-service") +defaultClient := openfeature.NewDefaultClient() +metadata := api.NamedProviderMetadata("user-service") + +// NEW +api.SetProvider(ctx, userProvider, openfeature.WithDomain("user-service")) +api.SetProviderAndWait(ctx, billingProvider, openfeature.WithDomain("billing-service")) +userClient := openfeature.NewClient(openfeature.WithDomain("user-service")) +billingClient := openfeature.NewClient(openfeature.WithDomain("billing-service")) +defaultClient := openfeature.NewClient() +metadata := api.ProviderMetadata(openfeature.WithDomain("user-service")) +``` diff --git a/Makefile b/Makefile index c1a9dcf3..9c1756f6 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,7 @@ MOCKGEN_VERSION:=v0.6.0 .PHONY: mockgen mockgen: go install go.uber.org/mock/mockgen@${MOCKGEN_VERSION} - mockgen -source=openfeature/provider.go -destination=openfeature/provider_mock.go -package=openfeature -build_constraint=testtools - mockgen -source=openfeature/hooks.go -destination=openfeature/hooks_mock.go -package=openfeature -build_constraint=testtools - mockgen -source=openfeature/interfaces.go -destination=openfeature/interfaces_mock.go -package=openfeature -build_constraint=testtools + mockgen -destination=interfaces_mock.go -package=openfeature -build_constraint=testtools -mock_names=FeatureProvider=MockProvider go.openfeature.dev/openfeature/v2 clientEvent,evaluationImpl,Hook,FeatureProvider,StateHandler,Tracker .PHONY: test test: @@ -26,4 +24,4 @@ fix: .PHONY: docs docs: - go run golang.org/x/pkgsite/cmd/pkgsite@latest -open . + go doc -http diff --git a/README.md b/README.md index 2f2a3920..2bb18b41 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,12 @@ [OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool. + ## 🚀 Quick start ### Requirements -Go language version: [1.24](https://go.dev/doc/devel/release#go1.24.0) +Go language version: [1.25](https://go.dev/doc/devel/release#go1.25.0) > [!NOTE] > The OpenFeature Go SDK only supports currently maintained Go language versions. @@ -51,7 +52,7 @@ Go language version: [1.24](https://go.dev/doc/devel/release#go1.24.0) ### Install ```shell -go get github.com/open-feature/go-sdk +go get go.openfeature.dev/openfeature/v2 ``` ### Usage @@ -62,14 +63,14 @@ package main import ( "fmt" "context" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) func main() { // Register your feature flag provider - openfeature.SetProviderAndWait(openfeature.NoopProvider{}) + openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}) // Create a new client - client := openfeature.NewClient("app") + client := openfeature.NewClient() // Evaluate your feature flag v2Enabled := client.Boolean( context.TODO(), "v2_enabled", true, openfeature.EvaluationContext{}, @@ -81,26 +82,26 @@ func main() { } ``` -Try this example in the [Go Playground](https://go.dev/play/p/fSSK8s42hA2). +Try this example in the [Go Playground](https://go.dev/play/p/BUrOs3VWNHu). ### API Reference -See [here](https://pkg.go.dev/github.com/open-feature/go-sdk/openfeature) for the complete API documentation. +See [here](https://pkg.go.dev/go.openfeature.dev/openfeature/v2) for the complete API documentation. ## 🌟 Features | Status | Features | Description | -| ------ |---------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | -| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | -| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | -| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | -| ✅ | [Logging](#logging) | Integrate with popular logging packages. | -| ✅ | [Domains](#domains) | Logically bind clients with providers. | -| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | -| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | -| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) | -| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | +| ------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. | +| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). | +| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. | +| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. | +| ✅ | [Logging](#logging) | Integrate with popular logging packages. | +| ✅ | [Domains](#domains) | Logically bind clients with providers. | +| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. | +| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. | +| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread) | +| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. | Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌ @@ -113,7 +114,7 @@ If the provider you're looking for hasn't been created yet, see the [develop a p Once you've added a provider as a dependency, it can be registered with OpenFeature like this: ```go -openfeature.SetProviderAndWait(MyProvider{}) +openfeature.SetProviderAndWait(context.TODO(), MyProvider{}) ``` In some situations, it may be beneficial to register multiple providers in the same application. @@ -135,7 +136,7 @@ openfeature.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( )) // set a value to the client context -client := openfeature.NewClient("my-app") +client := openfeature.NewClient() client.SetEvaluationContext(openfeature.NewTargetlessEvaluationContext( map[string]any{ "version": "1.4.6", @@ -149,10 +150,9 @@ evalCtx := openfeature.NewEvaluationContext( "company": "Initech", }, ) -boolValue, err := client.BooleanValue("boolFlag", false, evalCtx) +boolValue := client.Boolean(context.TODO(), "boolFlag", false, evalCtx) ``` - ### Hooks [Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle @@ -166,11 +166,11 @@ Once you've added a hook as a dependency, it can be registered at the global, cl openfeature.AddHooks(ExampleGlobalHook{}) // add a hook on this client, to run on all evaluations made by this client -client := openfeature.NewClient("my-app") +client := openfeature.NewClient() client.AddHooks(ExampleClientHook{}) // add a hook for this evaluation only -value, err := client.BooleanValue( +value := client.Boolean( context.TODO(), "boolFlag", false, openfeature.EvaluationContext{}, WithHooks(ExampleInvocationHook{}), ) ``` @@ -183,7 +183,7 @@ For example, a flag enhancing the appearance of a UI component might drive user ```go // initialize a client -client := openfeature.NewClient('my-app') +client := openfeature.NewClient() // trigger tracking event action client.Track( @@ -215,14 +215,15 @@ import ( "log/slog" "os" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/hooks" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + "go.openfeature.dev/openfeature/v2/hooks" + "go.openfeature.dev/openfeature/v2/providers/inmemory" ) func main() { + ctx := context.TODO() // Register an in-memory provider with no flags - openfeature.SetNamedProviderAndWait("example", memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{})) + openfeature.SetProviderAndWait(ctx, inmemory.NewProvider(map[string]inmemory.InMemoryFlag{}), openfeature.WithDomain("example")) // Configure slog handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}) @@ -233,10 +234,10 @@ func main() { openfeature.AddHooks(loggingHook) // Create a new client - client := openfeature.NewClient("example") + client := openfeature.NewClient(openfeature.WithDomain("example")) // Attempt to evaluate a flag that doesn't exist - _ = client.Boolean(context.TODO(), "not-exist", true, openfeature.EvaluationContext{}) + _ = client.Boolean(ctx, "not-exist", true, openfeature.EvaluationContext{}) } ``` @@ -254,17 +255,17 @@ See [hooks](#hooks) for more information on configuring hooks. Clients can be assigned to a domain. A domain is a logical identifier that can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used. ```go -import "github.com/open-feature/go-sdk/openfeature" +import "go.openfeature.dev/openfeature/v2" // Registering the default provider -openfeature.SetProviderAndWait(NewLocalProvider()) +openfeature.SetProviderAndWait(ctx, NewLocalProvider()) // Registering a named provider -openfeature.SetNamedProvider("clientForCache", NewCachedProvider()) +openfeature.SetProvider(ctx, NewCachedProvider(), openfeature.WithDomain("clientForCache")) // A Client backed by default provider -clientWithDefault := openfeature.NewDefaultClient() +clientWithDefault := openfeature.NewClient() // A Client backed by NewCachedProvider -clientForCache := openfeature.NewClient("clientForCache") +clientForCache := openfeature.NewClient(openfeature.WithDomain("clientForCache")) ``` ### Eventing @@ -276,7 +277,7 @@ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGE Please refer to the documentation of the provider you're using to see what events are supported. ```go -import "github.com/open-feature/go-sdk/openfeature" +import "go.openfeature.dev/openfeature/v2" ... var readyHandlerCallback = func(details openfeature.EventDetails) { @@ -284,7 +285,7 @@ var readyHandlerCallback = func(details openfeature.EventDetails) { } // Global event handler -openfeature.AddHandler(openfeature.ProviderReady, &readyHandlerCallback) +openfeature.AddHandler(openfeature.ProviderReady, readyHandlerCallback) ... @@ -292,10 +293,10 @@ var providerErrorCallback = func(details openfeature.EventDetails) { // callback implementation } -client := openfeature.NewDefaultClient() +client := openfeature.NewClient() // Client event handler -client.AddHandler(openfeature.ProviderError, &providerErrorCallback) +client.AddHandler(openfeature.ProviderError, providerErrorCallback) ``` ### Shutdown @@ -304,38 +305,37 @@ The OpenFeature API provides a close function to perform a cleanup of all regist This should only be called when your application is in the process of shutting down. ```go -import "github.com/open-feature/go-sdk/openfeature" +import "go.openfeature.dev/openfeature/v2" -openfeature.Shutdown() +err := openfeature.Shutdown(ctx) ``` - ### Transaction Context Propagation Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP). Transaction context can be set where specific data is available (e.g. an auth service or request handler), and by using the transaction context propagator, it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread). ```go -import "github.com/open-feature/go-sdk/openfeature" +import "go.openfeature.dev/openfeature/v2" // set the TransactionContext -ctx := openfeature.WithTransactionContext(context.TODO(), openfeature.EvaluationContext{}) +ctx := openfeature.ContextWithEvaluationContext(context.TODO(), openfeature.EvaluationContext{}) // get the TransactionContext from a context -ec := openfeature.TransactionContext(ctx) +ec := openfeature.EvaluationContextFromContext(ctx) // merge an EvaluationContext with the existing TransactionContext, preferring // the context that is passed to MergeTransactionContext tCtx := openfeature.MergeTransactionContext(ctx, openfeature.EvaluationContext{}) // use TransactionContext in a flag evaluation -client.BooleanValue(tCtx, ....) +client.Boolean(tCtx, ....) ``` ### Multi-Provider Implementation Included with this SDK is an _experimental_ multi-provider that can be used to query multiple feature flag providers simultaneously. -More information can be found in the [multi package's README](openfeature/multi/README.md). +More information can be found in the [multi package's README](providers/multi/README.md). ## Extending @@ -350,7 +350,7 @@ package myfeatureprovider import ( "context" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) // MyFeatureProvider implements the FeatureProvider interface and provides functions for evaluating flags @@ -391,7 +391,7 @@ func (i MyFeatureProvider) IntEvaluation(ctx context.Context, flag string, defau } // ObjectEvaluation returns an object flag -func (i MyFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { +func (i MyFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.ObjectResolutionDetail { // code to evaluate object } @@ -399,12 +399,13 @@ func (i MyFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, de // Providers can opt-in for initialization & shutdown behavior by implementing this interface // Init holds initialization logic of the provider -func (i MyFeatureProvider) Init(evaluationContext openfeature.EvaluationContext) error { +func (i MyFeatureProvider) Init(ctx context.Context) error { // code to initialize your provider + // evaluationContext := openfeature.EvaluationContextFromContext(ctx) } // Shutdown define the shutdown operation of the provider -func (i MyFeatureProvider) Shutdown() { +func (i MyFeatureProvider) Shutdown(ctx context.Context) error { // code to shutdown your provider } @@ -430,7 +431,7 @@ To avoid defining empty functions make use of the `UnimplementedHook` struct (wh ```go import ( "context" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) type MyHook struct { @@ -447,81 +448,19 @@ func (h MyHook) Error(context context.Context, hookContext openfeature.HookConte ## Testing -The SDK provides a `NewTestProvider` which allows you to set flags for the scope of a test. -The `TestProvider` is thread-safe and can be used in tests that run in parallel. - -Call `testProvider.UsingFlags(t, tt.flags)` to set flags for a test, and clean them up with `testProvider.Cleanup()` - -```go -import ( - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/testing" -) - -testProvider := NewTestProvider() -err := openfeature.SetProviderAndWait(testProvider) -if err != nil { - t.Errorf("unable to set provider") -} - -// configure flags for this test suite -tests := map[string]struct { - flags map[string]memprovider.InMemoryFlag - want bool -}{ - "test when flag is true": { - flags: map[string]memprovider.InMemoryFlag{ - "my_flag": { - State: memprovider.Enabled, - DefaultVariant: "on", - Variants: map[string]any{ - "on": true, - }, - }, - }, - want: true, - }, - "test when flag is false": { - flags: map[string]memprovider.InMemoryFlag{ - "my_flag": { - State: memprovider.Enabled, - DefaultVariant: "off", - Variants: map[string]any{ - "off": false, - }, - }, - }, - want: false, - }, -} - -for name, tt := range tests { - tt := tt - name := name - t.Run(name, func(t *testing.T) { - - // be sure to clean up your flags - defer testProvider.Cleanup() - testProvider.UsingFlags(t, tt.flags) - - // your code under test - got := functionUnderTest() - - if got != tt.want { - t.Fatalf("uh oh, value is not as expected: got %v, want %v", got, tt.want) - } - }) -} -``` +The SDK provides a `testing.NewProvider` which allows you to set flags for the scope of a test. +The `TestProvider` has proper isolation and can be used in tests that run in parallel. +More information can be found in the [testing package's README](providers/testing/README.md). ### Mocks -Mocks are also available for testing purposes for all interfaces within the OpenFeature SDK. These are primarily +Mocks are also available for testing purposes for some public interfaces within the OpenFeature SDK. These are primarily intended for internal use for testing the SDK, but have been exported to ease the testing burden for any extensions -or custom components (e.g. hooks & providers). These mocks are not include in builds by default. The build tag +or custom components (e.g. hooks & providers). These mocks are not include in builds by default. The build tag `testtools` must be used to have the mocks included in builds. + ## ⭐️ Support the project - Give this repo a ⭐️! @@ -542,4 +481,5 @@ Interested in contributing? Great, we'd love your help! To get started, take a l Made with [contrib.rocks](https://contrib.rocks). + diff --git a/openfeature/client.go b/client.go similarity index 63% rename from openfeature/client.go rename to client.go index f0ead4d6..94436ab3 100644 --- a/openfeature/client.go +++ b/client.go @@ -2,13 +2,10 @@ package openfeature import ( "context" - "errors" "fmt" "slices" "sync" "unicode/utf8" - - "github.com/go-logr/logr" ) // ClientMetadata provides a client's metadata @@ -24,13 +21,6 @@ func NewClientMetadata(domain string) ClientMetadata { } } -// Name returns the client's domain name -// -// Deprecated: Name() exists for historical compatibility, use [ClientMetadata.Domain] instead. -func (cm ClientMetadata) Name() string { - return cm.domain -} - // Domain returns the client's domain func (cm ClientMetadata) Domain() string { return cm.domain @@ -49,12 +39,15 @@ type Client struct { } // interface guard to ensure that Client implements IClient -var _ IClient = (*Client)(nil) +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. -func NewClient(domain string) *Client { - return newClient(domain, api, eventing) +// NewClient returns a new [Client]. +// By default it returns the client for the default domain. The default domain [Client] is the [IClient] instance that +// wraps around an unnamed [FeatureProvider]. +// To get the domain specific client use [WithDomain] option with a unique identifier for this client. +func NewClient(opts ...CallOption) *Client { + c := newCallOption(opts...) + return newClient(c.domain, api, eventing) } func newClient(domain string, apiRef evaluationImpl, eventRef clientEvent) *Client { @@ -73,15 +66,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() @@ -143,29 +127,27 @@ var typeToString = map[Type]string{ Object: "object", } -type EvaluationDetails struct { +type EvaluationDetails[T FlagTypes] struct { FlagKey string FlagType Type ResolutionDetail -} - -// GenericEvaluationDetails represents the result of the flag evaluation process. -type GenericEvaluationDetails[T any] struct { Value T - EvaluationDetails } type ( // BooleanEvaluationDetails represents the result of the flag evaluation process for boolean flags. - BooleanEvaluationDetails = GenericEvaluationDetails[bool] + BooleanEvaluationDetails = EvaluationDetails[bool] // StringEvaluationDetails represents the result of the flag evaluation process for string flags. - StringEvaluationDetails = GenericEvaluationDetails[string] + StringEvaluationDetails = EvaluationDetails[string] // FloatEvaluationDetails represents the result of the flag evaluation process for float64 flags. - FloatEvaluationDetails = GenericEvaluationDetails[float64] + FloatEvaluationDetails = EvaluationDetails[float64] // IntEvaluationDetails represents the result of the flag evaluation process for int64 flags. - IntEvaluationDetails = GenericEvaluationDetails[int64] - // InterfaceEvaluationDetails represents the result of the flag evaluation process for Object flags. - InterfaceEvaluationDetails = GenericEvaluationDetails[any] + IntEvaluationDetails = EvaluationDetails[int64] + // ObjectEvaluationDetails represents the result of the flag evaluation process for Object flags. + ObjectEvaluationDetails = EvaluationDetails[any] + + // HookEvaluationDetails represents the result for hooks usage. + HookEvaluationDetails = EvaluationDetails[FlagTypes] ) type ResolutionDetail struct { @@ -284,91 +266,6 @@ func WithHookHints(hookHints HookHints) Option { } } -// 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 any, evalCtx EvaluationContext, options ...Option) (any, 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: @@ -378,39 +275,16 @@ func (c *Client) ObjectValue(ctx context.Context, flag string, defaultValue any, // - 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") - boolEvalDetails := BooleanEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - boolEvalDetails.ErrorCode = TypeMismatchCode - boolEvalDetails.ErrorMessage = err.Error() - - return boolEvalDetails, err - } + c.mx.RLock() + evalDetails, err := evaluate(ctx, c, flag, Boolean, defaultValue, evalCtx, *evalOptions) + c.mx.RUnlock() - return BooleanEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil + return evalDetails, err } // StringValueDetails performs a flag evaluation that returns an evaluation details struct. @@ -422,39 +296,16 @@ func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultVa // - 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") - strEvalDetails := StringEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - strEvalDetails.ErrorCode = TypeMismatchCode - strEvalDetails.ErrorMessage = err.Error() - - return strEvalDetails, err - } + c.mx.RLock() + evalDetails, err := evaluate(ctx, c, flag, String, defaultValue, evalCtx, *evalOptions) + c.mx.RUnlock() - return StringEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil + return evalDetails, err } // FloatValueDetails performs a flag evaluation that returns an evaluation details struct. @@ -466,39 +317,16 @@ func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultVal // - 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") - floatEvalDetails := FloatEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - floatEvalDetails.ErrorCode = TypeMismatchCode - floatEvalDetails.ErrorMessage = err.Error() - - return floatEvalDetails, err - } + c.mx.RLock() + evalDetails, err := evaluate(ctx, c, flag, Float, defaultValue, evalCtx, *evalOptions) + c.mx.RUnlock() - return FloatEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil + return evalDetails, err } // IntValueDetails performs a flag evaluation that returns an evaluation details struct. @@ -510,39 +338,16 @@ func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValu // - 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") - intEvalDetails := IntEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: evalDetails.EvaluationDetails, - } - intEvalDetails.ErrorCode = TypeMismatchCode - intEvalDetails.ErrorMessage = err.Error() - - return intEvalDetails, err - } + c.mx.RLock() + evalDetails, err := evaluate(ctx, c, flag, Int, defaultValue, evalCtx, *evalOptions) + c.mx.RUnlock() - return IntEvaluationDetails{ - Value: value, - EvaluationDetails: evalDetails.EvaluationDetails, - }, nil + return evalDetails, err } // ObjectValueDetails performs a flag evaluation that returns an evaluation details struct. @@ -553,21 +358,21 @@ func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue // - 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 any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) { - c.mx.RLock() - defer c.mx.RUnlock() - +func (c *Client) ObjectValueDetails(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (ObjectEvaluationDetails, error) { evalOptions := &EvaluationOptions{} for _, option := range options { option(evalOptions) } - return c.evaluate(ctx, flag, Object, defaultValue, evalCtx, *evalOptions) + c.mx.RLock() + evalDetails, err := evaluate(ctx, c, flag, Object, defaultValue, evalCtx, *evalOptions) + c.mx.RUnlock() + return evalDetails, err } // Boolean performs a flag evaluation that returns a boolean. Any error // encountered during the evaluation will result in the default value being -// returned. To explicitly handle errors, use [Client.BooleanValue] or [Client.BooleanValueDetails] +// returned. To explicitly handle errors, use [Client.BooleanValueDetails] // // Parameters: // - ctx is the standard go context struct used to manage requests (e.g. timeouts) @@ -576,14 +381,16 @@ func (c *Client) ObjectValueDetails(ctx context.Context, flag string, defaultVal // - 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) Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool { - value, _ := c.BooleanValue(ctx, flag, defaultValue, evalCtx, options...) - - return value + value, err := c.BooleanValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue + } + return value.Value } // String performs a flag evaluation that returns a string. Any error // encountered during the evaluation will result in the default value being -// returned. To explicitly handle errors, use [Client.StringValue] or [Client.StringValueDetails] +// returned. To explicitly handle errors, use [Client.StringValueDetails] // // Parameters: // - ctx is the standard go context struct used to manage requests (e.g. timeouts) @@ -592,14 +399,16 @@ func (c *Client) Boolean(ctx context.Context, flag string, defaultValue bool, ev // - 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) String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string { - value, _ := c.StringValue(ctx, flag, defaultValue, evalCtx, options...) - - return value + value, err := c.StringValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue + } + return value.Value } // Float performs a flag evaluation that returns a float64. Any error // encountered during the evaluation will result in the default value being -// returned. To explicitly handle errors, use [Client.FloatValue] or [Client.FloatValueDetails] +// returned. To explicitly handle errors, use [Client.FloatValueDetails] // // Parameters: // - ctx is the standard go context struct used to manage requests (e.g. timeouts) @@ -608,14 +417,16 @@ func (c *Client) String(ctx context.Context, flag string, defaultValue string, e // - 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) Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 { - value, _ := c.FloatValue(ctx, flag, defaultValue, evalCtx, options...) - - return value + value, err := c.FloatValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue + } + return value.Value } // Int performs a flag evaluation that returns an int64. Any error // encountered during the evaluation will result in the default value being -// returned. To explicitly handle errors, use [Client.IntValue] or [Client.IntValueDetails] +// returned. To explicitly handle errors, use [Client.IntValueDetails] // // Parameters: // - ctx is the standard go context struct used to manage requests (e.g. timeouts) @@ -624,14 +435,16 @@ func (c *Client) Float(ctx context.Context, flag string, defaultValue float64, e // - 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) Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 { - value, _ := c.IntValue(ctx, flag, defaultValue, evalCtx, options...) - - return value + value, err := c.IntValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue + } + return value.Value } // Object performs a flag evaluation that returns an object. Any error // encountered during the evaluation will result in the default value being -// returned. To explicitly handle errors, use [Client.ObjectValue] or [Client.ObjectValueDetails] +// returned. To explicitly handle errors, use [Client.ObjectValueDetails] // // Parameters: // - ctx is the standard go context struct used to manage requests (e.g. timeouts) @@ -640,9 +453,11 @@ func (c *Client) Int(ctx context.Context, flag string, defaultValue int64, evalC // - 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) Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any { - value, _ := c.ObjectValue(ctx, flag, defaultValue, evalCtx, options...) - - return value + value, err := c.ObjectValueDetails(ctx, flag, defaultValue, evalCtx, options...) + if err != nil { + return defaultValue + } + return value.Value } // Track performs an action for tracking for occurrence of a particular action or application state. @@ -666,7 +481,7 @@ func (c *Client) Track(ctx context.Context, trackingEventName string, evalCtx Ev // - invocation (highest precedence) func (c *Client) forTracking(ctx context.Context, evalCtx EvaluationContext) (Tracker, EvaluationContext) { provider, _, globalEvalCtx := c.api.ForEvaluation(c.metadata.domain) - evalCtx = mergeContexts(evalCtx, c.evaluationContext, TransactionContext(ctx), globalEvalCtx) + evalCtx = mergeContexts(evalCtx, c.evaluationContext, EvaluationContextFromContext(ctx), globalEvalCtx) trackingProvider, ok := provider.(Tracker) if !ok { trackingProvider = NoopProvider{} @@ -674,15 +489,13 @@ func (c *Client) forTracking(ctx context.Context, evalCtx EvaluationContext) (Tr return trackingProvider, evalCtx } -func (c *Client) evaluate( - ctx context.Context, flag string, flagType Type, defaultValue any, evalCtx EvaluationContext, options EvaluationOptions, -) (InterfaceEvaluationDetails, error) { - evalDetails := InterfaceEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: EvaluationDetails{ - FlagKey: flag, - FlagType: flagType, - }, +func evaluate[T FlagTypes]( + ctx context.Context, c *Client, flag string, flagType Type, defaultValue T, evalCtx EvaluationContext, options EvaluationOptions, +) (EvaluationDetails[T], error) { + evalDetails := EvaluationDetails[T]{ + Value: defaultValue, + FlagKey: flag, + FlagType: flagType, } if !utf8.Valid([]byte(flag)) { @@ -692,8 +505,8 @@ 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) - 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 + evalCtx = mergeContexts(evalCtx, c.evaluationContext, EvaluationContextFromContext(ctx), globalEvalCtx) // API (global) -> transaction -> client -> invocation + hooks := slices.Concat(globalHooks, c.hooks, options.hooks, provider.Hooks()) // API, Client, Invocation, Provider var err error hookCtx := HookContext{ @@ -706,116 +519,124 @@ func (c *Client) evaluate( } defer func() { - c.finallyHooks(ctx, hookCtx, hooks, evalDetails, options) + finallyHooks(ctx, hookCtx, hooks, evalDetails, options) }() + ctx, evalCtx, err = beforeHooks(ctx, hookCtx, hooks, evalCtx, options) + hookCtx.evaluationContext = evalCtx + if err != nil { + err = fmt.Errorf("before hook: %w", err) + errorHooks(ctx, hookCtx, hooks, err, options) + return evalDetails, err + } + // bypass short-circuit logic for the Noop provider; it is essentially stateless and a "special case" if _, ok := provider.(NoopProvider); !ok { // short circuit if provider is in NOT READY state if c.State() == NotReadyState { - c.errorHooks(ctx, hookCtx, hooks, ProviderNotReadyError, options) - return evalDetails, ProviderNotReadyError + errorHooks(ctx, hookCtx, hooks, ErrProviderNotReady, options) + return evalDetails, ErrProviderNotReady } // short circuit if provider is in FATAL state if c.State() == FatalState { - c.errorHooks(ctx, hookCtx, hooks, ProviderFatalError, options) - return evalDetails, ProviderFatalError + errorHooks(ctx, hookCtx, hooks, ErrProviderFatal, options) + return evalDetails, ErrProviderFatal } } - evalCtx, err = c.beforeHooks(ctx, hookCtx, hooks, evalCtx, options) - hookCtx.evaluationContext = evalCtx - if err != nil { - err = fmt.Errorf("before hook: %w", err) - c.errorHooks(ctx, hookCtx, hooks, 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) + flatCtx := evalCtx.Flattened() + var resolution ObjectResolutionDetail + switch defValue := any(defaultValue).(type) { + case bool: res := provider.BooleanEvaluation(ctx, flag, defValue, flatCtx) resolution.ProviderResolutionDetail = res.ProviderResolutionDetail resolution.Value = res.Value - case String: - defValue := defaultValue.(string) + case string: res := provider.StringEvaluation(ctx, flag, defValue, flatCtx) resolution.ProviderResolutionDetail = res.ProviderResolutionDetail resolution.Value = res.Value - case Float: - defValue := defaultValue.(float64) + case float64: res := provider.FloatEvaluation(ctx, flag, defValue, flatCtx) resolution.ProviderResolutionDetail = res.ProviderResolutionDetail resolution.Value = res.Value - case Int: - defValue := defaultValue.(int64) + case int64: res := provider.IntEvaluation(ctx, flag, defValue, flatCtx) resolution.ProviderResolutionDetail = res.ProviderResolutionDetail resolution.Value = res.Value + default: + resolution = provider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx) } err = resolution.Error() if err != nil { err = fmt.Errorf("error code: %w", err) - c.errorHooks(ctx, hookCtx, hooks, err, options) + errorHooks(ctx, hookCtx, hooks, err, options) evalDetails.ResolutionDetail = resolution.ResolutionDetail() evalDetails.Reason = ErrorReason return evalDetails, err } - evalDetails.Value = resolution.Value + + if resolution.Value != nil { + var ok bool + evalDetails.Value, ok = resolution.Value.(T) + if !ok { + err := fmt.Errorf("evaluated value is not a %s", flagType) + errorHooks(ctx, hookCtx, hooks, err, options) + evalDetails.Value = defaultValue + evalDetails.ErrorCode = TypeMismatchCode + evalDetails.ErrorMessage = err.Error() + return evalDetails, err + } + } + evalDetails.ResolutionDetail = resolution.ResolutionDetail() - if err := c.afterHooks(ctx, hookCtx, hooks, evalDetails, options); err != nil { + if err := afterHooks(ctx, hookCtx, hooks, evalDetails, options); err != nil { err = fmt.Errorf("after hook: %w", err) - c.errorHooks(ctx, hookCtx, hooks, err, options) + errorHooks(ctx, hookCtx, hooks, 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 -} - // beforeHooks executes the Before hook for each hook in the collection. // Hooks are executed in forward order: API → Client → Invocation → Provider. -func (c *Client) beforeHooks( +func beforeHooks( ctx context.Context, hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions, -) (EvaluationContext, error) { +) (context.Context, EvaluationContext, error) { for _, hook := range hooks { - resultEvalCtx, err := hook.Before(ctx, hookCtx, options.hookHints) - if resultEvalCtx != nil { - hookCtx.evaluationContext = *resultEvalCtx + tctx, err := hook.Before(ctx, hookCtx, options.hookHints) + if tctx != nil { + ctx = tctx + if resultEvalCtx, ok := extractEvaluationContextFromContext(ctx); ok { + hookCtx.evaluationContext = resultEvalCtx + } } + if err != nil { - return mergeContexts(hookCtx.evaluationContext, evalCtx), err + return ctx, mergeContexts(hookCtx.evaluationContext, evalCtx), err } } - return mergeContexts(hookCtx.evaluationContext, evalCtx), nil + return ctx, mergeContexts(hookCtx.evaluationContext, evalCtx), nil } // afterHooks executes the After hook for each hook in the collection. // Hooks are executed in reverse order: Provider → Invocation → Client → API. -func (c *Client) afterHooks( - ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions, +func afterHooks[T FlagTypes]( + ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails EvaluationDetails[T], options EvaluationOptions, ) error { + e := EvaluationDetails[FlagTypes]{ + FlagType: evalDetails.FlagType, + FlagKey: evalDetails.FlagKey, + Value: evalDetails.Value, + ResolutionDetail: evalDetails.ResolutionDetail, + } // reverse order for _, hook := range slices.Backward(hooks) { - if err := hook.After(ctx, hookCtx, evalDetails, options.hookHints); err != nil { + if err := hook.After(ctx, hookCtx, e, options.hookHints); err != nil { return err } } @@ -825,7 +646,7 @@ func (c *Client) afterHooks( // errorHooks executes the Error hook for each hook in the collection. // Hooks are executed in reverse order: Provider → Invocation → Client → API. -func (c *Client) errorHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) { +func errorHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) { // reverse order for _, hook := range slices.Backward(hooks) { hook.Error(ctx, hookCtx, err, options.hookHints) @@ -834,10 +655,16 @@ func (c *Client) errorHooks(ctx context.Context, hookCtx HookContext, hooks []Ho // finallyHooks executes the Finally hook for each hook in the collection. // Hooks are executed in reverse order: Provider → Invocation → Client → API. -func (c *Client) finallyHooks(ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions) { +func finallyHooks[T FlagTypes](ctx context.Context, hookCtx HookContext, hooks []Hook, evalDetails EvaluationDetails[T], options EvaluationOptions) { + e := EvaluationDetails[FlagTypes]{ + FlagType: evalDetails.FlagType, + FlagKey: evalDetails.FlagKey, + Value: evalDetails.Value, + ResolutionDetail: evalDetails.ResolutionDetail, + } // reverse order for _, hook := range slices.Backward(hooks) { - hook.Finally(ctx, hookCtx, evalDetails, options.hookHints) + hook.Finally(ctx, hookCtx, e, options.hookHints) } } diff --git a/client_example_test.go b/client_example_test.go new file mode 100644 index 00000000..df664930 --- /dev/null +++ b/client_example_test.go @@ -0,0 +1,97 @@ +package openfeature_test + +import ( + "context" + "fmt" + "log" + + "go.openfeature.dev/openfeature/v2" +) + +func ExampleNewClient() { + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + fmt.Printf("Client Domain: %s", client.Metadata().Domain()) + // Output: Client Domain: example-client +} + +func ExampleClient_Boolean() { + if err := openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}, openfeature.WithDomain("example-client")); err != nil { + log.Fatalf("error setting up provider %v", err) + } + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + if client.Boolean(ctx, "myflag", true, openfeature.EvaluationContext{}) { + fmt.Println("myflag is true") + } else { + fmt.Println("myflag is false") + } + + // Output: myflag is true +} + +func ExampleClient_String() { + if err := openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}, openfeature.WithDomain("example-client")); err != nil { + log.Fatalf("error setting up provider %v", err) + } + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + fmt.Println(client.String(ctx, "myflag", "default", openfeature.EvaluationContext{})) + + // Output: default +} + +func ExampleClient_Float() { + if err := openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}, openfeature.WithDomain("example-client")); err != nil { + log.Fatalf("error setting up provider %v", err) + } + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + fmt.Println(client.Float(ctx, "myflag", 0.5, openfeature.EvaluationContext{})) + + // Output: 0.5 +} + +func ExampleClient_Int() { + if err := openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}, openfeature.WithDomain("example-client")); err != nil { + log.Fatalf("error setting up provider %v", err) + } + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + fmt.Println(client.Int(ctx, "myflag", 5, openfeature.EvaluationContext{})) + + // Output: 5 +} + +func ExampleClient_Object() { + if err := openfeature.SetProviderAndWait(context.TODO(), openfeature.NoopProvider{}, openfeature.WithDomain("example-client")); err != nil { + log.Fatalf("error setting up provider %v", err) + } + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + fmt.Println(client.Object(ctx, "myflag", map[string]string{"foo": "bar"}, openfeature.EvaluationContext{})) + + // Output: map[foo:bar] +} + +func ExampleClient_Track() { + ctx := context.TODO() + client := openfeature.NewClient(openfeature.WithDomain("example-client")) + + evaluationContext := openfeature.EvaluationContext{} + + // example tracking event recording that a subject reached a page associated with a business goal + client.Track(ctx, "visited-promo-page", evaluationContext, openfeature.TrackingEventDetails{}) + + // example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value + client.Track(ctx, "clicked-checkout", evaluationContext, openfeature.NewTrackingEventDetails(99.77)) + + // example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value + client.Track(ctx, "clicked-checkout", evaluationContext, openfeature.NewTrackingEventDetails(99.77).Add("currencyCode", "USD")) + + // Output: +} diff --git a/openfeature/client_test.go b/client_test.go similarity index 89% rename from openfeature/client_test.go rename to client_test.go index 2cd4f9a6..4cd4d1b0 100644 --- a/openfeature/client_test.go +++ b/client_test.go @@ -14,20 +14,20 @@ import ( type clientMocks struct { clientHandlerAPI *MockclientEvent evaluationAPI *MockevaluationImpl - providerAPI *MockFeatureProvider + providerAPI *MockProvider } func hydratedMocksForClientTests(t *testing.T, expectedEvaluations int) clientMocks { ctrl := gomock.NewController(t) mockClientAPI := NewMockclientEvent(ctrl) mockEvaluationAPI := NewMockevaluationImpl(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(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) { + mockEvaluationAPI.EXPECT().ForEvaluation(gomock.Any()).Times(expectedEvaluations).DoAndReturn(func(_ string) (*MockProvider, []Hook, EvaluationContext) { return mockProvider, nil, EvaluationContext{} }) @@ -47,7 +47,7 @@ func TestRequirement_1_2_1(t *testing.T) { mockHook := NewMockHook(ctrl) - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) client.AddHooks(mockHook) client.AddHooks(mockHook, mockHook) @@ -63,7 +63,7 @@ func TestRequirement_1_2_2(t *testing.T) { t.Cleanup(initSingleton) clientName := "test-client" - client := NewClient(clientName) + client := NewClient(WithDomain(clientName)) if client.Metadata().Domain() != clientName { t.Errorf("client domain not initiated as expected, got %s, want %s", client.Metadata().Domain(), clientName) @@ -87,14 +87,14 @@ func TestRequirement_1_2_2(t *testing.T) { // it's to be considered abnormal execution, and the supplied `default value` should be returned. func TestRequirements_1_3(t *testing.T) { t.Cleanup(initSingleton) - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) type requirements interface { - 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 any, evalCtx EvaluationContext, options ...Option) (any, error) + Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool + String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string + Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 + Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 + Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any } var clientI any = client @@ -108,14 +108,14 @@ func TestRequirements_1_3(t *testing.T) { // and `evaluation options` (optional), which returns an `evaluation details` structure. func TestRequirement_1_4_1(t *testing.T) { t.Cleanup(initSingleton) - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) type requirements interface { 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 any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) + ObjectValueDetails(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (ObjectEvaluationDetails, error) } var clientI any = client @@ -268,7 +268,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(InterfaceResolutionDetail{ + Return(ObjectResolutionDetail{ Value: objectValue, ProviderResolutionDetail: ProviderResolutionDetail{ Variant: objectVariant, @@ -276,7 +276,8 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.ObjectValueDetails(t.Context(), flagKey, nil, EvaluationContext{}) + var defaultValue any + evDetails, err := client.ObjectValueDetails(t.Context(), flagKey, defaultValue, EvaluationContext{}) if err != nil { t.Error(err) } @@ -395,7 +396,7 @@ func TestRequirement_1_4_4(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(InterfaceResolutionDetail{ + Return(ObjectResolutionDetail{ Value: objectValue, ProviderResolutionDetail: ProviderResolutionDetail{ Variant: objectVariant, @@ -430,8 +431,8 @@ func TestRequirement_1_4_7(t *testing.T) { }, }) - res, err := client.evaluate( - t.Context(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, + res, err := evaluate( + t.Context(), client, "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { t.Error("expected err, got nil") @@ -457,8 +458,8 @@ func TestRequirement_1_4_8(t *testing.T) { }, }) - res, err := client.evaluate( - t.Context(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, + res, err := evaluate( + t.Context(), client, "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { t.Error("expected err, got nil") @@ -480,7 +481,7 @@ func TestRequirement_1_4_8(t *testing.T) { func TestRequirement_1_4_9(t *testing.T) { flagKey := "flag-key" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("Boolean", func(t *testing.T) { t.Cleanup(initSingleton) @@ -497,10 +498,7 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - value, err := client.BooleanValue(t.Context(), flagKey, defaultValue, evalCtx) - if err == nil { - t.Error("expected BooleanValue to return an error, got nil") - } + value := client.Boolean(t.Context(), flagKey, defaultValue, evalCtx) if value != defaultValue { t.Errorf("expected default value from BooleanValue, got %v", value) @@ -531,10 +529,7 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - value, err := client.StringValue(t.Context(), flagKey, defaultValue, evalCtx) - if err == nil { - t.Error("expected StringValue to return an error, got nil") - } + value := client.String(t.Context(), flagKey, defaultValue, evalCtx) if value != defaultValue { t.Errorf("expected default value from StringValue, got %v", value) @@ -564,10 +559,7 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - value, err := client.FloatValue(t.Context(), flagKey, defaultValue, evalCtx) - if err == nil { - t.Error("expected FloatValue to return an error, got nil") - } + value := client.Float(t.Context(), flagKey, defaultValue, evalCtx) if value != defaultValue { t.Errorf("expected default value from FloatValue, got %v", value) @@ -596,10 +588,7 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - value, err := client.IntValue(t.Context(), flagKey, defaultValue, evalCtx) - if err == nil { - t.Error("expected IntValue to return an error, got nil") - } + value := client.Int(t.Context(), flagKey, defaultValue, evalCtx) if value != defaultValue { t.Errorf("expected default value from IntValue, got %v", value) @@ -624,15 +613,12 @@ func TestRequirement_1_4_9(t *testing.T) { } defaultValue := obj{foo: "bar"} mocks.providerAPI.EXPECT().ObjectEvaluation(t.Context(), flagKey, defaultValue, flatCtx). - Return(InterfaceResolutionDetail{ + Return(ObjectResolutionDetail{ ProviderResolutionDetail: ProviderResolutionDetail{ ResolutionError: NewGeneralResolutionError("test"), }, }).Times(2) - value, err := client.ObjectValue(t.Context(), flagKey, defaultValue, evalCtx) - if err == nil { - t.Error("expected ObjectValue to return an error, got nil") - } + value := client.Object(t.Context(), flagKey, defaultValue, evalCtx) if value != defaultValue { t.Errorf("expected default value from ObjectValue, got %v", value) @@ -673,8 +659,8 @@ func TestRequirement_1_4_12(t *testing.T) { ResolutionError: NewGeneralResolutionError(errMessage), }, }) - evalDetails, err := client.evaluate( - t.Context(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, + evalDetails, err := evaluate( + t.Context(), client, "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { t.Error("expected err, got nil") @@ -695,7 +681,7 @@ func TestRequirement_1_4_12(t *testing.T) { func TestRequirement_1_4_13(t *testing.T) { flagKey := "flag-key" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("No Metadata", func(t *testing.T) { t.Cleanup(initSingleton) @@ -775,7 +761,7 @@ func TestRequirement_1_4_13(t *testing.T) { // The `client` MUST define a function for tracking the occurrence of a particular action or application state, // with parameters `tracking event name` (string, required) and `tracking event details` (optional), which returns nothing. func TestRequirement_6_1(t *testing.T) { - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) type requirements interface { Track(ctx context.Context, trackingEventName string, evalCtx EvaluationContext, details TrackingEventDetails) @@ -808,7 +794,7 @@ func TestTrack(t *testing.T) { // mockTrackingProvider is a feature provider that implements tracker contract. type mockTrackingProvider struct { *MockTracker - *MockFeatureProvider + *MockProvider } type testcase struct { @@ -816,7 +802,7 @@ func TestTrack(t *testing.T) { eventName string outCtx EvaluationContext // allow asserting the input to provider - provider func(tc *testcase, provider *MockFeatureProvider) FeatureProvider + provider func(tc *testcase, provider *MockProvider) FeatureProvider } tests := map[string]*testcase{ @@ -858,10 +844,10 @@ func TestTrack(t *testing.T) { "4": "invocation", }, }, - provider: func(tc *testcase, mockProvider *MockFeatureProvider) FeatureProvider { + provider: func(tc *testcase, mockProvider *MockProvider) FeatureProvider { provider := &mockTrackingProvider{ - MockTracker: NewMockTracker(mockProvider.ctrl), - MockFeatureProvider: mockProvider, + MockTracker: NewMockTracker(mockProvider.ctrl), + MockProvider: mockProvider, } // assert AnyTimesif Track is called once with evalCtx expected provider.MockTracker.EXPECT().Track(gomock.Any(), gomock.Any(), tc.outCtx, TrackingEventDetails{}).Times(1) @@ -873,7 +859,7 @@ func TestTrack(t *testing.T) { inCtx: inputCtx{}, eventName: "example-event", outCtx: EvaluationContext{}, - provider: func(tc *testcase, provider *MockFeatureProvider) FeatureProvider { + provider: func(tc *testcase, provider *MockProvider) FeatureProvider { return provider }, }, @@ -891,7 +877,7 @@ func TestTrack(t *testing.T) { return provider, nil, test.inCtx.api }) client.evaluationContext = test.inCtx.client - ctx := WithTransactionContext(t.Context(), test.inCtx.txn) + ctx := ContextWithEvaluationContext(t.Context(), test.inCtx.txn) // action client.Track(ctx, test.eventName, test.inCtx.invocation, TrackingEventDetails{}) @@ -963,7 +949,7 @@ func TestFlattenContext(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - out := flattenContext(test.inCtx) + out := test.inCtx.Flattened() if !reflect.DeepEqual(test.outCtx, out) { t.Errorf( "%s, unexpected value received from flatten context, expected %v got %v", @@ -990,12 +976,7 @@ func TestBeforeHookNilContext(t *testing.T) { evalCtx := EvaluationContext{attributes: attributes} mocks.providerAPI.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), attributes) - _, err := client.BooleanValue( - t.Context(), "foo", false, evalCtx, WithHooks(hookNilContext), - ) - if err != nil { - t.Error(err) - } + _ = client.Boolean(t.Context(), "foo", false, evalCtx, WithHooks(hookNilContext)) } func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) { @@ -1013,8 +994,8 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) { }, }) - evalDetails, err := client.evaluate( - t.Context(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, + evalDetails, err := evaluate( + t.Context(), client, "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { t.Error("expected err, got nil") @@ -1038,7 +1019,7 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) { mocks := hydratedMocksForClientTests(t, 1) client := newClient("test-client", mocks.evaluationAPI, mocks.clientHandlerAPI) mocks.providerAPI.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(InterfaceResolutionDetail{ + Return(ObjectResolutionDetail{ Value: value, ProviderResolutionDetail: ProviderResolutionDetail{ Variant: variant, @@ -1183,7 +1164,7 @@ func TestFlagMetadataAccessors(t *testing.T) { // The client MUST define a provider status accessor which indicates the readiness of the associated provider. // with possible values NOT_READY, READY, STALE, ERROR, or FATAL. func TestRequirement_1_7_1(t *testing.T) { - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) type requirements interface { State() State @@ -1202,7 +1183,7 @@ func TestRequirement_1_7_1(t *testing.T) { func TestRequirement_1_7_2(t *testing.T) { t.Cleanup(initSingleton) - if NewClient(t.Name()).State() != NotReadyState { + if NewClient(WithDomain(t.Name())).State() != NotReadyState { t.Fatalf("expected client to report NOT READY state") } @@ -1216,11 +1197,11 @@ func TestRequirement_1_7_2(t *testing.T) { }, } - if err := SetNamedProviderAndWait(t.Name(), provider); err != nil { + if err := SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())); err != nil { t.Fatalf("failed to set up provider: %v", err) } - if NewClient(t.Name()).State() != ReadyState { + if NewClient(WithDomain(t.Name())).State() != ReadyState { t.Fatalf("expected client to report READY state") } } @@ -1236,15 +1217,15 @@ func TestRequirement_1_7_3(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return errors.New("whoops... error from initialization") }, }, &ProviderEventing{}, } - _ = SetNamedProviderAndWait(t.Name(), provider) - if NewClient(t.Name()).State() != ErrorState { + _ = SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())) + if NewClient(WithDomain(t.Name())).State() != ErrorState { t.Fatalf("expected client to report ERROR state") } } @@ -1260,16 +1241,16 @@ func TestRequirement_1_7_4(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return errors.New("whoops... error from initialization") }, }, &ProviderEventing{}, } - _ = SetNamedProviderAndWait(t.Name(), provider) + _ = SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())) - if NewClient(t.Name()).State() != ErrorState { + if NewClient(WithDomain(t.Name())).State() != ErrorState { t.Fatalf("expected client to report ERROR state") } } @@ -1285,16 +1266,16 @@ func TestRequirement_1_7_5(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return &ProviderInitError{ErrorCode: ProviderFatalCode} }, }, &ProviderEventing{}, } - _ = SetNamedProviderAndWait(t.Name(), provider) + _ = SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())) - if NewClient(t.Name()).State() != FatalState { + if NewClient(WithDomain(t.Name())).State() != FatalState { t.Fatalf("expected client to report ERROR state") } } @@ -1306,7 +1287,8 @@ func TestRequirement_1_7_6(t *testing.T) { ctrl := gomock.NewController(t) mockHook := NewMockHook(ctrl) - mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ProviderNotReadyError, gomock.Any()) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()) + mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ErrProviderNotReady, gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) notReadyEventingProvider := struct { @@ -1316,7 +1298,7 @@ func TestRequirement_1_7_6(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { <-time.After(math.MaxInt) return nil }, @@ -1324,9 +1306,9 @@ func TestRequirement_1_7_6(t *testing.T) { &ProviderEventing{}, } - _ = SetProvider(notReadyEventingProvider) + _ = SetProvider(t.Context(), notReadyEventingProvider) - client := NewClient("somOtherClient") + client := NewClient(WithDomain("somOtherClient")) client.AddHooks(mockHook) if client.State() != NotReadyState { @@ -1334,10 +1316,7 @@ func TestRequirement_1_7_6(t *testing.T) { } defaultVal := true - res, err := client.BooleanValue(t.Context(), "a-flag", defaultVal, EvaluationContext{}) - if err == nil { - t.Fatalf("expected client to report an error") - } + res := client.Boolean(t.Context(), "a-flag", defaultVal, EvaluationContext{}) if res != defaultVal { t.Fatalf("expected resolved boolean value to default to %t, got %t", defaultVal, res) @@ -1355,24 +1334,25 @@ func TestRequirement_1_7_7(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return &ProviderInitError{ErrorCode: ProviderFatalCode} }, }, &ProviderEventing{}, } - err := SetNamedProviderAndWait(t.Name(), provider) + err := SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())) if err == nil { t.Errorf("provider registration was expected to fail but succeeded unexpectedly") } ctrl := gomock.NewController(t) mockHook := NewMockHook(ctrl) - mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ProviderFatalError, gomock.Any()) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()) + mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ErrProviderFatal, gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) client.AddHooks(mockHook) if client.State() != FatalState { @@ -1380,10 +1360,7 @@ func TestRequirement_1_7_7(t *testing.T) { } defaultVal := true - res, err := client.BooleanValue(t.Context(), "a-flag", defaultVal, EvaluationContext{}) - if err == nil { - t.Fatalf("expected client to report an error") - } + res := client.Boolean(t.Context(), "a-flag", defaultVal, EvaluationContext{}) if res != defaultVal { t.Fatalf("expected resolved boolean value to default to %t, got %t", defaultVal, res) @@ -1405,7 +1382,7 @@ func TestRequirement_5_3_5(t *testing.T) { t.Cleanup(initSingleton) eventually(t, func() bool { - return NewDefaultClient().State() == NotReadyState + return NewClient().State() == NotReadyState }, time.Second, 100*time.Millisecond, "expected client to report NOT READY state") eventing := &ProviderEventing{ @@ -1420,31 +1397,31 @@ func TestRequirement_5_3_5(t *testing.T) { eventing, } - if err := SetNamedProviderAndWait(t.Name(), provider); err != nil { + if err := SetProviderAndWait(t.Context(), provider, WithDomain(t.Name())); err != nil { t.Fatalf("failed to set up provider: %v", err) } eventually(t, func() bool { - return NewClient(t.Name()).State() == ReadyState + return NewClient(WithDomain(t.Name())).State() == ReadyState }, time.Second, 100*time.Millisecond, "expected client to report READY state") eventing.Invoke(Event{EventType: ProviderStale}) eventually(t, func() bool { - return NewClient(t.Name()).State() == StaleState + return NewClient(WithDomain(t.Name())).State() == StaleState }, time.Second, 100*time.Millisecond, "expected client to report STALE state") eventing.Invoke(Event{EventType: ProviderError}) eventually(t, func() bool { - return NewClient(t.Name()).State() == ErrorState + return NewClient(WithDomain(t.Name())).State() == ErrorState }, time.Second, 100*time.Millisecond, "expected client to report ERROR state") eventing.Invoke(Event{EventType: ProviderReady}) eventually(t, func() bool { - return NewClient(t.Name()).State() == ReadyState + return NewClient(WithDomain(t.Name())).State() == ReadyState }, time.Second, 100*time.Millisecond, "expected client to report READY state") eventing.Invoke(Event{EventType: ProviderError, ProviderEventDetails: ProviderEventDetails{ErrorCode: ProviderFatalCode}}) eventually(t, func() bool { - return NewClient(t.Name()).State() == FatalState + return NewClient(WithDomain(t.Name())).State() == FatalState }, time.Second, 100*time.Millisecond, "expected client to report FATAL state") } diff --git a/codecov.yml b/codecov.yml index ca2113eb..b35e7884 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,3 @@ ignore: - "**/*_mock.go" + - "*_mock.go" diff --git a/openfeature/context_aware_test.go b/context_aware_test.go similarity index 89% rename from openfeature/context_aware_test.go rename to context_aware_test.go index bc161f6a..d559e037 100644 --- a/openfeature/context_aware_test.go +++ b/context_aware_test.go @@ -7,6 +7,8 @@ import ( "time" ) +var _ (StateHandler) = (*testContextAwareProvider)(nil) + // testContextAwareProvider is a test provider that implements ContextAwareStateHandler type testContextAwareProvider struct { initDelay time.Duration @@ -16,8 +18,8 @@ func (p *testContextAwareProvider) Metadata() Metadata { return Metadata{Name: "test-context-aware-provider"} } -// InitWithContext implements ContextAwareStateHandler -func (p *testContextAwareProvider) InitWithContext(ctx context.Context, evalCtx EvaluationContext) error { +// Init implements StateHandler +func (p *testContextAwareProvider) Init(ctx context.Context) error { select { case <-time.After(p.initDelay): return nil @@ -26,13 +28,8 @@ func (p *testContextAwareProvider) InitWithContext(ctx context.Context, evalCtx } } -// Init implements StateHandler for backward compatibility -func (p *testContextAwareProvider) Init(evalCtx EvaluationContext) error { - return p.InitWithContext(context.Background(), evalCtx) -} - -// ShutdownWithContext implements ContextAwareStateHandler -func (p *testContextAwareProvider) ShutdownWithContext(ctx context.Context) error { +// Shutdown implements StateHandler for backward compatibility +func (p *testContextAwareProvider) Shutdown(ctx context.Context) error { select { case <-time.After(p.initDelay): // Reuse delay for shutdown simulation return nil @@ -41,12 +38,6 @@ func (p *testContextAwareProvider) ShutdownWithContext(ctx context.Context) erro } } -// Shutdown implements StateHandler for backward compatibility -func (p *testContextAwareProvider) Shutdown() { - // For backward compatibility, use background context with no timeout - _ = p.ShutdownWithContext(context.Background()) -} - func (p *testContextAwareProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail { return BoolResolutionDetail{ Value: defaultValue, @@ -75,8 +66,8 @@ func (p *testContextAwareProvider) IntEvaluation(ctx context.Context, flag strin } } -func (p *testContextAwareProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail { - return InterfaceResolutionDetail{ +func (p *testContextAwareProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail { + return ObjectResolutionDetail{ Value: defaultValue, ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason}, } @@ -107,7 +98,7 @@ func TestContextAwareInitialization(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer cancel() - err := SetProviderWithContextAndWait(ctx, fastProvider) + err := SetProviderAndWait(ctx, fastProvider) if err != nil { t.Errorf("Expected fast provider to succeed, got error: %v", err) } @@ -119,7 +110,7 @@ func TestContextAwareInitialization(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond) defer cancel() - err := SetProviderWithContextAndWait(ctx, slowProvider) + err := SetProviderAndWait(ctx, slowProvider) if err == nil { t.Error("Expected timeout error but got success") } @@ -135,7 +126,7 @@ func TestContextAwareInitialization(t *testing.T) { defer cancel() start := time.Now() - err := SetProviderWithContext(ctx, asyncProvider) + err := SetProvider(ctx, asyncProvider) elapsed := time.Since(start) if err != nil { @@ -152,7 +143,7 @@ func TestContextAwareInitialization(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer cancel() - err := SetNamedProviderWithContextAndWait(ctx, "test-domain", namedProvider) + err := SetProviderAndWait(ctx, namedProvider, WithDomain("test-domain")) if err != nil { t.Errorf("Named provider should succeed: %v", err) } @@ -164,7 +155,7 @@ func TestContextAwareInitialization(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer cancel() - err := SetProviderWithContextAndWait(ctx, legacyProvider) + err := SetProviderAndWait(ctx, legacyProvider) if err != nil { t.Errorf("Legacy provider should work: %v", err) } @@ -246,14 +237,14 @@ func TestContextAwareShutdown(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 300*time.Millisecond) defer cancel() - err := SetProviderWithContextAndWait(ctx, provider) + err := SetProviderAndWait(ctx, provider) if err != nil { t.Errorf("Provider setup should succeed: %v", err) } // Now replace it to trigger shutdown newProvider := &testContextAwareProvider{initDelay: 10 * time.Millisecond} - err = SetProviderWithContextAndWait(ctx, newProvider) + err = SetProviderAndWait(ctx, newProvider) if err != nil { t.Errorf("Provider replacement should succeed: %v", err) } @@ -267,7 +258,7 @@ func TestContextAwareShutdown(t *testing.T) { ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) defer cancel() - err := SetProviderWithContextAndWait(ctx, slowShutdownProvider) + err := SetProviderAndWait(ctx, slowShutdownProvider) if err != nil { t.Errorf("Provider setup should succeed: %v", err) } @@ -275,7 +266,7 @@ func TestContextAwareShutdown(t *testing.T) { // Replace with new provider - shutdown happens in background, so this should succeed // even if the old provider takes a long time to shut down fastProvider := &testContextAwareProvider{initDelay: 10 * time.Millisecond} - err = SetProviderWithContextAndWait(ctx, fastProvider) + err = SetProviderAndWait(ctx, fastProvider) if err != nil { t.Errorf("Provider replacement should succeed even with slow shutdown: %v", err) } @@ -309,13 +300,13 @@ func TestGlobalContextAwareShutdown(t *testing.T) { defer cancel() // Set default provider - err := SetProviderWithContextAndWait(ctx, defaultProvider) + err := SetProviderAndWait(ctx, defaultProvider) if err != nil { t.Errorf("Default provider setup should succeed: %v", err) } // Set named provider - err = SetNamedProviderWithContextAndWait(ctx, "test-service", namedProvider) + err = SetProviderAndWait(ctx, namedProvider, WithDomain("test-service")) if err != nil { t.Errorf("Named provider setup should succeed: %v", err) } @@ -324,7 +315,7 @@ func TestGlobalContextAwareShutdown(t *testing.T) { shutdownCtx, shutdownCancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer shutdownCancel() - err = ShutdownWithContext(shutdownCtx) + err = Shutdown(shutdownCtx) if err != nil { t.Errorf("Global shutdown should succeed: %v", err) } @@ -344,7 +335,7 @@ func TestGlobalContextAwareShutdown(t *testing.T) { defer cancel() // Set the provider (this should succeed quickly) - err := SetProviderWithContextAndWait(ctx, slowShutdownProvider) + err := SetProviderAndWait(ctx, slowShutdownProvider) if err != nil { t.Errorf("Provider setup should succeed: %v", err) } @@ -363,7 +354,7 @@ func TestGlobalContextAwareShutdown(t *testing.T) { shutdownCtx, shutdownCancel := context.WithTimeout(t.Context(), 100*time.Millisecond) defer shutdownCancel() - err = ShutdownWithContext(shutdownCtx) + err = Shutdown(shutdownCtx) if err == nil { t.Error("Expected shutdown timeout error") } @@ -387,12 +378,12 @@ func TestGlobalContextAwareShutdown(t *testing.T) { defer cancel() // Set providers - err := SetProviderWithContextAndWait(ctx, defaultProvider) + err := SetProviderAndWait(ctx, defaultProvider) if err != nil { t.Errorf("Default provider setup should succeed: %v", err) } - err = SetNamedProviderWithContextAndWait(ctx, "test-service", namedProvider) + err = SetProviderAndWait(ctx, namedProvider, WithDomain("test-service")) if err != nil { t.Errorf("Named provider setup should succeed: %v", err) } @@ -401,7 +392,7 @@ func TestGlobalContextAwareShutdown(t *testing.T) { shutdownCtx, shutdownCancel := context.WithTimeout(t.Context(), 500*time.Millisecond) defer shutdownCancel() - err = ShutdownWithContext(shutdownCtx) + err = Shutdown(shutdownCtx) if err != nil { t.Errorf("Global shutdown should succeed with regular providers: %v", err) } @@ -472,8 +463,8 @@ func (p *testContextAwareProviderWithShutdownDelay) IntEvaluation(ctx context.Co } } -func (p *testContextAwareProviderWithShutdownDelay) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail { - return InterfaceResolutionDetail{ +func (p *testContextAwareProviderWithShutdownDelay) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail { + return ObjectResolutionDetail{ Value: defaultValue, ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason}, } @@ -509,7 +500,7 @@ func TestContextPropagationFixes(t *testing.T) { initCtx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() - err := SetProviderWithContextAndWait(initCtx, provider) + err := SetProviderAndWait(initCtx, provider) if err != nil { t.Errorf("Provider setup should succeed: %v", err) } @@ -522,7 +513,7 @@ func TestContextPropagationFixes(t *testing.T) { defer replaceCancel() start := time.Now() - err = SetProviderWithContextAndWait(replaceCtx, newProvider) + err = SetProviderAndWait(replaceCtx, newProvider) elapsed := time.Since(start) // The init should succeed quickly, shutdown happens async @@ -552,7 +543,7 @@ func TestContextPropagationFixes(t *testing.T) { } // Set up provider - err := SetProviderWithContextAndWait(t.Context(), provider) + err := SetProviderAndWait(t.Context(), provider) if err != nil { t.Errorf("Provider setup should succeed: %v", err) } @@ -567,7 +558,7 @@ func TestContextPropagationFixes(t *testing.T) { }() newProvider := &testContextAwareProvider{initDelay: 10 * time.Millisecond} - err = SetProviderWithContextAndWait(replaceCtx, newProvider) + err = SetProviderAndWait(replaceCtx, newProvider) // Should succeed because init is fast, shutdown is async if err != nil { t.Errorf("Provider replacement should succeed even with cancellation: %v", err) @@ -651,6 +642,8 @@ func TestSimplifiedErrorHandling(t *testing.T) { }) } +var _ (StateHandler) = (*testProviderInitError)(nil) + // testProviderInitError is a provider that returns a specific ProviderInitError type testProviderInitError struct { initDelay time.Duration @@ -661,7 +654,7 @@ func (p *testProviderInitError) Metadata() Metadata { return Metadata{Name: "test-provider-init-error"} } -func (p *testProviderInitError) InitWithContext(ctx context.Context, evalCtx EvaluationContext) error { +func (p *testProviderInitError) Init(ctx context.Context) error { select { case <-time.After(p.initDelay): return p.initError @@ -671,16 +664,10 @@ func (p *testProviderInitError) InitWithContext(ctx context.Context, evalCtx Eva } } -func (p *testProviderInitError) Init(evalCtx EvaluationContext) error { - return p.InitWithContext(context.Background(), evalCtx) -} - -func (p *testProviderInitError) ShutdownWithContext(ctx context.Context) error { +func (p *testProviderInitError) Shutdown(ctx context.Context) error { return nil } -func (p *testProviderInitError) Shutdown() {} - func (p *testProviderInitError) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail { return BoolResolutionDetail{ Value: defaultValue, @@ -709,8 +696,8 @@ func (p *testProviderInitError) IntEvaluation(ctx context.Context, flag string, } } -func (p *testProviderInitError) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail { - return InterfaceResolutionDetail{ +func (p *testProviderInitError) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail { + return ObjectResolutionDetail{ Value: defaultValue, ProviderResolutionDetail: ProviderResolutionDetail{Reason: DefaultReason}, } @@ -751,7 +738,7 @@ func TestEdgeCases(t *testing.T) { // Rapidly switch providers for i, provider := range providers { ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) - err := SetProviderWithContextAndWait(ctx, provider) + err := SetProviderAndWait(ctx, provider) cancel() if err != nil { @@ -779,7 +766,7 @@ func TestEdgeCases(t *testing.T) { defer cancel() provider := &testContextAwareProvider{initDelay: 50 * time.Millisecond} - err := SetProviderWithContextAndWait(ctx, provider) + err := SetProviderAndWait(ctx, provider) done <- err }() @@ -789,7 +776,7 @@ func TestEdgeCases(t *testing.T) { defer cancel() provider := &testContextAwareProvider{initDelay: 30 * time.Millisecond} - err := SetNamedProviderWithContextAndWait(ctx, "concurrent-test", provider) + err := SetProviderAndWait(ctx, provider, WithDomain("concurrent-test")) done <- err }() diff --git a/openfeature/doc.go b/doc.go similarity index 100% rename from openfeature/doc.go rename to doc.go diff --git a/e2e/common_test.go b/e2e/common_test.go index 64010087..6274d097 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/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) // ctxFunction is a context based evaluation callback diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go index 575a8d81..105d60fa 100644 --- a/e2e/evaluation_fuzz_test.go +++ b/e2e/evaluation_fuzz_test.go @@ -4,20 +4,20 @@ import ( "strings" "testing" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) func setupFuzzClient(f *testing.F) *openfeature.Client { f.Helper() - memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{}) - err := openfeature.SetNamedProviderAndWait(f.Name(), memoryProvider) + memoryProvider := memprovider.NewProvider(map[string]memprovider.InMemoryFlag{}) + err := openfeature.SetProviderAndWait(f.Context(), memoryProvider, openfeature.WithDomain(f.Name())) if err != nil { f.Errorf("error setting up provider %v", err) } - return openfeature.NewClient(f.Name()) + return openfeature.NewClient(openfeature.WithDomain(f.Name())) } func FuzzBooleanEvaluation(f *testing.F) { diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index 5a1bc789..cc127fda 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/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) // ctxStorageKey is the key used to pass test data across context.Context @@ -91,9 +91,9 @@ func initializeEvaluationScenario(ctx *godog.ScenarioContext) { } func aProviderIsRegisteredWithCacheDisabled(ctx context.Context) error { - memoryProvider := memprovider.NewInMemoryProvider(memoryFlags) + memoryProvider := memprovider.NewProvider(memoryFlags) - err := openfeature.SetNamedProvider("evaluation-test", memoryProvider) + err := openfeature.SetProvider(ctx, memoryProvider, openfeature.WithDomain("evaluation-test")) if err != nil { return err } @@ -109,10 +109,7 @@ func aBooleanFlagWithKeyIsEvaluatedWithDefaultValue( return ctx, errors.New("default value must be of type bool") } - got, err := openfeature.NewClient("evaluation-test").BooleanValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).Boolean(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, got), nil } @@ -138,10 +135,7 @@ func theResolvedBooleanValueShouldBe(ctx context.Context, expectedValueStr strin func aStringFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").StringValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).String(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, got), nil } @@ -162,10 +156,7 @@ func theResolvedStringValueShouldBe(ctx context.Context, expectedValue string) e func anIntegerFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").IntValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).Int(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, got), nil } @@ -186,10 +177,7 @@ func theResolvedIntegerValueShouldBe(ctx context.Context, expectedValue int64) e func aFloatFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey string, defaultValue float64, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").FloatValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).Float(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, got), nil } @@ -208,10 +196,7 @@ func theResolvedFloatValueShouldBe(ctx context.Context, expectedValue float64) e } func anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(ctx context.Context, flagKey string) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").ObjectValue(ctx, flagKey, nil, openfeature.EvaluationContext{}) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).Object(ctx, flagKey, nil, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, got), nil } @@ -270,7 +255,7 @@ func aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( return ctx, errors.New("default value must be of type bool") } - got, err := openfeature.NewClient("evaluation-test").BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -314,7 +299,7 @@ func theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldB func aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -353,7 +338,7 @@ func theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe func anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -392,7 +377,7 @@ func theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldB func aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey string, defaultValue float64, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -431,14 +416,14 @@ func theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( func anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue( ctx context.Context, flagKey string, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } - store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails) + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.ObjectEvaluationDetails) if !ok { - store = make(map[string]openfeature.InterfaceEvaluationDetails) + store = make(map[string]openfeature.ObjectEvaluationDetails) } store[flagKey] = got @@ -538,10 +523,8 @@ func aFlagWithKeyIsEvaluatedWithDefaultValue( return ctx, errors.New("no contextAwareEvaluationData found") } - got, err := openfeature.NewClient("evaluation-test").StringValue(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext) - if err != nil { - return ctx, fmt.Errorf("openfeature client: %w", err) - } + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).String(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext) + ctxAwareEvalData.flagKey = flagKey ctxAwareEvalData.defaultValue = defaultValue ctxAwareEvalData.response = got @@ -568,12 +551,9 @@ func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedRe return errors.New("no contextAwareEvaluationData found") } - got, err := openfeature.NewClient("evaluation-test").StringValue( + got := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).String( ctx, ctxAwareEvalData.flagKey, ctxAwareEvalData.defaultValue, openfeature.EvaluationContext{}, ) - if err != nil { - return fmt.Errorf("openfeature client: %w", err) - } if got != expectedResponse { return fmt.Errorf("expected response of '%s', got '%s'", expectedResponse, got) @@ -585,7 +565,7 @@ func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedRe func aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, stringFlagNotFoundData{ evalDetails: got, @@ -642,7 +622,7 @@ func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith func aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := openfeature.NewClient("evaluation-test").IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.NewClient(openfeature.WithDomain("evaluation-test")).IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, typeErrorData{ evalDetails: got, @@ -765,17 +745,17 @@ func getFirstFloatEvaluationDetails(ctx context.Context) (openfeature.FloatEvalu return openfeature.FloatEvaluationDetails{}, errors.New("no evaluation detail found in context") } -func getFirstInterfaceEvaluationDetails(ctx context.Context) (openfeature.InterfaceEvaluationDetails, error) { - store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails) +func getFirstInterfaceEvaluationDetails(ctx context.Context) (openfeature.ObjectEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.ObjectEvaluationDetails) if !ok { - return openfeature.InterfaceEvaluationDetails{}, errors.New("no flag resolution result") + return openfeature.ObjectEvaluationDetails{}, errors.New("no flag resolution result") } for _, evalDetails := range store { return evalDetails, nil } - return openfeature.InterfaceEvaluationDetails{}, errors.New("no evaluation detail found in context") + return openfeature.ObjectEvaluationDetails{}, errors.New("no evaluation detail found in context") } func boolOrString(str string) any { diff --git a/openfeature/evaluation_context.go b/evaluation_context.go similarity index 58% rename from openfeature/evaluation_context.go rename to evaluation_context.go index 17124758..3e332669 100644 --- a/openfeature/evaluation_context.go +++ b/evaluation_context.go @@ -3,8 +3,6 @@ package openfeature import ( "context" "maps" - - "github.com/open-feature/go-sdk/openfeature/internal" ) // EvaluationContext provides ambient information for the purposes of flag evaluation @@ -35,6 +33,18 @@ func (e EvaluationContext) Attributes() map[string]any { return attrs } +// Flattened converts EvaluationContext to a [FlattenedContext]. +func (e EvaluationContext) Flattened() FlattenedContext { + flatCtx := FlattenedContext{} + if e.attributes != nil { + flatCtx = e.Attributes() + } + if e.targetingKey != "" { + flatCtx[TargetingKey] = e.targetingKey + } + return flatCtx +} + // NewEvaluationContext constructs an EvaluationContext // // targetingKey - uniquely identifying the subject (end-user, or client service) of a flag evaluation @@ -57,12 +67,12 @@ func NewTargetlessEvaluationContext(attributes map[string]any) EvaluationContext return NewEvaluationContext("", attributes) } -// WithTransactionContext constructs a TransactionContext. +// ContextWithEvaluationContext constructs a TransactionContext. // // ctx - the context to embed the EvaluationContext in // ec - the EvaluationContext to embed into the context -func WithTransactionContext(ctx context.Context, ec EvaluationContext) context.Context { - return context.WithValue(ctx, internal.TransactionContext, ec) +func ContextWithEvaluationContext(ctx context.Context, ec EvaluationContext) context.Context { + return context.WithValue(ctx, transactionContext, ec) } // MergeTransactionContext merges the provided EvaluationContext with the current TransactionContext (if it exists) @@ -70,22 +80,39 @@ func WithTransactionContext(ctx context.Context, ec EvaluationContext) context.C // ctx - the context to pull existing TransactionContext from // ec - the EvaluationContext to merge with the existing TransactionContext func MergeTransactionContext(ctx context.Context, ec EvaluationContext) context.Context { - oldTc := TransactionContext(ctx) + oldTc := EvaluationContextFromContext(ctx) mergedTc := mergeContexts(ec, oldTc) - return WithTransactionContext(ctx, mergedTc) + return ContextWithEvaluationContext(ctx, mergedTc) } -// TransactionContext extracts a EvaluationContext from the current -// golang.org/x/net/context. if no EvaluationContext exist, it will construct -// an empty EvaluationContext +// EvaluationContextFromContext extracts a EvaluationContext from the current +// context. if no EvaluationContext exist, it will construct +// an empty EvaluationContext. // -// ctx - the context to pull EvaluationContext from -func TransactionContext(ctx context.Context) EvaluationContext { - ec, ok := ctx.Value(internal.TransactionContext).(EvaluationContext) - +// ctx - the context to pull EvaluationContext from TransactionContext +func EvaluationContextFromContext(ctx context.Context) EvaluationContext { + ec, ok := extractEvaluationContextFromContext(ctx) if !ok { return EvaluationContext{} } return ec } + +// extractEvaluationContextFromContext extracts an EvaluationContext from the context. +// It returns the EvaluationContext and a boolean indicating whether one was found. +// +// ctx - the context to extract the EvaluationContext from +func extractEvaluationContextFromContext(ctx context.Context) (EvaluationContext, bool) { + ec, ok := ctx.Value(transactionContext).(EvaluationContext) + return ec, ok +} + +// contextKey is just an empty struct. It exists so transactionContext can be +// an immutable variable with a unique type. It's immutable +// because nobody else can create a contextKey, being unexported. +type contextKey struct{} + +// transactionContext is the context key to use with golang.org/x/net/context's +// WithValue function to associate an EvaluationContext value with a context. +var transactionContext contextKey diff --git a/openfeature/evaluation_context_test.go b/evaluation_context_test.go similarity index 82% rename from openfeature/evaluation_context_test.go rename to evaluation_context_test.go index a3846f31..4fd4895c 100644 --- a/openfeature/evaluation_context_test.go +++ b/evaluation_context_test.go @@ -5,7 +5,6 @@ import ( "reflect" "testing" - "github.com/open-feature/go-sdk/openfeature/internal" "go.uber.org/mock/gomock" ) @@ -47,7 +46,7 @@ func TestRequirement_3_2_1(t *testing.T) { }) t.Run("client MUST have a method for supplying `evaluation context`", func(t *testing.T) { - client := NewClient("test") + client := NewClient(WithDomain("test")) type requirement interface { SetEvaluationContext(evalCtx EvaluationContext) @@ -60,19 +59,19 @@ func TestRequirement_3_2_1(t *testing.T) { }) t.Run("invocation MUST have a method for supplying `evaluation context`", func(t *testing.T) { - client := NewClient("test") + client := NewClient(WithDomain("test")) type requirement interface { - 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 any, evalCtx EvaluationContext, options ...Option) (any, error) + Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool + String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string + Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 + Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 + Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any 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 any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) + ObjectValueDetails(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (ObjectEvaluationDetails, error) } var clientI any = client @@ -106,17 +105,17 @@ func TestRequirement_3_2_2(t *testing.T) { "user": 2, }, } - transactionCtx := WithTransactionContext(t.Context(), transactionEvalCtx) + transactionCtx := ContextWithEvaluationContext(t.Context(), transactionEvalCtx) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) clientEvalCtx := EvaluationContext{ targetingKey: "Client", attributes: map[string]any{ @@ -147,13 +146,10 @@ func TestRequirement_3_2_2(t *testing.T) { "user": 1, }, } - flatCtx := flattenContext(expectedMergedEvalCtx) + flatCtx := expectedMergedEvalCtx.Flattened() mockProvider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), flatCtx) - _, err = client.StringValue(transactionCtx, "foo", "bar", invocationEvalCtx) - if err != nil { - t.Error(err) - } + _ = client.String(transactionCtx, "foo", "bar", invocationEvalCtx) } func TestEvaluationContext_AttributesNotPassedByReference(t *testing.T) { @@ -172,8 +168,8 @@ func TestEvaluationContext_AttributesNotPassedByReference(t *testing.T) { func TestRequirement_3_3_1(t *testing.T) { t.Run("The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.", func(t *testing.T) { ctx := t.Context() - ctx = WithTransactionContext(ctx, EvaluationContext{}) - val, ok := ctx.Value(internal.TransactionContext).(EvaluationContext) + ctx = ContextWithEvaluationContext(ctx, EvaluationContext{}) + val, ok := ctx.Value(transactionContext).(EvaluationContext) if !ok { t.Fatalf("failed to find transcation context set from WithTransactionContext: %v", val) @@ -218,7 +214,7 @@ func TestMergeTransactionContext(t *testing.T) { "overwrite": "new", }) - ctx := WithTransactionContext(t.Context(), oldEvalCtx) + ctx := ContextWithEvaluationContext(t.Context(), oldEvalCtx) ctx = MergeTransactionContext(ctx, newEvalCtx) expectedMergedEvalCtx := EvaluationContext{ @@ -230,7 +226,7 @@ func TestMergeTransactionContext(t *testing.T) { }, } - finalTransactionContext := TransactionContext(ctx) + finalTransactionContext := EvaluationContextFromContext(ctx) if finalTransactionContext.targetingKey != expectedMergedEvalCtx.targetingKey { t.Errorf( diff --git a/openfeature/event_executor.go b/event_executor.go similarity index 85% rename from openfeature/event_executor.go rename to event_executor.go index 01fa1718..662fce81 100644 --- a/openfeature/event_executor.go +++ b/event_executor.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "maps" + reflect "reflect" "slices" "sync" "time" @@ -90,12 +91,32 @@ func (e *eventExecutor) RemoveHandler(t EventType, c EventCallback) { // nothing to remove return } + // Get the unique pointer/address of the function we want to remove (c) + targetPtr := reflect.ValueOf(c).Pointer() e.apiRegistry[t] = slices.DeleteFunc(entrySlice, func(f EventCallback) bool { - return f == c + // Compare the unique pointer/address of the slice element (f) + // against the target pointer. + return reflect.ValueOf(f).Pointer() == targetPtr }) } +// isHandlerRegistered checks if a handler is already registered for event type. +func (e *eventExecutor) isHandlerRegistered(t EventType, c EventCallback) bool { + e.mu.Lock() + defer e.mu.Unlock() + + // Get the unique pointer/address of the function to check (c) + targetPtr := reflect.ValueOf(c).Pointer() + + for _, f := range e.apiRegistry[t] { + if reflect.ValueOf(f).Pointer() == targetPtr { + return true + } + } + return false +} + // AddClientHandler registers a client level handler func (e *eventExecutor) AddClientHandler(domain string, t EventType, c EventCallback) { e.mu.Lock() @@ -123,21 +144,45 @@ func (e *eventExecutor) AddClientHandler(domain string, t EventType, c EventCall e.emitOnRegistration(domain, reference, t, c) } +// isDomainHandlerRegistered checks if a handler is already registered for event type for this domain. +func (e *eventExecutor) isDomainHandlerRegistered(domain string, t EventType, c EventCallback) bool { + e.mu.Lock() + defer e.mu.Unlock() + + registry, ok := e.scopedRegistry[domain] + if !ok { + // nothing to remove + return false + } + // Get the unique pointer/address of the function to check (c) + targetPtr := reflect.ValueOf(c).Pointer() + + for _, f := range registry.callbacks[t] { + if reflect.ValueOf(f).Pointer() == targetPtr { + return true + } + } + return false +} + // RemoveClientHandler removes a client level handler func (e *eventExecutor) RemoveClientHandler(domain string, t EventType, c EventCallback) { e.mu.Lock() defer e.mu.Unlock() - _, ok := e.scopedRegistry[domain] + registry, ok := e.scopedRegistry[domain] if !ok { // nothing to remove return } - entrySlice := e.scopedRegistry[domain].callbacks[t] + entrySlice := registry.callbacks[t] + targetPtr := reflect.ValueOf(c).Pointer() e.scopedRegistry[domain].callbacks[t] = slices.DeleteFunc(entrySlice, func(f EventCallback) bool { - return f == c + // Compare the unique pointer/address of the slice element (f) + // against the target pointer. + return reflect.ValueOf(f).Pointer() == targetPtr }) } @@ -167,7 +212,7 @@ func (e *eventExecutor) emitOnRegistration(domain string, providerReference prov } if message != "" { - (*callback)(EventDetails{ + callback(EventDetails{ ProviderName: providerReference.featureProvider.Metadata().Name, ProviderEventDetails: ProviderEventDetails{ Message: message, @@ -289,7 +334,7 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { // first run API handlers for _, c := range e.apiRegistry[event.EventType] { - e.executeHandler(*c, event) + e.executeHandler(c, event) } // then run client handlers @@ -300,7 +345,7 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { e.states.Store(domain, stateFromEvent(event)) for _, c := range e.scopedRegistry[domain].callbacks[event.EventType] { - e.executeHandler(*c, event) + e.executeHandler(c, event) } } @@ -318,7 +363,7 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { } for _, c := range registry.callbacks[event.EventType] { - e.executeHandler(*c, event) + e.executeHandler(c, event) } } } diff --git a/openfeature/event_executor_test.go b/event_executor_test.go similarity index 82% rename from openfeature/event_executor_test.go rename to event_executor_test.go index 3e2a2030..0ac726b8 100644 --- a/openfeature/event_executor_test.go +++ b/event_executor_test.go @@ -1,6 +1,7 @@ package openfeature import ( + "context" "errors" "reflect" "slices" @@ -11,9 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func init() { -} - // Requirement 5.1.1 The provider MAY define a mechanism for signaling the occurrence of one of a set of events, // including PROVIDER_READY, PROVIDER_ERROR, PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE, // with a provider event details payload. @@ -68,7 +66,7 @@ func TestEventHandler_Eventing(t *testing.T) { eventingImpl, } - err := SetProvider(eventingProvider) + err := SetProvider(t.Context(), eventingProvider) if err != nil { t.Fatal(err) } @@ -79,7 +77,7 @@ func TestEventHandler_Eventing(t *testing.T) { } eventType := ProviderConfigChange - AddHandler(eventType, &callBack) + AddHandler(eventType, callBack) fCh := []string{"flagA"} meta := map[string]any{ @@ -136,7 +134,7 @@ func TestEventHandler_Eventing(t *testing.T) { // associated to client domain associatedName := "providerForClient" - err := SetNamedProviderAndWait(associatedName, eventingProvider) + err := SetProviderAndWait(t.Context(), eventingProvider, WithDomain(associatedName)) if err != nil { t.Fatal(err) } @@ -146,8 +144,8 @@ func TestEventHandler_Eventing(t *testing.T) { rsp <- details } - client := NewClient(associatedName) - client.AddHandler(ProviderError, &callBack) + client := NewClient(WithDomain(associatedName)) + client.AddHandler(ProviderError, callBack) fCh := []string{"flagA"} meta := map[string]any{ @@ -211,21 +209,16 @@ func TestEventHandler_clientAssociation(t *testing.T) { } // default provider - err := SetProviderAndWait(defaultProvider) + err := SetProviderAndWait(t.Context(), defaultProvider) if err != nil { t.Fatal(err) } // named provider(associated to domain someClient) - err = SetNamedProviderAndWait("someClient", struct { + err = SetProviderAndWait(t.Context(), struct { FeatureProvider EventHandler - }{ - NoopProvider{}, - &ProviderEventing{ - c: make(chan Event, 1), - }, - }) + }{NoopProvider{}, &ProviderEventing{c: make(chan Event, 1)}}, WithDomain("someClient")) if err != nil { t.Fatal(err) } @@ -236,8 +229,8 @@ func TestEventHandler_clientAssociation(t *testing.T) { } event := ProviderError - client := NewClient("someClient") - client.AddHandler(event, &callBack) + client := NewClient(WithDomain("someClient")) + client.AddHandler(event, callBack) // invoke default provider eventingImpl.Invoke(Event{ @@ -284,7 +277,7 @@ func TestEventHandler_ErrorHandling(t *testing.T) { rspClient <- e } - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -292,17 +285,17 @@ func TestEventHandler_ErrorHandling(t *testing.T) { successEventType := ProviderStale // api level handlers - AddHandler(ProviderConfigChange, &failingCallback) - AddHandler(successEventType, &successAPICallback) + AddHandler(ProviderConfigChange, failingCallback) + AddHandler(successEventType, successAPICallback) // provider association providerName := "providerA" - client := NewClient(providerName) + client := NewClient(WithDomain(providerName)) // client level handlers - client.AddHandler(ProviderConfigChange, &failingCallback) - client.AddHandler(successEventType, &successClientCallback) + client.AddHandler(ProviderConfigChange, failingCallback) + client.AddHandler(successEventType, successClientCallback) // trigger events manually go func() { @@ -351,8 +344,8 @@ func TestEventHandler_InitOfProvider(t *testing.T) { rsp <- e } - AddHandler(ProviderReady, &callback) - err := SetProvider(provider) + AddHandler(ProviderReady, callback) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -386,9 +379,9 @@ func TestEventHandler_InitOfProvider(t *testing.T) { rsp <- e } - client := NewClient("clientWithNoProviderAssociation") - client.AddHandler(ProviderReady, &callback) - err := SetProvider(provider) + client := NewClient(WithDomain("clientWithNoProviderAssociation")) + client.AddHandler(ProviderReady, callback) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -422,10 +415,10 @@ func TestEventHandler_InitOfProvider(t *testing.T) { rsp <- e } - client := NewClient("someClient") - client.AddHandler(ProviderReady, &callback) + client := NewClient(WithDomain("someClient")) + client.AddHandler(ProviderReady, callback) - err := SetNamedProvider("someClient", provider) + err := SetProvider(t.Context(), provider, WithDomain("someClient")) if err != nil { t.Fatal(err) } @@ -460,10 +453,10 @@ func TestEventHandler_InitOfProvider(t *testing.T) { rsp <- e } - client := NewClient("someClient") - client.AddHandler(ProviderReady, &callback) + client := NewClient(WithDomain("someClient")) + client.AddHandler(ProviderReady, callback) - err := SetNamedProvider("providerWithoutClient", provider) + err := SetProvider(t.Context(), provider, WithDomain("providerWithoutClient")) if err != nil { t.Fatal(err) } @@ -501,8 +494,8 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { rsp <- e } - AddHandler(ProviderError, &callback) - err := SetProvider(provider) + AddHandler(ProviderError, callback) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -537,10 +530,10 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { rsp <- e } - client := NewClient("clientWithNoProviderAssociation") - client.AddHandler(ProviderError, &callback) + client := NewClient(WithDomain("clientWithNoProviderAssociation")) + client.AddHandler(ProviderError, callback) - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -575,10 +568,10 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { rsp <- e } - client := NewClient("someClient") - client.AddHandler(ProviderError, &callback) + client := NewClient(WithDomain("someClient")) + client.AddHandler(ProviderError, callback) - err := SetNamedProvider("someClient", provider) + err := SetProvider(t.Context(), provider, WithDomain("someClient")) if err != nil { t.Fatal(err) } @@ -613,10 +606,10 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { rsp <- e } - client := NewClient("provider") - client.AddHandler(ProviderError, &callback) + client := NewClient(WithDomain("provider")) + client.AddHandler(ProviderError, callback) - err := SetNamedProvider("someClient", provider) + err := SetProvider(t.Context(), provider, WithDomain("someClient")) if err != nil { t.Fatal(err) } @@ -647,7 +640,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { eventingImpl, } - err := SetProvider(readyEventingProvider) + err := SetProvider(t.Context(), readyEventingProvider) if err != nil { t.Fatal(err) } @@ -657,7 +650,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { rsp <- e } - AddHandler(ProviderReady, &callback) + AddHandler(ProviderReady, callback) select { case <-rsp: @@ -683,7 +676,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { } clientAssociation := "clientA" - err := SetNamedProviderAndWait(clientAssociation, readyEventingProvider) + err := SetProviderAndWait(t.Context(), readyEventingProvider, WithDomain(clientAssociation)) if err != nil { t.Fatal(err) } @@ -694,7 +687,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { } client := api.GetNamedClient(clientAssociation) - client.AddHandler(ProviderReady, &callback) + client.AddHandler(ProviderReady, callback) select { case <-rsp: @@ -719,7 +712,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { eventingImpl, } - err := SetProviderAndWait(readyEventingProvider) + err := SetProviderAndWait(t.Context(), readyEventingProvider) if err != nil { t.Fatal(err) } @@ -730,7 +723,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { } client := api.GetNamedClient("someClient") - client.AddHandler(ProviderReady, &callback) + client.AddHandler(ProviderReady, callback) select { case <-rsp: @@ -750,14 +743,14 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return errors.New("some error from initialization") }, }, &ProviderEventing{}, } - err := SetProvider(notReadyEventingProvider) + err := SetProvider(t.Context(), notReadyEventingProvider) if err != nil { t.Fatal(err) } @@ -768,7 +761,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { } client := api.GetNamedClient("someClient") - client.AddHandler(ProviderReady, &callback) + client.AddHandler(ProviderReady, callback) select { case <-rsp: @@ -788,14 +781,14 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { return nil }, }, &ProviderEventing{}, } - err := SetProvider(readyEventingProvider) + err := SetProvider(t.Context(), readyEventingProvider) if err != nil { t.Fatal(err) } @@ -805,8 +798,8 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { rsp <- e } - client := NewClient("someClient") - client.AddHandler(ProviderError, &callback) + client := NewClient(WithDomain("someClient")) + client.AddHandler(ProviderError, callback) select { case <-rsp: @@ -835,7 +828,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProvider(provider); err != nil { + if err := SetProvider(t.Context(), provider); err != nil { t.Fatal(err) } @@ -844,7 +837,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderReady, &callback) + AddHandler(ProviderReady, callback) select { case <-rsp: @@ -870,7 +863,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProvider(provider); err != nil { + if err := SetProvider(t.Context(), provider); err != nil { t.Fatal(err) } @@ -879,7 +872,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderError, &callback) + AddHandler(ProviderError, callback) select { case <-rsp: @@ -905,7 +898,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProvider(provider); err != nil { + if err := SetProvider(t.Context(), provider); err != nil { t.Fatal(err) } @@ -914,7 +907,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderStale, &callback) + AddHandler(ProviderStale, callback) select { case <-rsp: @@ -939,7 +932,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProvider(provider); err != nil { + if err := SetProvider(t.Context(), provider); err != nil { t.Fatal(err) } @@ -948,9 +941,9 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderError, &callback) - AddHandler(ProviderStale, &callback) - AddHandler(ProviderConfigChange, &callback) + AddHandler(ProviderError, callback) + AddHandler(ProviderStale, callback) + AddHandler(ProviderConfigChange, callback) select { case <-rsp: @@ -976,7 +969,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProviderAndWait(provider); err != nil { + if err := SetProviderAndWait(t.Context(), provider); err != nil { t.Fatal(err) } @@ -985,14 +978,14 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderReady, &callback) + AddHandler(ProviderReady, callback) <-rsp // ignore first READY event which gets emitted after registration - AddHandler(ProviderStale, &callback) - AddHandler(ProviderConfigChange, &callback) + AddHandler(ProviderStale, callback) + AddHandler(ProviderConfigChange, callback) // assert client transitioned to ERROR eventually(t, func() bool { - return NewDefaultClient().State() == ErrorState + return NewClient().State() == ErrorState }, time.Second, time.Millisecond*100, "") select { @@ -1019,7 +1012,7 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { eventingImpl, } - if err := SetProviderAndWait(provider); err != nil { + if err := SetProviderAndWait(t.Context(), provider); err != nil { t.Fatal(err) } @@ -1028,14 +1021,14 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { rsp <- e } - AddHandler(ProviderReady, &callback) + AddHandler(ProviderReady, callback) <-rsp // ignore first READY event which gets emitted after registration - AddHandler(ProviderError, &callback) - AddHandler(ProviderConfigChange, &callback) + AddHandler(ProviderError, callback) + AddHandler(ProviderConfigChange, callback) // assert client transitioned to STALE eventually(t, func() bool { - return NewDefaultClient().State() == StaleState + return NewClient().State() == StaleState }, time.Second, time.Millisecond*100, "") select { @@ -1077,17 +1070,17 @@ func TestEventHandler_multiSubs(t *testing.T) { } // register for default and named providers - err := SetProvider(eventingProvider) + err := SetProvider(t.Context(), eventingProvider) if err != nil { t.Fatal(err) } - err = SetNamedProvider("clientA", eventingProvideOther) + err = SetProvider(t.Context(), eventingProvideOther, WithDomain("clientA")) if err != nil { t.Fatal(err) } - err = SetNamedProvider("clientB", eventingProvideOther) + err = SetProvider(t.Context(), eventingProvideOther, WithDomain("clientB")) if err != nil { t.Fatal(err) } @@ -1098,23 +1091,23 @@ func TestEventHandler_multiSubs(t *testing.T) { rspGlobal <- e } - AddHandler(ProviderStale, &globalF) + AddHandler(ProviderStale, globalF) rspClientA := make(chan EventDetails, 1) callbackA := func(e EventDetails) { rspClientA <- e } - clientA := NewClient("clientA") - clientA.AddHandler(ProviderStale, &callbackA) + clientA := NewClient(WithDomain("clientA")) + clientA.AddHandler(ProviderStale, callbackA) rspClientB := make(chan EventDetails, 1) callbackB := func(e EventDetails) { rspClientB <- e } - clientB := NewClient("clientB") - clientB.AddHandler(ProviderStale, &callbackB) + clientB := NewClient(WithDomain("clientB")) + clientB.AddHandler(ProviderStale, callbackB) emitDone := make(chan any) eventCount := 5 @@ -1355,18 +1348,18 @@ func TestEventHandler_Registration(t *testing.T) { executor := newEventExecutor() // Add multiple - ProviderReady - executor.AddHandler(ProviderReady, &h1) - executor.AddHandler(ProviderReady, &h2) - executor.AddHandler(ProviderReady, &h3) - executor.AddHandler(ProviderReady, &h4) + executor.AddHandler(ProviderReady, h1) + executor.AddHandler(ProviderReady, h2) + executor.AddHandler(ProviderReady, h3) + executor.AddHandler(ProviderReady, h4) // Add multiple - ProviderError - executor.AddHandler(ProviderError, &h2) - executor.AddHandler(ProviderError, &h2) + executor.AddHandler(ProviderError, h2) + executor.AddHandler(ProviderError, h2) // Add single types - executor.AddHandler(ProviderStale, &h3) - executor.AddHandler(ProviderConfigChange, &h4) + executor.AddHandler(ProviderStale, h3) + executor.AddHandler(ProviderConfigChange, h4) readyLen := len(executor.apiRegistry[ProviderReady]) if readyLen != 4 { @@ -1393,15 +1386,15 @@ func TestEventHandler_Registration(t *testing.T) { executor := newEventExecutor() // Add multiple - client a - executor.AddClientHandler("a", ProviderReady, &h1) - executor.AddClientHandler("a", ProviderReady, &h2) - executor.AddClientHandler("a", ProviderReady, &h3) - executor.AddClientHandler("a", ProviderReady, &h4) + executor.AddClientHandler("a", ProviderReady, h1) + executor.AddClientHandler("a", ProviderReady, h2) + executor.AddClientHandler("a", ProviderReady, h3) + executor.AddClientHandler("a", ProviderReady, h4) // Add single for rest of the client - executor.AddClientHandler("b", ProviderError, &h2) - executor.AddClientHandler("c", ProviderStale, &h3) - executor.AddClientHandler("d", ProviderConfigChange, &h4) + executor.AddClientHandler("b", ProviderError, h2) + executor.AddClientHandler("c", ProviderStale, h3) + executor.AddClientHandler("d", ProviderConfigChange, h4) readyLen := len(executor.scopedRegistry["a"].callbacks[ProviderReady]) if readyLen != 4 { @@ -1430,36 +1423,36 @@ func TestEventHandler_APIRemoval(t *testing.T) { executor := newEventExecutor() // Add multiple - ProviderReady - executor.AddHandler(ProviderReady, &h1) - executor.AddHandler(ProviderReady, &h2) - executor.AddHandler(ProviderReady, &h3) - executor.AddHandler(ProviderReady, &h4) + executor.AddHandler(ProviderReady, h1) + executor.AddHandler(ProviderReady, h2) + executor.AddHandler(ProviderReady, h3) + executor.AddHandler(ProviderReady, h4) // Add single types - executor.AddHandler(ProviderError, &h2) - executor.AddHandler(ProviderStale, &h3) - executor.AddHandler(ProviderConfigChange, &h4) + executor.AddHandler(ProviderError, h2) + executor.AddHandler(ProviderStale, h3) + executor.AddHandler(ProviderConfigChange, h4) // removal - executor.RemoveHandler(ProviderReady, &h1) - executor.RemoveHandler(ProviderError, &h2) - executor.RemoveHandler(ProviderStale, &h3) - executor.RemoveHandler(ProviderConfigChange, &h4) + executor.RemoveHandler(ProviderReady, h1) + executor.RemoveHandler(ProviderError, h2) + executor.RemoveHandler(ProviderStale, h3) + executor.RemoveHandler(ProviderConfigChange, h4) readyLen := len(executor.apiRegistry[ProviderReady]) if readyLen != 3 { t.Errorf("expected %d events, but got %d", 3, readyLen) } - if !slices.Contains(executor.apiRegistry[ProviderReady], EventCallback(&h2)) { + if !executor.isHandlerRegistered(ProviderReady, h2) { t.Errorf("expected callback to be present") } - if !slices.Contains(executor.apiRegistry[ProviderReady], EventCallback(&h3)) { + if !executor.isHandlerRegistered(ProviderReady, h3) { t.Errorf("expected callback to be present") } - if !slices.Contains(executor.apiRegistry[ProviderReady], EventCallback(&h4)) { + if !executor.isHandlerRegistered(ProviderReady, h4) { t.Errorf("expected callback to be present") } @@ -1483,36 +1476,35 @@ func TestEventHandler_APIRemoval(t *testing.T) { executor := newEventExecutor() // Add multiple - client a - executor.AddClientHandler("a", ProviderReady, &h1) - executor.AddClientHandler("a", ProviderReady, &h2) - executor.AddClientHandler("a", ProviderReady, &h3) - executor.AddClientHandler("a", ProviderReady, &h4) + executor.AddClientHandler("a", ProviderReady, h1) + executor.AddClientHandler("a", ProviderReady, h2) + executor.AddClientHandler("a", ProviderReady, h3) + executor.AddClientHandler("a", ProviderReady, h4) // Add single - executor.AddClientHandler("b", ProviderError, &h2) - executor.AddClientHandler("c", ProviderStale, &h3) - executor.AddClientHandler("d", ProviderConfigChange, &h4) + executor.AddClientHandler("b", ProviderError, h2) + executor.AddClientHandler("c", ProviderStale, h3) + executor.AddClientHandler("d", ProviderConfigChange, h4) // removal - executor.RemoveClientHandler("a", ProviderReady, &h1) - executor.RemoveClientHandler("b", ProviderError, &h2) - executor.RemoveClientHandler("c", ProviderStale, &h3) - executor.RemoveClientHandler("d", ProviderConfigChange, &h4) + executor.RemoveClientHandler("a", ProviderReady, h1) + executor.RemoveClientHandler("b", ProviderError, h2) + executor.RemoveClientHandler("c", ProviderStale, h3) + executor.RemoveClientHandler("d", ProviderConfigChange, h4) readyLen := len(executor.scopedRegistry["a"].callbacks[ProviderReady]) if readyLen != 3 { t.Errorf("expected %d events in client a, but got %d", 3, readyLen) } - - if !slices.Contains(executor.scopedRegistry["a"].callbacks[ProviderReady], EventCallback(&h2)) { + if !executor.isDomainHandlerRegistered("a", ProviderReady, h2) { t.Errorf("expected callback to be present") } - if !slices.Contains(executor.scopedRegistry["a"].callbacks[ProviderReady], EventCallback(&h3)) { + if !executor.isDomainHandlerRegistered("a", ProviderReady, h3) { t.Errorf("expected callback to be present") } - if !slices.Contains(executor.scopedRegistry["a"].callbacks[ProviderReady], EventCallback(&h4)) { + if !executor.isDomainHandlerRegistered("a", ProviderReady, h4) { t.Errorf("expected callback to be present") } @@ -1532,16 +1524,16 @@ func TestEventHandler_APIRemoval(t *testing.T) { } // removal referenced to non-existing clients does nothing & no panics - executor.RemoveClientHandler("non-existing", ProviderReady, &h1) - executor.RemoveClientHandler("b", ProviderReady, &h1) + executor.RemoveClientHandler("non-existing", ProviderReady, h1) + executor.RemoveClientHandler("b", ProviderReady, h1) }) t.Run("remove handlers that were not added", func(t *testing.T) { executor := newEventExecutor() // removal of non-added handlers shall not panic - executor.RemoveHandler(ProviderReady, &h1) - executor.RemoveClientHandler("a", ProviderReady, &h1) + executor.RemoveHandler(ProviderReady, h1) + executor.RemoveClientHandler("a", ProviderReady, h1) }) } @@ -1568,9 +1560,9 @@ func TestEventHandler_ChannelClosure(t *testing.T) { callBack := func(e EventDetails) { eventCount.Add(1) } - executor.AddHandler(ProviderReady, &callBack) + executor.AddHandler(ProviderReady, callBack) // watch for empty events - executor.AddHandler("", &callBack) + executor.AddHandler("", callBack) eventingImpl.Invoke(Event{EventType: ProviderReady}) require.Eventually(t, func() bool { diff --git a/openfeature/example_context_test.go b/example_context_test.go similarity index 65% rename from openfeature/example_context_test.go rename to example_context_test.go index 2652dee9..90e415c0 100644 --- a/openfeature/example_context_test.go +++ b/example_context_test.go @@ -6,18 +6,18 @@ import ( "log" "time" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) -// ExampleSetProviderWithContext demonstrates asynchronous provider setup with timeout control. -func ExampleSetProviderWithContext() { +// ExampleSetProvider demonstrates asynchronous provider setup with timeout control. +func ExampleSetProvider() { // Create a test provider for demonstration provider := &openfeature.NoopProvider{} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - err := openfeature.SetProviderWithContext(ctx, provider) + err := openfeature.SetProvider(ctx, provider) if err != nil { log.Printf("Failed to start provider setup: %v", err) return @@ -28,15 +28,15 @@ func ExampleSetProviderWithContext() { // Output: Provider setup initiated } -// ExampleSetProviderWithContextAndWait demonstrates synchronous provider setup with error handling. -func ExampleSetProviderWithContextAndWait() { +// ExampleSetProviderAndWait demonstrates synchronous provider setup with error handling. +func ExampleSetProviderAndWait() { // Create a test provider for demonstration provider := &openfeature.NoopProvider{} ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - err := openfeature.SetProviderWithContextAndWait(ctx, provider) + err := openfeature.SetProviderAndWait(ctx, provider) if err != nil { log.Printf("Provider initialization failed: %v", err) return @@ -47,8 +47,8 @@ func ExampleSetProviderWithContextAndWait() { // Output: Provider is ready } -// ExampleSetNamedProviderWithContext demonstrates multi-tenant provider setup. -func ExampleSetNamedProviderWithContext() { +// ExampleSetProvider_withDomain multi-tenant provider setup. +func ExampleSetProvider_withDomain() { // Create test providers for different services userProvider := &openfeature.NoopProvider{} billingProvider := &openfeature.NoopProvider{} @@ -56,21 +56,21 @@ func ExampleSetNamedProviderWithContext() { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - err := openfeature.SetNamedProviderWithContext(ctx, "user-service", userProvider) + err := openfeature.SetProvider(ctx, userProvider, openfeature.WithDomain("user-service")) if err != nil { log.Printf("Failed to setup user service provider: %v", err) return } - err = openfeature.SetNamedProviderWithContext(ctx, "billing-service", billingProvider) + err = openfeature.SetProvider(ctx, billingProvider, openfeature.WithDomain("billing-service")) if err != nil { log.Printf("Failed to setup billing service provider: %v", err) return } // Create clients for different domains - userClient := openfeature.NewClient("user-service") - billingClient := openfeature.NewClient("billing-service") + userClient := openfeature.NewClient(openfeature.WithDomain("user-service")) + billingClient := openfeature.NewClient(openfeature.WithDomain("billing-service")) fmt.Printf("User client domain: %s\n", userClient.Metadata().Domain()) fmt.Printf("Billing client domain: %s\n", billingClient.Metadata().Domain()) @@ -78,8 +78,8 @@ func ExampleSetNamedProviderWithContext() { // Billing client domain: billing-service } -// ExampleSetNamedProviderWithContextAndWait demonstrates critical service provider setup. -func ExampleSetNamedProviderWithContextAndWait() { +// ExampleSetProviderAndWait_withDomain critical service provider setup. +func ExampleSetProviderAndWait_withDomain() { // Create a test provider for demonstration criticalProvider := &openfeature.NoopProvider{} @@ -87,22 +87,22 @@ func ExampleSetNamedProviderWithContextAndWait() { defer cancel() // Wait for critical providers to be ready - err := openfeature.SetNamedProviderWithContextAndWait(ctx, "critical-service", criticalProvider) + err := openfeature.SetProviderAndWait(ctx, criticalProvider, openfeature.WithDomain("critical-service")) if err != nil { log.Printf("Critical provider failed to initialize: %v", err) return } // Now safe to use the client - client := openfeature.NewClient("critical-service") - enabled, _ := client.BooleanValue(context.Background(), "feature-x", false, openfeature.EvaluationContext{}) + client := openfeature.NewClient(openfeature.WithDomain("critical-service")) + enabled := client.Boolean(context.Background(), "feature-x", false, openfeature.EvaluationContext{}) fmt.Printf("Critical service ready, feature-x enabled: %v\n", enabled) // Output: Critical service ready, feature-x enabled: false } -// ExampleContextAwareStateHandler demonstrates how context-aware shutdown works automatically. -func ExampleContextAwareStateHandler() { +// ExampleStateHandler demonstrates how context-aware shutdown works automatically. +func ExampleStateHandler() { // Context-aware providers automatically use ShutdownWithContext when replaced provider1 := &openfeature.NoopProvider{} provider2 := &openfeature.NoopProvider{} @@ -111,14 +111,14 @@ func ExampleContextAwareStateHandler() { defer cancel() // Set first provider - err := openfeature.SetProviderWithContextAndWait(ctx, provider1) + err := openfeature.SetProviderAndWait(ctx, provider1) if err != nil { log.Printf("Provider setup failed: %v", err) return } // Replace with second provider - this triggers context-aware shutdown of provider1 if it supports it - err = openfeature.SetProviderWithContextAndWait(ctx, provider2) + err = openfeature.SetProviderAndWait(ctx, provider2) if err != nil { log.Printf("Provider replacement failed: %v", err) return @@ -128,8 +128,8 @@ func ExampleContextAwareStateHandler() { // Output: Context-aware provider lifecycle completed } -// ExampleShutdownWithContext demonstrates graceful application shutdown with timeout control. -func ExampleShutdownWithContext() { +// ExampleShutdown demonstrates graceful application shutdown with timeout control. +func ExampleShutdown() { // Set up providers provider1 := &openfeature.NoopProvider{} provider2 := &openfeature.NoopProvider{} @@ -138,13 +138,13 @@ func ExampleShutdownWithContext() { defer cancel() // Set up multiple providers - err := openfeature.SetProviderWithContextAndWait(ctx, provider1) + err := openfeature.SetProviderAndWait(ctx, provider1) if err != nil { log.Printf("Provider setup failed: %v", err) return } - err = openfeature.SetNamedProviderWithContextAndWait(ctx, "service-a", provider2) + err = openfeature.SetProviderAndWait(ctx, provider2, openfeature.WithDomain("service-a")) if err != nil { log.Printf("Named provider setup failed: %v", err) return @@ -156,7 +156,7 @@ func ExampleShutdownWithContext() { shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) defer shutdownCancel() - err = openfeature.ShutdownWithContext(shutdownCtx) + err = openfeature.Shutdown(shutdownCtx) if err != nil { log.Printf("Shutdown completed with errors: %v", err) } else { diff --git a/go.mod b/go.mod index 09afd4bf..afca89b3 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ -module github.com/open-feature/go-sdk +module go.openfeature.dev/openfeature/v2 -go 1.24.0 +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/mock v0.6.0 golang.org/x/sync v0.18.0 diff --git a/go.sum b/go.sum index a4b6edff..13844aa7 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/hooks.go b/hooks.go similarity index 82% rename from openfeature/hooks.go rename to hooks.go index e0ac03c0..2095a508 100644 --- a/openfeature/hooks.go +++ b/hooks.go @@ -6,10 +6,10 @@ import "context" // 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 + Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) + After(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) error Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) - Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) + Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) } // HookHints contains a map of hints for hooks @@ -24,7 +24,7 @@ func NewHookHints(mapOfHints map[string]any) HookHints { // Value returns the value at the given key in the underlying map. // Maintains immutability of the map. -func (h HookHints) Value(key string) any { +func (h HookHints) Value(key string) FlagTypes { return h.mapOfHints[key] } @@ -32,7 +32,7 @@ func (h HookHints) Value(key string) any { type HookContext struct { flagKey string flagType Type - defaultValue any + defaultValue FlagTypes clientMetadata ClientMetadata providerMetadata Metadata evaluationContext EvaluationContext @@ -49,7 +49,7 @@ func (h HookContext) FlagType() Type { } // DefaultValue returns the hook context's default value -func (h HookContext) DefaultValue() any { +func (h HookContext) DefaultValue() FlagTypes { return h.defaultValue } @@ -73,7 +73,7 @@ func (h HookContext) EvaluationContext() EvaluationContext { func NewHookContext( flagKey string, flagType Type, - defaultValue any, + defaultValue FlagTypes, clientMetadata ClientMetadata, providerMetadata Metadata, evaluationContext EvaluationContext, @@ -100,15 +100,15 @@ var _ Hook = UnimplementedHook{} // } type UnimplementedHook struct{} -func (UnimplementedHook) Before(context.Context, HookContext, HookHints) (*EvaluationContext, error) { - return nil, nil +func (UnimplementedHook) Before(ctx context.Context, _ HookContext, _ HookHints) (context.Context, error) { + return ctx, nil } -func (UnimplementedHook) After(context.Context, HookContext, InterfaceEvaluationDetails, HookHints) error { +func (UnimplementedHook) After(context.Context, HookContext, HookEvaluationDetails, HookHints) error { return nil } func (UnimplementedHook) Error(context.Context, HookContext, error, HookHints) {} -func (UnimplementedHook) Finally(context.Context, HookContext, InterfaceEvaluationDetails, HookHints) { +func (UnimplementedHook) Finally(context.Context, HookContext, HookEvaluationDetails, HookHints) { } diff --git a/openfeature/hooks/doc.go b/hooks/doc.go similarity index 100% rename from openfeature/hooks/doc.go rename to hooks/doc.go diff --git a/openfeature/hooks/logging_hook.go b/hooks/logging_hook.go similarity index 88% rename from openfeature/hooks/logging_hook.go rename to hooks/logging_hook.go index 9f75a8ae..fa3ab0ec 100644 --- a/openfeature/hooks/logging_hook.go +++ b/hooks/logging_hook.go @@ -4,7 +4,7 @@ import ( "context" "log/slog" - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" ) const ( @@ -28,6 +28,8 @@ type LoggingHook struct { logger *slog.Logger } +var _ of.Hook = (*LoggingHook)(nil) + // NewLoggingHook returns a new [LoggingHook] with the provided logger. func NewLoggingHook(includeEvaluationContext bool, logger *slog.Logger) *LoggingHook { return &LoggingHook{ @@ -54,15 +56,15 @@ func (h *LoggingHook) buildArgs(hookContext of.HookContext) []slog.Attr { return args } -func (h *LoggingHook) Before(ctx context.Context, hookContext of.HookContext, hookHints of.HookHints) (*of.EvaluationContext, error) { +func (h *LoggingHook) Before(ctx context.Context, hookContext of.HookContext, hookHints of.HookHints) (context.Context, error) { args := h.buildArgs(hookContext) args = append(args, slog.String(stageKey, "before")) h.logger.LogAttrs(ctx, slog.LevelDebug, "Before stage", args...) - return nil, nil + return ctx, nil } func (h *LoggingHook) After(ctx context.Context, hookContext of.HookContext, - flagEvaluationDetails of.InterfaceEvaluationDetails, hookHints of.HookHints, + flagEvaluationDetails of.EvaluationDetails[of.FlagTypes], hookHints of.HookHints, ) error { args := h.buildArgs(hookContext) args = append(args, @@ -84,5 +86,5 @@ func (h *LoggingHook) Error(ctx context.Context, hookContext of.HookContext, err h.logger.LogAttrs(ctx, slog.LevelError, "Error stage", args...) } -func (h *LoggingHook) Finally(ctx context.Context, hookContext of.HookContext, flagEvaluationDetails of.InterfaceEvaluationDetails, hookHints of.HookHints) { +func (h *LoggingHook) Finally(ctx context.Context, hookContext of.HookContext, flagEvaluationDetails of.EvaluationDetails[of.FlagTypes], hookHints of.HookHints) { } diff --git a/openfeature/hooks/logging_hook_test.go b/hooks/logging_hook_test.go similarity index 91% rename from openfeature/hooks/logging_hook_test.go rename to hooks/logging_hook_test.go index fdc95cba..09c2430c 100644 --- a/openfeature/hooks/logging_hook_test.go +++ b/hooks/logging_hook_test.go @@ -7,8 +7,8 @@ import ( "os" "testing" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) func TestCreateLoggingHookWithDefaultLoggerAndContextInclusion(t *testing.T) { @@ -70,7 +70,7 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger t.Errorf("Expected logger to be %v, got %v", logger, hook.logger) } - memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ + memoryProvider := memprovider.NewProvider(map[string]memprovider.InMemoryFlag{ "boolFlag": { Key: "boolFlag", State: memprovider.Enabled, @@ -83,15 +83,15 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger }, }) - err := openfeature.SetNamedProviderAndWait("test-app", memoryProvider) + err := openfeature.SetProviderAndWait(t.Context(), memoryProvider, openfeature.WithDomain("test-app")) if err != nil { t.Error("error setting provider", err) } openfeature.AddHooks(&hook) - client := openfeature.NewClient("test-app") + client := openfeature.NewClient(openfeature.WithDomain("test-app")) t.Run("test boolean success", func(t *testing.T) { - res, err := client.BooleanValue( + res := client.Boolean( t.Context(), "boolFlag", false, @@ -102,9 +102,6 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger }, ), ) - if err != nil { - t.Error("expected nil error") - } if res != true { t.Errorf("incorrect evaluation, expected %t, got %t", true, res) } @@ -129,7 +126,7 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger }) t.Run("test boolean error", func(t *testing.T) { - res, err := client.BooleanValue( + res := client.Boolean( t.Context(), "non-existing", false, @@ -140,9 +137,6 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger }, ), ) - if err == nil { - t.Error("expected error") - } if res != false { t.Errorf("incorrect evaluation, expected %t, got %t", false, res) } diff --git a/openfeature/hooks_test.go b/hooks_test.go similarity index 83% rename from openfeature/hooks_test.go rename to hooks_test.go index 36410bfa..c8e55d6c 100644 --- a/openfeature/hooks_test.go +++ b/hooks_test.go @@ -152,10 +152,10 @@ func TestRequirement_4_3_2(t *testing.T) { t.Run("before stage MUST run before flag resolution occurs", func(t *testing.T) { mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -163,7 +163,7 @@ func TestRequirement_4_3_2(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() mockProvider.EXPECT().Hooks().AnyTimes() @@ -173,7 +173,7 @@ func TestRequirement_4_3_2(t *testing.T) { mockHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -183,7 +183,7 @@ func TestRequirement_4_3_2(t *testing.T) { mockHook := NewMockHook(ctrl) type requirement interface { - Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) + Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) } var hookI any = mockHook @@ -198,17 +198,17 @@ func TestRequirement_4_3_3(t *testing.T) { t.Cleanup(initSingleton) ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) flagKey := "foo" defaultValue := "bar" @@ -228,8 +228,9 @@ func TestRequirement_4_3_3(t *testing.T) { providerMetadata: mockProvider.Metadata(), evaluationContext: evalCtx, } - hook1EvalCtxResult := &EvaluationContext{targetingKey: "mockHook1"} - mockHook1.EXPECT().Before(gomock.Any(), hook1Ctx, gomock.Any()).Return(hook1EvalCtxResult, nil) + hook1EvalCtxResult := EvaluationContext{targetingKey: "mockHook1"} + tctx := ContextWithEvaluationContext(t.Context(), hook1EvalCtxResult) + mockHook1.EXPECT().Before(gomock.Any(), hook1Ctx, gomock.Any()).Return(tctx, nil) mockProvider.EXPECT().StringEvaluation(gomock.Any(), flagKey, defaultValue, map[string]any{ "is": "a test", TargetingKey: "mockHook1", @@ -237,7 +238,7 @@ func TestRequirement_4_3_3(t *testing.T) { // assert that the evaluation context returned by the first hook is passed into the second hook hook2Ctx := hook1Ctx - hook2Ctx.evaluationContext = *hook1EvalCtxResult + hook2Ctx.evaluationContext = hook1EvalCtxResult mockHook2.EXPECT().Before(gomock.Any(), hook2Ctx, gomock.Any()) mockHook1.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) @@ -258,16 +259,16 @@ func TestRequirement_4_3_4(t *testing.T) { t.Cleanup(initSingleton) ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } mockHook := NewMockHook(ctrl) - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) apiEvalCtx := EvaluationContext{ attributes: map[string]any{ @@ -298,13 +299,14 @@ func TestRequirement_4_3_4(t *testing.T) { mockProvider.EXPECT().Hooks().AnyTimes() - hookEvalCtxResult := &EvaluationContext{ + hookEvalCtxResult := EvaluationContext{ attributes: map[string]any{ "key": "hook value", "multiplier": 3, }, } - mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(hookEvalCtxResult, nil) + tctx := ContextWithEvaluationContext(t.Context(), hookEvalCtxResult) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(tctx, nil) // assert that the EvaluationContext returned by Before hooks is merged with the invocation EvaluationContext expectedMergedContext := EvaluationContext{ @@ -316,7 +318,7 @@ func TestRequirement_4_3_4(t *testing.T) { "beatsClient": true, }, } - mockProvider.EXPECT().StringEvaluation(gomock.Any(), flagKey, defaultValue, flattenContext(expectedMergedContext)) + mockProvider.EXPECT().StringEvaluation(gomock.Any(), flagKey, defaultValue, expectedMergedContext.Flattened()) mockHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) @@ -335,10 +337,10 @@ func TestRequirement_4_3_5(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -346,7 +348,7 @@ func TestRequirement_4_3_5(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() mockProvider.EXPECT().Hooks().AnyTimes() @@ -356,7 +358,7 @@ func TestRequirement_4_3_5(t *testing.T) { After(mockProvider.EXPECT().StringEvaluation(gomock.Any(), flagKey, defaultValue, flatCtx)) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -366,7 +368,7 @@ func TestRequirement_4_3_5(t *testing.T) { mockHook := NewMockHook(ctrl) type requirement interface { - After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error + After(ctx context.Context, hookContext HookContext, flagEvaluationDetails EvaluationDetails[FlagTypes], hookHints HookHints) error } var hookI any = mockHook @@ -385,16 +387,16 @@ func TestRequirement_4_3_6(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("error hook MUST run when errors are encountered in the before stage", func(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -403,10 +405,10 @@ func TestRequirement_4_3_6(t *testing.T) { // assert that the Error hooks are executed after the failed Before hooks mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - After(mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced"))) + After(mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced"))) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -416,10 +418,10 @@ func TestRequirement_4_3_6(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -439,7 +441,7 @@ func TestRequirement_4_3_6(t *testing.T) { ) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -449,10 +451,10 @@ func TestRequirement_4_3_6(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -466,7 +468,7 @@ func TestRequirement_4_3_6(t *testing.T) { After(mockHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("forced"))) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -494,16 +496,16 @@ func TestRequirement_4_3_7(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("finally hook MUST run after the before & after stages", func(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -516,7 +518,7 @@ func TestRequirement_4_3_7(t *testing.T) { After(mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())) mockProvider.EXPECT().StringEvaluation(t.Context(), flagKey, defaultValue, flatCtx) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -526,22 +528,22 @@ func TestRequirement_4_3_7(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } mockProvider.EXPECT().Hooks().AnyTimes() - mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced")) // assert that the Finally hook runs after the Error stage mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). After(mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())) - _, err = NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -551,7 +553,7 @@ func TestRequirement_4_3_7(t *testing.T) { mockHook := NewMockHook(ctrl) type requirement interface { - Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) + Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails EvaluationDetails[FlagTypes], hookHints HookHints) } var hookI any = mockHook @@ -574,7 +576,7 @@ func TestRequirement_4_4_1(t *testing.T) { t.Run("client MUST have a method for registering hooks", func(t *testing.T) { mockHook := NewMockHook(ctrl) - client := NewClient("test") + client := NewClient(WithDomain("test")) client.AddHooks(mockHook) type requirement interface { @@ -588,7 +590,7 @@ func TestRequirement_4_4_1(t *testing.T) { }) t.Run("provider MUST have a method for registering hooks", func(t *testing.T) { - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) type requirement interface { Hooks() []Hook @@ -601,20 +603,20 @@ func TestRequirement_4_4_1(t *testing.T) { }) t.Run("invocation MUST have a method for registering hooks", func(t *testing.T) { - client := NewClient("test") + client := NewClient(WithDomain("test")) // EvaluationOptions contains the hooks registered at invocation type requirement interface { - 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 any, evalCtx EvaluationContext, options ...Option) (any, error) + Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool + String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string + Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 + Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 + Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any 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 any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) + ObjectValueDetails(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (ObjectEvaluationDetails, error) } var clientI any = client @@ -632,45 +634,49 @@ func TestRequirement_4_4_2(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("before, after & finally hooks MUST be evaluated in the following order", func(t *testing.T) { t.Cleanup(initSingleton) - mockAPIHook := NewMockHook(ctrl) - AddHooks(mockAPIHook) + mockAPIHook1 := NewMockHook(ctrl) + mockAPIHook2 := NewMockHook(ctrl) + AddHooks(mockAPIHook1, mockAPIHook2) mockClientHook := NewMockHook(ctrl) mockInvocationHook := NewMockHook(ctrl) mockProviderHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(1) - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) client.AddHooks(mockClientHook) // before: API, Client, Invocation, Provider mockProviderHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()). After(mockInvocationHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())). After(mockClientHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())). - After(mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())) + After(mockAPIHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())). + After(mockAPIHook2.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())) // after: Invocation, Client, API - mockAPIHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + mockAPIHook1.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + After(mockAPIHook2.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockClientHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockInvocationHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockProviderHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())) // finally: Invocation, Client, API - mockAPIHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + mockAPIHook1.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + After(mockAPIHook2.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockClientHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockInvocationHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())). After(mockProviderHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())) @@ -693,20 +699,20 @@ func TestRequirement_4_4_2(t *testing.T) { mockInvocationHook := NewMockHook(ctrl) mockProviderHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) client.AddHooks(mockClientHook) mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(1) - mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) + mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced")) // error: Provider, Invocation, Client, API mockAPIHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). @@ -751,7 +757,7 @@ func TestRequirement_4_4_6(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run( "if an error occurs during the evaluation of before hooks, any remaining before hooks MUST NOT be invoked", @@ -761,19 +767,19 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) mockProvider.EXPECT().Hooks().AnyTimes() - mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) + mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced")) // the lack of mockHook2.EXPECT().Before() asserts that remaining hooks aren't invoked after an error mockHook1.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook1.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) @@ -794,22 +800,22 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) mockProvider.EXPECT().Hooks().AnyTimes() mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()) mockHook2.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()) mockHook2.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("forced")) - // the lack of mockHook1.EXPECT().After() asserts that remaining hooks aren't invoked after an error + // the lack of mockHook2.EXPECT().After() asserts that remaining hooks aren't invoked after an error mockHook1.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook1.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook2.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) @@ -835,21 +841,21 @@ func TestRequirement_4_4_7(t *testing.T) { evalCtx := EvaluationContext{} mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } mockProvider.EXPECT().Hooks().AnyTimes() - mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced")) mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - res, err := NewClient(t.Name()).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + res, err := NewClient(WithDomain(t.Name())).StringValueDetails(t.Context(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -876,20 +882,20 @@ func TestRequirement_4_5_2(t *testing.T) { flagKey := "foo" defaultValue := "bar" evalCtx := EvaluationContext{} - flatCtx := flattenContext(evalCtx) + flatCtx := evalCtx.Flattened() t.Run("hook hints must be passed to before, after & finally hooks", func(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) mockProvider.EXPECT().Hooks().AnyTimes() @@ -911,15 +917,15 @@ func TestRequirement_4_5_2(t *testing.T) { t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetNamedProviderAndWait(t.Name(), mockProvider) + err := SetProviderAndWait(t.Context(), mockProvider, WithDomain(t.Name())) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient(t.Name()) + client := NewClient(WithDomain(t.Name())) // wait for initialization time.Sleep(200 * time.Millisecond) @@ -928,7 +934,7 @@ func TestRequirement_4_5_2(t *testing.T) { hookHints := NewHookHints(map[string]any{"foo": "bar"}) - mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) + mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("forced")) mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), hookHints) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) diff --git a/openfeature/interfaces.go b/interfaces.go similarity index 52% rename from openfeature/interfaces.go rename to interfaces.go index ad429e5b..458d40bd 100644 --- a/openfeature/interfaces.go +++ b/interfaces.go @@ -2,83 +2,65 @@ package openfeature import ( "context" - - "github.com/go-logr/logr" ) -// IEvaluation defines the OpenFeature API contract -type IEvaluation interface { - SetProvider(provider FeatureProvider) error - SetProviderAndWait(provider FeatureProvider) error - GetProviderMetadata() Metadata - SetNamedProvider(clientName string, provider FeatureProvider, async bool) error - GetNamedProviderMetadata(name string) Metadata - GetClient() IClient - GetNamedClient(clientName string) IClient - SetEvaluationContext(evalCtx EvaluationContext) - AddHooks(hooks ...Hook) - Shutdown() - ShutdownWithContext(ctx context.Context) error - IEventing -} - -// IClient defines the behaviour required of an OpenFeature client -type IClient interface { +// iClient defines the behaviour required of an OpenFeature client +type iClient interface { Metadata() ClientMetadata - AddHooks(hooks ...Hook) - 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 any, evalCtx EvaluationContext, options ...Option) (any, 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 any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) + Evaluator + DetailEvaluator + Eventing + Tracker +} +// Evaluator defines OpenFeature evaluator contract. +type Evaluator interface { Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool String(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) string Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any +} - State() State - - IEventing - Tracker +// DetailEvaluator defines OpenFeature details evaluator contract. +type DetailEvaluator interface { + 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 any, evalCtx EvaluationContext, options ...Option) (ObjectEvaluationDetails, error) } -// IEventing defines the OpenFeature eventing contract -type IEventing interface { +// Eventing defines the OpenFeature eventing contract +type Eventing interface { AddHandler(eventType EventType, callback EventCallback) RemoveHandler(eventType EventType, callback EventCallback) } // evaluationImpl is an internal reference interface extending IEvaluation type evaluationImpl interface { - IEvaluation + GetProviderMetadata() Metadata + GetNamedProviderMetadata(name string) Metadata + SetProvider(ctx context.Context, provider FeatureProvider) error + SetProviderAndWait(ctx context.Context, provider FeatureProvider) error + SetNamedProvider(ctx context.Context, clientName string, provider FeatureProvider) error + SetNamedProviderAndWait(ctx context.Context, clientName string, provider FeatureProvider) error + GetClient() *Client + GetNamedClient(clientName string) *Client + SetEvaluationContext(evalCtx EvaluationContext) + AddHooks(hooks ...Hook) + Shutdown(ctx context.Context) error + Eventing 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) - - // Context-aware provider setup methods - SetProviderWithContext(ctx context.Context, provider FeatureProvider) error - SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error - SetNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error - SetNamedProviderWithContextAndWait(ctx context.Context, clientName string, provider FeatureProvider) error } -// eventingImpl is an internal reference interface extending IEventing +// eventingImpl is an internal reference interface extending Eventing type eventingImpl interface { - IEventing + Eventing GetAPIRegistry() map[EventType][]EventCallback GetClientRegistry(client string) scopedCallback diff --git a/interfaces_mock.go b/interfaces_mock.go new file mode 100644 index 00000000..1ed27305 --- /dev/null +++ b/interfaces_mock.go @@ -0,0 +1,628 @@ +//go:build testtools + +// Code generated by MockGen. DO NOT EDIT. +// Source: go.openfeature.dev/openfeature/v2 (interfaces: clientEvent,evaluationImpl,Hook,FeatureProvider,StateHandler,Tracker) +// +// Generated by this command: +// +// mockgen -destination=interfaces_mock.go -package=openfeature -build_constraint=testtools -mock_names=FeatureProvider=MockProvider go.openfeature.dev/openfeature/v2 clientEvent,evaluationImpl,Hook,FeatureProvider,StateHandler,Tracker +// + +// Package openfeature is a generated GoMock package. +package openfeature + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockclientEvent is a mock of clientEvent interface. +type MockclientEvent struct { + ctrl *gomock.Controller + recorder *MockclientEventMockRecorder + isgomock struct{} +} + +// MockclientEventMockRecorder is the mock recorder for MockclientEvent. +type MockclientEventMockRecorder struct { + mock *MockclientEvent +} + +// NewMockclientEvent creates a new mock instance. +func NewMockclientEvent(ctrl *gomock.Controller) *MockclientEvent { + mock := &MockclientEvent{ctrl: ctrl} + mock.recorder = &MockclientEventMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockclientEvent) EXPECT() *MockclientEventMockRecorder { + return m.recorder +} + +// AddClientHandler mocks base method. +func (m *MockclientEvent) AddClientHandler(clientName string, t EventType, c EventCallback) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "AddClientHandler", clientName, t, c) +} + +// AddClientHandler indicates an expected call of AddClientHandler. +func (mr *MockclientEventMockRecorder) AddClientHandler(clientName, t, c any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClientHandler", reflect.TypeOf((*MockclientEvent)(nil).AddClientHandler), clientName, t, c) +} + +// RemoveClientHandler mocks base method. +func (m *MockclientEvent) RemoveClientHandler(name string, t EventType, c EventCallback) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RemoveClientHandler", name, t, c) +} + +// RemoveClientHandler indicates an expected call of RemoveClientHandler. +func (mr *MockclientEventMockRecorder) RemoveClientHandler(name, t, c any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveClientHandler", reflect.TypeOf((*MockclientEvent)(nil).RemoveClientHandler), name, t, c) +} + +// State mocks base method. +func (m *MockclientEvent) State(domain string) State { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "State", domain) + ret0, _ := ret[0].(State) + return ret0 +} + +// State indicates an expected call of State. +func (mr *MockclientEventMockRecorder) State(domain any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockclientEvent)(nil).State), domain) +} + +// 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() *Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClient") + ret0, _ := ret[0].(*Client) + 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) *Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNamedClient", clientName) + ret0, _ := ret[0].(*Client) + 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) +} + +// SetNamedProvider mocks base method. +func (m *MockevaluationImpl) SetNamedProvider(ctx context.Context, clientName string, provider FeatureProvider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetNamedProvider", ctx, clientName, provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetNamedProvider indicates an expected call of SetNamedProvider. +func (mr *MockevaluationImplMockRecorder) SetNamedProvider(ctx, clientName, provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProvider", reflect.TypeOf((*MockevaluationImpl)(nil).SetNamedProvider), ctx, clientName, provider) +} + +// SetNamedProviderAndWait mocks base method. +func (m *MockevaluationImpl) SetNamedProviderAndWait(ctx context.Context, clientName string, provider FeatureProvider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetNamedProviderAndWait", ctx, clientName, provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetNamedProviderAndWait indicates an expected call of SetNamedProviderAndWait. +func (mr *MockevaluationImplMockRecorder) SetNamedProviderAndWait(ctx, clientName, provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProviderAndWait", reflect.TypeOf((*MockevaluationImpl)(nil).SetNamedProviderAndWait), ctx, clientName, provider) +} + +// SetProvider mocks base method. +func (m *MockevaluationImpl) SetProvider(ctx context.Context, provider FeatureProvider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetProvider", ctx, provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProvider indicates an expected call of SetProvider. +func (mr *MockevaluationImplMockRecorder) SetProvider(ctx, provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProvider", reflect.TypeOf((*MockevaluationImpl)(nil).SetProvider), ctx, provider) +} + +// SetProviderAndWait mocks base method. +func (m *MockevaluationImpl) SetProviderAndWait(ctx context.Context, provider FeatureProvider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetProviderAndWait", ctx, provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetProviderAndWait indicates an expected call of SetProviderAndWait. +func (mr *MockevaluationImplMockRecorder) SetProviderAndWait(ctx, provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProviderAndWait", reflect.TypeOf((*MockevaluationImpl)(nil).SetProviderAndWait), ctx, provider) +} + +// Shutdown mocks base method. +func (m *MockevaluationImpl) Shutdown(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockevaluationImplMockRecorder) Shutdown(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockevaluationImpl)(nil).Shutdown), ctx) +} + +// MockHook is a mock of Hook interface. +type MockHook struct { + ctrl *gomock.Controller + recorder *MockHookMockRecorder + isgomock struct{} +} + +// MockHookMockRecorder is the mock recorder for MockHook. +type MockHookMockRecorder struct { + mock *MockHook +} + +// NewMockHook creates a new mock instance. +func NewMockHook(ctrl *gomock.Controller) *MockHook { + mock := &MockHook{ctrl: ctrl} + mock.recorder = &MockHookMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHook) EXPECT() *MockHookMockRecorder { + return m.recorder +} + +// After mocks base method. +func (m *MockHook) After(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "After", ctx, hookContext, flagEvaluationDetails, hookHints) + ret0, _ := ret[0].(error) + return ret0 +} + +// After indicates an expected call of After. +func (mr *MockHookMockRecorder) After(ctx, hookContext, flagEvaluationDetails, hookHints any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockHook)(nil).After), ctx, hookContext, flagEvaluationDetails, hookHints) +} + +// Before mocks base method. +func (m *MockHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (context.Context, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Before", ctx, hookContext, hookHints) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Before indicates an expected call of Before. +func (mr *MockHookMockRecorder) Before(ctx, hookContext, hookHints any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Before", reflect.TypeOf((*MockHook)(nil).Before), ctx, hookContext, hookHints) +} + +// Error mocks base method. +func (m *MockHook) Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Error", ctx, hookContext, err, hookHints) +} + +// Error indicates an expected call of Error. +func (mr *MockHookMockRecorder) Error(ctx, hookContext, err, hookHints any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockHook)(nil).Error), ctx, hookContext, err, hookHints) +} + +// Finally mocks base method. +func (m *MockHook) Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails HookEvaluationDetails, hookHints HookHints) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Finally", ctx, hookContext, flagEvaluationDetails, hookHints) +} + +// Finally indicates an expected call of Finally. +func (mr *MockHookMockRecorder) Finally(ctx, hookContext, flagEvaluationDetails, hookHints any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Finally", reflect.TypeOf((*MockHook)(nil).Finally), ctx, hookContext, flagEvaluationDetails, hookHints) +} + +// MockProvider is a mock of FeatureProvider interface. +type MockProvider struct { + ctrl *gomock.Controller + recorder *MockProviderMockRecorder + isgomock struct{} +} + +// MockProviderMockRecorder is the mock recorder for MockProvider. +type MockProviderMockRecorder struct { + mock *MockProvider +} + +// NewMockProvider creates a new mock instance. +func NewMockProvider(ctrl *gomock.Controller) *MockProvider { + mock := &MockProvider{ctrl: ctrl} + mock.recorder = &MockProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProvider) EXPECT() *MockProviderMockRecorder { + return m.recorder +} + +// BooleanEvaluation mocks base method. +func (m *MockProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BooleanEvaluation", ctx, flag, defaultValue, flatCtx) + ret0, _ := ret[0].(BoolResolutionDetail) + return ret0 +} + +// BooleanEvaluation indicates an expected call of BooleanEvaluation. +func (mr *MockProviderMockRecorder) BooleanEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanEvaluation", reflect.TypeOf((*MockProvider)(nil).BooleanEvaluation), ctx, flag, defaultValue, flatCtx) +} + +// FloatEvaluation mocks base method. +func (m *MockProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx FlattenedContext) FloatResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, flatCtx) + ret0, _ := ret[0].(FloatResolutionDetail) + return ret0 +} + +// FloatEvaluation indicates an expected call of FloatEvaluation. +func (mr *MockProviderMockRecorder) FloatEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockProvider)(nil).FloatEvaluation), ctx, flag, defaultValue, flatCtx) +} + +// Hooks mocks base method. +func (m *MockProvider) Hooks() []Hook { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Hooks") + ret0, _ := ret[0].([]Hook) + return ret0 +} + +// Hooks indicates an expected call of Hooks. +func (mr *MockProviderMockRecorder) Hooks() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hooks", reflect.TypeOf((*MockProvider)(nil).Hooks)) +} + +// IntEvaluation mocks base method. +func (m *MockProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx FlattenedContext) IntResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntEvaluation", ctx, flag, defaultValue, flatCtx) + ret0, _ := ret[0].(IntResolutionDetail) + return ret0 +} + +// IntEvaluation indicates an expected call of IntEvaluation. +func (mr *MockProviderMockRecorder) IntEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntEvaluation", reflect.TypeOf((*MockProvider)(nil).IntEvaluation), ctx, flag, defaultValue, flatCtx) +} + +// Metadata mocks base method. +func (m *MockProvider) Metadata() Metadata { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Metadata") + ret0, _ := ret[0].(Metadata) + return ret0 +} + +// Metadata indicates an expected call of Metadata. +func (mr *MockProviderMockRecorder) Metadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockProvider)(nil).Metadata)) +} + +// ObjectEvaluation mocks base method. +func (m *MockProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ObjectEvaluation", ctx, flag, defaultValue, flatCtx) + ret0, _ := ret[0].(ObjectResolutionDetail) + return ret0 +} + +// ObjectEvaluation indicates an expected call of ObjectEvaluation. +func (mr *MockProviderMockRecorder) ObjectEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEvaluation", reflect.TypeOf((*MockProvider)(nil).ObjectEvaluation), ctx, flag, defaultValue, flatCtx) +} + +// StringEvaluation mocks base method. +func (m *MockProvider) StringEvaluation(ctx context.Context, flag, defaultValue string, flatCtx FlattenedContext) StringResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, flatCtx) + ret0, _ := ret[0].(StringResolutionDetail) + return ret0 +} + +// StringEvaluation indicates an expected call of StringEvaluation. +func (mr *MockProviderMockRecorder) StringEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockProvider)(nil).StringEvaluation), ctx, flag, defaultValue, flatCtx) +} + +// MockStateHandler is a mock of StateHandler interface. +type MockStateHandler struct { + ctrl *gomock.Controller + recorder *MockStateHandlerMockRecorder + isgomock struct{} +} + +// MockStateHandlerMockRecorder is the mock recorder for MockStateHandler. +type MockStateHandlerMockRecorder struct { + mock *MockStateHandler +} + +// NewMockStateHandler creates a new mock instance. +func NewMockStateHandler(ctrl *gomock.Controller) *MockStateHandler { + mock := &MockStateHandler{ctrl: ctrl} + mock.recorder = &MockStateHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateHandler) EXPECT() *MockStateHandlerMockRecorder { + return m.recorder +} + +// Init mocks base method. +func (m *MockStateHandler) Init(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockStateHandlerMockRecorder) Init(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStateHandler)(nil).Init), arg0) +} + +// Shutdown mocks base method. +func (m *MockStateHandler) Shutdown(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockStateHandlerMockRecorder) Shutdown(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStateHandler)(nil).Shutdown), arg0) +} + +// MockTracker is a mock of Tracker interface. +type MockTracker struct { + ctrl *gomock.Controller + recorder *MockTrackerMockRecorder + isgomock struct{} +} + +// MockTrackerMockRecorder is the mock recorder for MockTracker. +type MockTrackerMockRecorder struct { + mock *MockTracker +} + +// NewMockTracker creates a new mock instance. +func NewMockTracker(ctrl *gomock.Controller) *MockTracker { + mock := &MockTracker{ctrl: ctrl} + mock.recorder = &MockTrackerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTracker) EXPECT() *MockTrackerMockRecorder { + return m.recorder +} + +// Track mocks base method. +func (m *MockTracker) Track(ctx context.Context, trackingEventName string, evaluationContext EvaluationContext, details TrackingEventDetails) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Track", ctx, trackingEventName, evaluationContext, details) +} + +// Track indicates an expected call of Track. +func (mr *MockTrackerMockRecorder) Track(ctx, trackingEventName, evaluationContext, details any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Track", reflect.TypeOf((*MockTracker)(nil).Track), ctx, trackingEventName, evaluationContext, details) +} diff --git a/openfeature/noop_provider.go b/noop_provider.go similarity index 95% rename from openfeature/noop_provider.go rename to noop_provider.go index 394d109e..fc6c6b92 100644 --- a/openfeature/noop_provider.go +++ b/noop_provider.go @@ -55,8 +55,8 @@ func (e NoopProvider) IntEvaluation(ctx context.Context, flag string, defaultVal } // ObjectEvaluation returns an object flag -func (e NoopProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail { - return InterfaceResolutionDetail{ +func (e NoopProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail { + return ObjectResolutionDetail{ Value: defaultValue, ProviderResolutionDetail: ProviderResolutionDetail{ Variant: "default-variant", diff --git a/openfeature/noop_provider_test.go b/noop_provider_test.go similarity index 91% rename from openfeature/noop_provider_test.go rename to noop_provider_test.go index 82986e21..914e03be 100644 --- a/openfeature/noop_provider_test.go +++ b/noop_provider_test.go @@ -3,7 +3,7 @@ package openfeature_test import ( "testing" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) func TestNoopProvider_Metadata(t *testing.T) { diff --git a/openfeature.go b/openfeature.go new file mode 100644 index 00000000..af8f2393 --- /dev/null +++ b/openfeature.go @@ -0,0 +1,116 @@ +package openfeature + +import ( + "context" + "strings" +) + +// api is the global evaluationImpl implementation. This is a singleton and there can only be one instance. +var ( + api evaluationImpl + eventing eventingImpl +) + +// init initializes the OpenFeature evaluation API +func init() { + initSingleton() +} + +func initSingleton() { + exec := newEventExecutor() + eventing = exec + + api = newEvaluationAPI(exec) +} + +// SetProvider sets the [FeatureProvider] with context-aware initialization. +// If the provider implements StateHandler, Init will be called with the provided context. +// Provider initialization is asynchronous and status can be checked from provider status. +// Returns an error immediately if provider is nil, or if context is cancelled during setup. +// +// Use this function for non-blocking provider setup with timeout control where you want +// to continue application startup while the provider initializes in background. +func SetProvider(ctx context.Context, provider FeatureProvider, opts ...CallOption) error { + c := newCallOption(opts...) + if c.domain != "" { + return api.SetNamedProvider(ctx, c.domain, provider) + } + return api.SetProvider(ctx, provider) +} + +// SetProviderAndWait sets the [FeatureProvider] with initialization and waits for completion. +// If the provider implements StateHandler, InitWithContext will be called with the provided context. +// Returns an error if initialization causes an error, or if context is cancelled during initialization. +// +// Use this function for synchronous provider setup with guaranteed readiness when you need +// application startup to wait for the provider before continuing. +// Recommended timeout values: 1-5s for local providers, 10-30s for network-based providers. +func SetProviderAndWait(ctx context.Context, provider FeatureProvider, opts ...CallOption) error { + c := newCallOption(opts...) + if c.domain != "" { + return api.SetNamedProviderAndWait(ctx, c.domain, provider) + } + return api.SetProviderAndWait(ctx, provider) +} + +// ProviderMetadata returns the [FeatureProvider] metadata +func ProviderMetadata(opts ...CallOption) Metadata { + c := newCallOption(opts...) + if c.domain != "" { + return api.GetNamedProviderMetadata(c.domain) + } + return api.GetProviderMetadata() +} + +// SetEvaluationContext sets the global [EvaluationContext]. +func SetEvaluationContext(evalCtx EvaluationContext) { + api.SetEvaluationContext(evalCtx) +} + +// AddHooks appends to the collection of any previously added hooks +func AddHooks(hooks ...Hook) { + api.AddHooks(hooks...) +} + +// AddHandler allows to add API level event handlers +func AddHandler(eventType EventType, callback EventCallback) { + api.AddHandler(eventType, callback) +} + +// RemoveHandler allows for removal of API level event handlers +func RemoveHandler(eventType EventType, callback EventCallback) { + api.RemoveHandler(eventType, callback) +} + +// Shutdown calls shutdown on all registered providers. +// It resets the state of the API, removing all hooks, event handlers, and providers. +// This is intended to be called when your application is terminating. +// Returns an error if any provider shutdown fails or if context is cancelled during shutdown. +func Shutdown(ctx context.Context) error { + err := api.Shutdown(ctx) + initSingleton() + return err +} + +type ( + callOption struct { + domain string + } + CallOption func(*callOption) +) + +// WithDomain is an option which allows different domains to use different feature flag providers or clients. +// It could be used with [NewClient], [SetProvider], [SetProviderAndWait] and [ProviderMetadata]. +func WithDomain(domain string) CallOption { + return func(co *callOption) { + co.domain = strings.TrimSpace(domain) + } +} + +func newCallOption(opts ...CallOption) callOption { + c := callOption{} + for _, o := range opts { + o(&c) + } + return c +} diff --git a/openfeature/client_example_test.go b/openfeature/client_example_test.go deleted file mode 100644 index ccf600bf..00000000 --- a/openfeature/client_example_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package openfeature_test - -import ( - "context" - "encoding/json" - "fmt" - "log" - - "github.com/open-feature/go-sdk/openfeature" -) - -func ExampleNewClient() { - client := openfeature.NewClient("example-client") - fmt.Printf("Client Domain: %s", client.Metadata().Domain()) - // Output: Client Domain: example-client -} - -func ExampleClient_BooleanValue() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - client := openfeature.NewClient("example-client") - value, err := client.BooleanValue( - context.TODO(), "test-flag", true, openfeature.EvaluationContext{}, - ) - if err != nil { - log.Fatal("error while getting boolean value : ", err) - } - - fmt.Printf("test-flag value: %v", value) - // Output: test-flag value: true -} - -func ExampleClient_StringValue() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - client := openfeature.NewClient("example-client") - value, err := client.StringValue( - context.TODO(), "test-flag", "openfeature", openfeature.EvaluationContext{}, - ) - if err != nil { - log.Fatal("error while getting string value : ", err) - } - - fmt.Printf("test-flag value: %v", value) - // Output: test-flag value: openfeature -} - -func ExampleClient_FloatValue() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - client := openfeature.NewClient("example-client") - value, err := client.FloatValue( - context.TODO(), "test-flag", 0.55, openfeature.EvaluationContext{}, - ) - if err != nil { - log.Fatalf("error while getting float value: %v", err) - } - - fmt.Printf("test-flag value: %v", value) - // Output: test-flag value: 0.55 -} - -func ExampleClient_IntValue() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - client := openfeature.NewClient("example-client") - value, err := client.IntValue( - context.TODO(), "test-flag", 3, openfeature.EvaluationContext{}, - ) - if err != nil { - log.Fatalf("error while getting int value: %v", err) - } - - fmt.Printf("test-flag value: %v", value) - // Output: test-flag value: 3 -} - -func ExampleClient_ObjectValue() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - client := openfeature.NewClient("example-client") - value, err := client.ObjectValue( - context.TODO(), "test-flag", map[string]string{"foo": "bar"}, openfeature.EvaluationContext{}, - ) - if err != nil { - log.Fatal("error while getting object value : ", err) - } - - str, _ := json.Marshal(value) - fmt.Printf("test-flag value: %v", string(str)) - // Output: test-flag value: {"foo":"bar"} -} - -func ExampleClient_Boolean() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - if client.Boolean(ctx, "myflag", true, openfeature.EvaluationContext{}) { - fmt.Println("myflag is true") - } else { - fmt.Println("myflag is false") - } - - // Output: myflag is true -} - -func ExampleClient_String() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - fmt.Println(client.String(ctx, "myflag", "default", openfeature.EvaluationContext{})) - - // Output: default -} - -func ExampleClient_Float() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - fmt.Println(client.Float(ctx, "myflag", 0.5, openfeature.EvaluationContext{})) - - // Output: 0.5 -} - -func ExampleClient_Int() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - fmt.Println(client.Int(ctx, "myflag", 5, openfeature.EvaluationContext{})) - - // Output: 5 -} - -func ExampleClient_Object() { - if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { - log.Fatalf("error setting up provider %v", err) - } - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - fmt.Println(client.Object(ctx, "myflag", map[string]string{"foo": "bar"}, openfeature.EvaluationContext{})) - - // Output: map[foo:bar] -} - -func ExampleClient_Track() { - ctx := context.TODO() - client := openfeature.NewClient("example-client") - - evaluationContext := openfeature.EvaluationContext{} - - // example tracking event recording that a subject reached a page associated with a business goal - client.Track(ctx, "visited-promo-page", evaluationContext, openfeature.TrackingEventDetails{}) - - // example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value - client.Track(ctx, "clicked-checkout", evaluationContext, openfeature.NewTrackingEventDetails(99.77)) - - // example tracking event recording that a subject performed an action associated with a business goal, with the tracking event details having a particular numeric value - client.Track(ctx, "clicked-checkout", evaluationContext, openfeature.NewTrackingEventDetails(99.77).Add("currencyCode", "USD")) - - // Output: -} diff --git a/openfeature/hooks_mock.go b/openfeature/hooks_mock.go deleted file mode 100644 index bb6cc68c..00000000 --- a/openfeature/hooks_mock.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build testtools - -// Code generated by MockGen. DO NOT EDIT. -// Source: openfeature/hooks.go -// -// Generated by this command: -// -// mockgen -source=openfeature/hooks.go -destination=openfeature/hooks_mock.go -package=openfeature -build_constraint=testtools -// - -// Package openfeature is a generated GoMock package. -package openfeature - -import ( - context "context" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockHook is a mock of Hook interface. -type MockHook struct { - ctrl *gomock.Controller - recorder *MockHookMockRecorder - isgomock struct{} -} - -// MockHookMockRecorder is the mock recorder for MockHook. -type MockHookMockRecorder struct { - mock *MockHook -} - -// NewMockHook creates a new mock instance. -func NewMockHook(ctrl *gomock.Controller) *MockHook { - mock := &MockHook{ctrl: ctrl} - mock.recorder = &MockHookMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockHook) EXPECT() *MockHookMockRecorder { - return m.recorder -} - -// After mocks base method. -func (m *MockHook) After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "After", ctx, hookContext, flagEvaluationDetails, hookHints) - ret0, _ := ret[0].(error) - return ret0 -} - -// After indicates an expected call of After. -func (mr *MockHookMockRecorder) After(ctx, hookContext, flagEvaluationDetails, hookHints any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockHook)(nil).After), ctx, hookContext, flagEvaluationDetails, hookHints) -} - -// Before mocks base method. -func (m *MockHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Before", ctx, hookContext, hookHints) - ret0, _ := ret[0].(*EvaluationContext) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Before indicates an expected call of Before. -func (mr *MockHookMockRecorder) Before(ctx, hookContext, hookHints any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Before", reflect.TypeOf((*MockHook)(nil).Before), ctx, hookContext, hookHints) -} - -// Error mocks base method. -func (m *MockHook) Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Error", ctx, hookContext, err, hookHints) -} - -// Error indicates an expected call of Error. -func (mr *MockHookMockRecorder) Error(ctx, hookContext, err, hookHints any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockHook)(nil).Error), ctx, hookContext, err, hookHints) -} - -// Finally mocks base method. -func (m *MockHook) Finally(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Finally", ctx, hookContext, flagEvaluationDetails, hookHints) -} - -// Finally indicates an expected call of Finally. -func (mr *MockHookMockRecorder) Finally(ctx, hookContext, flagEvaluationDetails, hookHints any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Finally", reflect.TypeOf((*MockHook)(nil).Finally), ctx, hookContext, flagEvaluationDetails, hookHints) -} diff --git a/openfeature/interfaces_mock.go b/openfeature/interfaces_mock.go deleted file mode 100644 index 08a398a6..00000000 --- a/openfeature/interfaces_mock.go +++ /dev/null @@ -1,1195 +0,0 @@ -//go:build testtools - -// Code generated by MockGen. DO NOT EDIT. -// Source: openfeature/interfaces.go -// -// Generated by this command: -// -// mockgen -source=openfeature/interfaces.go -destination=openfeature/interfaces_mock.go -package=openfeature -build_constraint=testtools -// - -// Package openfeature is a generated GoMock package. -package openfeature - -import ( - context "context" - reflect "reflect" - - logr "github.com/go-logr/logr" - gomock "go.uber.org/mock/gomock" -) - -// MockIEvaluation is a mock of IEvaluation interface. -type MockIEvaluation struct { - ctrl *gomock.Controller - recorder *MockIEvaluationMockRecorder - isgomock struct{} -} - -// MockIEvaluationMockRecorder is the mock recorder for MockIEvaluation. -type MockIEvaluationMockRecorder struct { - mock *MockIEvaluation -} - -// NewMockIEvaluation creates a new mock instance. -func NewMockIEvaluation(ctrl *gomock.Controller) *MockIEvaluation { - mock := &MockIEvaluation{ctrl: ctrl} - mock.recorder = &MockIEvaluationMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIEvaluation) EXPECT() *MockIEvaluationMockRecorder { - return m.recorder -} - -// AddHandler mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) AddHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*MockIEvaluation)(nil).AddHandler), eventType, callback) -} - -// AddHooks mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) AddHooks(hooks ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHooks", reflect.TypeOf((*MockIEvaluation)(nil).AddHooks), hooks...) -} - -// GetClient mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) GetClient() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockIEvaluation)(nil).GetClient)) -} - -// GetNamedClient mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) GetNamedClient(clientName any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamedClient", reflect.TypeOf((*MockIEvaluation)(nil).GetNamedClient), clientName) -} - -// GetNamedProviderMetadata mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) GetNamedProviderMetadata(name any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNamedProviderMetadata", reflect.TypeOf((*MockIEvaluation)(nil).GetNamedProviderMetadata), name) -} - -// GetProviderMetadata mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) GetProviderMetadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderMetadata", reflect.TypeOf((*MockIEvaluation)(nil).GetProviderMetadata)) -} - -// RemoveHandler mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) RemoveHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*MockIEvaluation)(nil).RemoveHandler), eventType, callback) -} - -// SetEvaluationContext mocks base method. -func (m *MockIEvaluation) SetEvaluationContext(evalCtx EvaluationContext) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetEvaluationContext", evalCtx) -} - -// SetEvaluationContext indicates an expected call of SetEvaluationContext. -func (mr *MockIEvaluationMockRecorder) SetEvaluationContext(evalCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEvaluationContext", reflect.TypeOf((*MockIEvaluation)(nil).SetEvaluationContext), evalCtx) -} - -// SetNamedProvider mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) SetNamedProvider(clientName, provider, async any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNamedProvider", reflect.TypeOf((*MockIEvaluation)(nil).SetNamedProvider), clientName, provider, async) -} - -// SetProvider mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) SetProvider(provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProvider", reflect.TypeOf((*MockIEvaluation)(nil).SetProvider), provider) -} - -// SetProviderAndWait mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) SetProviderAndWait(provider any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProviderAndWait", reflect.TypeOf((*MockIEvaluation)(nil).SetProviderAndWait), provider) -} - -// Shutdown mocks base method. -func (m *MockIEvaluation) Shutdown() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Shutdown") -} - -// Shutdown indicates an expected call of Shutdown. -func (mr *MockIEvaluationMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockIEvaluation)(nil).Shutdown)) -} - -// ShutdownWithContext mocks base method. -func (m *MockIEvaluation) 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 *MockIEvaluationMockRecorder) ShutdownWithContext(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownWithContext", reflect.TypeOf((*MockIEvaluation)(nil).ShutdownWithContext), ctx) -} - -// MockIClient is a mock of IClient interface. -type MockIClient struct { - ctrl *gomock.Controller - recorder *MockIClientMockRecorder - isgomock struct{} -} - -// MockIClientMockRecorder is the mock recorder for MockIClient. -type MockIClientMockRecorder struct { - mock *MockIClient -} - -// NewMockIClient creates a new mock instance. -func NewMockIClient(ctrl *gomock.Controller) *MockIClient { - mock := &MockIClient{ctrl: ctrl} - mock.recorder = &MockIClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIClient) EXPECT() *MockIClientMockRecorder { - return m.recorder -} - -// AddHandler mocks base method. -func (m *MockIClient) 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 *MockIClientMockRecorder) AddHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*MockIClient)(nil).AddHandler), eventType, callback) -} - -// AddHooks mocks base method. -func (m *MockIClient) 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 *MockIClientMockRecorder) AddHooks(hooks ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHooks", reflect.TypeOf((*MockIClient)(nil).AddHooks), hooks...) -} - -// Boolean mocks base method. -func (m *MockIClient) Boolean(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) bool { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Boolean", varargs...) - ret0, _ := ret[0].(bool) - return ret0 -} - -// Boolean indicates an expected call of Boolean. -func (mr *MockIClientMockRecorder) Boolean(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Boolean", reflect.TypeOf((*MockIClient)(nil).Boolean), varargs...) -} - -// BooleanValue mocks base method. -func (m *MockIClient) BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "BooleanValue", varargs...) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BooleanValue indicates an expected call of BooleanValue. -func (mr *MockIClientMockRecorder) BooleanValue(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanValue", reflect.TypeOf((*MockIClient)(nil).BooleanValue), varargs...) -} - -// BooleanValueDetails mocks base method. -func (m *MockIClient) BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "BooleanValueDetails", varargs...) - ret0, _ := ret[0].(BooleanEvaluationDetails) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// BooleanValueDetails indicates an expected call of BooleanValueDetails. -func (mr *MockIClientMockRecorder) BooleanValueDetails(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanValueDetails", reflect.TypeOf((*MockIClient)(nil).BooleanValueDetails), varargs...) -} - -// EvaluationContext mocks base method. -func (m *MockIClient) EvaluationContext() EvaluationContext { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EvaluationContext") - ret0, _ := ret[0].(EvaluationContext) - return ret0 -} - -// EvaluationContext indicates an expected call of EvaluationContext. -func (mr *MockIClientMockRecorder) EvaluationContext() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvaluationContext", reflect.TypeOf((*MockIClient)(nil).EvaluationContext)) -} - -// Float mocks base method. -func (m *MockIClient) Float(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) float64 { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Float", varargs...) - ret0, _ := ret[0].(float64) - return ret0 -} - -// Float indicates an expected call of Float. -func (mr *MockIClientMockRecorder) Float(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Float", reflect.TypeOf((*MockIClient)(nil).Float), varargs...) -} - -// FloatValue mocks base method. -func (m *MockIClient) FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "FloatValue", varargs...) - ret0, _ := ret[0].(float64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FloatValue indicates an expected call of FloatValue. -func (mr *MockIClientMockRecorder) FloatValue(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatValue", reflect.TypeOf((*MockIClient)(nil).FloatValue), varargs...) -} - -// FloatValueDetails mocks base method. -func (m *MockIClient) FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "FloatValueDetails", varargs...) - ret0, _ := ret[0].(FloatEvaluationDetails) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// FloatValueDetails indicates an expected call of FloatValueDetails. -func (mr *MockIClientMockRecorder) FloatValueDetails(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatValueDetails", reflect.TypeOf((*MockIClient)(nil).FloatValueDetails), varargs...) -} - -// Int mocks base method. -func (m *MockIClient) Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Int", varargs...) - ret0, _ := ret[0].(int64) - return ret0 -} - -// Int indicates an expected call of Int. -func (mr *MockIClientMockRecorder) Int(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Int", reflect.TypeOf((*MockIClient)(nil).Int), varargs...) -} - -// IntValue mocks base method. -func (m *MockIClient) IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "IntValue", varargs...) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IntValue indicates an expected call of IntValue. -func (mr *MockIClientMockRecorder) IntValue(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntValue", reflect.TypeOf((*MockIClient)(nil).IntValue), varargs...) -} - -// IntValueDetails mocks base method. -func (m *MockIClient) IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "IntValueDetails", varargs...) - ret0, _ := ret[0].(IntEvaluationDetails) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IntValueDetails indicates an expected call of IntValueDetails. -func (mr *MockIClientMockRecorder) IntValueDetails(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntValueDetails", reflect.TypeOf((*MockIClient)(nil).IntValueDetails), varargs...) -} - -// Metadata mocks base method. -func (m *MockIClient) Metadata() ClientMetadata { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Metadata") - ret0, _ := ret[0].(ClientMetadata) - return ret0 -} - -// Metadata indicates an expected call of Metadata. -func (mr *MockIClientMockRecorder) Metadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockIClient)(nil).Metadata)) -} - -// Object mocks base method. -func (m *MockIClient) Object(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) any { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Object", varargs...) - ret0, _ := ret[0].(any) - return ret0 -} - -// Object indicates an expected call of Object. -func (mr *MockIClientMockRecorder) Object(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Object", reflect.TypeOf((*MockIClient)(nil).Object), varargs...) -} - -// ObjectValue mocks base method. -func (m *MockIClient) ObjectValue(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (any, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ObjectValue", varargs...) - ret0, _ := ret[0].(any) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ObjectValue indicates an expected call of ObjectValue. -func (mr *MockIClientMockRecorder) ObjectValue(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectValue", reflect.TypeOf((*MockIClient)(nil).ObjectValue), varargs...) -} - -// ObjectValueDetails mocks base method. -func (m *MockIClient) ObjectValueDetails(ctx context.Context, flag string, defaultValue any, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "ObjectValueDetails", varargs...) - ret0, _ := ret[0].(InterfaceEvaluationDetails) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ObjectValueDetails indicates an expected call of ObjectValueDetails. -func (mr *MockIClientMockRecorder) ObjectValueDetails(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectValueDetails", reflect.TypeOf((*MockIClient)(nil).ObjectValueDetails), varargs...) -} - -// RemoveHandler mocks base method. -func (m *MockIClient) 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 *MockIClientMockRecorder) RemoveHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*MockIClient)(nil).RemoveHandler), eventType, callback) -} - -// SetEvaluationContext mocks base method. -func (m *MockIClient) SetEvaluationContext(evalCtx EvaluationContext) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "SetEvaluationContext", evalCtx) -} - -// SetEvaluationContext indicates an expected call of SetEvaluationContext. -func (mr *MockIClientMockRecorder) SetEvaluationContext(evalCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEvaluationContext", reflect.TypeOf((*MockIClient)(nil).SetEvaluationContext), evalCtx) -} - -// State mocks base method. -func (m *MockIClient) State() State { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "State") - ret0, _ := ret[0].(State) - return ret0 -} - -// State indicates an expected call of State. -func (mr *MockIClientMockRecorder) State() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIClient)(nil).State)) -} - -// String mocks base method. -func (m *MockIClient) String(ctx context.Context, flag, defaultValue string, evalCtx EvaluationContext, options ...Option) string { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "String", varargs...) - ret0, _ := ret[0].(string) - return ret0 -} - -// String indicates an expected call of String. -func (mr *MockIClientMockRecorder) String(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockIClient)(nil).String), varargs...) -} - -// StringValue mocks base method. -func (m *MockIClient) StringValue(ctx context.Context, flag, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "StringValue", varargs...) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// StringValue indicates an expected call of StringValue. -func (mr *MockIClientMockRecorder) StringValue(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringValue", reflect.TypeOf((*MockIClient)(nil).StringValue), varargs...) -} - -// StringValueDetails mocks base method. -func (m *MockIClient) StringValueDetails(ctx context.Context, flag, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) { - m.ctrl.T.Helper() - varargs := []any{ctx, flag, defaultValue, evalCtx} - for _, a := range options { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "StringValueDetails", varargs...) - ret0, _ := ret[0].(StringEvaluationDetails) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// StringValueDetails indicates an expected call of StringValueDetails. -func (mr *MockIClientMockRecorder) StringValueDetails(ctx, flag, defaultValue, evalCtx any, options ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, flag, defaultValue, evalCtx}, options...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringValueDetails", reflect.TypeOf((*MockIClient)(nil).StringValueDetails), varargs...) -} - -// Track mocks base method. -func (m *MockIClient) Track(ctx context.Context, trackingEventName string, evaluationContext EvaluationContext, details TrackingEventDetails) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Track", ctx, trackingEventName, evaluationContext, details) -} - -// Track indicates an expected call of Track. -func (mr *MockIClientMockRecorder) Track(ctx, trackingEventName, evaluationContext, details any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Track", reflect.TypeOf((*MockIClient)(nil).Track), ctx, trackingEventName, evaluationContext, details) -} - -// MockIEventing is a mock of IEventing interface. -type MockIEventing struct { - ctrl *gomock.Controller - recorder *MockIEventingMockRecorder - isgomock struct{} -} - -// MockIEventingMockRecorder is the mock recorder for MockIEventing. -type MockIEventingMockRecorder struct { - mock *MockIEventing -} - -// NewMockIEventing creates a new mock instance. -func NewMockIEventing(ctrl *gomock.Controller) *MockIEventing { - mock := &MockIEventing{ctrl: ctrl} - mock.recorder = &MockIEventingMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockIEventing) EXPECT() *MockIEventingMockRecorder { - return m.recorder -} - -// AddHandler mocks base method. -func (m *MockIEventing) 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 *MockIEventingMockRecorder) AddHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*MockIEventing)(nil).AddHandler), eventType, callback) -} - -// RemoveHandler mocks base method. -func (m *MockIEventing) 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 *MockIEventingMockRecorder) RemoveHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - 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) -} - -// MockeventingImpl is a mock of eventingImpl interface. -type MockeventingImpl struct { - ctrl *gomock.Controller - recorder *MockeventingImplMockRecorder - isgomock struct{} -} - -// MockeventingImplMockRecorder is the mock recorder for MockeventingImpl. -type MockeventingImplMockRecorder struct { - mock *MockeventingImpl -} - -// NewMockeventingImpl creates a new mock instance. -func NewMockeventingImpl(ctrl *gomock.Controller) *MockeventingImpl { - mock := &MockeventingImpl{ctrl: ctrl} - mock.recorder = &MockeventingImplMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockeventingImpl) EXPECT() *MockeventingImplMockRecorder { - return m.recorder -} - -// AddClientHandler mocks base method. -func (m *MockeventingImpl) AddClientHandler(clientName string, t EventType, c EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddClientHandler", clientName, t, c) -} - -// AddClientHandler indicates an expected call of AddClientHandler. -func (mr *MockeventingImplMockRecorder) AddClientHandler(clientName, t, c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClientHandler", reflect.TypeOf((*MockeventingImpl)(nil).AddClientHandler), clientName, t, c) -} - -// AddHandler mocks base method. -func (m *MockeventingImpl) 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 *MockeventingImplMockRecorder) AddHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddHandler", reflect.TypeOf((*MockeventingImpl)(nil).AddHandler), eventType, callback) -} - -// GetAPIRegistry mocks base method. -func (m *MockeventingImpl) GetAPIRegistry() map[EventType][]EventCallback { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAPIRegistry") - ret0, _ := ret[0].(map[EventType][]EventCallback) - return ret0 -} - -// GetAPIRegistry indicates an expected call of GetAPIRegistry. -func (mr *MockeventingImplMockRecorder) GetAPIRegistry() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIRegistry", reflect.TypeOf((*MockeventingImpl)(nil).GetAPIRegistry)) -} - -// GetClientRegistry mocks base method. -func (m *MockeventingImpl) GetClientRegistry(client string) scopedCallback { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetClientRegistry", client) - ret0, _ := ret[0].(scopedCallback) - return ret0 -} - -// GetClientRegistry indicates an expected call of GetClientRegistry. -func (mr *MockeventingImplMockRecorder) GetClientRegistry(client any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClientRegistry", reflect.TypeOf((*MockeventingImpl)(nil).GetClientRegistry), client) -} - -// RemoveClientHandler mocks base method. -func (m *MockeventingImpl) RemoveClientHandler(name string, t EventType, c EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RemoveClientHandler", name, t, c) -} - -// RemoveClientHandler indicates an expected call of RemoveClientHandler. -func (mr *MockeventingImplMockRecorder) RemoveClientHandler(name, t, c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveClientHandler", reflect.TypeOf((*MockeventingImpl)(nil).RemoveClientHandler), name, t, c) -} - -// RemoveHandler mocks base method. -func (m *MockeventingImpl) 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 *MockeventingImplMockRecorder) RemoveHandler(eventType, callback any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveHandler", reflect.TypeOf((*MockeventingImpl)(nil).RemoveHandler), eventType, callback) -} - -// State mocks base method. -func (m *MockeventingImpl) State(domain string) State { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "State", domain) - ret0, _ := ret[0].(State) - return ret0 -} - -// State indicates an expected call of State. -func (mr *MockeventingImplMockRecorder) State(domain any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockeventingImpl)(nil).State), domain) -} - -// MockclientEvent is a mock of clientEvent interface. -type MockclientEvent struct { - ctrl *gomock.Controller - recorder *MockclientEventMockRecorder - isgomock struct{} -} - -// MockclientEventMockRecorder is the mock recorder for MockclientEvent. -type MockclientEventMockRecorder struct { - mock *MockclientEvent -} - -// NewMockclientEvent creates a new mock instance. -func NewMockclientEvent(ctrl *gomock.Controller) *MockclientEvent { - mock := &MockclientEvent{ctrl: ctrl} - mock.recorder = &MockclientEventMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockclientEvent) EXPECT() *MockclientEventMockRecorder { - return m.recorder -} - -// AddClientHandler mocks base method. -func (m *MockclientEvent) AddClientHandler(clientName string, t EventType, c EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "AddClientHandler", clientName, t, c) -} - -// AddClientHandler indicates an expected call of AddClientHandler. -func (mr *MockclientEventMockRecorder) AddClientHandler(clientName, t, c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClientHandler", reflect.TypeOf((*MockclientEvent)(nil).AddClientHandler), clientName, t, c) -} - -// RemoveClientHandler mocks base method. -func (m *MockclientEvent) RemoveClientHandler(name string, t EventType, c EventCallback) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "RemoveClientHandler", name, t, c) -} - -// RemoveClientHandler indicates an expected call of RemoveClientHandler. -func (mr *MockclientEventMockRecorder) RemoveClientHandler(name, t, c any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveClientHandler", reflect.TypeOf((*MockclientEvent)(nil).RemoveClientHandler), name, t, c) -} - -// State mocks base method. -func (m *MockclientEvent) State(domain string) State { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "State", domain) - ret0, _ := ret[0].(State) - return ret0 -} - -// State indicates an expected call of State. -func (mr *MockclientEventMockRecorder) State(domain any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockclientEvent)(nil).State), domain) -} diff --git a/openfeature/internal/context_key.go b/openfeature/internal/context_key.go deleted file mode 100644 index c512f1c1..00000000 --- a/openfeature/internal/context_key.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package internal contains internal identifiers for the OpenFeature SDK. -package internal - -// ContextKey is just an empty struct. It exists so TransactionContext can be -// an immutable public variable with a unique type. It's immutable -// because nobody else can create a ContextKey, being unexported. -type ContextKey struct{} - -// TransactionContext is the context key to use with golang.org/x/net/context's -// WithValue function to associate an EvaluationContext value with a context. -var TransactionContext ContextKey diff --git a/openfeature/multi/first_success_strategy.go b/openfeature/multi/first_success_strategy.go deleted file mode 100644 index fed09952..00000000 --- a/openfeature/multi/first_success_strategy.go +++ /dev/null @@ -1,30 +0,0 @@ -package multi - -import ( - "context" - "errors" - - of "github.com/open-feature/go-sdk/openfeature" -) - -// newFirstSuccessStrategy returns a [StrategyFn] that returns the result of the First [of.FeatureProvider] whose response -// is not an error. This executed sequentially. -func newFirstSuccessStrategy(providers []NamedProvider) StrategyFn[FlagTypes] { - return firstSuccessStrategyFn[FlagTypes](providers) -} - -func firstSuccessStrategyFn[T FlagTypes](providers []NamedProvider) StrategyFn[T] { - return func(ctx context.Context, flag string, defaultValue T, flatCtx of.FlattenedContext) of.GenericResolutionDetail[T] { - resolutionErrors := make([]error, 0, len(providers)) - for _, provider := range providers { - resolution := Evaluate(ctx, provider, flag, defaultValue, flatCtx) - if resolution.Error() != nil { - resolutionErrors = append(resolutionErrors, resolution.Error()) - continue - } - resolution.FlagMetadata = setFlagMetadata(StrategyFirstSuccess, provider.Name(), resolution.FlagMetadata) - return resolution - } - return BuildDefaultResult(StrategyFirstSuccess, defaultValue, errors.Join(resolutionErrors...)) - } -} diff --git a/openfeature/openfeature.go b/openfeature/openfeature.go deleted file mode 100644 index 69db3f29..00000000 --- a/openfeature/openfeature.go +++ /dev/null @@ -1,164 +0,0 @@ -package openfeature - -import ( - "context" - - "github.com/go-logr/logr" -) - -// api is the global evaluationImpl implementation. This is a singleton and there can only be one instance. -var ( - api evaluationImpl - eventing eventingImpl -) - -// init initializes the OpenFeature evaluation API -func init() { - initSingleton() -} - -func initSingleton() { - exec := newEventExecutor() - eventing = exec - - api = newEvaluationAPI(exec) -} - -// 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 -} - -// NewDefaultClient returns a [Client] for the default domain. The default domain [Client] is the [IClient] instance that -// wraps around an unnamed [FeatureProvider] -func NewDefaultClient() *Client { - return newClient("", api, eventing) -} - -// 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) -} - -// 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) -} - -// SetProviderWithContext sets the default [FeatureProvider] with context-aware initialization. -// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context. -// Provider initialization is asynchronous and status can be checked from provider status. -// Returns an error immediately if provider is nil, or if context is cancelled during setup. -// -// Use this function for non-blocking provider setup with timeout control where you want -// to continue application startup while the provider initializes in background. -// 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) -} - -// SetProviderWithContextAndWait sets the default [FeatureProvider] with context-aware initialization and waits for completion. -// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context. -// Returns an error if initialization causes an error, or if context is cancelled during initialization. -// -// Use this function for synchronous provider setup with guaranteed readiness when you need -// 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) -} - -// ProviderMetadata returns the default [FeatureProvider] metadata -func ProviderMetadata() Metadata { - 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) -} - -// 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) -} - -// SetNamedProviderWithContext sets a [FeatureProvider] mapped to the given [Client] domain with context-aware initialization. -// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context. -// Provider initialization is asynchronous and status can be checked from provider status. -// Returns an error immediately if provider is nil, or if context is cancelled during setup. -// -// 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) -} - -// SetNamedProviderWithContextAndWait sets a provider mapped to the given [Client] domain with context-aware initialization and waits for completion. -// If the provider implements ContextAwareStateHandler, InitWithContext will be called with the provided context. -// Returns an error if initialization causes an error, or if context is cancelled during initialization. -// -// 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) -} - -// NamedProviderMetadata returns the named provider's Metadata -func NamedProviderMetadata(name string) Metadata { - return api.GetNamedProviderMetadata(name) -} - -// SetEvaluationContext sets the global [EvaluationContext]. -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...) -} - -// AddHandler allows to add API level event handlers -func AddHandler(eventType EventType, callback EventCallback) { - api.AddHandler(eventType, callback) -} - -// RemoveHandler allows for removal of API level event handlers -func RemoveHandler(eventType EventType, callback EventCallback) { - api.RemoveHandler(eventType, callback) -} - -// Shutdown unconditionally calls shutdown on all registered providers, -// regardless of their state. It resets the state of the API, removing all -// hooks, event handlers, and providers. -func Shutdown() { - api.Shutdown() - initSingleton() -} - -// ShutdownWithContext calls context-aware shutdown on all registered providers. -// If providers implement ContextAwareStateHandler, ShutdownWithContext will be called with the provided context. -// It resets the state of the API, removing all hooks, event handlers, and providers. -// This is intended to be called when your application is terminating. -// Returns an error if any provider shutdown fails or if context is cancelled during shutdown. -func ShutdownWithContext(ctx context.Context) error { - err := api.ShutdownWithContext(ctx) - initSingleton() - return err -} diff --git a/openfeature/provider_mock.go b/openfeature/provider_mock.go deleted file mode 100644 index 185bc2f7..00000000 --- a/openfeature/provider_mock.go +++ /dev/null @@ -1,343 +0,0 @@ -//go:build testtools - -// Code generated by MockGen. DO NOT EDIT. -// Source: openfeature/provider.go -// -// Generated by this command: -// -// mockgen -source=openfeature/provider.go -destination=openfeature/provider_mock.go -package=openfeature -build_constraint=testtools -// - -// Package openfeature is a generated GoMock package. -package openfeature - -import ( - context "context" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockFeatureProvider is a mock of FeatureProvider interface. -type MockFeatureProvider struct { - ctrl *gomock.Controller - recorder *MockFeatureProviderMockRecorder - isgomock struct{} -} - -// MockFeatureProviderMockRecorder is the mock recorder for MockFeatureProvider. -type MockFeatureProviderMockRecorder struct { - mock *MockFeatureProvider -} - -// NewMockFeatureProvider creates a new mock instance. -func NewMockFeatureProvider(ctrl *gomock.Controller) *MockFeatureProvider { - mock := &MockFeatureProvider{ctrl: ctrl} - mock.recorder = &MockFeatureProviderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockFeatureProvider) EXPECT() *MockFeatureProviderMockRecorder { - return m.recorder -} - -// BooleanEvaluation mocks base method. -func (m *MockFeatureProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BooleanEvaluation", ctx, flag, defaultValue, flatCtx) - ret0, _ := ret[0].(BoolResolutionDetail) - return ret0 -} - -// BooleanEvaluation indicates an expected call of BooleanEvaluation. -func (mr *MockFeatureProviderMockRecorder) BooleanEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).BooleanEvaluation), ctx, flag, defaultValue, flatCtx) -} - -// FloatEvaluation mocks base method. -func (m *MockFeatureProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx FlattenedContext) FloatResolutionDetail { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, flatCtx) - ret0, _ := ret[0].(FloatResolutionDetail) - return ret0 -} - -// FloatEvaluation indicates an expected call of FloatEvaluation. -func (mr *MockFeatureProviderMockRecorder) FloatEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).FloatEvaluation), ctx, flag, defaultValue, flatCtx) -} - -// Hooks mocks base method. -func (m *MockFeatureProvider) Hooks() []Hook { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Hooks") - ret0, _ := ret[0].([]Hook) - return ret0 -} - -// Hooks indicates an expected call of Hooks. -func (mr *MockFeatureProviderMockRecorder) Hooks() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hooks", reflect.TypeOf((*MockFeatureProvider)(nil).Hooks)) -} - -// IntEvaluation mocks base method. -func (m *MockFeatureProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx FlattenedContext) IntResolutionDetail { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IntEvaluation", ctx, flag, defaultValue, flatCtx) - ret0, _ := ret[0].(IntResolutionDetail) - return ret0 -} - -// IntEvaluation indicates an expected call of IntEvaluation. -func (mr *MockFeatureProviderMockRecorder) IntEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).IntEvaluation), ctx, flag, defaultValue, flatCtx) -} - -// Metadata mocks base method. -func (m *MockFeatureProvider) Metadata() Metadata { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Metadata") - ret0, _ := ret[0].(Metadata) - return ret0 -} - -// Metadata indicates an expected call of Metadata. -func (mr *MockFeatureProviderMockRecorder) Metadata() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockFeatureProvider)(nil).Metadata)) -} - -// ObjectEvaluation mocks base method. -func (m *MockFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ObjectEvaluation", ctx, flag, defaultValue, flatCtx) - ret0, _ := ret[0].(InterfaceResolutionDetail) - return ret0 -} - -// ObjectEvaluation indicates an expected call of ObjectEvaluation. -func (mr *MockFeatureProviderMockRecorder) ObjectEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).ObjectEvaluation), ctx, flag, defaultValue, flatCtx) -} - -// StringEvaluation mocks base method. -func (m *MockFeatureProvider) StringEvaluation(ctx context.Context, flag, defaultValue string, flatCtx FlattenedContext) StringResolutionDetail { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, flatCtx) - ret0, _ := ret[0].(StringResolutionDetail) - return ret0 -} - -// StringEvaluation indicates an expected call of StringEvaluation. -func (mr *MockFeatureProviderMockRecorder) StringEvaluation(ctx, flag, defaultValue, flatCtx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).StringEvaluation), ctx, flag, defaultValue, flatCtx) -} - -// MockStateHandler is a mock of StateHandler interface. -type MockStateHandler struct { - ctrl *gomock.Controller - recorder *MockStateHandlerMockRecorder - isgomock struct{} -} - -// MockStateHandlerMockRecorder is the mock recorder for MockStateHandler. -type MockStateHandlerMockRecorder struct { - mock *MockStateHandler -} - -// NewMockStateHandler creates a new mock instance. -func NewMockStateHandler(ctrl *gomock.Controller) *MockStateHandler { - mock := &MockStateHandler{ctrl: ctrl} - mock.recorder = &MockStateHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockStateHandler) EXPECT() *MockStateHandlerMockRecorder { - return m.recorder -} - -// Init mocks base method. -func (m *MockStateHandler) Init(evaluationContext EvaluationContext) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Init", evaluationContext) - ret0, _ := ret[0].(error) - return ret0 -} - -// Init indicates an expected call of Init. -func (mr *MockStateHandlerMockRecorder) Init(evaluationContext any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStateHandler)(nil).Init), evaluationContext) -} - -// Shutdown mocks base method. -func (m *MockStateHandler) Shutdown() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Shutdown") -} - -// Shutdown indicates an expected call of Shutdown. -func (mr *MockStateHandlerMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStateHandler)(nil).Shutdown)) -} - -// MockContextAwareStateHandler is a mock of ContextAwareStateHandler interface. -type MockContextAwareStateHandler struct { - ctrl *gomock.Controller - recorder *MockContextAwareStateHandlerMockRecorder - isgomock struct{} -} - -// MockContextAwareStateHandlerMockRecorder is the mock recorder for MockContextAwareStateHandler. -type MockContextAwareStateHandlerMockRecorder struct { - mock *MockContextAwareStateHandler -} - -// NewMockContextAwareStateHandler creates a new mock instance. -func NewMockContextAwareStateHandler(ctrl *gomock.Controller) *MockContextAwareStateHandler { - mock := &MockContextAwareStateHandler{ctrl: ctrl} - mock.recorder = &MockContextAwareStateHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockContextAwareStateHandler) EXPECT() *MockContextAwareStateHandlerMockRecorder { - return m.recorder -} - -// Init mocks base method. -func (m *MockContextAwareStateHandler) Init(evaluationContext EvaluationContext) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Init", evaluationContext) - ret0, _ := ret[0].(error) - return ret0 -} - -// Init indicates an expected call of Init. -func (mr *MockContextAwareStateHandlerMockRecorder) Init(evaluationContext any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockContextAwareStateHandler)(nil).Init), evaluationContext) -} - -// InitWithContext mocks base method. -func (m *MockContextAwareStateHandler) InitWithContext(ctx context.Context, evaluationContext EvaluationContext) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "InitWithContext", ctx, evaluationContext) - ret0, _ := ret[0].(error) - return ret0 -} - -// InitWithContext indicates an expected call of InitWithContext. -func (mr *MockContextAwareStateHandlerMockRecorder) InitWithContext(ctx, evaluationContext any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitWithContext", reflect.TypeOf((*MockContextAwareStateHandler)(nil).InitWithContext), ctx, evaluationContext) -} - -// Shutdown mocks base method. -func (m *MockContextAwareStateHandler) Shutdown() { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Shutdown") -} - -// Shutdown indicates an expected call of Shutdown. -func (mr *MockContextAwareStateHandlerMockRecorder) Shutdown() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockContextAwareStateHandler)(nil).Shutdown)) -} - -// ShutdownWithContext mocks base method. -func (m *MockContextAwareStateHandler) 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 *MockContextAwareStateHandlerMockRecorder) ShutdownWithContext(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownWithContext", reflect.TypeOf((*MockContextAwareStateHandler)(nil).ShutdownWithContext), ctx) -} - -// MockTracker is a mock of Tracker interface. -type MockTracker struct { - ctrl *gomock.Controller - recorder *MockTrackerMockRecorder - isgomock struct{} -} - -// MockTrackerMockRecorder is the mock recorder for MockTracker. -type MockTrackerMockRecorder struct { - mock *MockTracker -} - -// NewMockTracker creates a new mock instance. -func NewMockTracker(ctrl *gomock.Controller) *MockTracker { - mock := &MockTracker{ctrl: ctrl} - mock.recorder = &MockTrackerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTracker) EXPECT() *MockTrackerMockRecorder { - return m.recorder -} - -// Track mocks base method. -func (m *MockTracker) Track(ctx context.Context, trackingEventName string, evaluationContext EvaluationContext, details TrackingEventDetails) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Track", ctx, trackingEventName, evaluationContext, details) -} - -// Track indicates an expected call of Track. -func (mr *MockTrackerMockRecorder) Track(ctx, trackingEventName, evaluationContext, details any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Track", reflect.TypeOf((*MockTracker)(nil).Track), ctx, trackingEventName, evaluationContext, details) -} - -// MockEventHandler is a mock of EventHandler interface. -type MockEventHandler struct { - ctrl *gomock.Controller - recorder *MockEventHandlerMockRecorder - isgomock struct{} -} - -// MockEventHandlerMockRecorder is the mock recorder for MockEventHandler. -type MockEventHandlerMockRecorder struct { - mock *MockEventHandler -} - -// NewMockEventHandler creates a new mock instance. -func NewMockEventHandler(ctrl *gomock.Controller) *MockEventHandler { - mock := &MockEventHandler{ctrl: ctrl} - mock.recorder = &MockEventHandlerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockEventHandler) EXPECT() *MockEventHandlerMockRecorder { - return m.recorder -} - -// EventChannel mocks base method. -func (m *MockEventHandler) EventChannel() <-chan Event { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EventChannel") - ret0, _ := ret[0].(<-chan Event) - return ret0 -} - -// EventChannel indicates an expected call of EventChannel. -func (mr *MockEventHandlerMockRecorder) EventChannel() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventChannel", reflect.TypeOf((*MockEventHandler)(nil).EventChannel)) -} diff --git a/openfeature/telemetry/telemetry.go b/openfeature/telemetry/telemetry.go deleted file mode 100644 index 14f363be..00000000 --- a/openfeature/telemetry/telemetry.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package telemetry provides utilities for extracting data from the OpenFeature SDK for use in telemetry signals. -package telemetry - -import ( - "strings" - - "github.com/open-feature/go-sdk/openfeature" -) - -// EvaluationEvent represents an event that is emitted when a flag is evaluated. -// It is intended to be used to record flag evaluation events as OpenTelemetry log records. -// See the OpenFeature specification [Appendix D: Observability] and -// the OpenTelemetry [Semantic conventions for feature flags in logs] for more information. -// -// [Appendix D: Observability]: https://openfeature.dev/specification/appendix-d -// [Semantic conventions for feature flags in logs]: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ -type EvaluationEvent struct { - // Name is the name of the event. - // It is always "feature_flag.evaluation". - Name string - // Attributes represents the event's attributes. - Attributes map[string]any -} - -// The OpenTelemetry compliant event attributes for flag evaluation. -const ( - FlagKey string = "feature_flag.key" - ErrorTypeKey string = "error.type" - ResultValueKey string = "feature_flag.result.value" - ResultVariantKey string = "feature_flag.result.variant" - ErrorMessageKey string = "error.message" - ContextIDKey string = "feature_flag.context.id" - ProviderNameKey string = "feature_flag.provider.name" - ResultReasonKey string = "feature_flag.result.reason" - FlagSetIDKey string = "feature_flag.set.id" - VersionKey string = "feature_flag.version" -) - -// FlagEvaluationKey is the name of the feature flag evaluation event. -const FlagEvaluationKey string = "feature_flag.evaluation" - -const ( - flagMetaContextIDKey string = "contextId" - flagMetaFlagSetIDKey string = "flagSetId" - flagMetaVersionKey string = "version" -) - -// CreateEvaluationEvent creates an [EvaluationEvent]. -// It is intended to be used in the `Finally` stage of a [openfeature.Hook]. -func CreateEvaluationEvent(hookContext openfeature.HookContext, details openfeature.InterfaceEvaluationDetails) EvaluationEvent { - attributes := map[string]any{ - FlagKey: hookContext.FlagKey(), - ProviderNameKey: hookContext.ProviderMetadata().Name, - } - - attributes[ResultReasonKey] = strings.ToLower(string(openfeature.UnknownReason)) - if details.Reason != "" { - attributes[ResultReasonKey] = strings.ToLower(string(details.Reason)) - } - - if details.Variant != "" { - attributes[ResultVariantKey] = details.Variant - } else { - attributes[ResultValueKey] = details.Value - } - - attributes[ContextIDKey] = hookContext.EvaluationContext().TargetingKey() - if contextID, ok := details.FlagMetadata[flagMetaContextIDKey]; ok { - attributes[ContextIDKey] = contextID - } - - if setID, ok := details.FlagMetadata[flagMetaFlagSetIDKey]; ok { - attributes[FlagSetIDKey] = setID - } - - if version, ok := details.FlagMetadata[flagMetaVersionKey]; ok { - attributes[VersionKey] = version - } - - if details.Reason != openfeature.ErrorReason { - return EvaluationEvent{ - Name: FlagEvaluationKey, - Attributes: attributes, - } - } - - attributes[ErrorTypeKey] = strings.ToLower(string(openfeature.GeneralCode)) - if details.ErrorCode != "" { - attributes[ErrorTypeKey] = strings.ToLower(string(details.ErrorCode)) - } - - if details.ErrorMessage != "" { - attributes[ErrorMessageKey] = details.ErrorMessage - } - - return EvaluationEvent{ - Name: FlagEvaluationKey, - Attributes: attributes, - } -} diff --git a/openfeature/telemetry/telemetry_test.go b/openfeature/telemetry/telemetry_test.go deleted file mode 100644 index 0ae8bf31..00000000 --- a/openfeature/telemetry/telemetry_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package telemetry - -import ( - "strings" - "testing" - - "github.com/open-feature/go-sdk/openfeature" -) - -func TestCreateEvaluationEvent_1_3_1_BasicEvent(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: true, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - FlagType: openfeature.Boolean, - ResolutionDetail: openfeature.ResolutionDetail{ - Reason: openfeature.StaticReason, - FlagMetadata: openfeature.FlagMetadata{}, - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Name != "feature_flag.evaluation" { - t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name) - } - - if event.Attributes[FlagKey] != flagKey { - t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[FlagKey]) - } - - if event.Attributes[ResultReasonKey] != strings.ToLower(string(openfeature.StaticReason)) { - t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.StaticReason)), event.Attributes[ResultReasonKey]) - } - - if event.Attributes[ProviderNameKey] != "test-provider" { - t.Errorf("Expected provider name to be 'test-provider', got '%s'", event.Attributes[ProviderNameKey]) - } - - if event.Attributes[ResultValueKey] != true { - t.Errorf("Expected event attribute 'VALUE' to be 'true', got '%v'", event.Attributes[ResultValueKey]) - } -} - -func TestCreateEvaluationEvent_1_4_6_WithVariant(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: true, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - FlagType: openfeature.Boolean, - ResolutionDetail: openfeature.ResolutionDetail{ - Variant: "true", - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Name != "feature_flag.evaluation" { - t.Errorf("Expected event name to be 'feature_flag.evaluation', got '%s'", event.Name) - } - - if event.Attributes[FlagKey] != flagKey { - t.Errorf("Expected event attribute 'KEY' to be '%s', got '%s'", flagKey, event.Attributes[FlagKey]) - } - - if event.Attributes[ResultVariantKey] != "true" { - t.Errorf("Expected event attribute 'VARIANT' to be 'true', got '%s'", event.Attributes[ResultVariantKey]) - } -} - -func TestCreateEvaluationEvent_1_4_14_WithFlagMetaData(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: false, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - FlagType: openfeature.Boolean, - ResolutionDetail: openfeature.ResolutionDetail{ - FlagMetadata: openfeature.FlagMetadata{ - flagMetaFlagSetIDKey: "test-set", - flagMetaContextIDKey: "metadata-context", - flagMetaVersionKey: "v1.0", - }, - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Attributes[FlagSetIDKey] != "test-set" { - t.Errorf("Expected 'Flag SetID' in Flag Metadata name to be 'test-set', got '%s'", event.Attributes[flagMetaFlagSetIDKey]) - } - - if event.Attributes[ContextIDKey] != "metadata-context" { - t.Errorf("Expected 'Flag ContextID' in Flag Metadata name to be 'metadata-context', got '%s'", event.Attributes[flagMetaContextIDKey]) - } - - if event.Attributes[VersionKey] != "v1.0" { - t.Errorf("Expected 'Flag Version' in Flag Metadata name to be 'v1.0', got '%s'", event.Attributes[flagMetaVersionKey]) - } -} - -func TestCreateEvaluationEvent_1_4_8_WithErrors(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: false, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - ResolutionDetail: openfeature.ResolutionDetail{ - Reason: openfeature.ErrorReason, - ErrorCode: openfeature.FlagNotFoundCode, - ErrorMessage: "a test error", - FlagMetadata: openfeature.FlagMetadata{}, - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Attributes[ErrorTypeKey] != strings.ToLower(string(openfeature.FlagNotFoundCode)) { - t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[ErrorTypeKey]) - } - - if event.Attributes[ErrorMessageKey] != "a test error" { - t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[ErrorMessageKey]) - } -} - -func TestCreateEvaluationEvent_1_4_8_WithGeneralErrors(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, false, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: false, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - ResolutionDetail: openfeature.ResolutionDetail{ - Reason: openfeature.ErrorReason, - ErrorMessage: "a test error", - FlagMetadata: openfeature.FlagMetadata{}, - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Attributes[ErrorTypeKey] != strings.ToLower(string(openfeature.GeneralCode)) { - t.Errorf("Expected 'ERROR_CODE' to be 'GENERAL', got '%s'", event.Attributes[ErrorTypeKey]) - } - - if event.Attributes[ErrorMessageKey] != "a test error" { - t.Errorf("Expected 'ERROR_MESSAGE' to be 'a test error', got '%s'", event.Attributes[ErrorMessageKey]) - } -} - -func TestCreateEvaluationEvent_1_4_7_WithUnknownReason(t *testing.T) { - flagKey := "test-flag" - - mockProviderMetadata := openfeature.Metadata{ - Name: "test-provider", - } - - mockClientMetadata := openfeature.NewClientMetadata("test-client") - - mockEvalCtx := openfeature.NewEvaluationContext( - "test-target-key", map[string]any{ - "is": "a test", - }) - - mockHookContext := openfeature.NewHookContext(flagKey, openfeature.Boolean, true, mockClientMetadata, mockProviderMetadata, mockEvalCtx) - - mockDetails := openfeature.InterfaceEvaluationDetails{ - Value: true, - EvaluationDetails: openfeature.EvaluationDetails{ - FlagKey: flagKey, - ResolutionDetail: openfeature.ResolutionDetail{ - FlagMetadata: openfeature.FlagMetadata{}, - }, - }, - } - - event := CreateEvaluationEvent(mockHookContext, mockDetails) - - if event.Attributes[ResultReasonKey] != strings.ToLower(string(openfeature.UnknownReason)) { - t.Errorf("Expected evaluation reason to be '%s', got '%s'", strings.ToLower(string(openfeature.UnknownReason)), event.Attributes[ResultReasonKey]) - } -} diff --git a/openfeature/openfeature_api.go b/openfeature_api.go similarity index 66% rename from openfeature/openfeature_api.go rename to openfeature_api.go index 715a4ca0..b54c3e26 100644 --- a/openfeature/openfeature_api.go +++ b/openfeature_api.go @@ -4,11 +4,10 @@ import ( "context" "errors" "fmt" + "log/slog" "maps" "slices" "sync" - - "github.com/go-logr/logr" ) // evaluationAPI wraps OpenFeature evaluation API functionalities @@ -33,12 +32,12 @@ func newEvaluationAPI(eventExecutor *eventExecutor) *evaluationAPI { } } -func (api *evaluationAPI) SetProvider(provider FeatureProvider) error { - return api.setProvider(provider, true) +func (api *evaluationAPI) SetProvider(ctx context.Context, provider FeatureProvider) error { + return api.setProviderWithContext(ctx, provider, true) } -func (api *evaluationAPI) SetProviderAndWait(provider FeatureProvider) error { - return api.setProvider(provider, false) +func (api *evaluationAPI) SetProviderAndWait(ctx context.Context, provider FeatureProvider) error { + return api.setProviderWithContext(ctx, provider, false) } // GetProviderMetadata returns the default FeatureProvider's metadata @@ -49,33 +48,6 @@ func (api *evaluationAPI) GetProviderMetadata() Metadata { return api.defaultProvider.Metadata() } -// SetNamedProvider sets a provider with client name. Returns an error if FeatureProvider is nil -func (api *evaluationAPI) SetNamedProvider(clientName string, provider FeatureProvider, async bool) error { - api.mu.Lock() - defer api.mu.Unlock() - - if provider == nil { - return errors.New("provider cannot be set to nil") - } - - // Initialize new named provider and Shutdown the old one - // Provider update must be non-blocking, hence initialization & Shutdown happens concurrently - oldProvider := api.namedProviders[clientName] - api.namedProviders[clientName] = provider - - err := api.initNewAndShutdownOld(context.Background(), clientName, provider, oldProvider, async) - if err != nil { - return err - } - - err = api.eventExecutor.registerNamedEventingProvider(clientName, provider) - if err != nil { - return err - } - - return nil -} - // GetNamedProviderMetadata returns the default FeatureProvider's metadata func (api *evaluationAPI) GetNamedProviderMetadata(name string) Metadata { api.mu.RLock() @@ -89,18 +61,6 @@ func (api *evaluationAPI) GetNamedProviderMetadata(name string) Metadata { return provider.Metadata() } -// Context-aware provider setup methods - -// SetProviderWithContext sets the default FeatureProvider with context-aware initialization. -func (api *evaluationAPI) SetProviderWithContext(ctx context.Context, provider FeatureProvider) error { - return api.setProviderWithContext(ctx, provider, true) -} - -// SetProviderWithContextAndWait sets the default FeatureProvider with context-aware initialization and waits for completion. -func (api *evaluationAPI) SetProviderWithContextAndWait(ctx context.Context, provider FeatureProvider) error { - return api.setProviderWithContext(ctx, provider, false) -} - // setProviderWithContext sets the default FeatureProvider of the evaluationAPI with context-aware initialization. func (api *evaluationAPI) setProviderWithContext(ctx context.Context, provider FeatureProvider, async bool) error { api.mu.Lock() @@ -126,8 +86,8 @@ func (api *evaluationAPI) setProviderWithContext(ctx context.Context, provider F return nil } -// SetNamedProviderWithContext sets a provider with client name using context-aware initialization. -func (api *evaluationAPI) SetNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error { +// setNamedProviderWithContext sets a provider with client name using context-aware initialization. +func (api *evaluationAPI) setNamedProviderWithContext(ctx context.Context, clientName string, provider FeatureProvider, async bool) error { api.mu.Lock() defer api.mu.Unlock() @@ -152,9 +112,14 @@ func (api *evaluationAPI) SetNamedProviderWithContext(ctx context.Context, clien return nil } +// SetNamedProviderWithContext sets a provider with client name using context-aware initialization. +func (api *evaluationAPI) SetNamedProvider(ctx context.Context, clientName string, provider FeatureProvider) error { + return api.setNamedProviderWithContext(ctx, clientName, provider, true) +} + // SetNamedProviderWithContextAndWait sets a provider with client name using context-aware initialization and waits for completion. -func (api *evaluationAPI) SetNamedProviderWithContextAndWait(ctx context.Context, clientName string, provider FeatureProvider) error { - return api.SetNamedProviderWithContext(ctx, clientName, provider, false) +func (api *evaluationAPI) SetNamedProviderAndWait(ctx context.Context, clientName string, provider FeatureProvider) error { + return api.setNamedProviderWithContext(ctx, clientName, provider, false) } // initNewAndShutdownOld is the main helper to initialise new FeatureProvider and Shutdown the old FeatureProvider. @@ -194,13 +159,8 @@ func (api *evaluationAPI) initNewAndShutdownOld(ctx context.Context, clientName } go func(forShutdown StateHandler, parentCtx context.Context) { - // Check if the provider supports context-aware shutdown - if contextHandler, ok := forShutdown.(ContextAwareStateHandler); ok { - // Use the provided context directly - user controls timeout - _ = contextHandler.ShutdownWithContext(parentCtx) - } else { - // Fall back to regular shutdown for backward compatibility - forShutdown.Shutdown() + if err := forShutdown.Shutdown(parentCtx); err != nil { + slog.Error("async provider shutdown failed", slog.Any("error", err)) } }(v, ctx) @@ -216,12 +176,12 @@ func (api *evaluationAPI) GetNamedProviders() map[string]FeatureProvider { } // GetClient returns a IClient bound to the default provider -func (api *evaluationAPI) GetClient() IClient { +func (api *evaluationAPI) GetClient() *Client { return newClient("", api, api.eventExecutor) } // GetNamedClient returns a IClient bound to the given named provider -func (api *evaluationAPI) GetNamedClient(clientName string) IClient { +func (api *evaluationAPI) GetNamedClient(clientName string) *Client { return newClient(clientName, api, api.eventExecutor) } @@ -232,10 +192,6 @@ func (api *evaluationAPI) SetEvaluationContext(evalCtx EvaluationContext) { api.evalCtx = evalCtx } -// Deprecated: use [github.com/open-feature/go-sdk/openfeature/hooks.LoggingHook] instead. -func (api *evaluationAPI) SetLogger(l logr.Logger) { -} - func (api *evaluationAPI) AddHooks(hooks ...Hook) { api.mu.Lock() defer api.mu.Unlock() @@ -260,16 +216,9 @@ func (api *evaluationAPI) RemoveHandler(eventType EventType, callback EventCallb api.eventExecutor.RemoveHandler(eventType, callback) } -func (api *evaluationAPI) Shutdown() { - // Use the context-aware shutdown with background context and ignore errors - // to maintain backward compatibility (Shutdown doesn't return an error) - _ = api.ShutdownWithContext(context.Background()) -} - -// ShutdownWithContext calls context-aware shutdown on all registered providers. -// If providers implement ContextAwareStateHandler, ShutdownWithContext will be called with the provided context. +// Shutdown calls shutdown on all registered providers. // Returns an error if any provider shutdown fails or if context is cancelled during shutdown. -func (api *evaluationAPI) ShutdownWithContext(ctx context.Context) error { +func (api *evaluationAPI) Shutdown(ctx context.Context) error { api.mu.Lock() defer api.mu.Unlock() @@ -277,23 +226,19 @@ func (api *evaluationAPI) ShutdownWithContext(ctx context.Context) error { // Shutdown default provider if api.defaultProvider != nil { - if contextHandler, ok := api.defaultProvider.(ContextAwareStateHandler); ok { - if err := contextHandler.ShutdownWithContext(ctx); err != nil { + if stateHandler, ok := api.defaultProvider.(StateHandler); ok { + if err := stateHandler.Shutdown(ctx); err != nil { errs = append(errs, fmt.Errorf("default provider shutdown failed: %w", err)) } - } else if stateHandler, ok := api.defaultProvider.(StateHandler); ok { - stateHandler.Shutdown() } } // Shutdown all named providers for name, provider := range api.namedProviders { - if contextHandler, ok := provider.(ContextAwareStateHandler); ok { - if err := contextHandler.ShutdownWithContext(ctx); err != nil { + if stateHandler, ok := provider.(StateHandler); ok { + if err := stateHandler.Shutdown(ctx); err != nil { errs = append(errs, fmt.Errorf("named provider %q shutdown failed: %w", name, err)) } - } else if stateHandler, ok := provider.(StateHandler); ok { - stateHandler.Shutdown() } } @@ -324,32 +269,6 @@ func (api *evaluationAPI) GetProvider() FeatureProvider { return api.defaultProvider } -// SetProvider sets the default FeatureProvider of the evaluationAPI. -// Returns an error if provider registration cause an error -func (api *evaluationAPI) setProvider(provider FeatureProvider, async bool) error { - api.mu.Lock() - defer api.mu.Unlock() - - if provider == nil { - return errors.New("default provider cannot be set to nil") - } - - oldProvider := api.defaultProvider - api.defaultProvider = provider - - err := api.initNewAndShutdownOld(context.Background(), "", provider, oldProvider, async) - if err != nil { - return err - } - - err = api.eventExecutor.registerDefaultProvider(provider) - if err != nil { - return err - } - - return nil -} - // initializerWithContext is a context-aware helper to execute provider initialization and generate appropriate event for the initialization // If the provider implements ContextAwareStateHandler, InitWithContext is called; otherwise, Init is called for backward compatibility. // It also returns an error if the initialization resulted in an error or if the context is cancelled. @@ -361,10 +280,10 @@ func initializerWithContext(ctx context.Context, provider FeatureProvider, evalC Message: "Provider initialization successful", }, } + var err error - // Check for context-aware handler first - if contextHandler, ok := provider.(ContextAwareStateHandler); ok { - err := contextHandler.InitWithContext(ctx, evalCtx) + if contextHandler, ok := provider.(StateHandler); ok { + err = contextHandler.Init(ContextWithEvaluationContext(ctx, evalCtx)) if err != nil { event.EventType = ProviderError @@ -381,26 +300,6 @@ func initializerWithContext(ctx context.Context, provider FeatureProvider, evalC event.Message = fmt.Sprintf("Provider initialization failed: %v", err) } } - return event, err - } - - // Fall back to regular StateHandler for backward compatibility - handler, ok := provider.(StateHandler) - if !ok { - // Note - a provider without state handling capability can be assumed to be ready immediately. - return event, nil - } - - err := handler.Init(evalCtx) - if err != nil { - event.EventType = ProviderError - event.Message = fmt.Sprintf("Provider initialization failed: %v", err) - var initErr *ProviderInitError - if errors.As(err, &initErr) { - event.EventType = ProviderError - event.ErrorCode = initErr.ErrorCode - event.Message = initErr.Message - } } return event, err diff --git a/openfeature/openfeature_test.go b/openfeature_test.go similarity index 84% rename from openfeature/openfeature_test.go rename to openfeature_test.go index fa031cc2..8e468edf 100644 --- a/openfeature/openfeature_test.go +++ b/openfeature_test.go @@ -1,6 +1,7 @@ package openfeature import ( + "context" "errors" "reflect" "testing" @@ -15,13 +16,13 @@ func TestRequirement_1_1_1(t *testing.T) { t.Cleanup(initSingleton) ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() 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) } @@ -38,11 +39,11 @@ func TestRequirement_1_1_2_1(t *testing.T) { t.Cleanup(initSingleton) ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) mockProviderName := "mock-provider" mockProvider.EXPECT().Metadata().Return(Metadata{Name: mockProviderName}).AnyTimes() - err := SetProvider(mockProvider) + err := SetProvider(t.Context(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -60,7 +61,7 @@ func TestRequirement_1_1_2_2(t *testing.T) { provider, initSem, _ := setupProviderWithSemaphores() - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -85,7 +86,7 @@ func TestRequirement_1_1_2_2(t *testing.T) { client := "client" - err := SetNamedProvider(client, provider) + err := SetProvider(t.Context(), provider, WithDomain(client)) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -112,7 +113,7 @@ func TestRequirement_1_1_2_3(t *testing.T) { provider, initSem, shutdownSem := setupProviderWithSemaphores() - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -127,7 +128,7 @@ func TestRequirement_1_1_2_3(t *testing.T) { providerOverride, _, _ := setupProviderWithSemaphores() - err = SetProvider(providerOverride) + err = SetProvider(t.Context(), providerOverride) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -148,7 +149,7 @@ func TestRequirement_1_1_2_3(t *testing.T) { client := "client" - err := SetNamedProvider(client, provider) + err := SetProvider(t.Context(), provider, WithDomain(client)) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -163,7 +164,7 @@ func TestRequirement_1_1_2_3(t *testing.T) { providerOverride, _, _ := setupProviderWithSemaphores() - err = SetNamedProvider(client, providerOverride) + err = SetProvider(t.Context(), providerOverride, WithDomain(client)) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -184,21 +185,21 @@ func TestRequirement_1_1_2_3(t *testing.T) { provider, _, shutdownSem := setupProviderWithSemaphores() // register provider multiple times - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Errorf("error setting up provider %v", err) } clientName := "clientA" - err = SetNamedProvider(clientName, provider) + err = SetProvider(t.Context(), provider, WithDomain(clientName)) if err != nil { t.Errorf("error setting up provider %v", err) } providerOverride, _, _ := setupProviderWithSemaphores() - err = SetNamedProvider(clientName, providerOverride) + err = SetProvider(t.Context(), providerOverride, WithDomain(clientName)) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -224,19 +225,19 @@ func TestRequirement_1_1_2_3(t *testing.T) { clientA := "clientA" clientB := "clientB" - err := SetNamedProvider(clientA, providerA) + err := SetProvider(t.Context(), providerA, WithDomain(clientA)) if err != nil { t.Errorf("error setting up provider %v", err) } - err = SetNamedProvider(clientB, providerA) + err = SetProvider(t.Context(), providerA, WithDomain(clientB)) if err != nil { t.Errorf("error setting up provider %v", err) } providerOverride, _, _ := setupProviderWithSemaphores() - err = SetNamedProvider(clientA, providerOverride) + err = SetProvider(t.Context(), providerOverride, WithDomain(clientA)) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -266,7 +267,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { <-time.After(200 * time.Millisecond) initialized = true return nil @@ -275,7 +276,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { } // when - registered - err := SetProviderAndWait(provider) + err := SetProviderAndWait(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -296,7 +297,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { <-time.After(200 * time.Millisecond) initialized = true return nil @@ -305,7 +306,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { } // when - registered - err := SetNamedProviderAndWait("someName", provider) + err := SetProviderAndWait(t.Context(), provider, WithDomain("someName")) if err != nil { t.Fatal(err) } @@ -324,7 +325,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { <-time.After(200 * time.Millisecond) return errors.New("some initialization error") }, @@ -336,10 +337,10 @@ func TestRequirement_1_1_2_4(t *testing.T) { errChan <- details } - AddHandler(ProviderError, &errHandler) + AddHandler(ProviderError, errHandler) // when - err := SetProviderAndWait(provider) + err := SetProviderAndWait(t.Context(), provider) // then if err == nil { @@ -371,7 +372,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { s <- struct{}{} // initialization is blocked until read from the channel initialized = true return nil @@ -380,7 +381,7 @@ func TestRequirement_1_1_2_4(t *testing.T) { } // when - registered async - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Fatal(err) } @@ -401,18 +402,18 @@ func TestRequirement_1_1_3(t *testing.T) { // Setup ctrl := gomock.NewController(t) - providerA := NewMockFeatureProvider(ctrl) + providerA := NewMockProvider(ctrl) providerA.EXPECT().Metadata().Return(Metadata{Name: "providerA"}).AnyTimes() - providerB := NewMockFeatureProvider(ctrl) + providerB := NewMockProvider(ctrl) providerB.EXPECT().Metadata().Return(Metadata{Name: "providerB"}).AnyTimes() - err := SetNamedProvider("clientA", providerA) + err := SetProvider(t.Context(), providerA, WithDomain("clientA")) if err != nil { t.Errorf("error setting up provider %v", err) } - err = SetNamedProvider("clientB", providerB) + err = SetProvider(t.Context(), providerB, WithDomain("clientB")) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -447,10 +448,10 @@ func TestRequirement_1_1_3(t *testing.T) { // Validate overriding: If the client-domain already has a bound provider, it is overwritten with the new mapping. - providerB2 := NewMockFeatureProvider(ctrl) + providerB2 := NewMockProvider(ctrl) providerB2.EXPECT().Metadata().Return(Metadata{Name: "providerB2"}).AnyTimes() - err = SetNamedProvider("clientB", providerB2) + err = SetProvider(t.Context(), providerB2, WithDomain("clientB")) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -491,7 +492,7 @@ func TestRequirement_1_1_5(t *testing.T) { t.Run("default provider", func(t *testing.T) { defaultProvider := NoopProvider{} - err := SetProvider(defaultProvider) + err := SetProvider(t.Context(), defaultProvider) if err != nil { t.Errorf("provider registration failed %v", err) } @@ -504,11 +505,11 @@ func TestRequirement_1_1_5(t *testing.T) { defaultProvider := NoopProvider{} name := "test-provider" - err := SetNamedProvider(name, defaultProvider) + err := SetProvider(t.Context(), defaultProvider, WithDomain(name)) if err != nil { t.Errorf("provider registration failed %v", err) } - if NamedProviderMetadata(name) != defaultProvider.Metadata() { + if ProviderMetadata(WithDomain(name)) != defaultProvider.Metadata() { t.Error("default global provider's metadata isn't NoopProvider's metadata") } }) @@ -520,7 +521,7 @@ func TestRequirement_1_1_6(t *testing.T) { t.Cleanup(initSingleton) t.Run("client from direct invocation", func(t *testing.T) { - client := NewClient("test-client") + client := NewClient(WithDomain("test-client")) if client == nil { t.Errorf("expected an Client instance, but got invalid") } @@ -544,7 +545,7 @@ func TestRequirement_1_1_6(t *testing.T) { // The client creation function MUST NOT throw, or otherwise abnormally terminate. func TestRequirement_1_1_7(t *testing.T) { t.Cleanup(initSingleton) - type clientCreationFunc func(name string) *Client + type clientCreationFunc func(...CallOption) *Client // asserting that our NewClient method matches this signature is enough to deduce that no error is returned var f clientCreationFunc = NewClient @@ -559,7 +560,7 @@ func TestRequirement_1_6_1(t *testing.T) { provider, initSem, shutdownSem := setupProviderWithSemaphores() // Setup provider and wait for initialization done - err := SetProvider(provider) + err := SetProvider(t.Context(), provider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -572,7 +573,10 @@ func TestRequirement_1_6_1(t *testing.T) { break } - Shutdown() + err = Shutdown(t.Context()) + if err != nil { + t.Errorf("error while shutting down %v", err) + } select { // short enough wait time, but not too long @@ -594,12 +598,15 @@ func TestRequirement_1_6_2(t *testing.T) { provider1, _, shutdownSem1 := setupProviderWithSemaphores() // Setup provider and wait for initialization done - err := SetProviderAndWait(provider1) + err := SetProviderAndWait(t.Context(), provider1) if err != nil { t.Errorf("error setting up provider %v", err) } - Shutdown() + err = Shutdown(t.Context()) + if err != nil { + t.Errorf("error while shutting down %v", err) + } // Shutdown should be synchronous. Try a non-blocking receive and fail // immediately if there is not a value in the channel. @@ -614,12 +621,15 @@ func TestRequirement_1_6_2(t *testing.T) { // again, since it is now inactive. provider2, _, shutdownSem2 := setupProviderWithSemaphores() - err = SetProviderAndWait(provider2) + err = SetProviderAndWait(t.Context(), provider2) if err != nil { t.Errorf("error setting up provider %v", err) } - Shutdown() + err = Shutdown(t.Context()) + if err != nil { + t.Errorf("error while shutting down %v", err) + } select { case <-shutdownSem2: @@ -644,13 +654,13 @@ func TestRequirement_EventCompliance(t *testing.T) { clientName := "OFClient" - client := NewClient(clientName) + client := NewClient(WithDomain(clientName)) // adding handlers - client.AddHandler(ProviderReady, &h1) - client.AddHandler(ProviderError, &h1) - client.AddHandler(ProviderStale, &h1) - client.AddHandler(ProviderConfigChange, &h1) + client.AddHandler(ProviderReady, h1) + client.AddHandler(ProviderError, h1) + client.AddHandler(ProviderStale, h1) + client.AddHandler(ProviderConfigChange, h1) registry := eventing.GetClientRegistry(clientName) @@ -671,10 +681,10 @@ func TestRequirement_EventCompliance(t *testing.T) { } // removing handlers - client.RemoveHandler(ProviderReady, &h1) - client.RemoveHandler(ProviderError, &h1) - client.RemoveHandler(ProviderStale, &h1) - client.RemoveHandler(ProviderConfigChange, &h1) + client.RemoveHandler(ProviderReady, h1) + client.RemoveHandler(ProviderError, h1) + client.RemoveHandler(ProviderStale, h1) + client.RemoveHandler(ProviderConfigChange, h1) if len(registry.eventCallbacks()[ProviderReady]) > 0 { t.Errorf("expected empty registrations") @@ -698,10 +708,10 @@ func TestRequirement_EventCompliance(t *testing.T) { t.Cleanup(initSingleton) // adding handlers - AddHandler(ProviderReady, &h1) - AddHandler(ProviderError, &h1) - AddHandler(ProviderStale, &h1) - AddHandler(ProviderConfigChange, &h1) + AddHandler(ProviderReady, h1) + AddHandler(ProviderError, h1) + AddHandler(ProviderStale, h1) + AddHandler(ProviderConfigChange, h1) registry := eventing.GetAPIRegistry() @@ -722,10 +732,10 @@ func TestRequirement_EventCompliance(t *testing.T) { } // removing handlers - RemoveHandler(ProviderReady, &h1) - RemoveHandler(ProviderError, &h1) - RemoveHandler(ProviderStale, &h1) - RemoveHandler(ProviderConfigChange, &h1) + RemoveHandler(ProviderReady, h1) + RemoveHandler(ProviderError, h1) + RemoveHandler(ProviderStale, h1) + RemoveHandler(ProviderConfigChange, h1) registry = eventing.GetAPIRegistry() @@ -754,10 +764,10 @@ func TestDefaultClientUsage(t *testing.T) { t.Cleanup(initSingleton) ctrl := gomock.NewController(t) - defaultProvider := NewMockFeatureProvider(ctrl) + defaultProvider := NewMockProvider(ctrl) defaultProvider.EXPECT().Metadata().Return(Metadata{Name: "defaultClientReplacement"}).AnyTimes() - err := SetProvider(defaultProvider) + err := SetProvider(t.Context(), defaultProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -777,30 +787,24 @@ func TestLateBindingOfDefaultProvider(t *testing.T) { expectedResultFromLateDefaultProvider := "value-from-late-default-provider" ctrl := gomock.NewController(t) - defaultProvider := NewMockFeatureProvider(ctrl) + defaultProvider := NewMockProvider(ctrl) defaultProvider.EXPECT().Metadata().Return(Metadata{Name: "defaultClientReplacement"}).AnyTimes() defaultProvider.EXPECT().Hooks().AnyTimes().Return([]Hook{}) defaultProvider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(StringResolutionDetail{Value: expectedResultFromLateDefaultProvider}) - client := NewClient("app") - strResult, err := client.StringValue(t.Context(), "flag", expectedResultUnboundProvider, EvaluationContext{}) - if err != nil { - t.Errorf("flag evaluation failed %v", err) - } + client := NewClient(WithDomain("app")) + strResult := client.String(t.Context(), "flag", expectedResultUnboundProvider, EvaluationContext{}) if strResult != expectedResultUnboundProvider { t.Errorf("expected %s, but got %s", expectedResultUnboundProvider, strResult) } - err = SetProviderAndWait(defaultProvider) + err := SetProviderAndWait(t.Context(), defaultProvider) if err != nil { t.Errorf("provider registration failed %v", err) } - strResult, err = client.StringValue(t.Context(), "flag", "default", EvaluationContext{}) - if err != nil { - t.Errorf("flag evaluation failed %v", err) - } + strResult = client.String(t.Context(), "flag", "default", EvaluationContext{}) if strResult != expectedResultFromLateDefaultProvider { t.Errorf("expected %s, but got %s", expectedResultFromLateDefaultProvider, strResult) @@ -811,12 +815,12 @@ func TestLateBindingOfDefaultProvider(t *testing.T) { func TestForNilProviders(t *testing.T) { t.Cleanup(initSingleton) - err := SetProvider(nil) + err := SetProvider(t.Context(), nil) if err == nil { t.Errorf("setting nil provider must result in an error") } - err = SetNamedProvider("client", nil) + err = SetProvider(t.Context(), nil, WithDomain("client")) if err == nil { t.Errorf("setting nil named provider must result in an error") } @@ -839,13 +843,14 @@ func setupProviderWithSemaphores() (struct { sh := &stateHandlerForTests{ // Semaphore must be invoked - initF: func(e EvaluationContext) error { + initF: func(context.Context) error { intiSem <- "" return nil }, // Semaphore must be invoked - shutdownF: func() { + shutdownF: func(context.Context) error { shutdownSem <- "" + return nil }, } diff --git a/openfeature_v1_to_v2.patch b/openfeature_v1_to_v2.patch new file mode 100644 index 00000000..3afba65a --- /dev/null +++ b/openfeature_v1_to_v2.patch @@ -0,0 +1,426 @@ +# Gopatch file for migrating OpenFeature Go SDK from v1 to v2 +# Usage: gopatch -p openfeature_v1_to_v2.patch ./... +# StringValue +@@ +var value, err, client identifier +@@ +-value, err := client.StringValue(...) ++value := client.String(...) +@@ +var value, err, client identifier +@@ +-value, err = client.StringValue(...) ++value = client.String(...) +#BooleanValue +@@ +var value, err, client identifier +@@ +-value, err := client.BooleanValue(...) ++value := client.Boolean(...) +@@ +var value, err, client identifier +@@ +-value, err = client.BooleanValue(...) ++value = client.Boolean(...) +#IntValue +@@ +var value, err, client identifier +@@ +-value, err := client.IntValue(...) ++value := client.Int(...) +@@ +var value, err, client identifier +@@ +-value, err = client.IntValue(...) ++value = client.Int(...) +# FloatValue +@@ +var value, err, client identifier +@@ +-value, err := client.FloatValue(...) ++value := client.Float(...) +@@ +var value, err, client identifier +@@ +-value, err = client.FloatValue(...) ++value = client.Float(...) +# ObjectValue +@@ +var value, err, client identifier +@@ +-value, err := client.ObjectValue(...) ++value := client.Object(...) +@@ +var value, err, client identifier +@@ +-value, err = client.ObjectValue(...) ++value = client.Object(...) + +@@ +var client identifier +@@ +-client.Metadata().Name ++client.Metadata().Domain() + +# set provider +@@ +var openfeature identifier +var provider expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetProvider(provider) ++openfeature.SetProvider(context.TODO(), provider) + +@@ +var openfeature identifier +var ctx, provider expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetProviderWithContext(ctx, provider) ++openfeature.SetProvider(ctx, provider) + +@@ +var openfeature identifier +var provider expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetProviderAndWait(provider) ++openfeature.SetProviderAndWait(context.TODO(), provider) + +@@ +var openfeature identifier +var ctx, provider expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetProviderWithContextAndWait(ctx, provider) ++openfeature.SetProviderAndWait(ctx, provider) + +@@ +var openfeature identifier +var provider, domain expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetNamedProvider(domain, provider) ++openfeature.SetProvider(context.TODO(), provider, openfeature.WithDomain(domain)) + +@@ +var openfeature identifier +var provider, domain expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetNamedProviderAndWait(domain, provider) ++openfeature.SetProviderAndWait(context.TODO(), provider, openfeature.WithDomain(domain)) + +@@ +var openfeature identifier +var ctx, provider, domain expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetNamedProviderWithContext(ctx, domain, provider) ++openfeature.SetProvider(ctx, provider, openfeature.WithDomain(domain)) + +@@ +var openfeature identifier +var ctx, provider, domain expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.SetNamedProviderWithContextAndWait(ctx, domain, provider) ++openfeature.SetProviderAndWait(ctx, provider, openfeature.WithDomain(domain)) + +@@ +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.Shutdown() ++openfeature.Shutdown(context.TODO()) + +@@ +var openfeature identifier +var ctx expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.ShutdownWithContext(ctx) ++openfeature.Shutdown(ctx) + +# some renames +@@ +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.WithTransactionContext(...) ++openfeature.ContextWithEvaluationContext(...) + +@@ +var openfeature identifier +@@ + import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.TransactionContext(...) ++openfeature.EvaluationContextFromContext(...) + +@@ +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.InterfaceEvaluationDetails ++openfeature.ObjectEvaluationDetails + +# change imports + +# change contrib aws-ssm import +@@ +var x, awsssm identifier +@@ +-import awsssm "github.com/open-feature/go-sdk-contrib/providers/aws-ssm" ++import awsssm "go.openfeature.dev/contrib/providers/aws-ssm/v2" + +awsssm.x + +# change contrib configcat import +@@ +var x, configcat identifier +@@ +-import configcat "github.com/open-feature/go-sdk-contrib/providers/configcat/pkg" ++import configcat "go.openfeature.dev/contrib/providers/configcat/v2/pkg" + +configcat.x + +# change contrib flagd import +@@ +var x, flagd identifier +@@ +-import flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" ++import flagd "go.openfeature.dev/contrib/providers/flagd/v2/pkg" + +flagd.x + +# change contrib flagsmith import +@@ +var x, flagsmith identifier +@@ +-import flagsmith "github.com/open-feature/go-sdk-contrib/providers/flagsmith/pkg" ++import flagsmith "go.openfeature.dev/contrib/providers/flagsmith/v2/pkg" + +flagsmith.x + +# change contrib flipt import +@@ +var x, flipt identifier +@@ +-import flipt "github.com/open-feature/go-sdk-contrib/providers/flipt/pkg" ++import flipt "go.openfeature.dev/contrib/providers/flipt/v2/pkg" + +flipt.x + +# change contrib envvar import +@@ +var x, envvar identifier +@@ +-import envvar "github.com/open-feature/go-sdk-contrib/providers/from-env/pkg" ++import envvar "go.openfeature.dev/contrib/providers/envvar/v2" + +envvar.x + +@@ +var envvar identifier +@@ +import envvar "go.openfeature.dev/contrib/providers/envvar/v2" + +-envvar.FromEnvProvider ++envvar.EnvVarProvider + +# change contrib go-feature-flag-in-process import +@@ +var x, gofeatureflaginprocess identifier +@@ +-import gofeatureflaginprocess "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag-in-process/pkg" ++import gofeatureflaginprocess "go.openfeature.dev/contrib/providers/go-feature-flag-in-process/v2/pkg" + +gofeatureflaginprocess.x + +# change contrib go-feature-flag import +@@ +var x, gofeatureflag identifier +@@ +-import gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" ++import gofeatureflag "go.openfeature.dev/contrib/providers/go-feature-flag/v2/pkg" + +gofeatureflag.x + +# change contrib harness import +@@ +var x, harness identifier +@@ +-import harness "github.com/open-feature/go-sdk-contrib/providers/harness/pkg" ++import harness "go.openfeature.dev/contrib/providers/harness/v2/pkg" + +harness.x + +# change contrib launchdarkly import +@@ +var x, launchdarkly identifier +@@ +-import launchdarkly "github.com/open-feature/go-sdk-contrib/providers/launchdarkly/pkg" ++import launchdarkly "go.openfeature.dev/contrib/providers/launchdarkly/v2/pkg" + +launchdarkly.x + +# change contrib ofrep import +@@ +var ofrep identifier +var uri expression +@@ +import ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep" + +-ofrep.NewProvider(uri,...) ++ofrep.NewProvider(ofrep.WithBaseURI(uri),...) + +@@ +var x, ofrep identifier +@@ + +-import ofrep "github.com/open-feature/go-sdk-contrib/providers/ofrep" ++import ofrep "go.openfeature.dev/contrib/providers/ofrep/v2" + +ofrep.x + +# change contrib prefab import +@@ +var x, prefab identifier +@@ +-import prefab "github.com/open-feature/go-sdk-contrib/providers/prefab/pkg" ++import prefab "go.openfeature.dev/contrib/providers/prefab/v2/pkg" + +prefab.x + +# change contrib rocketflag import +@@ +var x, rocketflag identifier +@@ +-import rocketflag "github.com/open-feature/go-sdk-contrib/providers/rocketflag" ++import rocketflag "go.openfeature.dev/contrib/providers/rocketflag/v2" + +rocketflag.x + +# change contrib statsig import +@@ +var x, statsig identifier +@@ +-import statsig "github.com/open-feature/go-sdk-contrib/providers/statsig/pkg" ++import statsig "go.openfeature.dev/contrib/providers/statsig/v2/pkg" + +statsig.x + +# change contrib unleash import +@@ +var x, unleash identifier +@@ +-import unleash "github.com/open-feature/go-sdk-contrib/providers/unleash/pkg" ++import unleash "go.openfeature.dev/contrib/providers/unleash/v2/pkg" + +unleash.x + +# change contrib otel hooks +@@ +var x, otel identifier +@@ +- import otel "github.com/open-feature/go-sdk-contrib/hooks/open-telemetry/pkg" ++ import otel "go.openfeature.dev/contrib/hooks/open-telemetry/v2" + +otel.x + +# change named openfeature import +@@ +var memprovider identifier +@@ +import memprovider "github.com/open-feature/go-sdk/openfeature/memprovider" + +-memprovider.NewInMemoryProvider(...) ++memprovider.NewProvider(...) + +@@ +var x, memprovider identifier +@@ +-import memprovider "github.com/open-feature/go-sdk/openfeature/memprovider" ++import memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" + +memprovider.x + +@@ +var testing identifier +@@ +import testing "github.com/open-feature/go-sdk/openfeature/testing" + +-testing.NewTestProvider(...) ++testing.NewProvider(...) + +@@ +var x, testing identifier +@@ +-import testing "github.com/open-feature/go-sdk/openfeature/testing" ++import testing "go.openfeature.dev/openfeature/v2/providers/testing" + +testing.x + +@@ +var x, multi identifier +@@ +-import multi "github.com/open-feature/go-sdk/openfeature/multi" ++import multi "go.openfeature.dev/openfeature/v2/providers/multi" + +multi.x + +@@ +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.NewClient("") ++openfeature.NewClient() + +@@ +var openfeature identifier +var domain expression +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.NewClient(domain) ++openfeature.NewClient(openfeature.WithDomain(domain)) + +@@ +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.NewDefaultClient() ++openfeature.NewClient() + +@@ +var domain expression +var openfeature identifier +@@ +import openfeature "github.com/open-feature/go-sdk/openfeature" + +-openfeature.NamedProviderMetadata(domain) ++openfeature.ProviderMetadata(openfeature.WithDomain(domain)) + +@@ +var x, openfeature identifier +@@ +-import openfeature "github.com/open-feature/go-sdk/openfeature" ++import openfeature "go.openfeature.dev/openfeature/v2" + +openfeature.x + diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go deleted file mode 100644 index 479c086d..00000000 --- a/pkg/openfeature/client.go +++ /dev/null @@ -1,126 +0,0 @@ -//nolint:staticcheck -package openfeature - -import ( - "github.com/open-feature/go-sdk/openfeature" -) - -// IClient defines the behaviour required of an openfeature client -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.IClient, instead. -type IClient = openfeature.IClient - -// ClientMetadata provides a client's metadata -// -// 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(domain string) ClientMetadata { - return openfeature.NewClientMetadata(domain) -} - -// Client implements the behaviour required of an openfeature client -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.Client, instead. -type Client = openfeature.Client - -// NewClient returns a new Client. Domain is a unique identifier for this client -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.NewClient, -// instead. -func NewClient(domain string) *Client { - return openfeature.NewClient(domain) -} - -// Type represents the type of a flag -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.Type, instead. -type Type = openfeature.Type - -const ( - // 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 -) - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.EvaluationDetails, instead. -type EvaluationDetails = openfeature.EvaluationDetails - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.BooleanEvaluationDetails, -// instead. -type BooleanEvaluationDetails = openfeature.BooleanEvaluationDetails - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.StringEvaluationDetails, instead. -type StringEvaluationDetails = openfeature.StringEvaluationDetails - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.FloatEvaluationDetails, instead. -type FloatEvaluationDetails = openfeature.FloatEvaluationDetails - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.IntEvaluationDetails, instead. -type IntEvaluationDetails = openfeature.IntEvaluationDetails - -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.InterfaceEvaluationDetails, -// instead. -type InterfaceEvaluationDetails = openfeature.InterfaceEvaluationDetails - -// 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). -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.FlagMetadata, -// instead. -type FlagMetadata = openfeature.FlagMetadata - -// Option applies a change to 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 -// -// 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 openfeature.WithHooks(hooks...) -} - -// WithHookHints applies provided hook hints. -// -// 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 deleted file mode 100644 index aa0cf38a..00000000 --- a/pkg/openfeature/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -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 deleted file mode 100644 index 32951240..00000000 --- a/pkg/openfeature/evaluation_context.go +++ /dev/null @@ -1,34 +0,0 @@ -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 -// -// 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]any) EvaluationContext { - 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]any) EvaluationContext { - return openfeature.NewTargetlessEvaluationContext(attributes) -} diff --git a/pkg/openfeature/hooks.go b/pkg/openfeature/hooks.go deleted file mode 100644 index fc0a72d9..00000000 --- a/pkg/openfeature/hooks.go +++ /dev/null @@ -1,60 +0,0 @@ -package openfeature - -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 -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.Hook, instead. -type Hook = openfeature.Hook - -// HookHints contains a map of hints for hooks -// -// 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]any) HookHints { - return openfeature.NewHookHints(mapOfHints) -} - -// HookContext defines the base level fields of a hook context -// -// 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, - defaultValue any, - clientMetadata ClientMetadata, - providerMetadata Metadata, - evaluationContext EvaluationContext, -) HookContext { - return openfeature.NewHookContext(flagKey, flagType, defaultValue, clientMetadata, providerMetadata, evaluationContext) -} - -// UnimplementedHook implements all hook methods with empty functions -// Include UnimplementedHook in your hook struct to avoid defining empty functions -// e.g. -// -// type MyHook = openfeature.MyHook -// UnimplementedHook -// } -// -// 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 deleted file mode 100644 index ed7f09e8..00000000 --- a/pkg/openfeature/memprovider/in_memory_provider.go +++ /dev/null @@ -1,52 +0,0 @@ -//nolint:staticcheck -package memprovider - -import ( - "github.com/open-feature/go-sdk/openfeature/memprovider" -) - -const ( - // 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 -) - -// 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 memprovider.NewInMemoryProvider(from) -} - -// Type Definitions for InMemoryProvider flag - -// State of the feature flag -// -// 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. -// -// 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 -// -// 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 deleted file mode 100644 index 97a1832b..00000000 --- a/pkg/openfeature/noop_provider.go +++ /dev/null @@ -1,10 +0,0 @@ -package openfeature - -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 deleted file mode 100644 index a0d97503..00000000 --- a/pkg/openfeature/openfeature.go +++ /dev/null @@ -1,81 +0,0 @@ -package openfeature - -import ( - "github.com/go-logr/logr" - "github.com/open-feature/go-sdk/openfeature" -) - -// 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 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 -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.SetNamedProvider, -// instead. -func SetNamedProvider(domain string, provider FeatureProvider) error { - return openfeature.SetNamedProvider(domain, provider) -} - -// SetEvaluationContext sets the global evaluation context. -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.SetEvaluationContext, instead. -func SetEvaluationContext(evalCtx EvaluationContext) { - openfeature.SetEvaluationContext(evalCtx) -} - -// SetLogger sets the global Logger. -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.SetLogger, -// instead. -func SetLogger(l logr.Logger) { - 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 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) { - 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) { - 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) { - openfeature.RemoveHandler(eventType, callback) -} - -// Shutdown active providers -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.Shutdown, -// instead. -func Shutdown() { - openfeature.Shutdown() -} diff --git a/pkg/openfeature/provider.go b/pkg/openfeature/provider.go deleted file mode 100644 index 0fc3169a..00000000 --- a/pkg/openfeature/provider.go +++ /dev/null @@ -1,188 +0,0 @@ -//nolint:staticcheck -package openfeature - -import ( - "github.com/open-feature/go-sdk/openfeature" -) - -const ( - // DefaultReason - the resolved value was configured statically, or otherwise fell back to a pre-configured value. - // - // 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. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.TargetingMatchReason, instead. - TargetingMatchReason = openfeature.TargetingMatchReason - // SplitReason - the resolved value was the result of pseudorandom assignment. - // - // 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. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.DisabledReason, instead. - DisabledReason = openfeature.DisabledReason - // StaticReason - the resolved value is static (no dynamic evaluation) - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.StaticReason, instead. - StaticReason = openfeature.StaticReason - // CachedReason - the resolved value was retrieved from cache - // - // 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. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. - UnknownReason = openfeature.UnknownReason - // ErrorReason - the resolved value was the result of an error. - // - // 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. -// -// 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 -// -// 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 -// -// 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 -// -// 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 -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.NoopStateHandler, -// instead. -type NoopStateHandler = openfeature.NoopStateHandler - -// EventHandler is the eventing contract enforced for FeatureProvider -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventHandler, -// instead. -type EventHandler = openfeature.EventHandler - -// EventType emitted by a provider implementation -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventType, -// instead. -type EventType = openfeature.EventType - -// ProviderEventDetails is the event payload emitted by FeatureProvider -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.ProviderEventDetails, instead. -type ProviderEventDetails = openfeature.ProviderEventDetails - -// Event is an event emitted by a FeatureProvider. -// -// Deprecated: use github.com/open-feature/go-sdk/openfeature.Event, instead. -type Event = openfeature.Event - -// Deprecated: use github.com/open-feature/go-sdk/openfeature.EventDetails, -// instead. -type EventDetails = openfeature.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 -// -// 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 -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.BoolResolutionDetail, instead. -type BoolResolutionDetail = openfeature.BoolResolutionDetail - -// StringResolutionDetail provides a resolution detail with string type -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.StringResolutionDetail, instead. -type StringResolutionDetail = openfeature.StringResolutionDetail - -// FloatResolutionDetail provides a resolution detail with float64 type -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.FloatResolutionDetail, instead. -type FloatResolutionDetail = openfeature.FloatResolutionDetail - -// IntResolutionDetail provides a resolution detail with int64 type -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.IntResolutionDetail, instead. -type IntResolutionDetail = openfeature.IntResolutionDetail - -// InterfaceResolutionDetail provides a resolution detail with any type -// -// Deprecated: use -// github.com/open-feature/go-sdk/openfeature.InterfaceResolutionDetail, -// instead. -type InterfaceResolutionDetail = openfeature.InterfaceResolutionDetail - -// Metadata provides provider name -// -// 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 deleted file mode 100644 index c64dbf64..00000000 --- a/pkg/openfeature/resolution_error.go +++ /dev/null @@ -1,106 +0,0 @@ -//nolint:staticcheck -package openfeature - -import "github.com/open-feature/go-sdk/openfeature" - -// 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. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. - ProviderNotReadyCode = openfeature.ProviderNotReadyCode - // FlagNotFoundCode - the flag could not be 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. - // - // 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. - // - // 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. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. - TargetingKeyMissingCode = openfeature.TargetingKeyMissingCode - // InvalidContextCode - the evaluation context does not meet provider requirements. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. - InvalidContextCode = openfeature.InvalidContextCode - // GeneralCode - the error was for a reason not enumerated above. - // - // Deprecated: use github.com/open-feature/go-sdk/openfeature.UnknownReason, instead. - GeneralCode = openfeature.GeneralCode -) - -// ResolutionError is an enumerated error code with an optional 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 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 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 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 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 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 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 openfeature.NewGeneralResolutionError(msg) -} diff --git a/openfeature/provider.go b/provider.go similarity index 83% rename from openfeature/provider.go rename to provider.go index c97b6675..04e55089 100644 --- a/openfeature/provider.go +++ b/provider.go @@ -53,7 +53,7 @@ type FeatureProvider interface { StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx FlattenedContext) StringResolutionDetail FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx FlattenedContext) FloatResolutionDetail IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx FlattenedContext) IntResolutionDetail - ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail + ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail Hooks() []Hook } @@ -63,28 +63,8 @@ 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() -} - -// ContextAwareStateHandler extends StateHandler with context-aware initialization and shutdown -// for providers that need to respect request timeouts and cancellation. -// If a provider implements this interface, InitWithContext and ShutdownWithContext will be called instead of Init and Shutdown. -// -// Use this interface when your provider needs to: -// - Respect initialization/shutdown timeouts (e.g., network calls, database connections) -// - Support graceful cancellation during setup and teardown -// - Integrate with request-scoped contexts -// -// Best practices: -// - Always check ctx.Done() in long-running initialization and shutdown operations -// - Use reasonable timeout values (typically 5-30 seconds) -// - Return ctx.Err() when the context is cancelled -// - Maintain backward compatibility by implementing both interfaces -type ContextAwareStateHandler interface { - StateHandler // Embed existing interface for backward compatibility - InitWithContext(ctx context.Context, evaluationContext EvaluationContext) error - ShutdownWithContext(ctx context.Context) error + Init(context.Context) error + Shutdown(context.Context) error } // Tracker is the contract for tracking @@ -93,16 +73,19 @@ type Tracker interface { Track(ctx context.Context, trackingEventName string, evaluationContext EvaluationContext, details TrackingEventDetails) } +var _ StateHandler = (*NoopStateHandler)(nil) + // NoopStateHandler is a noop StateHandler implementation type NoopStateHandler struct{} -func (s *NoopStateHandler) Init(e EvaluationContext) error { +func (s *NoopStateHandler) Init(context.Context) error { // NOOP return nil } -func (s *NoopStateHandler) Shutdown() { +func (s *NoopStateHandler) Shutdown(context.Context) error { // NOOP + return nil } // Eventing @@ -135,7 +118,7 @@ type EventDetails struct { ProviderEventDetails } -type EventCallback *func(details EventDetails) +type EventCallback func(details EventDetails) // NoopEventHandler is the out-of-the-box EventHandler which is noop type NoopEventHandler struct{} @@ -175,8 +158,13 @@ func (p ProviderResolutionDetail) Error() error { return errors.New(p.ResolutionError.Error()) } +// FlagTypes defines the types that can be used for flag values. +type FlagTypes interface { + int64 | float64 | string | bool | any +} + // GenericResolutionDetail represents the result of the provider's flag resolution process. -type GenericResolutionDetail[T any] struct { +type GenericResolutionDetail[T FlagTypes] struct { Value T ProviderResolutionDetail } @@ -190,8 +178,8 @@ type ( FloatResolutionDetail = GenericResolutionDetail[float64] // IntResolutionDetail represents the result of the provider's flag resolution process for int64 flags. IntResolutionDetail = GenericResolutionDetail[int64] - // InterfaceResolutionDetail represents the result of the provider's flag resolution process for Object flags. - InterfaceResolutionDetail = GenericResolutionDetail[any] + // ObjectResolutionDetail represents the result of the provider's flag resolution process for Object flags. + ObjectResolutionDetail = GenericResolutionDetail[any] ) // Metadata provides provider name diff --git a/openfeature/provider_test.go b/provider_test.go similarity index 95% rename from openfeature/provider_test.go rename to provider_test.go index 9d542bcf..f7d2ce47 100644 --- a/openfeature/provider_test.go +++ b/provider_test.go @@ -12,7 +12,7 @@ import ( // of type string, which identifies the provider implementation. func TestRequirement_2_1_1(t *testing.T) { ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) type requirements interface { Metadata() Metadata @@ -39,14 +39,14 @@ func TestRequirement_2_1_1(t *testing.T) { // and `evaluation context` (optional), which returns a `flag resolution` structure. func TestRequirement_2_2_1(t *testing.T) { ctrl := gomock.NewController(t) - mockProvider := NewMockFeatureProvider(ctrl) + mockProvider := NewMockProvider(ctrl) type requirements interface { BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx FlattenedContext) BoolResolutionDetail StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx FlattenedContext) StringResolutionDetail FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx FlattenedContext) FloatResolutionDetail IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx FlattenedContext) IntResolutionDetail - ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) InterfaceResolutionDetail + ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx FlattenedContext) ObjectResolutionDetail } var mockProviderI any = mockProvider diff --git a/openfeature/memprovider/README.md b/providers/inmemory/README.md similarity index 100% rename from openfeature/memprovider/README.md rename to providers/inmemory/README.md diff --git a/openfeature/memprovider/in_memory_provider.go b/providers/inmemory/inmemory_provider.go similarity index 91% rename from openfeature/memprovider/in_memory_provider.go rename to providers/inmemory/inmemory_provider.go index 7fc95853..988124c1 100644 --- a/openfeature/memprovider/in_memory_provider.go +++ b/providers/inmemory/inmemory_provider.go @@ -1,11 +1,11 @@ -// Package memprovider provides an in-memory feature flag provider for OpenFeature. -package memprovider +// Package inmemory provides an in-memory feature flag provider for OpenFeature. +package inmemory import ( "context" "fmt" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) const ( @@ -18,7 +18,7 @@ type InMemoryProvider struct { trackingEvents map[string][]InMemoryEvent } -func NewInMemoryProvider(from map[string]InMemoryFlag) InMemoryProvider { +func NewProvider(from map[string]InMemoryFlag) InMemoryProvider { return InMemoryProvider{ flags: from, trackingEvents: map[string][]InMemoryEvent{}, @@ -41,7 +41,7 @@ func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, de } resolveFlag, detail := memoryFlag.Resolve(defaultValue, flatCtx) - result := genericResolve[bool](resolveFlag, defaultValue, &detail) + result := genericResolve(resolveFlag, defaultValue, &detail) return openfeature.BoolResolutionDetail{ Value: result, @@ -59,7 +59,7 @@ func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, def } resolveFlag, detail := memoryFlag.Resolve(defaultValue, flatCtx) - result := genericResolve[string](resolveFlag, defaultValue, &detail) + result := genericResolve(resolveFlag, defaultValue, &detail) return openfeature.StringResolutionDetail{ Value: result, @@ -77,7 +77,7 @@ func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defa } resolveFlag, detail := memoryFlag.Resolve(defaultValue, flatCtx) - result := genericResolve[float64](resolveFlag, defaultValue, &detail) + result := genericResolve(resolveFlag, defaultValue, &detail) return openfeature.FloatResolutionDetail{ Value: result, @@ -95,7 +95,7 @@ func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaul } resolveFlag, detail := memoryFlag.Resolve(defaultValue, flatCtx) - result := genericResolve[int64](resolveFlag, defaultValue, &detail) + result := genericResolve(resolveFlag, defaultValue, &detail) return openfeature.IntResolutionDetail{ Value: result, @@ -103,10 +103,10 @@ func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaul } } -func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { +func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.ObjectResolutionDetail { memoryFlag, details, ok := i.find(flag) if !ok { - return openfeature.InterfaceResolutionDetail{ + return openfeature.ObjectResolutionDetail{ Value: defaultValue, ProviderResolutionDetail: *details, } @@ -123,7 +123,7 @@ func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, def detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } - return openfeature.InterfaceResolutionDetail{ + return openfeature.ObjectResolutionDetail{ Value: result, ProviderResolutionDetail: detail, } diff --git a/openfeature/memprovider/in_memory_provider_test.go b/providers/inmemory/inmemory_provider_test.go similarity index 90% rename from openfeature/memprovider/in_memory_provider_test.go rename to providers/inmemory/inmemory_provider_test.go index 13d79648..e8d9287a 100644 --- a/openfeature/memprovider/in_memory_provider_test.go +++ b/providers/inmemory/inmemory_provider_test.go @@ -1,14 +1,14 @@ -package memprovider +package inmemory import ( "math" "testing" - "github.com/open-feature/go-sdk/openfeature" + "go.openfeature.dev/openfeature/v2" ) func TestInMemoryProvider_boolean(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "boolFlag": { Key: "boolFlag", State: Enabled, @@ -33,7 +33,7 @@ func TestInMemoryProvider_boolean(t *testing.T) { } func TestInMemoryProvider_String(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "stringFlag": { Key: "stringFlag", State: Enabled, @@ -58,7 +58,7 @@ func TestInMemoryProvider_String(t *testing.T) { } func TestInMemoryProvider_Float(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "floatFlag": { Key: "floatFlag", State: Enabled, @@ -149,7 +149,7 @@ func TestInMemoryProvider_Int(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "intFlag": { State: Enabled, DefaultVariant: "value", @@ -167,7 +167,7 @@ func TestInMemoryProvider_Int(t *testing.T) { } func TestInMemoryProvider_Object(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "objectFlag": { Key: "objectFlag", State: Enabled, @@ -199,7 +199,7 @@ func TestInMemoryProvider_WithContext(t *testing.T) { return callerFlag.Variants[s.(string)], openfeature.ProviderResolutionDetail{} } - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "contextFlag": { Key: "contextFlag", State: Enabled, @@ -226,7 +226,7 @@ func TestInMemoryProvider_WithContext(t *testing.T) { } func TestInMemoryProvider_MissingFlag(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{}) + memoryProvider := NewProvider(map[string]InMemoryFlag{}) ctx := t.Context() @@ -248,7 +248,7 @@ func TestInMemoryProvider_MissingFlag(t *testing.T) { } func TestInMemoryProvider_TypeMismatch(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "boolFlag": { Key: "boolFlag", State: Enabled, @@ -277,7 +277,7 @@ func TestInMemoryProvider_TypeMismatch(t *testing.T) { } func TestInMemoryProvider_Disabled(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + memoryProvider := NewProvider(map[string]InMemoryFlag{ "boolFlag": { Key: "boolFlag", State: Disabled, @@ -306,7 +306,7 @@ func TestInMemoryProvider_Disabled(t *testing.T) { } func TestInMemoryProvider_Metadata(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{}) + memoryProvider := NewProvider(map[string]InMemoryFlag{}) metadata := memoryProvider.Metadata() @@ -320,6 +320,6 @@ func TestInMemoryProvider_Metadata(t *testing.T) { } func TestInMemoryProvider_Track(t *testing.T) { - memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{}) + memoryProvider := NewProvider(map[string]InMemoryFlag{}) memoryProvider.Track(t.Context(), "example-event-name", openfeature.EvaluationContext{}, openfeature.TrackingEventDetails{}) } diff --git a/openfeature/multi/README.md b/providers/multi/README.md similarity index 72% rename from openfeature/multi/README.md rename to providers/multi/README.md index 75e3359f..149f8ee9 100644 --- a/openfeature/multi/README.md +++ b/providers/multi/README.md @@ -1,9 +1,7 @@ -OpenFeature Multi-Provider ------------- +## OpenFeature Multi-Provider > [!WARNING] -> The multi package for the go-sdk is experimental. - +> The multi package for the go-sdk is experimental. The multi-provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK. The multi-provider acts as a wrapper providing a unified interface to interact with all of those providers at once. @@ -11,8 +9,8 @@ When a flag is being evaluated, the Multi-Provider will consult each underlying determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and which result is used. -The multi-provider is defined within [Appendix A: Included Utilities](https://openfeature.dev/specification/appendix-a#multi-provider) -of the openfeature spec. +The multi-provider is defined within [Appendix A: Included Utilities](https://openfeature.dev/specification/appendix-a#multi-provider) +of the openfeature spec. The multi-provider is a powerful tool for performing migrations between flag providers, or combining multiple providers into a single feature flagging interface. For example: @@ -27,21 +25,21 @@ into a single feature flagging interface. For example: ```go import ( - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/multi" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + "go.openfeature.dev/openfeature/v2/providers/inmemory" + "go.openfeature.dev/openfeature/v2/providers/multi" ) mprovider, err := multi.NewProvider( multi.StrategyFirstMatch, - multi.WithProvider("providerA", memprovider.NewInMemoryProvider(/*...*/)), + multi.WithProvider("providerA", inmemory.NewProvider(/*...*/)), multi.WithProvider("providerB", myCustomProvider), ) if err != nil { return err } -openfeature.SetNamedProviderAndWait("multiprovider", mprovider) +openfeature.SetNamedProviderAndWait(context.TODO(), "multiprovider", mprovider) ``` # Strategies @@ -57,7 +55,7 @@ The three provided strategies are: ## First Match Strategy -The first match strategy works by **sequentially** calling each provider until a valid result is returned. +The first match strategy works by **sequentially** calling each provider until a valid result is returned. The first provider that returns a result will be used. It will try calling the next provider whenever it encounters a `FLAG_NOT_FOUND` error. However, if a provider returns an error other than `FLAG_NOT_FOUND` the provider will stop and return the default value along with setting the error details if a detailed request is issued. @@ -65,22 +63,22 @@ value along with setting the error details if a detailed request is issued. ## First Success Strategy The first success strategy also works by calling each provider **sequentially**. The first provider that returns a response -with no errors is used. This differs from the first match strategy in that any provider raising an error will not halt -calling the next provider if a successful result has not yet been encountered. If no provider provides a successful result +with no errors is used. This differs from the first match strategy in that any provider raising an error will not halt +calling the next provider if a successful result has not yet been encountered. If no provider provides a successful result the default value will be returned to the caller. ## Comparison Strategy The comparison strategy works by calling each provider in **parallel**. All results are collected from each provider and then the resolved results are compared to each other. If they all agree then that value is returned. If not a fallback -provider can be specified to be executed instead or the default value will be returned. If a provider returns -`FLAG_NOT_FOUND` that result will not be included in the comparison. If all providers return not found then the default -value is returned. Finally, if any provider returns an error other than `FLAG_NOT_FOUND` the evaluation immediately stops -and that error result is returned with the default value. +provider can be specified to be executed instead or the default value will be returned. If a provider returns +`FLAG_NOT_FOUND` that result will not be included in the comparison. If all providers return not found then the default +value is returned. Finally, if any provider returns an error other than `FLAG_NOT_FOUND` the evaluation immediately stops +and that error result is returned with the default value. The fallback provider can be set using the `WithFallbackProvider` [`Option`](#options). -Special care must be taken when this strategy is used with `ObjectEvaluation`. If the resulting value is not a +Special care must be taken when this strategy is used with `ObjectEvaluation`. If the resulting value is not a [`comparable`](https://go.dev/blog/comparable) type then the default result or fallback provider will always be used. In order to evaluate non `comparable` types a `Comparator` function must be provided as an `Option` to the constructor. @@ -90,31 +88,45 @@ A custom strategy can be defined using the `WithCustomStrategy` `Option` along w A custom strategy is defined by the following generic function signature: ```go -StrategyFn[T FlagTypes] func(ctx context.Context, flag string, defaultValue T, flatCtx openfeature.FlattenedContext) openfeature.GenericResolutionDetail[T] +StrategyFn[T FlagTypes] func(resolutions ResolutionIterator[T], defaultValue T, fallbackEvaluator FallbackEvaluator[T]) openfeature.GenericResolutionDetail[T] ``` -However, this doesn't provide any way to retrieve the providers! Therefore, there's the type `StrategyConstructor` that -is called for you to close over the providers inside your `StratetegyFn` implementation. +Where: ```go -type StrategyConstructor func(providers []*NamedProvider) StrategyFn[FlagTypes] +ResolutionIterator[T FlagTypes] = iter.Seq2[string, openfeature.GenericResolutionDetail[T]] +FallbackEvaluator[T FlagTypes] = func(fallbackProvider openfeature.FeatureProvider) openfeature.GenericResolutionDetail[T] ``` -Build your strategy to wrap around the slice of providers +The strategy function receives: + +- `resolutions`: An iterator of provider names and their resolution results +- `defaultValue`: The default value to return if strategy fails +- `fallbackEvaluator`: A function to evaluate the fallback provider if needed + +The `StrategyConstructor` type is used to create your custom strategy: + ```go -option := multi.WithCustomStrategy(func(providers []NamedProvider) StrategyFn[FlagTypes] { - return func[T FlagTypes](ctx context.Context, flag string, defaultValue T, flatCtx openfeature.FlattenedContext) openfeature.GenericResolutionDetail[T] { - // implementation - // ... +type StrategyConstructor func() StrategyFn[FlagTypes] +``` + +Build your custom strategy like this: + +```go +option := multi.WithCustomStrategy(func() StrategyFn[openfeature.FlagTypes] { + return func(resolutions ResolutionIterator[openfeature.FlagTypes], defaultValue openfeature.FlagTypes, fallbackEvaluator FallbackEvaluator[openfeature.FlagTypes]) *openfeature.GenericResolutionDetail[openfeature.FlagTypes] { + // Iterate through provider resolutions + for name, resolution := range resolutions { + // Your custom logic here + // ... + } + // Return selected resolution or use fallbackEvaluator if needed + return resolution } }) ``` -It is highly recommended to use the provided exposed functions to build your custom strategy. Specifically, the functions -`BuildDefaultResult` & `Evaluate` are exposed for those implementing their own custom strategies. - -The `Evaluate` method should be used for evaluating the result of a single `NamedProvider`. It determines the evaluation -type via the type of the generic `defaultVal` parameter. +It is highly recommended to use the provided exposed function `BuildDefaultResult` when building your custom strategy. The `BuildDefaultResult` method should be called when an error is encountered or the strategy "fails" and needs to return the default result passed to one of the Evaluation methods of `openfeature.FeatureProvider`. diff --git a/openfeature/multi/comparison_strategy.go b/providers/multi/comparison_strategy.go similarity index 61% rename from openfeature/multi/comparison_strategy.go rename to providers/multi/comparison_strategy.go index bd0fe15c..0a2a550e 100644 --- a/openfeature/multi/comparison_strategy.go +++ b/providers/multi/comparison_strategy.go @@ -1,19 +1,23 @@ package multi import ( - "context" "errors" "reflect" "slices" "strings" - of "github.com/open-feature/go-sdk/openfeature" - "golang.org/x/sync/errgroup" + of "go.openfeature.dev/openfeature/v2" ) -// ErrAggregationNotAllowed is an error returned if [of.FeatureProvider.ObjectEvaluation] is called using the [StrategyComparison] -// strategy without a custom [Comparator] function configured when response objects are not comparable. -var ErrAggregationNotAllowed = errors.New(errAggregationNotAllowedText) +var ( + // ErrAggregationNotAllowed is an error returned if [of.FeatureProvider.ObjectEvaluation] is called using the [StrategyComparison] + // strategy without a custom [Comparator] function configured when response objects are not comparable. + ErrAggregationNotAllowed = errors.New(errAggregationNotAllowedText) + + // errNoFallbackProvider is an error returned when a comparison failure occurs in [StrategyComparison] + // and no fallback provider is configured to handle the disagreement. + errNoFallbackProvider = errors.New("no fallback provider configured") +) // Comparator is used to compare the results of [of.FeatureProvider.ObjectEvaluation]. // This is required if returned results are not comparable. @@ -24,8 +28,8 @@ type Comparator func(values []any) bool // can be passed as long as ObjectEvaluation is never called with objects that are not comparable. The custom [Comparator] // will only be used for [of.FeatureProvider.ObjectEvaluation] if set. If [of.FeatureProvider.ObjectEvaluation] is // called without setting a [Comparator], and the returned object(s) are not comparable, then an error will occur. -func newComparisonStrategy(providers []NamedProvider, fallbackProvider of.FeatureProvider, comparator Comparator) StrategyFn[FlagTypes] { - return evaluateComparison[FlagTypes](providers, fallbackProvider, comparator) +func newComparisonStrategy(fallbackProvider of.FeatureProvider, comparator Comparator) StrategyFn[of.FlagTypes] { + return evaluateComparison[of.FlagTypes](fallbackProvider, comparator) } func defaultComparator(values []any) bool { @@ -81,8 +85,8 @@ func comparisonResolutionError(metadata of.FlagMetadata) of.ResolutionError { return of.NewGeneralResolutionError("comparison failure") } -func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider of.FeatureProvider, comparator Comparator) StrategyFn[T] { - return func(ctx context.Context, flag string, defaultValue T, evalCtx of.FlattenedContext) of.GenericResolutionDetail[T] { +func evaluateComparison[T of.FlagTypes](fallbackProvider of.FeatureProvider, comparator Comparator) StrategyFn[T] { + return func(resolutions ResolutionIterator[T], defaultValue T, evaluator FallbackEvaluator[T]) *of.GenericResolutionDetail[T] { if comparator == nil { comparator = defaultComparator switch any(defaultValue).(type) { @@ -100,82 +104,53 @@ func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider } } - // Short circuit if there's only one provider as no comparison nor workers are needed - if len(providers) == 1 { - result := Evaluate(ctx, providers[0], flag, defaultValue, evalCtx) - metadata := setFlagMetadata(StrategyComparison, providers[0].Name(), make(of.FlagMetadata)) - metadata[MetadataFallbackUsed] = false - result.FlagMetadata = mergeFlagMeta(result.FlagMetadata, metadata) - return result - } - type namedResult struct { name string res *of.GenericResolutionDetail[T] } - resultChan := make(chan *namedResult, len(providers)) - notFoundChan := make(chan any) - errGrp, grpCtx := errgroup.WithContext(ctx) - for _, provider := range providers { - closedProvider := provider - errGrp.Go(func() error { - result := Evaluate(grpCtx, closedProvider, flag, defaultValue, evalCtx) - notFound := result.ResolutionDetail().ErrorCode == of.FlagNotFoundCode - if !notFound && result.Error() != nil { - return &ProviderError{ - ProviderName: closedProvider.Name(), - err: result.Error(), - } - } - if !notFound { - resultChan <- &namedResult{ - name: closedProvider.Name(), - res: &result, - } - } else { - notFoundChan <- struct{}{} + results := make([]*namedResult, 0) + resultValues := make([]T, 0) + notFoundCount := 0 + total := 0 + for name, result := range resolutions { + total += 1 + notFound := result.ResolutionDetail().ErrorCode == of.FlagNotFoundCode + if !notFound && result.Error() != nil { + resultError := BuildDefaultResult(StrategyComparison, defaultValue, result.Error()) + resultError.FlagMetadata[MetadataFallbackUsed] = false + resultError.FlagMetadata[MetadataIsDefaultValue] = true + resultError.FlagMetadata[MetadataEvaluationError] = result.Error() + resultError.ResolutionError = comparisonResolutionError(result.FlagMetadata) + return resultError + } + if !notFound { + r := &namedResult{ + name: name, + res: result, } - return nil - }) + results = append(results, r) + resultValues = append(resultValues, r.res.Value) + } else { + notFoundCount++ + } } - results := make([]namedResult, 0, len(providers)) - resultValues := make([]T, 0, len(providers)) - notFoundCount := 0 + if notFoundCount == total { + result := BuildDefaultResult(StrategyComparison, defaultValue, nil) + result.FlagMetadata[MetadataFallbackUsed] = false + result.FlagMetadata[MetadataIsDefaultValue] = true + result.ResolutionError = comparisonResolutionError(result.FlagMetadata) + return result + } - ListenerLoop: - for { - select { - case <-grpCtx.Done(): - // Error occurred - result := BuildDefaultResult(StrategyComparison, defaultValue, grpCtx.Err()) - result.FlagMetadata[MetadataFallbackUsed] = false - result.FlagMetadata[MetadataIsDefaultValue] = true - result.FlagMetadata[MetadataEvaluationError] = grpCtx.Err().Error() - result.ResolutionError = comparisonResolutionError(result.FlagMetadata) - return result - case r := <-resultChan: - results = append(results, *r) - resultValues = append(resultValues, r.res.Value) - if (len(results) + notFoundCount) == len(providers) { - // All results accounted for - break ListenerLoop - } - case <-notFoundChan: - notFoundCount += 1 - if notFoundCount == len(providers) { - result := BuildDefaultResult(StrategyComparison, defaultValue, nil) - result.FlagMetadata[MetadataFallbackUsed] = false - result.FlagMetadata[MetadataIsDefaultValue] = true - result.ResolutionError = comparisonResolutionError(result.FlagMetadata) - return result - } - if (len(results) + notFoundCount) == len(providers) { - // All results accounted for - break ListenerLoop - } - } + // Short circuit if there's only one provider as no comparison nor workers are needed + if total == 1 { + result := results[0].res + metadata := setFlagMetadata(StrategyComparison, results[0].name, make(of.FlagMetadata)) + metadata[MetadataFallbackUsed] = false + result.FlagMetadata = mergeFlagMeta(result.FlagMetadata, metadata) + return result } // Evaluate Results Are Equal metadata := make(of.FlagMetadata) @@ -192,8 +167,8 @@ func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider metadata[MetadataFallbackUsed] = false metadata[MetadataIsDefaultValue] = false metadata[MetadataComparisonDisagreeingProviders] = []string{} - success := make([]string, 0, len(providers)) - variants := make([]string, 0, len(providers)) + success := make([]string, 0, total) + variants := make([]string, 0, total) // Gather metadata from provider results for _, r := range results { metadata[r.name] = r.res.FlagMetadata @@ -212,7 +187,7 @@ func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider } else { variantResults = strings.Join(variants, ", ") } - return of.GenericResolutionDetail[T]{ + return &of.GenericResolutionDetail[T]{ Value: resultValues[0], // All values should be equal ProviderResolutionDetail: of.ProviderResolutionDetail{ Reason: ReasonAggregated, @@ -223,13 +198,7 @@ func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider } if fallbackProvider != nil { - fallbackResult := Evaluate( - ctx, - &namedProvider{name: "fallback", FeatureProvider: fallbackProvider}, - flag, - defaultValue, - evalCtx, - ) + fallbackResult := evaluator(fallbackProvider) fallbackResult.FlagMetadata = mergeFlagMeta(fallbackResult.FlagMetadata, metadata) fallbackResult.FlagMetadata[MetadataFallbackUsed] = true fallbackResult.FlagMetadata[MetadataIsDefaultValue] = false @@ -239,7 +208,7 @@ func evaluateComparison[T FlagTypes](providers []NamedProvider, fallbackProvider return fallbackResult } - defaultResult := BuildDefaultResult(StrategyComparison, defaultValue, errors.New("no fallback provider configured")) + defaultResult := BuildDefaultResult(StrategyComparison, defaultValue, errNoFallbackProvider) defaultResult.FlagMetadata = mergeFlagMeta(defaultResult.FlagMetadata, metadata) defaultResult.FlagMetadata[MetadataFallbackUsed] = false defaultResult.FlagMetadata[MetadataIsDefaultValue] = true diff --git a/openfeature/multi/comparison_strategy_test.go b/providers/multi/comparison_strategy_test.go similarity index 74% rename from openfeature/multi/comparison_strategy_test.go rename to providers/multi/comparison_strategy_test.go index 1c246288..e7e23b0d 100644 --- a/openfeature/multi/comparison_strategy_test.go +++ b/providers/multi/comparison_strategy_test.go @@ -5,12 +5,12 @@ import ( "fmt" "testing" - of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" + of "go.openfeature.dev/openfeature/v2" "go.uber.org/mock/gomock" ) -func configureComparisonProvider[R any](provider *of.MockFeatureProvider, resultVal R, state bool, error int, forceObj bool) { +func configureComparisonProvider[R any](provider *of.MockProvider, resultVal R, state bool, error int, forceObj bool) { var rErr of.ResolutionError var variant string var reason of.Reason @@ -34,9 +34,9 @@ func configureComparisonProvider[R any](provider *of.MockFeatureProvider, result FlagMetadata: make(of.FlagMetadata), } provider.EXPECT().Metadata().Return(of.Metadata{Name: "mock provider"}).MaxTimes(1) - objFunc := func(p *of.MockFeatureProvider) { - p.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { - return of.InterfaceResolutionDetail{ + objFunc := func(p *of.MockProvider) { + p.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.ObjectResolutionDetail { + return of.ObjectResolutionDetail{ Value: resultVal, ProviderResolutionDetail: details, } @@ -85,8 +85,8 @@ func configureComparisonProvider[R any](provider *of.MockFeatureProvider, result func Test_ComparisonStrategy_Evaluation(t *testing.T) { tests := []struct { kind of.Type - successVal FlagTypes - defaultVal FlagTypes + successVal of.FlagTypes + defaultVal of.FlagTypes }{ {of.Boolean, true, false}, {of.String, "success", "default"}, @@ -100,18 +100,20 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { defaultVal := tt.defaultVal t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) - provider := of.NewMockFeatureProvider(ctrl) - fallback := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(provider, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider", FeatureProvider: provider, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -122,24 +124,27 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("two success", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) + } + + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -150,31 +155,34 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("multiple success", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, successVal, true, TestErrorNone, false) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureComparisonProvider(provider3, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider3", FeatureProvider: provider3, }, - }, fallback, nil) + } + + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -185,30 +193,32 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("multiple not found with single success", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound, false) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureComparisonProvider(provider3, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider3", FeatureProvider: provider3, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -220,37 +230,40 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("multiple not found with multiple success", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound, false) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureComparisonProvider(provider3, successVal, true, TestErrorNone, false) - provider4 := of.NewMockFeatureProvider(ctrl) + provider4 := of.NewMockProvider(ctrl) configureComparisonProvider(provider4, successVal, true, TestErrorNone, false) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider3", FeatureProvider: provider3, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider4", FeatureProvider: provider4, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -262,31 +275,33 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("comparison failure uses fallback", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, successVal, true, TestErrorNone, false) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNone, false) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureComparisonProvider(provider3, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider3", FeatureProvider: provider3, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -298,25 +313,27 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("not found all providers", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, defaultVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -329,37 +346,39 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("comparison failure with not found", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, successVal, true, TestErrorNone, false) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound, false) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureComparisonProvider(provider3, successVal, true, TestErrorNone, false) - provider4 := of.NewMockFeatureProvider(ctrl) + provider4 := of.NewMockProvider(ctrl) configureComparisonProvider(provider4, defaultVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider3", FeatureProvider: provider3, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider4", FeatureProvider: provider4, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) @@ -372,24 +391,27 @@ func Test_ComparisonStrategy_Evaluation(t *testing.T) { t.Run("non FLAG_NOT_FOUND error causes default", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorError, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorError, false) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, defaultVal, result.Value) assert.Equal(t, of.ErrorReason, result.Reason) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) @@ -491,24 +513,27 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { tc := testCase t.Run(fmt.Sprintf("with orderable type %s success", tc.typeName), func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, testCase.successValue, true, TestErrorNone, true) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, testCase.successValue, true, TestErrorNone, true) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, tc.defaultValue, of.FlattenedContext{}) assert.Equal(t, tc.successValue, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregated, result.Reason) @@ -521,24 +546,28 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { t.Run(fmt.Sprintf("with orderable type %s no match fallback", tc.typeName), func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, tc.successValue, true, TestErrorNone, true) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, tc.successValue, true, TestErrorNone, true) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, tc.defaultValue, true, TestErrorNone, true) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) - result := strategy(t.Context(), testFlag, tc.defaultValue, of.FlattenedContext{}) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, tc.successValue, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregatedFallback, result.Reason) @@ -552,24 +581,27 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { t.Run("with comparable custom type success", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, true) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, successVal, true, TestErrorNone, true) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) + } + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregated, result.Reason) @@ -582,24 +614,29 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { t.Run("with comparable custom type no match fallback", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, successVal, true, TestErrorNone, true) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, true) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNone, true) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + } + + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregatedFallback, result.Reason) @@ -612,26 +649,30 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { t.Run("with comparable custom type force custom comparator", func(t *testing.T) { ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, defaultVal, true, TestErrorNone, true) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, true) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, successVal, true, TestErrorNone, true) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, func(val []any) bool { + } + strategy := newComparisonStrategy(fallback, func(val []any) bool { return true }) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregated, result.Reason) @@ -645,26 +686,29 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { successVal := []string{"test1", "test2"} defaultVal := []string{"test3"} ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, successVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, func(val []any) bool { - return true - }) + } + strategy := newComparisonStrategy( + fallback, func(val []any) bool { + return true + }) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) assert.Equal(t, successVal, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregated, result.Reason) @@ -679,26 +723,29 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { successVal := []string{"test1", "test2"} defaultVal := []string{"test3"} ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) configureComparisonProvider(fallback, successVal, true, TestErrorNone, false) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, defaultVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureComparisonProvider(provider2, defaultVal, true, TestErrorNone, false) - - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, func(val []any) bool { + } + + strategy := newComparisonStrategy(fallback, func(val []any) bool { return false }) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) assert.NoError(t, result.Error()) assert.Equal(t, ReasonAggregatedFallback, result.Reason) @@ -713,22 +760,27 @@ func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { successVal := []string{"test1", "test2"} defaultVal := []string{"test3"} ctrl := gomock.NewController(t) - fallback := of.NewMockFeatureProvider(ctrl) - provider1 := of.NewMockFeatureProvider(ctrl) + fallback := of.NewMockProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureComparisonProvider(provider1, successVal, true, TestErrorNone, false) - provider2 := of.NewMockFeatureProvider(ctrl) - configureComparisonProvider(provider2, successVal, true, TestErrorError, false) - strategy := newComparisonStrategy([]NamedProvider{ - &namedProvider{ + provider2 := of.NewMockProvider(ctrl) + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "test-provider2", FeatureProvider: provider2, }, - }, fallback, nil) - result := strategy(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + } + + configureComparisonProvider(provider2, successVal, true, TestErrorError, false) + strategy := newComparisonStrategy(fallback, nil) + + fn := newEvaluationFunc(providers, runModeParallel, strategy) + result := fn(t.Context(), testFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) assert.Equal(t, of.ErrorReason, result.Reason) assert.Equal(t, of.NewGeneralResolutionError(ErrAggregationNotAllowed.Error()), result.ResolutionError) diff --git a/openfeature/multi/errors.go b/providers/multi/errors.go similarity index 97% rename from openfeature/multi/errors.go rename to providers/multi/errors.go index 7fdd325a..f0b7edb7 100644 --- a/openfeature/multi/errors.go +++ b/providers/multi/errors.go @@ -64,15 +64,13 @@ type multiErrGroup struct { // Go starts a function in a goroutine. func (g *multiErrGroup) Go(fn func() error) { - g.wg.Add(1) - go func() { - defer g.wg.Done() + g.wg.Go(func() { if err := fn(); err != nil { g.mu.Lock() g.errors = append(g.errors, err) g.mu.Unlock() } - }() + }) } // Wait waits for all goroutines to complete. diff --git a/openfeature/multi/errors_test.go b/providers/multi/errors_test.go similarity index 100% rename from openfeature/multi/errors_test.go rename to providers/multi/errors_test.go diff --git a/openfeature/multi/first_match_strategy.go b/providers/multi/first_match_strategy.go similarity index 54% rename from openfeature/multi/first_match_strategy.go rename to providers/multi/first_match_strategy.go index 430de0df..f85c6205 100644 --- a/openfeature/multi/first_match_strategy.go +++ b/providers/multi/first_match_strategy.go @@ -1,21 +1,18 @@ package multi import ( - "context" - - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" ) // newFirstMatchStrategy returns a [StrategyFn] that returns the result of the first [of.FeatureProvider] whose response is -// not [of.FlagNotFoundCode]. This is executed sequentially, and not in parallel. -func newFirstMatchStrategy(providers []NamedProvider) StrategyFn[FlagTypes] { - return firstMatchStrategyFn[FlagTypes](providers) +// not [of.FlagNotFoundCode]. The definition of "first" depends on the configured run-mode. With sequential execution, it's the first provider in order. With parallel, it's the first to return a result. +func newFirstMatchStrategy() StrategyFn[of.FlagTypes] { + return firstMatchStrategyFn[of.FlagTypes]() } -func firstMatchStrategyFn[T FlagTypes](providers []NamedProvider) StrategyFn[T] { - return func(ctx context.Context, flag string, defaultValue T, flatCtx of.FlattenedContext) of.GenericResolutionDetail[T] { - for _, provider := range providers { - resolution := Evaluate(ctx, provider, flag, defaultValue, flatCtx) +func firstMatchStrategyFn[T of.FlagTypes]() StrategyFn[T] { + return func(resolutions ResolutionIterator[T], defaultValue T, _ FallbackEvaluator[T]) *of.GenericResolutionDetail[T] { + for providerName, resolution := range resolutions { if resolution.Error() != nil && resolution.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { continue } @@ -30,7 +27,7 @@ func firstMatchStrategyFn[T FlagTypes](providers []NamedProvider) StrategyFn[T] } // success! - resolution.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name(), resolution.FlagMetadata) + resolution.FlagMetadata = setFlagMetadata(StrategyFirstMatch, providerName, resolution.FlagMetadata) return resolution } diff --git a/openfeature/multi/first_match_strategy_test.go b/providers/multi/first_match_strategy_test.go similarity index 76% rename from openfeature/multi/first_match_strategy_test.go rename to providers/multi/first_match_strategy_test.go index b3a08d58..d1ea9a39 100644 --- a/openfeature/multi/first_match_strategy_test.go +++ b/providers/multi/first_match_strategy_test.go @@ -4,16 +4,16 @@ import ( "strconv" "testing" - of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" + of "go.openfeature.dev/openfeature/v2" "go.uber.org/mock/gomock" ) func Test_FirstMatchStrategy_Evaluation(t *testing.T) { tests := []struct { kind of.Type - successVal FlagTypes - defaultVal FlagTypes + successVal of.FlagTypes + defaultVal of.FlagTypes }{ {kind: of.Boolean, successVal: true, defaultVal: false}, {kind: of.Int, successVal: int64(123), defaultVal: int64(0)}, @@ -28,15 +28,18 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { t.Run("Single Provider Match", func(t *testing.T) { mocks := createMockProviders(ctrl, 1) configureFirstMatchProviderMock(mocks[0], tt.successVal, TestErrorNone, "mock provider") - providers := make([]NamedProvider, 0, 5) + providers := make([]namedProvider, 0, 5) for i, m := range mocks { - providers = append(providers, &namedProvider{ + providers = append(providers, ®isteredProvider{ name: strconv.Itoa(i), FeatureProvider: m, }) } - strategy := newFirstMatchStrategy(providers) - result := strategy(t.Context(), "test-string", tt.defaultVal, of.FlattenedContext{}) + + strategy := newFirstMatchStrategy() + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), "test-string", tt.defaultVal, of.FlattenedContext{}) + assert.Equal(t, tt.successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) assert.Equal(t, providers[0].Name(), result.FlagMetadata[MetadataSuccessfulProviderName]) @@ -45,15 +48,18 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { t.Run("Default Resolution", func(t *testing.T) { mocks := createMockProviders(ctrl, 1) configureFirstMatchProviderMock(mocks[0], tt.defaultVal, TestErrorNotFound, "mock provider") - providers := make([]NamedProvider, 0, 5) + providers := make([]namedProvider, 0, 5) for i, m := range mocks { - providers = append(providers, &namedProvider{ + providers = append(providers, ®isteredProvider{ name: strconv.Itoa(i), FeatureProvider: m, }) } - strategy := newFirstMatchStrategy(providers) - result := strategy(t.Context(), "test-string", tt.defaultVal, of.FlattenedContext{}) + + strategy := newFirstMatchStrategy() + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), "test-string", tt.defaultVal, of.FlattenedContext{}) + assert.Equal(t, tt.defaultVal, result.Value) assert.Equal(t, of.DefaultReason, result.Reason) assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) @@ -65,16 +71,18 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { mocks := createMockProviders(ctrl, 5) configureFirstMatchProviderMock(mocks[0], tt.defaultVal, TestErrorNotFound, "mock provider 1") configureFirstMatchProviderMock(mocks[1], tt.successVal, TestErrorNone, "mock provider 2") - providers := make([]NamedProvider, 0, 5) + providers := make([]namedProvider, 0, 5) for i, m := range mocks { - providers = append(providers, &namedProvider{ + providers = append(providers, ®isteredProvider{ name: strconv.Itoa(i), FeatureProvider: m, }) } - strategy := newFirstMatchStrategy(providers) - result := strategy(t.Context(), "test-flag", tt.defaultVal, of.FlattenedContext{}) + strategy := newFirstMatchStrategy() + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), "test-string", tt.defaultVal, of.FlattenedContext{}) + assert.Equal(t, tt.successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) assert.Equal(t, providers[1].Name(), result.FlagMetadata[MetadataSuccessfulProviderName]) @@ -83,9 +91,9 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { mocks := createMockProviders(ctrl, 5) expectedErr := of.NewGeneralResolutionError("test error") - providers := make([]NamedProvider, 0, 5) + providers := make([]namedProvider, 0, 5) for i, m := range mocks { - providers = append(providers, &namedProvider{ + providers = append(providers, ®isteredProvider{ name: strconv.Itoa(i), FeatureProvider: m, }) @@ -97,8 +105,11 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { } } - strategy := newFirstMatchStrategy(providers) - result := strategy(t.Context(), "test-string", tt.successVal, of.FlattenedContext{}) + strategy := newFirstMatchStrategy() + + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), "test-string", tt.successVal, of.FlattenedContext{}) + assert.Equal(t, tt.successVal, result.Value) assert.Equal(t, of.ErrorReason, result.Reason) assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) @@ -109,7 +120,7 @@ func Test_FirstMatchStrategy_Evaluation(t *testing.T) { } } -func configureFirstMatchProviderMock[R FlagTypes](mock *of.MockFeatureProvider, value R, error int, providerName string) { +func configureFirstMatchProviderMock[R of.FlagTypes](mock *of.MockProvider, value R, error int, providerName string) { var rErr of.ResolutionError var reason of.Reason switch error { @@ -146,7 +157,7 @@ func configureFirstMatchProviderMock[R FlagTypes](mock *of.MockFeatureProvider, default: mock.EXPECT(). ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(of.InterfaceResolutionDetail{Value: v, ProviderResolutionDetail: details}) + Return(of.ObjectResolutionDetail{Value: v, ProviderResolutionDetail: details}) } mock.EXPECT().Metadata().Return(of.Metadata{Name: providerName}) } diff --git a/providers/multi/first_success_strategy.go b/providers/multi/first_success_strategy.go new file mode 100644 index 00000000..fd715224 --- /dev/null +++ b/providers/multi/first_success_strategy.go @@ -0,0 +1,28 @@ +package multi + +import ( + "errors" + + of "go.openfeature.dev/openfeature/v2" +) + +// newFirstSuccessStrategy returns a [StrategyFn] that returns the result of the First [of.FeatureProvider] whose response +// is not an error. The definition of "first" depends on the configured run-mode. With sequential execution, it's the first provider in order. With parallel, it's the first to return a result. +func newFirstSuccessStrategy() StrategyFn[of.FlagTypes] { + return firstSuccessStrategyFn[of.FlagTypes]() +} + +func firstSuccessStrategyFn[T of.FlagTypes]() StrategyFn[T] { + return func(resolutions ResolutionIterator[T], defaultValue T, _ FallbackEvaluator[T]) *of.GenericResolutionDetail[T] { + resolutionErrors := make([]error, 0) + for name, resolution := range resolutions { + if resolution.Error() != nil { + resolutionErrors = append(resolutionErrors, resolution.Error()) + continue + } + resolution.FlagMetadata = setFlagMetadata(StrategyFirstSuccess, name, resolution.FlagMetadata) + return resolution + } + return BuildDefaultResult(StrategyFirstSuccess, defaultValue, errors.Join(resolutionErrors...)) + } +} diff --git a/openfeature/multi/first_success_strategy_test.go b/providers/multi/first_success_strategy_test.go similarity index 77% rename from openfeature/multi/first_success_strategy_test.go rename to providers/multi/first_success_strategy_test.go index 925ceb81..d1765b79 100644 --- a/openfeature/multi/first_success_strategy_test.go +++ b/providers/multi/first_success_strategy_test.go @@ -4,12 +4,12 @@ import ( "context" "testing" - of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" + of "go.openfeature.dev/openfeature/v2" "go.uber.org/mock/gomock" ) -func configureFirstSuccessProvider[R any](provider *of.MockFeatureProvider, resultVal R, state bool, error int) { +func configureFirstSuccessProvider[R any](provider *of.MockProvider, resultVal R, state bool, error int) { var rErr of.ResolutionError var variant string var reason of.Reason @@ -65,8 +65,8 @@ func configureFirstSuccessProvider[R any](provider *of.MockFeatureProvider, resu } }).MaxTimes(1) default: - provider.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { - return of.InterfaceResolutionDetail{ + provider.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.ObjectResolutionDetail { + return of.ObjectResolutionDetail{ Value: resultVal, ProviderResolutionDetail: details, } @@ -77,8 +77,8 @@ func configureFirstSuccessProvider[R any](provider *of.MockFeatureProvider, resu func Test_FirstSuccessStrategyEvaluation(t *testing.T) { tests := []struct { kind of.Type - successVal FlagTypes - defaultVal FlagTypes + successVal of.FlagTypes + defaultVal of.FlagTypes }{ {kind: of.Boolean, successVal: true, defaultVal: false}, {kind: of.String, successVal: "success", defaultVal: "default"}, @@ -90,16 +90,20 @@ func Test_FirstSuccessStrategyEvaluation(t *testing.T) { t.Run(tt.kind.String(), func(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider, tt.successVal, true, TestErrorNone) - strategy := newFirstSuccessStrategy([]NamedProvider{ - &namedProvider{ + strategy := newFirstSuccessStrategy() + providers := []namedProvider{ + ®isteredProvider{ name: "test-provider", FeatureProvider: provider, }, - }) - result := strategy(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) + } + + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) + assert.Equal(t, tt.successVal, result.Value) assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) @@ -109,23 +113,26 @@ func Test_FirstSuccessStrategyEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider1, tt.successVal, true, TestErrorNone) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider2, tt.defaultVal, false, TestErrorError) - strategy := newFirstSuccessStrategy([]NamedProvider{ - &namedProvider{ + strategy := newFirstSuccessStrategy() + providers := []namedProvider{ + ®isteredProvider{ name: "success-provider", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "failure-provider", FeatureProvider: provider2, }, - }) + } + + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) assert.Equal(t, tt.successVal, result.Value) assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) @@ -134,23 +141,25 @@ func Test_FirstSuccessStrategyEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider1, tt.successVal, true, TestErrorNone) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider2, tt.defaultVal, false, TestErrorError) - - strategy := newFirstSuccessStrategy([]NamedProvider{ - &namedProvider{ + providers := []namedProvider{ + ®isteredProvider{ name: "success-provider", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "failure-provider", FeatureProvider: provider2, }, - }) + } + strategy := newFirstSuccessStrategy() + + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) assert.Equal(t, tt.successVal, result.Value) assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) @@ -159,29 +168,32 @@ func Test_FirstSuccessStrategyEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) - provider1 := of.NewMockFeatureProvider(ctrl) + provider1 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider1, tt.defaultVal, false, TestErrorError) - provider2 := of.NewMockFeatureProvider(ctrl) + provider2 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider2, tt.defaultVal, false, TestErrorNotFound) - provider3 := of.NewMockFeatureProvider(ctrl) + provider3 := of.NewMockProvider(ctrl) configureFirstSuccessProvider(provider3, tt.defaultVal, false, TestErrorError) - strategy := newFirstSuccessStrategy([]NamedProvider{ - &namedProvider{ + strategy := newFirstSuccessStrategy() + providers := []namedProvider{ + ®isteredProvider{ name: "provider1", FeatureProvider: provider1, }, - &namedProvider{ + ®isteredProvider{ name: "provider2", FeatureProvider: provider2, }, - &namedProvider{ + ®isteredProvider{ name: "provider3", FeatureProvider: provider3, }, - }) + } + + fn := newEvaluationFunc(providers, runModeSequential, strategy) + result := fn(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) - result := strategy(t.Context(), testFlag, tt.defaultVal, of.FlattenedContext{}) assert.Equal(t, tt.defaultVal, result.Value) assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) diff --git a/openfeature/multi/isolation.go b/providers/multi/isolation.go similarity index 85% rename from openfeature/multi/isolation.go rename to providers/multi/isolation.go index 12382506..d463ea00 100644 --- a/openfeature/multi/isolation.go +++ b/providers/multi/isolation.go @@ -5,7 +5,7 @@ import ( "fmt" "sync" - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" ) type ( @@ -29,14 +29,14 @@ type ( // Compile-time interface compliance checks var ( - _ NamedProvider = (*hookIsolator)(nil) + _ namedProvider = (*hookIsolator)(nil) _ of.FeatureProvider = (*hookIsolator)(nil) _ of.Hook = (*hookIsolator)(nil) _ of.EventHandler = (*eventHandlingHookIsolator)(nil) ) // isolateProvider wraps a [of.FeatureProvider] to execute its hooks along with any additional ones. -func isolateProvider(provider NamedProvider, extraHooks []of.Hook) *hookIsolator { +func isolateProvider(provider namedProvider, extraHooks []of.Hook) *hookIsolator { return &hookIsolator{ FeatureProvider: provider, hooks: append(provider.Hooks(), extraHooks...), @@ -46,7 +46,7 @@ func isolateProvider(provider NamedProvider, extraHooks []of.Hook) *hookIsolator // isolateProviderWithEvents wraps a [of.FeatureProvider] to execute its hooks along with any additional ones. This is // identical to [isolateProvider], but also this will also implement [of.EventHandler]. -func isolateProviderWithEvents(provider NamedProvider, extraHooks []of.Hook) *eventHandlingHookIsolator { +func isolateProviderWithEvents(provider namedProvider, extraHooks []of.Hook) *eventHandlingHookIsolator { return &eventHandlingHookIsolator{*isolateProvider(provider, extraHooks)} } @@ -62,7 +62,7 @@ func (h *hookIsolator) unwrap() of.FeatureProvider { return h.FeatureProvider } -func (h *hookIsolator) Before(_ context.Context, hookContext of.HookContext, hookHints of.HookHints) (*of.EvaluationContext, error) { +func (h *hookIsolator) Before(ctx context.Context, hookContext of.HookContext, hookHints of.HookHints) (context.Context, error) { // Used for capturing the context and hints h.mu.Lock() defer h.mu.Unlock() @@ -70,7 +70,7 @@ func (h *hookIsolator) Before(_ context.Context, hookContext of.HookContext, hoo h.capturedHints = hookHints // Return copy of original evaluation context so any changes are isolated to each provider's hooks evalCtx := h.capturedContext.EvaluationContext() - return &evalCtx, nil + return of.ContextWithEvaluationContext(ctx, evalCtx), nil } func (h *hookIsolator) Metadata() of.Metadata { @@ -113,17 +113,17 @@ func (h *hookIsolator) IntEvaluation(ctx context.Context, flag string, defaultVa } } -func (h *hookIsolator) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx of.FlattenedContext) of.InterfaceResolutionDetail { +func (h *hookIsolator) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx of.FlattenedContext) of.ObjectResolutionDetail { completeEval := h.evaluate(ctx, flag, of.Object, defaultValue, flatCtx) - return of.InterfaceResolutionDetail{ + return of.ObjectResolutionDetail{ Value: completeEval.Value, ProviderResolutionDetail: toProviderResolutionDetail(completeEval), } } -// toProviderResolutionDetail Converts a [of.InterfaceEvaluationDetails] to a [of.ProviderResolutionDetail]. -func toProviderResolutionDetail(evalDetails of.InterfaceEvaluationDetails) of.ProviderResolutionDetail { +// toProviderResolutionDetail Converts a [of.GenericEvaluationDetails] to a [of.ProviderResolutionDetail]. +func toProviderResolutionDetail(evalDetails of.EvaluationDetails[of.FlagTypes]) of.ProviderResolutionDetail { var resolutionErr of.ResolutionError var reason of.Reason switch evalDetails.ErrorCode { @@ -160,20 +160,18 @@ func (h *hookIsolator) Hooks() []of.Hook { } // evaluate Executes evaluation of the flag wrapped by executing hooks. -func (h *hookIsolator) evaluate(ctx context.Context, flag string, flagType of.Type, defaultValue any, flatCtx of.FlattenedContext) of.InterfaceEvaluationDetails { - evalDetails := of.InterfaceEvaluationDetails{ - Value: defaultValue, - EvaluationDetails: of.EvaluationDetails{ - FlagKey: flag, - FlagType: flagType, - }, +func (h *hookIsolator) evaluate(ctx context.Context, flag string, flagType of.Type, defaultValue any, flatCtx of.FlattenedContext) of.EvaluationDetails[of.FlagTypes] { + evalDetails := of.EvaluationDetails[of.FlagTypes]{ + Value: defaultValue, + FlagKey: flag, + FlagType: flagType, } defer func() { h.finallyHooks(ctx, evalDetails) }() - evalCtx, err := h.beforeHooks(ctx) + ctx, evalCtx, err := h.beforeHooks(ctx) // Update hook context unconditionally h.updateEvalContext(evalCtx) if err != nil { @@ -190,9 +188,9 @@ func (h *hookIsolator) evaluate(ctx context.Context, flag string, flagType of.Ty // Merge together the passed in flat context and the captured evaluation context and transform back into a flattened // context for evaluation - flatCtx = flattenContext(mergeContexts(h.capturedContext.EvaluationContext(), deepenContext(flatCtx))) + flatCtx = (mergeContexts(h.capturedContext.EvaluationContext(), deepenContext(flatCtx))).Flattened() - var resolution of.InterfaceResolutionDetail + var resolution of.ObjectResolutionDetail switch flagType { case of.Object: resolution = h.FeatureProvider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx) @@ -240,23 +238,26 @@ func (h *hookIsolator) evaluate(ctx context.Context, flag string, flagType of.Ty // beforeHooks Executes all before hooks together, merging the changes to the [of.EvaluationContext] as it goes. The // return of this function is a merged version of the evaluation context -func (h *hookIsolator) beforeHooks(ctx context.Context) (of.EvaluationContext, error) { +func (h *hookIsolator) beforeHooks(ctx context.Context) (context.Context, of.EvaluationContext, error) { contexts := []of.EvaluationContext{h.capturedContext.EvaluationContext()} for _, hook := range h.hooks { - resultEvalCtx, err := hook.Before(ctx, h.capturedContext, h.capturedHints) - if resultEvalCtx != nil { - contexts = append(contexts, *resultEvalCtx) + tctx, err := hook.Before(ctx, h.capturedContext, h.capturedHints) + if tctx != nil { + ctx = tctx + // FIXME: any good way to do this in public and not to add empty or duplication? + resultEvalCtx := of.EvaluationContextFromContext(ctx) + contexts = append(contexts, resultEvalCtx) } if err != nil { - return mergeContexts(contexts...), err + return ctx, mergeContexts(contexts...), err } } - return mergeContexts(contexts...), nil + return ctx, mergeContexts(contexts...), nil } // afterHooks executes all after [of.Hook] instances together. -func (h *hookIsolator) afterHooks(ctx context.Context, evalDetails of.InterfaceEvaluationDetails) error { +func (h *hookIsolator) afterHooks(ctx context.Context, evalDetails of.EvaluationDetails[of.FlagTypes]) error { for _, hook := range h.hooks { if err := hook.After(ctx, h.capturedContext, evalDetails, h.capturedHints); err != nil { return err @@ -274,7 +275,7 @@ func (h *hookIsolator) errorHooks(ctx context.Context, err error) { } // finallyHooks execute all finally [of.Hook] instances together. -func (h *hookIsolator) finallyHooks(ctx context.Context, details of.InterfaceEvaluationDetails) { +func (h *hookIsolator) finallyHooks(ctx context.Context, details of.EvaluationDetails[of.FlagTypes]) { for _, hook := range h.hooks { hook.Finally(ctx, h.capturedContext, details, h.capturedHints) } @@ -311,13 +312,6 @@ func deepenContext(flatCtx of.FlattenedContext) of.EvaluationContext { return of.NewEvaluationContext(targetingKey, noTargetingKey) } -// flattenContext converts a [of.EvaluationContext] to a [of.FlattenedContext] -func flattenContext(evalCtx of.EvaluationContext) of.FlattenedContext { - flatCtx := evalCtx.Attributes() - flatCtx[of.TargetingKey] = evalCtx.TargetingKey() - return flatCtx -} - // mergeContexts merges attributes from the given EvaluationContexts with the nth [of.EvaluationContext] taking precedence // in case of any conflicts with the (n+1)th [of.EvaluationContext]. func mergeContexts(evaluationContexts ...of.EvaluationContext) of.EvaluationContext { diff --git a/openfeature/multi/isolation_test.go b/providers/multi/isolation_test.go similarity index 84% rename from openfeature/multi/isolation_test.go rename to providers/multi/isolation_test.go index 531cfba3..43d8aca1 100644 --- a/openfeature/multi/isolation_test.go +++ b/providers/multi/isolation_test.go @@ -4,9 +4,9 @@ import ( "errors" "testing" - of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + of "go.openfeature.dev/openfeature/v2" "go.uber.org/mock/gomock" ) @@ -21,16 +21,18 @@ func Test_HookIsolator_BeforeCapturesData(t *testing.T) { ) hookHints := of.NewHookHints(map[string]any{"foo": "bar"}) ctrl := gomock.NewController(t) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) provider.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - isolator := isolateProvider(&namedProvider{ + isolator := isolateProvider(®isteredProvider{ FeatureProvider: provider, name: "test-provider", }, []of.Hook{}) assert.Zero(t, isolator.capturedContext) assert.Zero(t, isolator.capturedHints) - evalCtx, err := isolator.Before(t.Context(), hookCtx, hookHints) + ctx, err := isolator.Before(t.Context(), hookCtx, hookHints) require.NoError(t, err) + assert.NotNil(t, ctx) + evalCtx := of.EvaluationContextFromContext(ctx) assert.NotNil(t, evalCtx) assert.Equal(t, hookCtx, isolator.capturedContext) assert.Equal(t, hookHints, isolator.capturedHints) @@ -39,9 +41,9 @@ func Test_HookIsolator_BeforeCapturesData(t *testing.T) { func Test_HookIsolator_Hooks_ReturnsSelf(t *testing.T) { ctrl := gomock.NewController(t) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) provider.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - isolator := isolateProvider(&namedProvider{ + isolator := isolateProvider(®isteredProvider{ FeatureProvider: provider, name: "test-provider", }, []of.Hook{}) @@ -53,19 +55,19 @@ func Test_HookIsolator_Hooks_ReturnsSelf(t *testing.T) { func Test_HookIsolator_ExecutesHooksDuringEvaluation_NoError(t *testing.T) { ctrl := gomock.NewController(t) testHook := of.NewMockHook(ctrl) - testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) + testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), nil) testHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) testHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) testHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) provider.EXPECT().Hooks().Return([]of.Hook{testHook}) provider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.BoolResolutionDetail{ Value: true, ProviderResolutionDetail: of.ProviderResolutionDetail{}, }) - isolator := isolateProvider(&namedProvider{ + isolator := isolateProvider(®isteredProvider{ FeatureProvider: provider, name: "test-provider", }, nil) @@ -76,15 +78,15 @@ func Test_HookIsolator_ExecutesHooksDuringEvaluation_NoError(t *testing.T) { func Test_HookIsolator_ExecutesHooksDuringEvaluation_BeforeErrorAbortsExecution(t *testing.T) { ctrl := gomock.NewController(t) testHook := of.NewMockHook(ctrl) - testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("test error")) + testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), errors.New("test error")) testHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) testHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) testHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) provider.EXPECT().Hooks().Return([]of.Hook{testHook}) - isolator := isolateProvider(&namedProvider{ + isolator := isolateProvider(®isteredProvider{ FeatureProvider: provider, name: "test-provider", }, nil) @@ -95,19 +97,19 @@ func Test_HookIsolator_ExecutesHooksDuringEvaluation_BeforeErrorAbortsExecution( func Test_HookIsolator_ExecutesHooksDuringEvaluation_WithAfterError(t *testing.T) { ctrl := gomock.NewController(t) testHook := of.NewMockHook(ctrl) - testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil) + testHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(t.Context(), nil) testHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("test error")) testHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) testHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) provider.EXPECT().Hooks().Return([]of.Hook{testHook}) provider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.BoolResolutionDetail{ Value: false, ProviderResolutionDetail: of.ProviderResolutionDetail{}, }) - isolator := isolateProvider(&namedProvider{ + isolator := isolateProvider(®isteredProvider{ FeatureProvider: provider, name: "test-provider", }, nil) diff --git a/openfeature/multi/multiprovider.go b/providers/multi/multi_provider.go similarity index 83% rename from openfeature/multi/multiprovider.go rename to providers/multi/multi_provider.go index 8ad9a5e7..a8b59189 100644 --- a/openfeature/multi/multiprovider.go +++ b/providers/multi/multi_provider.go @@ -12,7 +12,7 @@ import ( "strings" "sync" - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" "golang.org/x/sync/errgroup" ) @@ -33,15 +33,15 @@ type ( // Provider is an implementation of [of.FeatureProvider] that can execute multiple providers using various // strategies. Provider struct { - providers []NamedProvider + providers []namedProvider metadata of.Metadata initialized bool overallStatus of.State overallStatusLock sync.RWMutex providerStatus map[string]of.State providerStatusLock sync.Mutex - strategyName EvaluationStrategy // the name of the strategy used for evaluation - strategyFunc StrategyFn[FlagTypes] // used for evaluating strategies + strategyName EvaluationStrategy // the name of the strategy used for evaluation + evaluationFunc evaluationFn[of.FlagTypes] logger *slog.Logger outboundEvents chan of.Event workerGroup sync.WaitGroup @@ -49,16 +49,16 @@ type ( globalHooks []of.Hook } - // NamedProvider extends [of.FeatureProvider] by adding a unique provider name. - NamedProvider interface { + // namedProvider extends [of.FeatureProvider] by adding a unique provider name. + namedProvider interface { of.FeatureProvider // Name returns the unique name assigned to the provider. Name() string } - // namedProvider allows for a unique name to be assigned to a provider during a multi-provider set up. + // registeredProvider allows for a unique name to be assigned to a provider during a multi-provider set up. // The name will be used when reporting errors & results to specify the provider associated with them. - namedProvider struct { + registeredProvider struct { of.FeatureProvider name string extraHooks []of.Hook @@ -70,7 +70,7 @@ type ( // Private Types namedEvent struct { of.Event - providerName string + registeredProviderName string } // configuration is the internal configuration of a [multi.Provider] @@ -80,8 +80,9 @@ type ( customStrategy StrategyConstructor logger *slog.Logger hooks []of.Hook - providers []*namedProvider + providers []*registeredProvider customComparator Comparator + runMode runModeFn[of.FlagTypes] } // namedEventHandler is a wrapper around an [of.EventHandler] that includes the provider name. @@ -92,12 +93,12 @@ type ( ) // Name returns the unique name assigned to the provider. -func (n *namedProvider) Name() string { +func (n *registeredProvider) Name() string { return n.name } // unwrap returns the underlying [of.FeatureProvider] instance wrapped by this [namedProvider]. -func (n *namedProvider) unwrap() of.FeatureProvider { +func (n *registeredProvider) unwrap() of.FeatureProvider { return n.FeatureProvider } @@ -107,11 +108,11 @@ var ( eventTypeToState map[of.EventType]of.State // Compile-time interface compliance checks - _ of.FeatureProvider = (*Provider)(nil) - _ of.EventHandler = (*Provider)(nil) - _ of.ContextAwareStateHandler = (*Provider)(nil) - _ of.Tracker = (*Provider)(nil) - _ NamedProvider = (*namedProvider)(nil) + _ of.FeatureProvider = (*Provider)(nil) + _ of.EventHandler = (*Provider)(nil) + _ of.StateHandler = (*Provider)(nil) + _ of.Tracker = (*Provider)(nil) + _ namedProvider = (*registeredProvider)(nil) ) // init Initialize "constants" used for event handling priorities and filtering. @@ -165,7 +166,7 @@ func WithCustomComparator(comparator Comparator) Option { } // WithCustomStrategy sets a custom strategy function by defining a "constructor" that acts as closure over a slice of -// [NamedProvider] instances with your returned custom strategy function. This must be used in conjunction with [StrategyCustom] +// [namedProvider] instances with your returned custom strategy function. This must be used in conjunction with [StrategyCustom] func WithCustomStrategy(s StrategyConstructor) Option { return func(conf *configuration) { conf.customStrategy = s @@ -188,7 +189,7 @@ func WithGlobalHooks(hooks ...of.Hook) Option { // are provided determines the order in which the providers are registered and evaluated. func WithProvider(providerName string, provider of.FeatureProvider, hooks ...of.Hook) Option { return func(conf *configuration) { - conf.providers = append(conf.providers, &namedProvider{ + conf.providers = append(conf.providers, ®isteredProvider{ name: providerName, FeatureProvider: provider, extraHooks: hooks, @@ -196,8 +197,15 @@ func WithProvider(providerName string, provider of.FeatureProvider, hooks ...of. } } +// WithRunModeParallel configures the run mode to evaluate providers in parallel. +func WithRunModeParallel() Option { + return func(conf *configuration) { + conf.runMode = runModeParallel[of.FlagTypes] + } +} + // Multiprovider Implementation -func buildMetadata(m []NamedProvider) of.Metadata { +func buildMetadata(m []namedProvider) of.Metadata { var separator string var metaName strings.Builder metaName.WriteString("MultiProvider {") @@ -218,7 +226,8 @@ func buildMetadata(m []NamedProvider) of.Metadata { func NewProvider(evaluationStrategy EvaluationStrategy, options ...Option) (*Provider, error) { config := &configuration{ logger: slog.New(slog.DiscardHandler), - providers: make([]*namedProvider, 0, 2), + providers: make([]*registeredProvider, 0, 2), + runMode: runModeSequential[of.FlagTypes], } for _, opt := range options { @@ -229,7 +238,7 @@ func NewProvider(evaluationStrategy EvaluationStrategy, options ...Option) (*Pro return nil, errors.New("no providers configured: at least one provider must be registered using WithProvider()") } - providers := make([]NamedProvider, 0, len(config.providers)) + providers := make([]namedProvider, 0, len(config.providers)) collectedHooks := make([]of.Hook, 0, len(config.providers)) for i, provider := range config.providers { // Validate Providers @@ -246,7 +255,7 @@ func NewProvider(evaluationStrategy EvaluationStrategy, options ...Option) (*Pro continue } - var wrappedProvider NamedProvider + var wrappedProvider namedProvider if _, ok := provider.FeatureProvider.(of.EventHandler); ok { wrappedProvider = isolateProviderWithEvents(provider, provider.extraHooks) } else { @@ -267,28 +276,43 @@ func NewProvider(evaluationStrategy EvaluationStrategy, options ...Option) (*Pro globalHooks: append(config.hooks, collectedHooks...), } - var strategy StrategyFn[FlagTypes] + var strategy StrategyFn[of.FlagTypes] switch evaluationStrategy { case StrategyFirstMatch: - strategy = newFirstMatchStrategy(multiProvider.Providers()) + strategy = newFirstMatchStrategy() case StrategyFirstSuccess: - strategy = newFirstSuccessStrategy(multiProvider.Providers()) + strategy = newFirstSuccessStrategy() case StrategyComparison: - strategy = newComparisonStrategy(multiProvider.Providers(), config.fallbackProvider, config.customComparator) + strategy = newComparisonStrategy(config.fallbackProvider, config.customComparator) default: if config.customStrategy == nil { return nil, fmt.Errorf("%s is an unknown evaluation strategy", evaluationStrategy) } - strategy = config.customStrategy(multiProvider.Providers()) + strategy = config.customStrategy() } - multiProvider.strategyFunc = strategy + multiProvider.evaluationFunc = newEvaluationFunc(providers, config.runMode, strategy) multiProvider.strategyName = evaluationStrategy return multiProvider, nil } -// Providers returns slice of providers wrapped in [NamedProvider] structs. -func (p *Provider) Providers() []NamedProvider { +// newEvaluationFunc creates an evaluation function that: +// 1. Executes providers using the specified runMode (parallel/sequential) +// 2. Collects resolutions into an iterator +// 3. Applies the strategy to select the final result +func newEvaluationFunc[T of.FlagTypes](providers []namedProvider, runMode runModeFn[T], strategy StrategyFn[T]) evaluationFn[T] { + return func(ctx context.Context, flag string, defaultValue T, flatCtx of.FlattenedContext) *of.GenericResolutionDetail[T] { + return strategy( + runMode(ctx, providers, flag, defaultValue, flatCtx), + defaultValue, + func(p of.FeatureProvider) *of.GenericResolutionDetail[T] { + return evaluate(ctx, p, "fallback", flag, defaultValue, flatCtx) + }) + } +} + +// Providers returns slice of providers wrapped in [namedProvider] structs. +func (p *Provider) Providers() []namedProvider { return p.providers } @@ -309,7 +333,7 @@ func (p *Provider) Hooks() []of.Hook { // BooleanEvaluation evaluates the flag and returns a [of.BoolResolutionDetail]. func (p *Provider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx of.FlattenedContext) of.BoolResolutionDetail { - res := p.strategyFunc(ctx, flag, defaultValue, flatCtx) + res := p.evaluationFunc(ctx, flag, defaultValue, flatCtx) return of.BoolResolutionDetail{ Value: res.Value.(bool), ProviderResolutionDetail: res.ProviderResolutionDetail, @@ -318,7 +342,7 @@ func (p *Provider) BooleanEvaluation(ctx context.Context, flag string, defaultVa // StringEvaluation evaluates the flag and returns a [of.StringResolutionDetail]. func (p *Provider) StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx of.FlattenedContext) of.StringResolutionDetail { - res := p.strategyFunc(ctx, flag, defaultValue, flatCtx) + res := p.evaluationFunc(ctx, flag, defaultValue, flatCtx) return of.StringResolutionDetail{ Value: res.Value.(string), ProviderResolutionDetail: res.ProviderResolutionDetail, @@ -327,7 +351,7 @@ func (p *Provider) StringEvaluation(ctx context.Context, flag string, defaultVal // FloatEvaluation evaluates the flag and returns a [of.FloatResolutionDetail]. func (p *Provider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx of.FlattenedContext) of.FloatResolutionDetail { - res := p.strategyFunc(ctx, flag, defaultValue, flatCtx) + res := p.evaluationFunc(ctx, flag, defaultValue, flatCtx) return of.FloatResolutionDetail{ Value: res.Value.(float64), ProviderResolutionDetail: res.ProviderResolutionDetail, @@ -336,32 +360,27 @@ func (p *Provider) FloatEvaluation(ctx context.Context, flag string, defaultValu // IntEvaluation evaluates the flag and returns an [of.IntResolutionDetail]. func (p *Provider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx of.FlattenedContext) of.IntResolutionDetail { - res := p.strategyFunc(ctx, flag, defaultValue, flatCtx) + res := p.evaluationFunc(ctx, flag, defaultValue, flatCtx) return of.IntResolutionDetail{ Value: res.Value.(int64), ProviderResolutionDetail: res.ProviderResolutionDetail, } } -// ObjectEvaluation evaluates the flag and returns an [of.InterfaceResolutionDetail]. For the purposes of evaluation +// ObjectEvaluation evaluates the flag and returns an [of.ObjectResolutionDetail]. For the purposes of evaluation // within strategies, the type of the default value is used as the assumed type of the returned responses from each provider. // This is especially important when using the [StrategyComparison] configuration as an internal error will occur if this // is not a comparable type unless the [WithCustomComparator] [Option] is configured. -func (p *Provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx of.FlattenedContext) of.InterfaceResolutionDetail { - res := p.strategyFunc(ctx, flag, defaultValue, flatCtx) - return of.InterfaceResolutionDetail{ +func (p *Provider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx of.FlattenedContext) of.ObjectResolutionDetail { + res := p.evaluationFunc(ctx, flag, defaultValue, flatCtx) + return of.ObjectResolutionDetail{ Value: res.Value, ProviderResolutionDetail: res.ProviderResolutionDetail, } } // Init will run the initialize method for all internal [of.FeatureProvider] instances and aggregate any errors. -func (p *Provider) Init(evalCtx of.EvaluationContext) error { - return p.InitWithContext(context.Background(), evalCtx) -} - -// InitWithContext will run the initialize method for all internal [of.FeatureProvider] instances and aggregate any errors. -func (p *Provider) InitWithContext(ctx context.Context, evalCtx of.EvaluationContext) error { +func (p *Provider) Init(ctx context.Context) error { eg, ctx := errgroup.WithContext(ctx) // wrapper type used only for initialization of event listener workers p.logger.LogAttrs(ctx, slog.LevelDebug, "start initialization") @@ -375,13 +394,7 @@ func (p *Provider) InitWithContext(ctx context.Context, evalCtx of.EvaluationCon eg.Go(func() error { l.LogAttrs(ctx, slog.LevelDebug, "starting initialization") if stateHandle, ok := tryAs[of.StateHandler](prov); ok { - var err error - if contextAwareHandle, ok := stateHandle.(of.ContextAwareStateHandler); ok { - err = contextAwareHandle.InitWithContext(ctx, evalCtx) - } else { - err = stateHandle.Init(evalCtx) - } - + err := stateHandle.Init(ctx) if err != nil { l.LogAttrs(ctx, slog.LevelError, "initialization failed", slog.Any("error", err)) p.updateProviderState(name, of.ErrorState) @@ -464,8 +477,8 @@ func (p *Provider) forwardProviderEvents(workerCtx context.Context, handlers cha e.EventMetadata[MetadataProviderType] = p.Metadata().Name } out <- namedEvent{ - Event: e, - providerName: name, + Event: e, + registeredProviderName: name, } } } @@ -479,7 +492,7 @@ func (p *Provider) forwardProviderEvents(workerCtx context.Context, handlers cha for e := range pipe { l := workerLogger.With( - slog.String(MetadataProviderName, e.providerName), + slog.String(MetadataProviderName, e.registeredProviderName), slog.String(MetadataProviderType, e.ProviderName), ) l.LogAttrs(workerCtx, slog.LevelDebug, "received event from provider", slog.String("event-type", string(e.EventType))) @@ -514,10 +527,10 @@ func (p *Provider) updateProviderStateFromEvent(e namedEvent) bool { p.logger.LogAttrs(context.Background(), slog.LevelDebug, "ProviderConfigChange event", slog.String("event-message", e.Message)) } p.providerStatusLock.Lock() - previousState := p.providerStatus[e.providerName] + previousState := p.providerStatus[e.registeredProviderName] p.providerStatusLock.Unlock() logProviderState(p.logger, e, previousState) - return p.updateProviderState(e.providerName, eventTypeToState[e.EventType]) + return p.updateProviderState(e.registeredProviderName, eventTypeToState[e.EventType]) } // evaluateState Determines the overall state of the provider using the weights specified in Appendix A of the @@ -538,30 +551,21 @@ func logProviderState(l *slog.Logger, e namedEvent, previousState of.State) { case of.ReadyState: if previousState != of.NotReadyState { l.LogAttrs(context.Background(), slog.LevelInfo, "provider has returned to ready state", - slog.String(MetadataProviderName, e.providerName), slog.String("previous-state", string(previousState))) + slog.String(MetadataProviderName, e.registeredProviderName), slog.String("previous-state", string(previousState))) return } - l.LogAttrs(context.Background(), slog.LevelDebug, "provider is ready", slog.String(MetadataProviderName, e.providerName)) + l.LogAttrs(context.Background(), slog.LevelDebug, "provider is ready", slog.String(MetadataProviderName, e.registeredProviderName)) case of.StaleState: l.LogAttrs(context.Background(), slog.LevelWarn, "provider is stale", - slog.String(MetadataProviderName, e.providerName), slog.String("event-message", e.Message)) + slog.String(MetadataProviderName, e.registeredProviderName), slog.String("event-message", e.Message)) case of.ErrorState: l.LogAttrs(context.Background(), slog.LevelError, "provider is in an error state", - slog.String(MetadataProviderName, e.providerName), slog.String("event-message", e.Message)) + slog.String(MetadataProviderName, e.registeredProviderName), slog.String("event-message", e.Message)) } } // Shutdown Shuts down all internal [of.FeatureProvider] instances and internal event listeners -func (p *Provider) Shutdown() { - ctx := context.Background() - err := p.ShutdownWithContext(ctx) - if err != nil { - p.logger.LogAttrs(ctx, slog.LevelWarn, "error during shutdown", slog.Any("error", err)) - } -} - -// ShutdownWithContext shuts down all internal [of.FeatureProvider] instances and internal event listeners -func (p *Provider) ShutdownWithContext(ctx context.Context) error { +func (p *Provider) Shutdown(ctx context.Context) error { if !p.initialized { // Don't do anything if we were never initialized p.logger.LogAttrs(ctx, slog.LevelDebug, "provider not initialized, skipping shutdown") @@ -577,12 +581,8 @@ func (p *Provider) ShutdownWithContext(ctx context.Context) error { name := provider.Name() if stateHandle, ok := tryAs[of.StateHandler](provider); ok { meg.Go(func() error { - if contextAwareHandle, ok := stateHandle.(of.ContextAwareStateHandler); ok { - if err := contextAwareHandle.ShutdownWithContext(ctx); err != nil { - return &ProviderError{ProviderName: name, err: err} - } - } else { - stateHandle.Shutdown() + if err := stateHandle.Shutdown(ctx); err != nil { + return &ProviderError{ProviderName: name, err: err} } return nil }) @@ -633,7 +633,7 @@ func (p *Provider) Track(ctx context.Context, trackingEventName string, evaluati p.providerStatusLock.Lock() statuses := maps.Clone(p.providerStatus) p.providerStatusLock.Unlock() - providers := make([]NamedProvider, 0, len(p.providers)) + providers := make([]namedProvider, 0, len(p.providers)) for _, p := range p.providers { if statuses[p.Name()] == of.ReadyState { providers = append(providers, p) @@ -646,13 +646,13 @@ func (p *Provider) Track(ctx context.Context, trackingEventName string, evaluati } } -// tryAs attempts to extract and type-assert the underlying [of.FeatureProvider] from a [NamedProvider]. +// tryAs attempts to extract and type-assert the underlying [of.FeatureProvider] from a [namedProvider]. // It first checks if the provider implements an unwrap() method to access the wrapped provider, // then attempts to cast that provider to type T. Returns the casted value and true if successful, // or the zero value of T and false if the provider doesn't support unwrapping or doesn't implement type T. // This is used internally to check if wrapped providers implement optional interfaces like // [of.StateHandler], [of.EventHandler], or [of.Tracker]. -func tryAs[T any](p NamedProvider) (T, bool) { +func tryAs[T any](p namedProvider) (T, bool) { var v T unwrapped, ok := p.(interface { diff --git a/openfeature/multi/multiprovider_test.go b/providers/multi/multi_provider_test.go similarity index 82% rename from openfeature/multi/multiprovider_test.go rename to providers/multi/multi_provider_test.go index aa1ba2b6..862fef7d 100644 --- a/openfeature/multi/multiprovider_test.go +++ b/providers/multi/multi_provider_test.go @@ -6,16 +6,16 @@ import ( "testing" "time" - of "github.com/open-feature/go-sdk/openfeature" - imp "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + of "go.openfeature.dev/openfeature/v2" + imp "go.openfeature.dev/openfeature/v2/providers/inmemory" "go.uber.org/mock/gomock" ) func TestMultiProvider_ProvidersMethod(t *testing.T) { - testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) - testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider1 := imp.NewProvider(map[string]imp.InMemoryFlag{}) + testProvider2 := imp.NewProvider(map[string]imp.InMemoryFlag{}) mp, err := NewProvider(StrategyFirstSuccess, WithProvider("provider1", testProvider1), WithProvider("provider2", testProvider2)) require.NoError(t, err) @@ -37,7 +37,7 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { }) t.Run("naming a provider the empty string returns an error", func(t *testing.T) { - _, err := NewProvider(StrategyFirstMatch, WithProvider("", imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}))) + _, err := NewProvider(StrategyFirstMatch, WithProvider("", imp.NewProvider(map[string]imp.InMemoryFlag{}))) require.Errorf(t, err, "provider name cannot be the empty string") }) @@ -47,31 +47,31 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { }) t.Run("unknown evaluation strategyFunc returns an error", func(t *testing.T) { - _, err := NewProvider("unknown", WithProvider("provider1", imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}))) + _, err := NewProvider("unknown", WithProvider("provider1", imp.NewProvider(map[string]imp.InMemoryFlag{}))) require.Errorf(t, err, "unknown is an unknown evaluation strategyFunc") }) t.Run("setting custom strategyFunc without custom strategyFunc option returns error", func(t *testing.T) { - _, err := NewProvider(StrategyCustom, WithProvider("provider1", imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}))) + _, err := NewProvider(StrategyCustom, WithProvider("provider1", imp.NewProvider(map[string]imp.InMemoryFlag{}))) require.Errorf(t, err, "A custom strategyFunc must be set via an option if StrategyCustom is set") }) t.Run("success", func(t *testing.T) { - mp, err := NewProvider(StrategyComparison, WithProvider("provider1", imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}))) + mp, err := NewProvider(StrategyComparison, WithProvider("provider1", imp.NewProvider(map[string]imp.InMemoryFlag{}))) require.NoError(t, err) assert.NotZero(t, mp) }) t.Run("success with custom provider", func(t *testing.T) { - mp, err := NewProvider(StrategyCustom, WithCustomStrategy(func(providers []NamedProvider) StrategyFn[FlagTypes] { - return func(ctx context.Context, flag string, defaultValue FlagTypes, evalCtx of.FlattenedContext) of.GenericResolutionDetail[FlagTypes] { - return of.GenericResolutionDetail[FlagTypes]{ + mp, err := NewProvider(StrategyCustom, WithCustomStrategy(func() StrategyFn[of.FlagTypes] { + return func(resolutions ResolutionIterator[of.FlagTypes], defaultValue of.FlagTypes, _ FallbackEvaluator[of.FlagTypes]) *of.GenericResolutionDetail[of.FlagTypes] { + return &of.GenericResolutionDetail[of.FlagTypes]{ Value: defaultValue, ProviderResolutionDetail: of.ProviderResolutionDetail{Reason: of.UnknownReason}, } } }), - WithProvider("provider1", imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{})), + WithProvider("provider1", imp.NewProvider(map[string]imp.InMemoryFlag{})), ) require.NoError(t, err) assert.NotZero(t, mp) @@ -80,9 +80,9 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { func TestMultiProvider_MetaData(t *testing.T) { t.Run("two providers", func(t *testing.T) { - testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider1 := imp.NewProvider(map[string]imp.InMemoryFlag{}) ctrl := gomock.NewController(t) - testProvider2 := of.NewMockFeatureProvider(ctrl) + testProvider2 := of.NewMockProvider(ctrl) testProvider2.EXPECT().Metadata().Return(of.Metadata{ Name: "MockProvider", }) @@ -101,14 +101,14 @@ func TestMultiProvider_MetaData(t *testing.T) { }) t.Run("three providers", func(t *testing.T) { - testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider1 := imp.NewProvider(map[string]imp.InMemoryFlag{}) ctrl := gomock.NewController(t) - testProvider2 := of.NewMockFeatureProvider(ctrl) + testProvider2 := of.NewMockProvider(ctrl) testProvider2.EXPECT().Metadata().Return(of.Metadata{ Name: "MockProvider", }) testProvider2.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - testProvider3 := of.NewMockFeatureProvider(ctrl) + testProvider3 := of.NewMockProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{ Name: "MockProvider", }) @@ -134,15 +134,15 @@ func TestMultiProvider_Init(t *testing.T) { } ctrl := gomock.NewController(t) - testProvider1 := of.NewMockFeatureProvider(ctrl) + testProvider1 := of.NewMockProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider1.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - initProvider := of.NewMockFeatureProvider(ctrl) + initProvider := of.NewMockProvider(ctrl) initProvider.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) initProvider.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) initHandler := of.NewMockStateHandler(ctrl) initHandler.EXPECT().Init(gomock.Any()).Return(nil) - initHandler.EXPECT().Shutdown().MaxTimes(1) + initHandler.EXPECT().Shutdown(gomock.Any()).MaxTimes(1) testProvider2 := struct { of.FeatureProvider of.StateHandler @@ -150,7 +150,7 @@ func TestMultiProvider_Init(t *testing.T) { initProvider, initHandler, } - testProvider3 := of.NewMockFeatureProvider(ctrl) + testProvider3 := of.NewMockProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider3.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) @@ -162,22 +162,20 @@ func TestMultiProvider_Init(t *testing.T) { ) require.NoError(t, err) - t.Cleanup(func() { - mp.Shutdown() - }) - attributes := map[string]any{ "foo": "bar", } evalCtx := of.NewTargetlessEvaluationContext(attributes) - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.NoError(t, err) assert.Equal(t, of.ReadyState, mp.Status()) + err = mp.Shutdown(t.Context()) + require.NoError(t, err) } func TestMultiProvider_InitErrorWithProvider(t *testing.T) { ctrl := gomock.NewController(t) - errProvider := of.NewMockFeatureProvider(ctrl) + errProvider := of.NewMockProvider(ctrl) errProvider.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) errProvider.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) errHandler := of.NewMockStateHandler(ctrl) @@ -190,10 +188,10 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { errHandler, } - testProvider1 := of.NewMockFeatureProvider(ctrl) + testProvider1 := of.NewMockProvider(ctrl) testProvider1.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) - testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider2 := imp.NewProvider(map[string]imp.InMemoryFlag{}) mp, err := NewProvider( StrategyFirstMatch, @@ -207,7 +205,7 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { "foo": "bar", } evalCtx := of.NewTargetlessEvaluationContext(attributes) - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.Errorf(t, err, "Provider provider3: test error") assert.Equal(t, of.ErrorState, mp.overallStatus) } @@ -215,11 +213,11 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { func TestMultiProvider_Shutdown_WithoutInit(t *testing.T) { ctrl := gomock.NewController(t) - testProvider1 := of.NewMockFeatureProvider(ctrl) + testProvider1 := of.NewMockProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider1.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) - testProvider3 := of.NewMockFeatureProvider(ctrl) + testProvider2 := imp.NewProvider(map[string]imp.InMemoryFlag{}) + testProvider3 := of.NewMockProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider3.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) @@ -231,22 +229,23 @@ func TestMultiProvider_Shutdown_WithoutInit(t *testing.T) { ) require.NoError(t, err) - mp.Shutdown() + err = mp.Shutdown(t.Context()) + require.NoError(t, err) } func TestMultiProvider_Shutdown_WithInit(t *testing.T) { ctrl := gomock.NewController(t) - testProvider1 := of.NewMockFeatureProvider(ctrl) + testProvider1 := of.NewMockProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider1.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) - testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) - handlingProvider := of.NewMockFeatureProvider(ctrl) + testProvider2 := imp.NewProvider(map[string]imp.InMemoryFlag{}) + handlingProvider := of.NewMockProvider(ctrl) handlingProvider.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) handlingProvider.EXPECT().Hooks().Return([]of.Hook{}).MinTimes(1) handledHandler := of.NewMockStateHandler(ctrl) handledHandler.EXPECT().Init(gomock.Any()).Return(nil) - handledHandler.EXPECT().Shutdown() + handledHandler.EXPECT().Shutdown(gomock.Any()) testProvider3 := struct { of.FeatureProvider of.StateHandler @@ -275,11 +274,12 @@ func TestMultiProvider_Shutdown_WithInit(t *testing.T) { return } }() - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.NoError(t, err) assert.Equal(t, of.ReadyState, mp.Status()) cancel() - mp.Shutdown() + err = mp.Shutdown(t.Context()) + require.NoError(t, err) assert.Equal(t, of.NotReadyState, mp.Status()) } @@ -331,11 +331,10 @@ func TestMultiProvider_StateUpdateWithSameTypeProviders(t *testing.T) { if err != nil { t.Fatalf("failed to create multi-provider: %v", err) } - t.Cleanup(mp.Shutdown) // Initialize the provider - ctx := of.NewEvaluationContext("test", nil) - if err := mp.Init(ctx); err != nil { + evalCtx := of.NewEvaluationContext("test", nil) + if err := mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)); err != nil { t.Fatalf("failed to initialize multi-provider: %v", err) } @@ -369,6 +368,8 @@ func TestMultiProvider_StateUpdateWithSameTypeProviders(t *testing.T) { if numProviders != 2 { t.Errorf("Expected 2 providers in status map, got %d", numProviders) } + err = mp.Shutdown(t.Context()) + require.NoError(t, err) } func TestMultiProvider_Track(t *testing.T) { @@ -378,7 +379,7 @@ func TestMultiProvider_Track(t *testing.T) { provider1 := newMockProviderWithEvents(ctrl, "provider1") provider2 := newMockProviderWithEvents(ctrl, "provider2") - provider3 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) // Does not implement Tracker + provider3 := imp.NewProvider(map[string]imp.InMemoryFlag{}) // Does not implement Tracker mp, err := NewProvider( StrategyFirstSuccess, @@ -387,10 +388,9 @@ func TestMultiProvider_Track(t *testing.T) { WithProvider("provider3", provider3), ) require.NoError(t, err) - t.Cleanup(mp.Shutdown) evalCtx := of.NewEvaluationContext("user-123", map[string]any{"plan": "premium"}) - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.NoError(t, err) trackingEventName := "button-clicked" @@ -402,6 +402,8 @@ func TestMultiProvider_Track(t *testing.T) { provider2.MockTracker.EXPECT().Track(ctx, trackingEventName, evalCtx, details).Times(1) mp.Track(ctx, trackingEventName, evalCtx, details) + err = mp.Shutdown(t.Context()) + require.NoError(t, err) }) t.Run("does not track when provider is not initialized", func(t *testing.T) { @@ -410,11 +412,13 @@ func TestMultiProvider_Track(t *testing.T) { provider1 := newMockProviderWithEvents(ctrl, "provider1") // manual shutdown on cleanup because multi-provider won't be initialized - t.Cleanup(provider1.Shutdown) + t.Cleanup(func() { + err := provider1.Shutdown(t.Context()) + assert.NoError(t, err) + }) mp, err := NewProvider(StrategyFirstSuccess, WithProvider("provider1", provider1)) require.NoError(t, err) - t.Cleanup(mp.Shutdown) // Don't initialize the multi-provider ctx := t.Context() @@ -426,6 +430,8 @@ func TestMultiProvider_Track(t *testing.T) { provider1.MockTracker.EXPECT().Track(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) mp.Track(ctx, trackingEventName, evalCtx, details) + err = mp.Shutdown(t.Context()) + require.NoError(t, err) }) t.Run("only tracks on providers in ready state", func(t *testing.T) { @@ -441,10 +447,9 @@ func TestMultiProvider_Track(t *testing.T) { WithProvider("error-provider", errorProvider), ) require.NoError(t, err) - t.Cleanup(mp.Shutdown) evalCtx := of.NewEvaluationContext("user-456", map[string]any{}) - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.NoError(t, err) // Simulate error state for one provider @@ -468,6 +473,8 @@ func TestMultiProvider_Track(t *testing.T) { errorProvider.MockTracker.EXPECT().Track(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) mp.Track(ctx, trackingEventName, evalCtx, details) + err = mp.Shutdown(t.Context()) + require.NoError(t, err) }) t.Run("handles providers that don't implement Tracker", func(t *testing.T) { @@ -475,7 +482,7 @@ func TestMultiProvider_Track(t *testing.T) { t.Cleanup(ctrl.Finish) trackerProvider := newMockProviderWithEvents(ctrl, "tracker-provider") - nonTrackerProvider := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + nonTrackerProvider := imp.NewProvider(map[string]imp.InMemoryFlag{}) mp, err := NewProvider( StrategyFirstSuccess, @@ -483,10 +490,9 @@ func TestMultiProvider_Track(t *testing.T) { WithProvider("non-tracker", nonTrackerProvider), ) require.NoError(t, err) - t.Cleanup(mp.Shutdown) evalCtx := of.NewEvaluationContext("user-789", map[string]any{}) - err = mp.Init(evalCtx) + err = mp.Init(of.ContextWithEvaluationContext(t.Context(), evalCtx)) require.NoError(t, err) trackingEventName := "conversion" @@ -495,6 +501,8 @@ func TestMultiProvider_Track(t *testing.T) { ctx := t.Context() trackerProvider.MockTracker.EXPECT().Track(ctx, trackingEventName, evalCtx, details).Times(1) mp.Track(ctx, trackingEventName, evalCtx, details) + err = mp.Shutdown(t.Context()) + require.NoError(t, err) }) } @@ -502,7 +510,7 @@ var _ of.StateHandler = (*mockProviderWithEvents)(nil) // mockProviderWithEvents wraps a mock provider to add EventHandler and optional Tracker capability type mockProviderWithEvents struct { - *of.MockFeatureProvider + *of.MockProvider *of.MockStateHandler *of.MockTracker eventChannel chan of.Event @@ -510,7 +518,7 @@ type mockProviderWithEvents struct { } func newMockProviderWithEvents(ctrl *gomock.Controller, name string) *mockProviderWithEvents { - mockProvider := of.NewMockFeatureProvider(ctrl) + mockProvider := of.NewMockProvider(ctrl) mockStateHandler := of.NewMockStateHandler(ctrl) mockTracker := of.NewMockTracker(ctrl) eventChan := make(chan of.Event, 10) @@ -520,7 +528,7 @@ func newMockProviderWithEvents(ctrl *gomock.Controller, name string) *mockProvid // Set up expectations mockProvider.EXPECT().Metadata().Return(metadata).AnyTimes() mockProvider.EXPECT().Hooks().Return([]of.Hook{}).AnyTimes() - mockStateHandler.EXPECT().Init(gomock.Any()).DoAndReturn(func(ctx of.EvaluationContext) error { + mockStateHandler.EXPECT().Init(gomock.Any()).DoAndReturn(func(ctx context.Context) error { // Emit READY event on init eventChan <- of.Event{ ProviderName: name, @@ -531,24 +539,25 @@ func newMockProviderWithEvents(ctrl *gomock.Controller, name string) *mockProvid } return nil }).AnyTimes() - mockStateHandler.EXPECT().Shutdown() + mockStateHandler.EXPECT().Shutdown(gomock.Any()) return &mockProviderWithEvents{ - MockFeatureProvider: mockProvider, - MockStateHandler: mockStateHandler, - eventChannel: eventChan, - metadata: metadata, - MockTracker: mockTracker, + MockProvider: mockProvider, + MockStateHandler: mockStateHandler, + eventChannel: eventChan, + metadata: metadata, + MockTracker: mockTracker, } } -func (m *mockProviderWithEvents) Init(evalCtx of.EvaluationContext) error { - return m.MockStateHandler.Init(evalCtx) +func (m *mockProviderWithEvents) Init(ctx context.Context) error { + return m.MockStateHandler.Init(ctx) } -func (m *mockProviderWithEvents) Shutdown() { - m.MockStateHandler.Shutdown() +func (m *mockProviderWithEvents) Shutdown(ctx context.Context) error { + err := m.MockStateHandler.Shutdown(ctx) close(m.eventChannel) + return err } func (m *mockProviderWithEvents) EventChannel() <-chan of.Event { diff --git a/providers/multi/run_mode.go b/providers/multi/run_mode.go new file mode 100644 index 00000000..c575cf01 --- /dev/null +++ b/providers/multi/run_mode.go @@ -0,0 +1,72 @@ +package multi + +import ( + "context" + "sync" + + of "go.openfeature.dev/openfeature/v2" +) + +// runModeFn is a function type that defines how flag evaluations are executed across multiple providers. +// It returns an iterator that yields provider names and their corresponding resolution details. +type runModeFn[T of.FlagTypes] func(ctx context.Context, providers []namedProvider, flag string, defaultValue T, flatCtx of.FlattenedContext) ResolutionIterator[T] + +// runModeParallel evaluates a flag across multiple providers concurrently. +// It launches a goroutine for each provider and yields results as they complete. +// Evaluation stops early if the iterator consumer stops consuming results or if the context is cancelled. +func runModeParallel[T of.FlagTypes](ctx context.Context, providers []namedProvider, flag string, defaultValue T, flatCtx of.FlattenedContext) ResolutionIterator[T] { + type namedResult struct { + name string + resolution *of.GenericResolutionDetail[T] + } + return func(yield func(string, *of.GenericResolutionDetail[T]) bool) { + resolutions := make(chan *namedResult, len(providers)) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + go func() { + var wg sync.WaitGroup + for _, provider := range providers { + wg.Go(func() { + resolution := evaluate(ctx, provider, provider.Name(), flag, defaultValue, flatCtx) + select { + case <-ctx.Done(): + return + case resolutions <- &namedResult{name: provider.Name(), resolution: resolution}: + } + }) + } + wg.Wait() + close(resolutions) + }() + + for result := range resolutions { + select { + case <-ctx.Done(): + return + default: + if !yield(result.name, result.resolution) { + return + } + } + } + } +} + +// runModeSequential evaluates a flag across multiple providers one at a time in order. +// It yields each provider's result sequentially and stops if the iterator consumer stops consuming results +// or if the context is cancelled. +func runModeSequential[T of.FlagTypes](ctx context.Context, providers []namedProvider, flag string, defaultValue T, flatCtx of.FlattenedContext) ResolutionIterator[T] { + return func(yield func(string, *of.GenericResolutionDetail[T]) bool) { + for _, provider := range providers { + select { + case <-ctx.Done(): + return + default: + resolution := evaluate(ctx, provider, provider.Name(), flag, defaultValue, flatCtx) + if !yield(provider.Name(), resolution) { + return + } + } + } + } +} diff --git a/openfeature/multi/strategies.go b/providers/multi/strategies.go similarity index 75% rename from openfeature/multi/strategies.go rename to providers/multi/strategies.go index 0cd4da02..ae8a3a22 100644 --- a/openfeature/multi/strategies.go +++ b/providers/multi/strategies.go @@ -2,11 +2,12 @@ package multi import ( "context" + "iter" "maps" "regexp" "strings" - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" ) // EvaluationStrategy options @@ -47,15 +48,28 @@ type ( // EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers. EvaluationStrategy = string - // FlagTypes defines the types that can be used for flag values. - FlagTypes interface { - int64 | float64 | string | bool | any - } - // StrategyFn defines the signature for a strategy function. - StrategyFn[T FlagTypes] func(ctx context.Context, flag string, defaultValue T, flatCtx of.FlattenedContext) of.GenericResolutionDetail[T] + // ResolutionIterator provides an iterator over provider names and their resolution results. + ResolutionIterator[T of.FlagTypes] = iter.Seq2[string, *of.GenericResolutionDetail[T]] + + // FallbackEvaluator evaluates the fallback provider when the strategy needs it. + FallbackEvaluator[T of.FlagTypes] = func(fallbackProvider of.FeatureProvider) *of.GenericResolutionDetail[T] + + // StrategyFn defines the signature for a strategy function that processes resolutions from multiple providers. + // Parameters: + // - resolutions: An iterator of provider names and their resolution results + // - defaultValue: The default value to return if strategy fails + // - fallbackEvaluator: A function to evaluate the fallback provider if needed + // Returns: The final resolution detail selected by the strategy + StrategyFn[T of.FlagTypes] func(resolutions ResolutionIterator[T], defaultValue T, fallbackEvaluator FallbackEvaluator[T]) *of.GenericResolutionDetail[T] + // StrategyConstructor defines the signature for the function that will be called to retrieve the closure that acts // as the custom strategy implementation. This function should return a [StrategyFn] - StrategyConstructor func(providers []NamedProvider) StrategyFn[FlagTypes] + StrategyConstructor func() StrategyFn[of.FlagTypes] + + // evaluationFn wraps a strategy with run mode logic to create a complete evaluation function. + // It executes providers using the specified run mode (parallel/sequential), collects resolutions + // into an iterator, and applies the strategy to select the final result. + evaluationFn[T of.FlagTypes] func(ctx context.Context, flag string, defaultValue T, flatCtx of.FlattenedContext) *of.GenericResolutionDetail[T] ) // Common Components @@ -112,7 +126,7 @@ func mergeFlagMeta(tags ...of.FlagMetadata) of.FlagMetadata { // BuildDefaultResult should be called when a [StrategyFn] is in a failure state and needs to return a default value. // This method will build a resolution detail with the internal provided error set. This method is exported for those // writing their own custom [StrategyFn]. -func BuildDefaultResult[R FlagTypes](strategy EvaluationStrategy, defaultValue R, err error) of.GenericResolutionDetail[R] { +func BuildDefaultResult[R of.FlagTypes](strategy EvaluationStrategy, defaultValue R, err error) *of.GenericResolutionDetail[R] { var rErr of.ResolutionError var reason of.Reason if err != nil { @@ -123,7 +137,7 @@ func BuildDefaultResult[R FlagTypes](strategy EvaluationStrategy, defaultValue R reason = of.DefaultReason } - return of.GenericResolutionDetail[R]{ + return &of.GenericResolutionDetail[R]{ Value: defaultValue, ProviderResolutionDetail: of.ProviderResolutionDetail{ ResolutionError: rErr, @@ -133,10 +147,10 @@ func BuildDefaultResult[R FlagTypes](strategy EvaluationStrategy, defaultValue R } } -// Evaluate is a generic method used to resolve a flag from a single [NamedProvider] without losing type information. +// evaluate is a generic method used to resolve a flag from a single [namedProvider] without losing type information. // This method is exported for those writing their own custom [StrategyFn]. Since any is an allowed [FlagTypes] this can // be set to any type, but this should be done with care outside the specified primitive [FlagTypes] -func Evaluate[T FlagTypes](ctx context.Context, provider NamedProvider, flag string, defaultVal T, flatCtx of.FlattenedContext) of.GenericResolutionDetail[T] { +func evaluate[T of.FlagTypes](ctx context.Context, provider of.FeatureProvider, registeredName string, flag string, defaultVal T, flatCtx of.FlattenedContext) *of.GenericResolutionDetail[T] { var resolution of.GenericResolutionDetail[T] switch v := any(defaultVal).(type) { case bool: @@ -165,8 +179,8 @@ func Evaluate[T FlagTypes](ctx context.Context, provider NamedProvider, flag str resolution.FlagMetadata = make(of.FlagMetadata, 2) } - resolution.FlagMetadata[MetadataProviderName] = provider.Name() + resolution.FlagMetadata[MetadataProviderName] = registeredName resolution.FlagMetadata[MetadataProviderType] = provider.Metadata().Name - return resolution + return &resolution } diff --git a/openfeature/multi/strategies_test.go b/providers/multi/strategies_test.go similarity index 64% rename from openfeature/multi/strategies_test.go rename to providers/multi/strategies_test.go index 0b31c843..d19800c6 100644 --- a/openfeature/multi/strategies_test.go +++ b/providers/multi/strategies_test.go @@ -1,14 +1,14 @@ package multi import ( - of "github.com/open-feature/go-sdk/openfeature" + of "go.openfeature.dev/openfeature/v2" "go.uber.org/mock/gomock" ) -func createMockProviders(ctrl *gomock.Controller, count int) []*of.MockFeatureProvider { - providerMocks := make([]*of.MockFeatureProvider, 0, count) +func createMockProviders(ctrl *gomock.Controller, count int) []*of.MockProvider { + providerMocks := make([]*of.MockProvider, 0, count) for range count { - provider := of.NewMockFeatureProvider(ctrl) + provider := of.NewMockProvider(ctrl) providerMocks = append(providerMocks, provider) } diff --git a/providers/testing/README.md b/providers/testing/README.md new file mode 100644 index 00000000..51e76fa6 --- /dev/null +++ b/providers/testing/README.md @@ -0,0 +1,36 @@ +# Testing provider + +`TestProvider` is an OpenFeature compliant provider implementation designed for testing applications with feature flags. + +The testing provider allows you to define feature flag values scoped to individual tests, ensuring test isolation and preventing flag state from leaking between tests. It uses the `InMemoryProvider` internally with per-test flag storage. + +# Usage + +```go +import ( + "testing" + + "go.openfeature.dev/openfeature/v2" + "go.openfeature.dev/openfeature/v2/providers/inmemory" + "go.openfeature.dev/openfeature/v2/providers/testing" +) + +testProvider := testing.NewProvider() +err := openfeature.SetProviderAndWait(t.Context(), testProvider) +if err != nil { + t.Fatal(err) +} + +ctx := testProvider.UsingFlags(t, map[string]memprovider.InMemoryFlag{ + "my_feature": { + State: memprovider.Enabled, + DefaultVariant: "on", + Variants: map[string]any{"on": true}, + }, +}) + +client := openfeature.NewClient() +result := client.Boolean(ctx, "my_feature", false, openfeature.EvaluationContext{}) +``` + +The testing provider supports parallel test execution with proper isolation. diff --git a/openfeature/testing/testprovider.go b/providers/testing/test_provider.go similarity index 67% rename from openfeature/testing/testprovider.go rename to providers/testing/test_provider.go index 2b10b8d3..2b1513a0 100644 --- a/openfeature/testing/testprovider.go +++ b/providers/testing/test_provider.go @@ -6,15 +6,18 @@ import ( "fmt" "runtime" "sync" + "testing" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) -const testNameKey = "testName" +type contextKey string -// NewTestProvider creates a new `TestAwareProvider` -func NewTestProvider() TestProvider { +const testNameKey contextKey = "testName" + +// NewProvider creates a new `TestAwareProvider` +func NewProvider() TestProvider { return TestProvider{ providers: &sync.Map{}, } @@ -28,39 +31,55 @@ type TestProvider struct { providers *sync.Map } -type TestFramework = interface{ Name() string } +type TestFramework = interface { + Name() string + Context() context.Context +} // UsingFlags sets flags for the scope of a test. -func (tp TestProvider) UsingFlags(test TestFramework, flags map[string]memprovider.InMemoryFlag) { +func (tp TestProvider) UsingFlags(test TestFramework, flags map[string]memprovider.InMemoryFlag) context.Context { storeGoroutineLocal(test.Name()) - tp.providers.Store(test.Name(), memprovider.NewInMemoryProvider(flags)) + tp.providers.Store(test.Name(), memprovider.NewProvider(flags)) + ctx := test.Context() + if ctx == nil { + ctx = context.Background() + } + + // if test is testing.TB add the auto Cleanup + if t, ok := test.(testing.TB); ok { + t.Cleanup(func() { + tp.Cleanup(ctx) + }) + } + + return context.WithValue(ctx, testNameKey, test.Name()) } // Cleanup deletes the flags provider bound to the current test and should be executed after each test execution // e.g. using a defer statement. -func (tp TestProvider) Cleanup() { +func (tp TestProvider) Cleanup(context.Context) { tp.providers.Delete(getGoroutineLocal()) deleteGoroutineLocal() } func (tp TestProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, flatCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - return tp.getProvider().BooleanEvaluation(ctx, flag, defaultValue, flatCtx) + return tp.getProvider(ctx).BooleanEvaluation(ctx, flag, defaultValue, flatCtx) } func (tp TestProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, flatCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - return tp.getProvider().StringEvaluation(ctx, flag, defaultValue, flatCtx) + return tp.getProvider(ctx).StringEvaluation(ctx, flag, defaultValue, flatCtx) } func (tp TestProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, flatCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - return tp.getProvider().FloatEvaluation(ctx, flag, defaultValue, flatCtx) + return tp.getProvider(ctx).FloatEvaluation(ctx, flag, defaultValue, flatCtx) } func (tp TestProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, flatCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - return tp.getProvider().IntEvaluation(ctx, flag, defaultValue, flatCtx) + return tp.getProvider(ctx).IntEvaluation(ctx, flag, defaultValue, flatCtx) } -func (tp TestProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - return tp.getProvider().ObjectEvaluation(ctx, flag, defaultValue, flatCtx) +func (tp TestProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext) openfeature.ObjectResolutionDetail { + return tp.getProvider(ctx).ObjectEvaluation(ctx, flag, defaultValue, flatCtx) } func (tp TestProvider) Hooks() []openfeature.Hook { @@ -71,11 +90,14 @@ func (tp TestProvider) Metadata() openfeature.Metadata { return tp.NoopProvider.Metadata() } -func (tp TestProvider) getProvider() openfeature.FeatureProvider { +func (tp TestProvider) getProvider(ctx context.Context) openfeature.FeatureProvider { // Retrieve the test name from the goroutine-local storage. - testName, ok := getGoroutineLocal().(string) + testName, ok := ctx.Value(testNameKey).(string) if !ok { - panic("unable to detect test name; be sure to call `UsingFlags` in the scope of a test (in T.run)!") + testName, ok = getGoroutineLocal().(string) + if !ok { + panic("unable to detect test name; be sure to call `UsingFlags` in the scope of a test (in T.run)!") + } } // Load the feature provider corresponding to the test name. diff --git a/openfeature/testing/testprovider_test.go b/providers/testing/test_provider_test.go similarity index 64% rename from openfeature/testing/testprovider_test.go rename to providers/testing/test_provider_test.go index 31403e1a..9d1097c9 100644 --- a/openfeature/testing/testprovider_test.go +++ b/providers/testing/test_provider_test.go @@ -1,18 +1,22 @@ package testing import ( - "context" + "net/http" + "net/http/httptest" "testing" + "time" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/memprovider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.openfeature.dev/openfeature/v2" + memprovider "go.openfeature.dev/openfeature/v2/providers/inmemory" ) func TestParallelSingletonUsage(t *testing.T) { t.Parallel() - testProvider := NewTestProvider() - err := openfeature.SetProviderAndWait(testProvider) + testProvider := NewProvider() + err := openfeature.SetProviderAndWait(t.Context(), testProvider) if err != nil { t.Errorf("unable to set provider") } @@ -49,11 +53,11 @@ func TestParallelSingletonUsage(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { - defer testProvider.Cleanup() + defer testProvider.Cleanup(t.Context()) t.Parallel() testProvider.UsingFlags(t, tt.flags) - got := functionUnderTest() + got := functionUnderTest(t) if got != tt.want { t.Fatalf("uh oh, value is not as expected: got %v, want %v", got, tt.want) @@ -63,7 +67,7 @@ func TestParallelSingletonUsage(t *testing.T) { } func TestTestAwareProvider(t *testing.T) { - taw := NewTestProvider() + taw := NewProvider() flags := map[string]memprovider.InMemoryFlag{ "ff-bool": { @@ -151,13 +155,68 @@ func Test_TestAwareProviderPanics(t *testing.T) { } }() - taw := NewTestProvider() + taw := NewProvider() taw.BooleanEvaluation(t.Context(), "my-flag", true, openfeature.FlattenedContext{}) }) } -func functionUnderTest() bool { - got := openfeature.NewDefaultClient(). - Boolean(context.TODO(), "my_flag", false, openfeature.EvaluationContext{}) +func TestServeWithAnotherGoroutine(t *testing.T) { + testProvider := NewProvider() + ctx := testProvider.UsingFlags(t, map[string]memprovider.InMemoryFlag{ + "myflag": { + DefaultVariant: "defaultVariant", + Variants: map[string]any{"defaultVariant": true}, + }, + }) + t.Cleanup(func() { + testProvider.Cleanup(ctx) + }) + + err := openfeature.SetProviderAndWait(ctx, testProvider) + require.NoError(t, err) + + handlerDone := make(chan struct{}) + handler := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + _ = openfeature.NewClient().Boolean(ctx, "myflag", false, openfeature.EvaluationContextFromContext(ctx)) + + select { + case <-r.Context().Done(): + w.WriteHeader(http.StatusServiceUnavailable) + default: + w.WriteHeader(http.StatusOK) + } + close(handlerDone) + } + + req := httptest.NewRequestWithContext(ctx, http.MethodGet, "/drain", nil) + w := httptest.NewRecorder() + + // Start the handler in a goroutine for _reasons_ + // This is what triggers the TestProvider bug + go func() { + handler(w, req) + }() + + timedout := false + + // Wait for handler to complete + select { + case <-handlerDone: + assert.Equal(t, http.StatusOK, w.Code) + case <-time.After(time.Second): + t.Log("drain not completed within timeout") + timedout = true + } + + assert.Equal(t, http.StatusOK, w.Code) + require.False(t, timedout) +} + +func functionUnderTest(tb testing.TB) bool { + tb.Helper() + got := openfeature.NewClient(). + Boolean(tb.Context(), "my_flag", false, openfeature.EvaluationContext{}) return got } diff --git a/openfeature/reference.go b/reference.go similarity index 100% rename from openfeature/reference.go rename to reference.go diff --git a/openfeature/reference_test.go b/reference_test.go similarity index 100% rename from openfeature/reference_test.go rename to reference_test.go diff --git a/openfeature/resolution_error.go b/resolution_error.go similarity index 92% rename from openfeature/resolution_error.go rename to resolution_error.go index b9c71445..d1f84be9 100644 --- a/openfeature/resolution_error.go +++ b/resolution_error.go @@ -150,8 +150,8 @@ func (e *ProviderInitError) Error() string { } var ( - // ProviderNotReadyError signifies that an operation failed because the provider is in a NOT_READY state. - ProviderNotReadyError = NewProviderNotReadyResolutionError("provider not yet initialized") - // ProviderFatalError signifies that an operation failed because the provider is in a FATAL state. - ProviderFatalError = NewProviderFatalResolutionError("provider is in an irrecoverable error state") + // ErrProviderNotReady signifies that an operation failed because the provider is in a NOT_READY state. + ErrProviderNotReady = NewProviderNotReadyResolutionError("provider not yet initialized") + // ErrProviderFatal signifies that an operation failed because the provider is in a FATAL state. + ErrProviderFatal = NewProviderFatalResolutionError("provider is in an irrecoverable error state") ) diff --git a/openfeature/resolution_error_test.go b/resolution_error_test.go similarity index 100% rename from openfeature/resolution_error_test.go rename to resolution_error_test.go diff --git a/openfeature/util_test.go b/util_test.go similarity index 80% rename from openfeature/util_test.go rename to util_test.go index 2bc0540d..f25faebc 100644 --- a/openfeature/util_test.go +++ b/util_test.go @@ -1,6 +1,7 @@ package openfeature import ( + "context" "testing" "time" ) @@ -35,21 +36,22 @@ func init() { // stateHandlerForTests is a StateHandler with callbacks type stateHandlerForTests struct { - initF func(e EvaluationContext) error - shutdownF func() + initF func(context.Context) error + shutdownF func(context.Context) error } -func (s *stateHandlerForTests) Init(e EvaluationContext) error { +func (s *stateHandlerForTests) Init(ctx context.Context) error { if s.initF != nil { - return s.initF(e) + return s.initF(ctx) } return nil } -func (s *stateHandlerForTests) Shutdown() { +func (s *stateHandlerForTests) Shutdown(ctx context.Context) error { if s.shutdownF != nil { - s.shutdownF() + return s.shutdownF(ctx) } + return nil } // ProviderEventing is an eventing implementation with invoke capability