diff --git a/api/feature.go b/api/feature.go index 6a4dce4..c862659 100644 --- a/api/feature.go +++ b/api/feature.go @@ -9,7 +9,7 @@ import ( "github.com/Unleash/unleash-go-sdk/v5/internal/strategies" ) -type ParameterMap map[string]interface{} +type ParameterMap map[string]any type FeatureResponse struct { Response @@ -67,8 +67,8 @@ type Dependency struct { Enabled *bool `json:"enabled"` } -func (fr FeatureResponse) FeatureMap() map[string]interface{} { - features := map[string]interface{}{} +func (fr FeatureResponse) FeatureMap() map[string]any { + features := map[string]any{} for _, f := range fr.Features { features[f.Name] = f } diff --git a/api/variant.go b/api/variant.go index 7bfefd0..bf03aa4 100644 --- a/api/variant.go +++ b/api/variant.go @@ -1,5 +1,7 @@ package api +import "slices" + import "github.com/Unleash/unleash-go-sdk/v5/context" var DISABLED_VARIANT = &Variant{ @@ -81,12 +83,7 @@ func (o Override) matchValue(ctx *context.Context) bool { if len(o.Values) == 0 { return false } - for _, value := range o.Values { - if value == o.getIdentifier(ctx) { - return true - } - } - return false + return slices.Contains(o.Values, o.getIdentifier(ctx)) } // Get default variant if feature is not found or if the feature is disabled. diff --git a/benchmark_test.go b/benchmark_test.go index c597d56..56b7848 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -7,25 +7,66 @@ import ( "time" "github.com/Unleash/unleash-go-sdk/v5" + "github.com/Unleash/unleash-go-sdk/v5/api" ) +type mockStorage map[string]api.Feature + +func (m mockStorage) Get(in string) (any, bool) { + out, found := m[in] + return out, found +} + +func (m mockStorage) List() []any { + res := make([]any, 0, len(m)) + for _, feature := range m { + res = append(res, feature) + } + return res + +} + +func (m mockStorage) Init(backupPath string, appName string) {} +func (m mockStorage) Load() error { return nil } +func (m mockStorage) Persist() error { return nil } +func (m mockStorage) Reset(data map[string]any, persist bool) error { return nil } + +func ptr[T any](v T) *T { + return &v +} + func BenchmarkFeatureToggleEvaluation(b *testing.B) { - unleash.Initialize( + err := unleash.Initialize( unleash.WithListener(&unleash.NoopListener{}), unleash.WithAppName("go-benchmark"), unleash.WithUrl("https://app.unleash-hosted.com/demo/api/"), unleash.WithCustomHeaders(http.Header{"Authorization": {"Go-Benchmark:development.be6b5d318c8e77469efb58590022bb6416100261accf95a15046c04d"}}), + unleash.WithStorage(mockStorage{ + "foo": api.Feature{ + Name: "foo", + Enabled: true, + Dependencies: &[]api.Dependency{ + {Feature: "bar", Enabled: ptr(true)}, + }, + }, + "bar": api.Feature{ + Name: "bar", + Enabled: true, + }, + }), ) + if err != nil { + b.Fatal(err) + } - b.ResetTimer() startTime := time.Now() + b.ReportAllocs() for i := 0; i < b.N; i++ { - _ = unleash.IsEnabled("go-benchmark") + _ = unleash.IsEnabled("foo") } endTime := time.Now() - b.StopTimer() // Calculate ns/op (nanoseconds per operation) nsPerOp := float64(endTime.Sub(startTime).Nanoseconds()) / float64(b.N) diff --git a/bootstrap_storage.go b/bootstrap_storage.go index a326d24..aa2765d 100644 --- a/bootstrap_storage.go +++ b/bootstrap_storage.go @@ -38,7 +38,7 @@ func (bs *BootstrapStorage) Init(backupPath string, appName string) { } } -func (bs *BootstrapStorage) Reset(data map[string]interface{}, persist bool) error { +func (bs *BootstrapStorage) Reset(data map[string]any, persist bool) error { return bs.backingStore.Reset(data, persist) } @@ -46,10 +46,10 @@ func (bs *BootstrapStorage) Persist() error { return bs.backingStore.Persist() } -func (bs *BootstrapStorage) Get(key string) (interface{}, bool) { +func (bs *BootstrapStorage) Get(key string) (any, bool) { return bs.backingStore.Get(key) } -func (bs *BootstrapStorage) List() []interface{} { +func (bs *BootstrapStorage) List() []any { return bs.backingStore.List() } diff --git a/client.go b/client.go index c8d71b8..d8f90ae 100644 --- a/client.go +++ b/client.go @@ -2,6 +2,7 @@ package unleash import ( "fmt" + "slices" "net/http" "net/url" @@ -368,14 +369,14 @@ func (uc *Client) isEnabled(feature string, options ...FeatureOption) (api.Strat }, f } - allConstraints := make([]api.Constraint, 0) + allConstraints := make([]api.Constraint, 0, len(segmentConstraints)+len(s.Constraints)) allConstraints = append(allConstraints, segmentConstraints...) allConstraints = append(allConstraints, s.Constraints...) if ok, err := constraints.Check(ctx, allConstraints); err != nil { uc.errors <- err } else if ok && foundStrategy.IsEnabled(s.Parameters, ctx) { - if s.Variants != nil && len(s.Variants) > 0 { + if len(s.Variants) > 0 { groupIdValue := s.Parameters[strategy.ParamGroupId] groupId, ok := groupIdValue.(string) if !ok { @@ -423,7 +424,7 @@ func (uc *Client) isParentDependencySatisfied(feature *api.Feature, context cont // According to the schema, if the enabled property is absent we assume it's true. if parent.Enabled == nil || *parent.Enabled { if parent.Variants != nil && len(*parent.Variants) > 0 && enabledResult.Variant != nil { - return enabledResult.Enabled && contains(*parent.Variants, enabledResult.Variant.Name) + return enabledResult.Enabled && slices.Contains(*parent.Variants, enabledResult.Variant.Name) } return enabledResult.Enabled } @@ -431,8 +432,8 @@ func (uc *Client) isParentDependencySatisfied(feature *api.Feature, context cont return !enabledResult.Enabled } - allDependenciesSatisfied := every(*feature.Dependencies, func(parent interface{}) bool { - return dependenciesSatisfied(parent.(api.Dependency)) + allDependenciesSatisfied := every(*feature.Dependencies, func(parent api.Dependency) bool { + return dependenciesSatisfied(parent) }) return allDependenciesSatisfied diff --git a/client_test.go b/client_test.go index 0da683d..a2906a1 100644 --- a/client_test.go +++ b/client_test.go @@ -172,12 +172,12 @@ func TestClient_ListFeatures(t *testing.T) { Values: []string{"constraint-value-1", "constraint-value-2"}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "strategy-param-1": "strategy-value-1", }, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, }, @@ -302,12 +302,12 @@ func TestClientWithVariantContext(t *testing.T) { Values: []string{"custom-ctx"}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "strategy-param-1": "strategy-value-1", }, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, Variants: []api.VariantInternal{ @@ -397,11 +397,11 @@ func TestClient_WithSegment(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{}, + Parameters: map[string]any{}, Segments: []int{1}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, }, @@ -482,11 +482,11 @@ func TestClient_WithNonExistingSegment(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{}, + Parameters: map[string]any{}, Segments: []int{1}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, }, @@ -561,11 +561,11 @@ func TestClient_WithMultipleSegments(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{}, + Parameters: map[string]any{}, Segments: []int{1, 4, 6, 2}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, }, @@ -664,11 +664,11 @@ func TestClient_VariantShouldRespectConstraint(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{}, + Parameters: map[string]any{}, Segments: []int{1, 4, 6, 2}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, Variants: []api.VariantInternal{ @@ -783,11 +783,11 @@ func TestClient_VariantShouldFailWhenSegmentConstraintsDontMatch(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{}, + Parameters: map[string]any{}, Segments: []int{1, 4, 6, 2}, }, }, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "feature-param-1": "feature-value-1", }, Variants: []api.VariantInternal{ @@ -899,7 +899,7 @@ func TestClient_ShouldFavorStrategyVariantOverFeatureVariant(t *testing.T) { Id: 1, Name: "default", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "groupId": "strategyVariantName", }, Variants: []api.VariantInternal{ @@ -988,7 +988,7 @@ func TestClient_ShouldReturnOldVariantForNonMatchingStrategyVariant(t *testing.T Id: 1, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 0, "stickiness": "default", }, @@ -1010,7 +1010,7 @@ func TestClient_ShouldReturnOldVariantForNonMatchingStrategyVariant(t *testing.T Id: 2, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, diff --git a/config.go b/config.go index 79700d2..33f7fce 100644 --- a/config.go +++ b/config.go @@ -21,7 +21,7 @@ type configOption struct { disableMetrics bool backupPath string strategies []strategy.Strategy - listener interface{} + listener any storage Storage httpClient *http.Client customHeaders http.Header @@ -35,7 +35,7 @@ type ConfigOption func(*configOption) // the listener interfaces. If no listener is registered then the user is responsible // for draining the various channels on the client. Failure to do so will stop the client // from working as the worker routines will be blocked. -func WithListener(listener interface{}) ConfigOption { +func WithListener(listener any) ConfigOption { return func(o *configOption) { o.listener = listener } @@ -266,7 +266,7 @@ type repositoryOptions struct { backupPath string refreshInterval time.Duration storage Storage - httpClient *http.Client + httpClient *http.Client headers http.Header isStreaming bool } @@ -281,5 +281,4 @@ type metricsOptions struct { disableMetrics bool httpClient *http.Client headers http.Header - started *time.Time } diff --git a/example_custom_strategy_test.go b/example_custom_strategy_test.go index 6b5869d..a1d83f1 100644 --- a/example_custom_strategy_test.go +++ b/example_custom_strategy_test.go @@ -2,6 +2,7 @@ package unleash_test import ( "fmt" + "slices" "strings" "time" @@ -15,7 +16,7 @@ func (s ActiveForUserWithEmailStrategy) Name() string { return "ActiveForUserWithEmail" } -func (s ActiveForUserWithEmailStrategy) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s ActiveForUserWithEmailStrategy) IsEnabled(params map[string]any, ctx *context.Context) bool { if ctx == nil { return false @@ -30,13 +31,7 @@ func (s ActiveForUserWithEmailStrategy) IsEnabled(params map[string]interface{}, return false } - for _, e := range strings.Split(emails, ",") { - if e == ctx.Properties["emails"] { - return true - } - } - - return false + return slices.Contains(strings.Split(emails, ","), ctx.Properties["emails"]) } // ExampleCustomStrategy demonstrates using a custom strategy. diff --git a/impression_test.go b/impression_test.go index 5a5ba5e..930a94b 100644 --- a/impression_test.go +++ b/impression_test.go @@ -45,7 +45,7 @@ func TestImpression_Off(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -94,7 +94,7 @@ func TestImpression_IsEnabled(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -152,7 +152,7 @@ func TestImpression_GetVariant(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", "groupId": variant, @@ -227,7 +227,7 @@ func TestImpression_WithContext(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -307,7 +307,7 @@ func TestImpression_WithContextAndMultipleEvents(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -398,7 +398,7 @@ func TestImpression_GetChannelMethod(t *testing.T) { { Id: 1, Name: "flexibleRollout", - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, diff --git a/internal/constraints/operator_in.go b/internal/constraints/operator_in.go index 10c1135..1b2c720 100644 --- a/internal/constraints/operator_in.go +++ b/internal/constraints/operator_in.go @@ -3,6 +3,7 @@ package constraints import ( "github.com/Unleash/unleash-go-sdk/v5/api" "github.com/Unleash/unleash-go-sdk/v5/context" + "slices" ) func operatorNotIn(ctx *context.Context, constraint api.Constraint) bool { @@ -12,11 +13,5 @@ func operatorNotIn(ctx *context.Context, constraint api.Constraint) bool { func operatorIn(ctx *context.Context, constraint api.Constraint) bool { contextValue := ctx.Field(constraint.ContextName) - for _, constraint := range constraint.Values { - if contextValue == constraint { - return true - } - } - - return false + return slices.Contains(constraint.Values, contextValue) } diff --git a/internal/strategies/application_hostname.go b/internal/strategies/application_hostname.go index 6dd0c13..6ce2b87 100644 --- a/internal/strategies/application_hostname.go +++ b/internal/strategies/application_hostname.go @@ -22,7 +22,7 @@ func (s applicationHostnameStrategy) Name() string { return "applicationHostname" } -func (s applicationHostnameStrategy) IsEnabled(params map[string]interface{}, _ *context.Context) bool { +func (s applicationHostnameStrategy) IsEnabled(params map[string]any, _ *context.Context) bool { value, found := params[strategy.ParamHostNames] if !found { return false diff --git a/internal/strategies/application_hostname_test.go b/internal/strategies/application_hostname_test.go index 063c42e..e6d0586 100644 --- a/internal/strategies/application_hostname_test.go +++ b/internal/strategies/application_hostname_test.go @@ -30,7 +30,7 @@ func TestApplicationHostnameStrategy_IsEnabled(t *testing.T) { t.Run("h=os.hostname", func(t *testing.T) { hostname, _ := resolveHostname() - isEnabled := s.IsEnabled(map[string]interface{}{ + isEnabled := s.IsEnabled(map[string]any{ strategy.ParamHostNames: hostname, }, nil) @@ -39,7 +39,7 @@ func TestApplicationHostnameStrategy_IsEnabled(t *testing.T) { t.Run("h=list(os.hostname)", func(t *testing.T) { hostname, _ := resolveHostname() - isEnabled := s.IsEnabled(map[string]interface{}{ + isEnabled := s.IsEnabled(map[string]any{ strategy.ParamHostNames: "localhost," + hostname, }, nil) @@ -52,7 +52,7 @@ func TestApplicationHostnameStrategy_IsEnabled(t *testing.T) { // needed to re-read env-var s = NewApplicationHostnameStrategy() - isEnabled := s.IsEnabled(map[string]interface{}{ + isEnabled := s.IsEnabled(map[string]any{ strategy.ParamHostNames: "localhost,some-random-name", }, nil) @@ -65,7 +65,7 @@ func TestApplicationHostnameStrategy_IsEnabled(t *testing.T) { // needed to re-read env-var s = NewApplicationHostnameStrategy() - isEnabled := s.IsEnabled(map[string]interface{}{ + isEnabled := s.IsEnabled(map[string]any{ strategy.ParamHostNames: "localhost,some-random-name", }, nil) diff --git a/internal/strategies/default.go b/internal/strategies/default.go index fdedd04..38a2650 100644 --- a/internal/strategies/default.go +++ b/internal/strategies/default.go @@ -12,6 +12,6 @@ func (s defaultStrategy) Name() string { return "default" } -func (s defaultStrategy) IsEnabled(_ map[string]interface{}, _ *context.Context) bool { +func (s defaultStrategy) IsEnabled(_ map[string]any, _ *context.Context) bool { return true } diff --git a/internal/strategies/flexible_rollout.go b/internal/strategies/flexible_rollout.go index 7dc8caa..3ddfcb6 100644 --- a/internal/strategies/flexible_rollout.go +++ b/internal/strategies/flexible_rollout.go @@ -39,7 +39,7 @@ func (s flexibleRolloutStrategy) resolveStickiness(st stickiness, ctx context.Co } } -func (s flexibleRolloutStrategy) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s flexibleRolloutStrategy) IsEnabled(params map[string]any, ctx *context.Context) bool { groupID := "" if gID, found := params[strategy.ParamGroupId]; found { groupID = gID.(string) diff --git a/internal/strategies/flexible_rollout_test.go b/internal/strategies/flexible_rollout_test.go index 263c291..a4a0e35 100644 --- a/internal/strategies/flexible_rollout_test.go +++ b/internal/strategies/flexible_rollout_test.go @@ -4,6 +4,7 @@ package strategies import ( + "math" "testing" "github.com/Unleash/unleash-go-sdk/v5/context" @@ -29,7 +30,7 @@ func TestFlexibleRolloutStrategy_IsWellDistributed(t *testing.T) { } } - actualPercentage := round(100.0 * float64(enabledCount) / float64(rounds)) + actualPercentage := math.Round(100.0 * float64(enabledCount) / float64(rounds)) assert.InDelta(t, 50, actualPercentage, 1.0) } diff --git a/internal/strategies/gradual_rollout_random.go b/internal/strategies/gradual_rollout_random.go index 4855a7e..3aeaf9c 100644 --- a/internal/strategies/gradual_rollout_random.go +++ b/internal/strategies/gradual_rollout_random.go @@ -20,7 +20,7 @@ func (s gradualRolloutRandomStrategy) Name() string { return "gradualRolloutRandom" } -func (s gradualRolloutRandomStrategy) IsEnabled(params map[string]interface{}, _ *context.Context) bool { +func (s gradualRolloutRandomStrategy) IsEnabled(params map[string]any, _ *context.Context) bool { value, found := params[strategy.ParamPercentage] if !found { return false diff --git a/internal/strategies/gradual_rollout_random_test.go b/internal/strategies/gradual_rollout_random_test.go index a780ac8..47fc162 100644 --- a/internal/strategies/gradual_rollout_random_test.go +++ b/internal/strategies/gradual_rollout_random_test.go @@ -4,6 +4,7 @@ package strategies import ( + "math" "strconv" "testing" @@ -40,7 +41,7 @@ func TestGradualRolloutRandomStrategy_IsEnabled(t *testing.T) { } } - actualPercentage := round(100.0 * float64(enabledCount) / float64(rounds)) + actualPercentage := math.Round(100.0 * float64(enabledCount) / float64(rounds)) assert.InDelta(t, expectedPercentage, actualPercentage, 1.0) } diff --git a/internal/strategies/gradual_rollout_session_id.go b/internal/strategies/gradual_rollout_session_id.go index d3bfe14..7cb774e 100644 --- a/internal/strategies/gradual_rollout_session_id.go +++ b/internal/strategies/gradual_rollout_session_id.go @@ -17,7 +17,7 @@ func (s gradualRolloutSessionId) Name() string { return "gradualRolloutSessionId" } -func (s gradualRolloutSessionId) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s gradualRolloutSessionId) IsEnabled(params map[string]any, ctx *context.Context) bool { if ctx == nil || ctx.SessionId == "" { return false } diff --git a/internal/strategies/gradual_rollout_session_id_test.go b/internal/strategies/gradual_rollout_session_id_test.go index 0acbb75..b262d3f 100644 --- a/internal/strategies/gradual_rollout_session_id_test.go +++ b/internal/strategies/gradual_rollout_session_id_test.go @@ -4,6 +4,7 @@ package strategies import ( + "math" "strconv" "testing" @@ -95,7 +96,7 @@ func TestGradualRolloutSessionId_IsEnabled(t *testing.T) { } } - actualPercentage := round(100.0 * float64(enabledCount) / float64(rounds)) + actualPercentage := math.Round(100.0 * float64(enabledCount) / float64(rounds)) assert.InDelta(expectedPercentage, actualPercentage, 1.0) } diff --git a/internal/strategies/gradual_rollout_user_id.go b/internal/strategies/gradual_rollout_user_id.go index 8106027..93d5e74 100644 --- a/internal/strategies/gradual_rollout_user_id.go +++ b/internal/strategies/gradual_rollout_user_id.go @@ -17,7 +17,7 @@ func (s gradualRolloutUserId) Name() string { return "gradualRolloutUserId" } -func (s gradualRolloutUserId) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s gradualRolloutUserId) IsEnabled(params map[string]any, ctx *context.Context) bool { if ctx == nil || ctx.UserId == "" { return false } diff --git a/internal/strategies/gradual_rollout_user_id_test.go b/internal/strategies/gradual_rollout_user_id_test.go index 6ecde6c..9382b15 100644 --- a/internal/strategies/gradual_rollout_user_id_test.go +++ b/internal/strategies/gradual_rollout_user_id_test.go @@ -4,6 +4,7 @@ package strategies import ( + "math" "strconv" "testing" @@ -95,7 +96,7 @@ func TestGradualRolloutUserId_IsEnabled(t *testing.T) { } } - actualPercentage := round(100.0 * float64(enabledCount) / float64(rounds)) + actualPercentage := math.Round(100.0 * float64(enabledCount) / float64(rounds)) assert.InDelta(expectedPercentage, actualPercentage, 1.0) } diff --git a/internal/strategies/helpers.go b/internal/strategies/helpers.go index fa876c3..194c4ad 100644 --- a/internal/strategies/helpers.go +++ b/internal/strategies/helpers.go @@ -1,7 +1,7 @@ package strategies import ( - "math/rand" + "math/rand/v2" "os" "strconv" "sync" @@ -12,16 +12,6 @@ import ( var VariantNormalizationSeed uint32 = 86028157 -func round(f float64) int { - if f < -0.5 { - return int(f - 0.5) - } - if f > 0.5 { - return int(f + 0.5) - } - return 0 -} - func resolveHostname() (string, error) { var err error hostname := os.Getenv("HOSTNAME") @@ -34,7 +24,7 @@ func resolveHostname() (string, error) { return hostname, err } -func parameterAsFloat64(param interface{}) (result float64, ok bool) { +func parameterAsFloat64(param any) (result float64, ok bool) { if f, isFloat := param.(float64); isFloat { result, ok = f, true } else if i, isInt := param.(int); isInt { @@ -80,9 +70,8 @@ type rng struct { func (r *rng) int() int { r.Lock() - n := r.random.Intn(100) + 1 - r.Unlock() - return n + defer r.Unlock() + return r.random.IntN(100) + 1 } func (r *rng) float() float64 { @@ -91,14 +80,12 @@ func (r *rng) float() float64 { func (r *rng) string() string { r.Lock() - n := r.random.Intn(10000) + 1 - r.Unlock() - return strconv.Itoa(n) + defer r.Unlock() + return strconv.Itoa(r.random.IntN(10000) + 1) } // newRng creates a new random number generator and uses a mutex // internally to ensure safe concurrent reads. func newRng() *rng { - seed := time.Now().UnixNano() + int64(os.Getpid()) - return &rng{random: rand.New(rand.NewSource(seed))} + return &rng{random: rand.New(rand.NewPCG(uint64(time.Now().UnixNano()), uint64(os.Getpid())))} } diff --git a/internal/strategies/helpers_test.go b/internal/strategies/helpers_test.go index fe24e38..8f94251 100644 --- a/internal/strategies/helpers_test.go +++ b/internal/strategies/helpers_test.go @@ -27,7 +27,7 @@ func TestResolveHostname(t *testing.T) { } func TestParameterAsFloat64(t *testing.T) { - goodData := map[interface{}]float64{ + goodData := map[any]float64{ "30": 30.0, "-0.01": -0.01, 42: 42.0, @@ -42,7 +42,7 @@ func TestParameterAsFloat64(t *testing.T) { assert.InDelta(t, actual, expected, 0.0000001) } - badData := map[interface{}]float64{ + badData := map[any]float64{ "pizza": -1.0, "0.0.1": -1.0, } @@ -75,7 +75,7 @@ func TestNewRng(t *testing.T) { wg := sync.WaitGroup{} testGen := func(n int) { - for i := 0; i < n; i++ { + for range n { randomInt := rng.int() assert.True(t, randomInt >= 0 && randomInt <= 100) @@ -90,7 +90,7 @@ func TestNewRng(t *testing.T) { goRoutines := 20 wg.Add(goRoutines) - for j := 0; j < goRoutines; j++ { + for range goRoutines { go testGen(100) } wg.Wait() diff --git a/internal/strategies/remote_address_test.go b/internal/strategies/remote_address_test.go index 091edfd..c605969 100644 --- a/internal/strategies/remote_address_test.go +++ b/internal/strategies/remote_address_test.go @@ -18,7 +18,7 @@ func TestRemoteAddressStrategy_IsEnabled(t *testing.T) { assert := assert.New(t) t.Run("r=", func(t *testing.T) { - var params map[string]interface{} + var params map[string]any ctx := &context.Context{ RemoteAddress: "123", } @@ -26,7 +26,7 @@ func TestRemoteAddressStrategy_IsEnabled(t *testing.T) { }) t.Run("r=i", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamIps: "127.0.0.1", } ctx := &context.Context{ @@ -36,7 +36,7 @@ func TestRemoteAddressStrategy_IsEnabled(t *testing.T) { }) t.Run("r!=list(i)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamIps: "127.0.1.1, 127.0.1.2, 127.0.1.3", } ctx := &context.Context{ @@ -46,7 +46,7 @@ func TestRemoteAddressStrategy_IsEnabled(t *testing.T) { }) t.Run("r=list(i)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamIps: "127.0.0.1, 127.0.0.2,127.0.0.213", } ctx := &context.Context{ @@ -56,7 +56,7 @@ func TestRemoteAddressStrategy_IsEnabled(t *testing.T) { }) t.Run("r=range(i)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamIps: "127.0.1.1, 127.0.1.2,127.0.1.3, 160.33.0.0/16", } ctx := &context.Context{ diff --git a/internal/strategies/remote_addresss.go b/internal/strategies/remote_addresss.go index 3394da6..788b4b4 100644 --- a/internal/strategies/remote_addresss.go +++ b/internal/strategies/remote_addresss.go @@ -19,7 +19,7 @@ func (s remoteAddressStrategy) Name() string { return "remoteAddress" } -func (s remoteAddressStrategy) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s remoteAddressStrategy) IsEnabled(params map[string]any, ctx *context.Context) bool { value, found := params[strategy.ParamIps] if !found { return false diff --git a/internal/strategies/user_with_id.go b/internal/strategies/user_with_id.go index 7ccf0f7..4e52d38 100644 --- a/internal/strategies/user_with_id.go +++ b/internal/strategies/user_with_id.go @@ -17,7 +17,7 @@ func (s userWithIdStrategy) Name() string { return "userWithId" } -func (s userWithIdStrategy) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { +func (s userWithIdStrategy) IsEnabled(params map[string]any, ctx *context.Context) bool { value, found := params[strategy.ParamUserIds] if !found { return false diff --git a/internal/strategies/user_with_id_test.go b/internal/strategies/user_with_id_test.go index e8298e5..fd532bf 100644 --- a/internal/strategies/user_with_id_test.go +++ b/internal/strategies/user_with_id_test.go @@ -18,7 +18,7 @@ func TestUserWithIdStrategy_IsEnabled(t *testing.T) { assert := assert.New(t) t.Run("u=u", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamUserIds: "123", } ctx := &context.Context{ @@ -28,7 +28,7 @@ func TestUserWithIdStrategy_IsEnabled(t *testing.T) { }) t.Run("u=list(a, u)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamUserIds: "123, 122, 12312312", } ctx := &context.Context{ @@ -38,7 +38,7 @@ func TestUserWithIdStrategy_IsEnabled(t *testing.T) { }) t.Run("u!=list(a, b)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamUserIds: "123, 122, 122", } ctx := &context.Context{ @@ -48,7 +48,7 @@ func TestUserWithIdStrategy_IsEnabled(t *testing.T) { }) t.Run("u=list(a,u)", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamUserIds: "123,122,12312312", } ctx := &context.Context{ @@ -57,7 +57,7 @@ func TestUserWithIdStrategy_IsEnabled(t *testing.T) { assert.True(s.IsEnabled(params, ctx), "user-with-id-strategy should be enabled for userId in list") }) t.Run("u=empty", func(t *testing.T) { - params := map[string]interface{}{ + params := map[string]any{ strategy.ParamUserIds: "", } ctx := &context.Context{} diff --git a/metrics.go b/metrics.go index edce0a6..56abe80 100644 --- a/metrics.go +++ b/metrics.go @@ -249,7 +249,7 @@ func (m *metrics) sendMetrics() { } } -func (m *metrics) doPost(url *url.URL, payload interface{}) (*http.Response, error) { +func (m *metrics) doPost(url *url.URL, payload any) (*http.Response, error) { var body bytes.Buffer enc := json.NewEncoder(&body) if err := enc.Encode(payload); err != nil { @@ -313,7 +313,7 @@ func (m *metrics) countVariants(name string, enabled bool, variantName string) { m.bucketMu.Lock() defer m.bucketMu.Unlock() - t, _ := m.bucket.Toggles[name] + t := m.bucket.Toggles[name] if len(t.Variants) == 0 { t.Variants = make(map[string]int32) } diff --git a/metrics_test.go b/metrics_test.go index d45c94e..a0617f1 100644 --- a/metrics_test.go +++ b/metrics_test.go @@ -288,7 +288,7 @@ func TestMetrics_ShouldNotCountMetricsForParentToggles(t *testing.T) { Id: 1, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -304,7 +304,7 @@ func TestMetrics_ShouldNotCountMetricsForParentToggles(t *testing.T) { Id: 1, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -540,7 +540,7 @@ func TestMetrics_metricsData_includes_new_metadata(t *testing.T) { Id: 1, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, @@ -556,7 +556,7 @@ func TestMetrics_metricsData_includes_new_metadata(t *testing.T) { Id: 1, Name: "flexibleRollout", Constraints: []api.Constraint{}, - Parameters: map[string]interface{}{ + Parameters: map[string]any{ "rollout": 100, "stickiness": "default", }, diff --git a/repository.go b/repository.go index 6f00d5c..7e45b62 100644 --- a/repository.go +++ b/repository.go @@ -222,11 +222,14 @@ func (r *repository) fetch() error { } r.Lock() + defer r.Unlock() r.etag = resp.Header.Get("Etag") r.segments = featureResp.SegmentsMap() - r.options.storage.Reset(featureResp.FeatureMap(), true) + if err := r.options.storage.Reset(featureResp.FeatureMap(), true); err != nil { + return fmt.Errorf("resetting storage: %w", err) + } r.successfulFetch() - r.Unlock() + return nil } diff --git a/storage.go b/storage.go index 675eee6..6eab760 100644 --- a/storage.go +++ b/storage.go @@ -20,7 +20,7 @@ type Storage interface { // Reset is called after the repository has fetched the feature toggles from the server. // If persist is true the implementation of this function should call Persist(). The data // passed in here should be owned by the implementer of this interface. - Reset(data map[string]interface{}, persist bool) error + Reset(data map[string]any, persist bool) error // Load is called to load the data from persistent storage and hold it in memory for fast // querying. @@ -30,27 +30,27 @@ type Storage interface { Persist() error // Get returns the data for the specified feature toggle. - Get(string) (interface{}, bool) + Get(string) (any, bool) // List returns a list of all feature toggles. - List() []interface{} + List() []any } // DefaultStorage is a default Storage implementation. type DefaultStorage struct { appName string path string - data map[string]interface{} + data map[string]any } func (ds *DefaultStorage) Init(backupPath, appName string) { ds.appName = appName ds.path = filepath.Join(backupPath, fmt.Sprintf("unleash-repo-schema-v1-%s.json", appName)) - ds.data = map[string]interface{}{} + ds.data = map[string]any{} ds.Load() } -func (ds *DefaultStorage) Reset(data map[string]interface{}, persist bool) error { +func (ds *DefaultStorage) Reset(data map[string]any, persist bool) error { ds.data = data if persist { return ds.Persist() @@ -88,13 +88,13 @@ func (ds *DefaultStorage) Persist() error { return nil } -func (ds DefaultStorage) Get(key string) (interface{}, bool) { +func (ds DefaultStorage) Get(key string) (any, bool) { val, ok := ds.data[key] return val, ok } -func (ds *DefaultStorage) List() []interface{} { - var features []interface{} +func (ds *DefaultStorage) List() []any { + var features []any for _, val := range ds.data { features = append(features, val) } diff --git a/strategy/strategy.go b/strategy/strategy.go index ec8d630..abd53b8 100644 --- a/strategy/strategy.go +++ b/strategy/strategy.go @@ -33,5 +33,5 @@ type Strategy interface { // IsEnabled should look at the map of parameters and optionally // the supplied context and return true if the feature should be // enabled. - IsEnabled(map[string]interface{}, *context.Context) bool + IsEnabled(map[string]any, *context.Context) bool } diff --git a/unleash_mock.go b/unleash_mock.go index ae995d5..e891aba 100644 --- a/unleash_mock.go +++ b/unleash_mock.go @@ -49,7 +49,7 @@ func (l *MockedListener) OnImpression(event ImpressionEvent) { l.Called(event) } -func writeJSON(rw http.ResponseWriter, x interface{}) { +func writeJSON(rw http.ResponseWriter, x any) { enc := json.NewEncoder(rw) if err := enc.Encode(x); err != nil { panic(err) diff --git a/utils.go b/utils.go index 6488040..f281b72 100644 --- a/utils.go +++ b/utils.go @@ -3,12 +3,10 @@ package unleash import ( cryptoRand "crypto/rand" "fmt" - "math/rand" + "math/rand/v2" "os" "os/user" - "reflect" "sync" - "time" ) func getTmpDirPath() string { @@ -21,8 +19,7 @@ func generateInstanceId() string { if user, err := user.Current(); err == nil && user.Username != "" { prefix = user.Username } else { - rand.Seed(time.Now().Unix()) - prefix = fmt.Sprintf("generated-%d-%d", rand.Intn(1000000), os.Getpid()) + prefix = fmt.Sprintf("generated-%d-%d", rand.N(1000000), os.Getpid()) } if hostname, err := os.Hostname(); err == nil && hostname != "" { @@ -58,15 +55,6 @@ func getFetchURLPath(projectName string) string { return "./client/features" } -func contains(arr []string, str string) bool { - for _, item := range arr { - if item == str { - return true - } - } - return false -} - // WarnOnce is a type for handling warnings that should only be displayed once. type WarnOnce struct { once sync.Once @@ -79,20 +67,13 @@ func (wo *WarnOnce) Warn(message string) { }) } -func every(slice interface{}, condition func(interface{}) bool) bool { - sliceValue := reflect.ValueOf(slice) - - if sliceValue.Kind() != reflect.Slice { - fmt.Println("Input is not a slice returning false") +// every returns true iff condition returns true for all elements in the input slice. +// This function will return false for empty slices (unlike the convention used in mathematical logic). +func every[T any](slice []T, condition func(T) bool) bool { + if len(slice) == 0 { return false } - - if sliceValue.Len() == 0 { - return false - } - - for i := 0; i < sliceValue.Len(); i++ { - element := sliceValue.Index(i).Interface() + for _, element := range slice { if !condition(element) { return false } diff --git a/utils_test.go b/utils_test.go index c66ba5b..80a7bcb 100644 --- a/utils_test.go +++ b/utils_test.go @@ -1,6 +1,7 @@ package unleash import ( + "slices" "testing" "github.com/stretchr/testify/assert" @@ -19,13 +20,8 @@ func TestGetFetchURLPath(t *testing.T) { func TestEvery(t *testing.T) { t.Run("All Even Integers", func(t *testing.T) { numbers := []int{2, 4, 6, 8, 10} - allEven := every(numbers, func(item interface{}) bool { - num, ok := item.(int) - if !ok { - t.Errorf("Expected an integer, got %T", item) - return false - } - return num%2 == 0 + allEven := every(numbers, func(item int) bool { + return item%2 == 0 }) if !allEven { t.Errorf("Expected all numbers to be even, but got false") @@ -34,13 +30,8 @@ func TestEvery(t *testing.T) { t.Run("All Long Strings", func(t *testing.T) { words := []string{"apple", "banana", "cherry"} - allLong := every(words, func(item interface{}) bool { - str, ok := item.(string) - if !ok { - t.Errorf("Expected a string, got %T", item) - return false - } - return len(str) > 3 + allLong := every(words, func(item string) bool { + return len(item) > 3 }) if !allLong { t.Errorf("Expected all words to be long, but got false") @@ -49,7 +40,7 @@ func TestEvery(t *testing.T) { t.Run("Empty Slice", func(t *testing.T) { emptySlice := []int{} - allEmpty := every(emptySlice, func(item interface{}) bool { + allEmpty := every(emptySlice, func(item int) bool { // This condition should not be reached for an empty slice. t.Errorf("Unexpected condition reached") return false @@ -60,27 +51,10 @@ func TestEvery(t *testing.T) { } }) - t.Run("invalid inout", func(t *testing.T) { - invalidInput := "string" - result := every(invalidInput, func(item interface{}) bool { - // This condition should not be reached for an empty slice. - return true - }) - - if result == true { - t.Errorf("Expected result to be false") - } - }) - t.Run("Result should be false if one doesn't match the predicate", func(t *testing.T) { words := []string{"apple", "banana", "cherry", "he"} - allLong := every(words, func(item interface{}) bool { - str, ok := item.(string) - if !ok { - t.Errorf("Expected a string, got %T", item) - return false - } - return len(str) > 3 + allLong := every(words, func(item string) bool { + return len(item) > 3 }) if allLong == true { t.Errorf("Expected all words to be long, but got false") @@ -92,7 +66,7 @@ func TestContains(t *testing.T) { t.Run("Element is present in the slice", func(t *testing.T) { arr := []string{"apple", "banana", "cherry", "date", "fig"} str := "banana" - result := contains(arr, str) + result := slices.Contains(arr, str) if !result { t.Errorf("Expected '%s' to be in the slice, but it was not found", str) } @@ -101,7 +75,7 @@ func TestContains(t *testing.T) { t.Run("Element is not present in the slice", func(t *testing.T) { arr := []string{"apple", "banana", "cherry", "date", "fig"} str := "grape" - result := contains(arr, str) + result := slices.Contains(arr, str) if result { t.Errorf("Expected '%s' not to be in the slice, but it was found", str) } @@ -110,7 +84,7 @@ func TestContains(t *testing.T) { t.Run("Empty slice should return false", func(t *testing.T) { arr := []string{} str := "apple" - result := contains(arr, str) + result := slices.Contains(arr, str) if result { t.Errorf("Expected an empty slice to return false, but it returned true") } @@ -118,7 +92,7 @@ func TestContains(t *testing.T) { } func TestGetConnectionId(t *testing.T) { - for i := 0; i < 100; i++ { + for range 100 { uuid := getConnectionId() t.Run("Correct length", func(t *testing.T) {