From 9234c087cdbc5693c5c2afb602af30c35d52bbfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Justin=20Nu=C3=9F?= Date: Mon, 12 Aug 2024 20:32:51 +0200 Subject: [PATCH] WIP3 --- .github/workflows/golangci-lint.yml | 2 +- .github/workflows/govulncheck.yml | 2 +- .github/workflows/test.yml | 4 +- doc.go | 2 +- feature.go | 276 ++++++++++------- feature_test.go | 451 +++++++++++++++++----------- go.mod | 4 +- go.sum | 2 + sorted_map.go | 36 +++ 9 files changed, 496 insertions(+), 283 deletions(-) create mode 100644 sorted_map.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index c682af9..6f9cb17 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -4,7 +4,7 @@ jobs: lint: strategy: matrix: - go-version: [1.22.x] + go-version: [1.23.x] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index f34a97e..734a530 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -4,7 +4,7 @@ jobs: govulncheck: strategy: matrix: - go-version: [1.22.x] + go-version: [1.23.x] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cdcff80..c52a041 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,9 +4,11 @@ jobs: test: strategy: matrix: - go-version: [1.22.x] + go-version: [1.23.x] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} + env: + GOTOOLCHAIN: local steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 diff --git a/doc.go b/doc.go index c23adbc..404d52d 100644 --- a/doc.go +++ b/doc.go @@ -1,2 +1,2 @@ -// Package feature implements a simple abstraction for feature flags with arbitrary values. +// Package feature implements a simple abstraction for feature flags with dynamic values. package feature diff --git a/feature.go b/feature.go index 9ad36a0..e354728 100644 --- a/feature.go +++ b/feature.go @@ -3,18 +3,15 @@ package feature import ( "context" "errors" - "maps" - "reflect" - "slices" + "fmt" "sync" "sync/atomic" - "unsafe" ) // ErrDuplicateFlag is returned by [Register] if a with the given name is already registered. var ErrDuplicateFlag = errors.New("duplicate flag") -// Flag represents a flag registered with a [Set]. +// Flag represents a flag registered with a [FlagSet]. type Flag struct { // Name is the name of the feature as passed to [Register]. Name string @@ -22,59 +19,44 @@ type Flag struct { // Description is an optional description specified using [WithDescription]. Description string - // Description contains additional labels specified via [WithLabels]. - // - // The map must not be modified. - Labels map[string]string + // Labels contains the labels specified via [WithLabels]. + Labels Labels - // Type is the type returned by the flags callback returned by [Register]. - Type reflect.Type - - // Valuer reflects over the [Valuer] returned by [Register] for this flag. - Valuer reflect.Value -} - -type flagsMap struct { - m map[string]Flag - keys []string + // Func is callback that returns the value for the flag and is either a [BoolFunc], [IntFunc] or [StringFunc]. + Func any } -func (fm flagsMap) add(f Flag) flagsMap { - if _, ok := fm.m[f.Name]; ok { - panic(ErrDuplicateFlag) - } - - m := maps.Clone(fm.m) - if m == nil { - m = make(map[string]Flag, 1) - } - m[f.Name] = f - - keys := make([]string, len(fm.keys)+1) - copy(keys, fm.keys) - keys[len(fm.keys)] = f.Name - slices.Sort(keys) +// FlagSet represents a set of defined feature flags. +// +// The zero value is valid and returns zero values for all flags. +type FlagSet struct { + registry atomic.Pointer[Registry] - return flagsMap{m: m, keys: keys} + flagsMu sync.Mutex + flags sortedMap[Flag] } -// Set defines a scope for registering flags. -type Set struct { - loader atomic.Pointer[Loader] - - flagsMu sync.Mutex - flags flagsMap +// Labels is a read only map collection of labels associated with a feature flag. +type Labels struct { + m sortedMap[string] } -var globalSet Set +// All yields all labels. +func (l *Labels) All(yield func(string, string) bool) { + for _, key := range l.m.keys { + if !yield(key, l.m.m[key]) { + return + } + } +} -// Global returns a global Set. -func Global() *Set { - return &globalSet +// Len returns the number of labels. +func (l *Labels) Len() int { + return len(l.m.keys) } // All yields all registered flags sorted by name. -func (s *Set) All(yield func(Flag) bool) { +func (s *FlagSet) All(yield func(Flag) bool) { s.flagsMu.Lock() flags := s.flags s.flagsMu.Unlock() @@ -86,47 +68,108 @@ func (s *Set) All(yield func(Flag) bool) { } } -// Register registers a new [Flag] on the given [Set] and returns a [Valuer] for the flag. +// Lookup returns the flag with the given name. +func (s *FlagSet) Lookup(name string) (Flag, bool) { + s.flagsMu.Lock() + defer s.flagsMu.Unlock() + + f, ok := s.flags.m[name] + return f, ok +} + +// SetRegistry sets the Registry to be used for looking up flag values. // -// If a [Flag] with the same name is already registered, Register will panic with an error that is [ErrDuplicateFlag]. -func Register[T any](set *Set, name string, opts ...Option) Valuer[T] { - typ := reflect.TypeFor[T]() +// A nil value will cause all flags to return zero values. +func (s *FlagSet) SetRegistry(r Registry) { + if r == nil { + s.registry.Store(nil) + } else { + s.registry.Store(&r) + } +} - v := newValuer[T](set, name, typ) +func (s *FlagSet) add(name string, func_ any, opts ...Option) { + f := Flag{Name: name, Func: func_} + for _, opt := range opts { + opt(&f) + } + + s.flagsMu.Lock() + defer s.flagsMu.Unlock() - set.addFlag(name, typ, reflect.ValueOf(v), opts...) + if _, ok := s.flags.m[f.Name]; ok { + panic(fmt.Errorf("%w: %s", ErrDuplicateFlag, f.Name)) + } - return v + s.flags = s.flags.add(f.Name, f) } -func newFlag(name string, typ reflect.Type, v reflect.Value, opts []Option) Flag { - flag := Flag{Name: name, Type: typ, Valuer: v} +// Bool registers a new flag that represents a boolean value. +// +// If a [Flag] with the same name is already registered, the call will panic with an error that is [ErrDuplicateFlag]. +func (s *FlagSet) Bool(name string, opts ...Option) func(context.Context) bool { + f := func(ctx context.Context) bool { + r := s.registry.Load() + if r == nil { + return false + } + return (*r).Bool(ctx, name) + } + + s.add(name, f, opts...) - for _, opt := range opts { - opt(&flag) + return f +} + +// Float registers a new flag that represents a float value. +// +// If a [Flag] with the same name is already registered, the call will panic with an error that is [ErrDuplicateFlag]. +func (s *FlagSet) Float(name string, opts ...Option) func(context.Context) float64 { + f := func(ctx context.Context) float64 { + r := s.registry.Load() + if r == nil { + return 0.0 + } + return (*r).Float(ctx, name) } - return flag + s.add(name, f, opts...) + + return f } -func (s *Set) addFlag(name string, typ reflect.Type, v reflect.Value, opts ...Option) { - flag := newFlag(name, typ, v, opts) +// Int registers a new flag that represents an int value. +// +// If a [Flag] with the same name is already registered, the call will panic with an error that is [ErrDuplicateFlag]. +func (s *FlagSet) Int(name string, opts ...Option) func(context.Context) int { + f := func(ctx context.Context) int { + r := s.registry.Load() + if r == nil { + return 0 + } + return (*r).Int(ctx, name) + } - s.flagsMu.Lock() - defer s.flagsMu.Unlock() + s.add(name, f, opts...) - s.flags = s.flags.add(flag) + return f } -// SetProvider sets the loader used for the set. +// String registers a new flag that represents a string value. // -// A nil value will cause all flags to return zero values. -func (s *Set) SetProvider(r Loader) { - if r == nil { - s.loader.Store(nil) - } else { - s.loader.Store(&r) +// If a [Flag] with the same name is already registered, the call will panic with an error that is [ErrDuplicateFlag]. +func (s *FlagSet) String(name string, opts ...Option) func(context.Context) string { + f := func(ctx context.Context) string { + r := s.registry.Load() + if r == nil { + return "" + } + return (*r).String(ctx, name) } + + s.add(name, f, opts...) + + return f } // Option defines options for new flags which can be passed to [Register]. @@ -141,59 +184,76 @@ func WithDescription(desc string) Option { } } +// WithLabel adds a label to a flag. +func WithLabel(key, value string) Option { + return func(f *Flag) { + f.Labels.m = f.Labels.m.add(key, value) + } +} + // WithLabels adds labels to a flag. // // If used multiple times, the maps will be merged with later values replacing prior ones. func WithLabels(labels map[string]string) Option { return func(f *Flag) { - if f.Labels == nil { - f.Labels = maps.Clone(labels) - } else { - maps.Copy(f.Labels, labels) - } + f.Labels.m = f.Labels.m.addMany(labels) } } -// Loader defines methods used for loading flag values. -type Loader interface { - // Load gets the current value for the flag with the given name and stores it in dst. - // - // The value of dst will be a pointer to a value of the given type, e.g. a *int. - // - // The pointer must not be accessed after Load returns. - Load(ctx context.Context, dst any, name string, typ reflect.Type) -} +// Registry defines method for getting the feature flag values by name. +// +// Calling a method when the corresponding struct field is not set will cause the call to panic. +// +// This interface can not be implemented by other packages other except by embedding an existing implementation. +type Registry interface { + // Bool returns the boolean value for the flag with the given name. + Bool(ctx context.Context, name string) bool + + // Float returns the float value for the flag with the given name. + Float(ctx context.Context, name string) float64 -// LoaderFunc implements a [Loader] using a function that matches the signature of [Loader.Load]. -type LoaderFunc func(ctx context.Context, dst any, name string, typ reflect.Type) + // Int returns the integer value for the flag with the given name. + Int(ctx context.Context, name string) int -// Load returns the result of calling p with the given arguments. -func (p LoaderFunc) Load(ctx context.Context, dst any, name string, typ reflect.Type) { - p(ctx, dst, name, typ) + // String returns the string value for the flag with the given name. + String(ctx context.Context, name string) string + + registry() } -// Valuer is the type for functions returned by [Register]. -// -// Calling a Valuer will call the registered [Loader] for its flag and return the resolved value. -// -// If no loader is configured, a zero T will be returned. -type Valuer[T any] func(context.Context) T +// SimpleRegistry implements a [Registry] using callbacks set as struct fields. +type SimpleRegistry struct { + // BoolFunc contains the implementation for the Registry.Bool function. + BoolFunc func(ctx context.Context, name string) bool -func newValuer[T any](set *Set, name string, typ reflect.Type) Valuer[T] { - return func(ctx context.Context) (val T) { - r := set.loader.Load() - if r == nil { - return - } + // FloatFunc contains the implementation for the Registry.Float function. + FloatFunc func(ctx context.Context, name string) float64 - ptr := noescape(unsafe.Pointer(&val)) + // IntFunc contains the implementation for the Registry.Int function. + IntFunc func(ctx context.Context, name string) int - (*r).Load(ctx, (*T)(ptr), name, typ) - return - } + // StringFunc contains the implementation for the Registry.String function. + StringFunc func(ctx context.Context, name string) string } -func noescape(p unsafe.Pointer) unsafe.Pointer { - x := uintptr(p) - return unsafe.Pointer(x ^ 0) +// Bool implements the [Registry] interface by calling s.BoolFunc and returning the result. +func (s *SimpleRegistry) Bool(ctx context.Context, name string) bool { + return s.BoolFunc(ctx, name) } + +// Float implements the [Registry] interface by calling s.FloatFunc and returning the result. +func (s *SimpleRegistry) Float(ctx context.Context, name string) float64 { + return s.FloatFunc(ctx, name) +} + +// Int implements the [Registry] interface by calling s.IntFunc and returning the result. +func (s *SimpleRegistry) Int(ctx context.Context, name string) int { + return s.IntFunc(ctx, name) +} + +// String implements the [Registry] interface by calling s.StringFunc and returning the result. +func (s *SimpleRegistry) String(ctx context.Context, name string) string { + return s.StringFunc(ctx, name) +} + +func (s *SimpleRegistry) registry() {} diff --git a/feature_test.go b/feature_test.go index 1ca3ded..7f4a7b5 100644 --- a/feature_test.go +++ b/feature_test.go @@ -3,254 +3,365 @@ package feature_test import ( "context" "errors" - "fmt" - "reflect" - "strconv" + "maps" + "slices" "testing" "github.com/nussjustin/feature" + + "github.com/google/go-cmp/cmp" ) -func TestAll(t *testing.T) { - var set feature.Set - set.SetProvider(feature.LoaderFunc(func(_ context.Context, dst any, name string, typ reflect.Type) { - switch v := dst.(type) { - case *bool: - *v = true - return - case *int: - *v = 5 - return - case *string: - *v = "test" - return - } - panic(fmt.Sprintf("invalid type %s", typ.Name())) - })) - - type flag struct { - name, description string - typ string - labels map[string]string - provided any - } +var testRegistry = &feature.SimpleRegistry{ + BoolFunc: func(context.Context, string) bool { + return true + }, + FloatFunc: func(context.Context, string) float64 { + return 2.5 + }, + IntFunc: func(context.Context, string) int { + return 1 + }, + StringFunc: func(context.Context, string) string { + return "string" + }, +} - assertFlags := func(want ...flag) { - var got []feature.Flag +func TestFlagSet_All(t *testing.T) { + var set feature.FlagSet - set.All(func(f feature.Flag) bool { - got = append(got, f) - return true - }) + set.Int("int", + feature.WithDescription("int value"), + feature.WithLabel("type", "int")) - assertEqualsf(t, len(want), len(got), "number of flags does not match") + set.Bool("bool", + feature.WithDescription("bool value"), + feature.WithLabel("type", "bool")) - callArgs := []reflect.Value{reflect.ValueOf(testCtx)} + set.String("string", + feature.WithDescription("string value"), + feature.WithLabel("type", "string")) - for i := range got { - provided := got[i].Valuer.Call(callArgs)[0].Interface() + want := make([]feature.Flag, 3) + want[0], _ = set.Lookup("bool") + want[1], _ = set.Lookup("int") + want[2], _ = set.Lookup("string") - assertEqualsf(t, want[i].name, got[i].Name, "names do not match") - assertEqualsf(t, want[i].description, got[i].Description, "descriptions do not match") + assertEquals(t, want, slices.Collect(set.All), "") +} - assertEqualsf(t, len(want[i].labels), len(got[i].Labels), "number of sortedLabels do not match") +func TestFlagSet_Lookup(t *testing.T) { + var set feature.FlagSet - for wantKey, wantVal := range want[i].labels { - gotVal, ok := got[i].Labels[wantKey] + set.Bool("flagA") + set.Bool("flagB", feature.WithDescription("description")) - assertEqualsf(t, true, ok, "key %q not found", wantKey) - assertEqualsf(t, wantVal, gotVal, "value for %q does not match", wantKey) - } + flagA, okA := set.Lookup("flagA") + assertEquals(t, "flagA", flagA.Name, "flagA name mismatch") + assertEquals(t, "", flagA.Description, "flagA description mismatch") + assertEquals(t, true, okA, "flagA not marked as ok") - assertEqualsf(t, want[i].typ, got[i].Type.Name(), "types does not match") - assertEqualsf(t, want[i].provided, provided, "provided value does not match") - } - } + flagB, okB := set.Lookup("flagB") + assertEquals(t, "flagB", flagB.Name, "flagB name mismatch") + assertEquals(t, "description", flagB.Description, "flagB name mismatch") + assertEquals(t, true, okB, "flagB not marked as ok") - assertFlags() - - feature.Register[bool](&set, "TestAll/B") - - assertFlags(flag{name: "TestAll/B", typ: "bool", provided: true}) - - feature.Register[int](&set, "TestAll/A", - feature.WithDescription("feature a")) - - feature.Register[string](&set, "TestAll/C", - feature.WithDescription("feature c"), - feature.WithLabels(map[string]string{"foo": "bar", "name": "c"}), - feature.WithLabels(map[string]string{"foo": "baz", "type": "string"})) - - assertFlags( - flag{ - name: "TestAll/A", - description: "feature a", - typ: "int", - provided: 5, - }, - flag{ - name: "TestAll/B", - provided: true, - typ: "bool", - }, - flag{ - name: "TestAll/C", - description: "feature c", - labels: map[string]string{"foo": "baz", "name": "c", "type": "string"}, - provided: "test", - typ: "string", - }, - ) + flagC, okC := set.Lookup("flagC") + assertEquals(t, "", flagC.Name, "flagC name mismatch") + assertEquals(t, "", flagC.Description, "flagC name mismatch") + assertEquals(t, false, okC, "flagC marked as ok") } -func TestRegisterOn(t *testing.T) { +func TestFlagSet_Bool(t *testing.T) { t.Run("Duplicate", func(t *testing.T) { - var set feature.Set - register(t, &set) + var set feature.FlagSet + set.String("test") + assertPanic(t, feature.ErrDuplicateFlag, func() { - register(t, &set) + set.Bool("test") }) }) - t.Run("New", func(t *testing.T) { - var set feature.Set - set.SetProvider(feature.LoaderFunc(func(_ context.Context, dst any, name string, typ reflect.Type) { - assertEqualsf(t, t.Name(), name, "names do not match") - assertEqualsf(t, "string", typ.Name(), "types do not match") - })) - assertEquals(t, "", register(t, &set)(testCtx)) + t.Run("Register", func(t *testing.T) { + ctx := context.Background() + + var set feature.FlagSet + v := set.Bool("test") + v2 := mustLookup(t, &set, "test").Func.(func(context.Context) bool) + + assertEquals(t, false, v(ctx), "") + assertEquals(t, false, v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{BoolFunc: func(context.Context, string) bool { + return true + }}) + + assertEquals(t, true, v(ctx), "") + assertEquals(t, true, v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{BoolFunc: func(context.Context, string) bool { + return false + }}) + + assertEquals(t, false, v(ctx), "") + assertEquals(t, false, v2(ctx), "") }) } -func TestSetResolver(t *testing.T) { - t.Run("Dynamic", func(t *testing.T) { - var calls int +func TestFlagSet_Float(t *testing.T) { + t.Run("Duplicate", func(t *testing.T) { + var set feature.FlagSet + set.Bool("test") + + assertPanic(t, feature.ErrDuplicateFlag, func() { + set.Float("test") + }) + }) - var set feature.Set - set.SetProvider(feature.LoaderFunc(func(_ context.Context, dst any, _ string, typ reflect.Type) { - calls++ + t.Run("Register", func(t *testing.T) { + ctx := context.Background() - v := dst.(*string) - *v = strconv.Itoa(calls) - })) + var set feature.FlagSet + v := set.Float("test") + v2 := mustLookup(t, &set, "test").Func.(func(context.Context) float64) - p := register(t, &set) - assertEquals(t, "1", p(testCtx)) - assertEquals(t, "2", p(testCtx)) - assertEquals(t, "3", p(testCtx)) - }) + assertEquals(t, 0.0, v(ctx), "") + assertEquals(t, 0.0, v2(ctx), "") - t.Run("Default", func(t *testing.T) { - var set feature.Set - assertEquals(t, "", register(t, &set)(testCtx)) + set.SetRegistry(&feature.SimpleRegistry{FloatFunc: func(context.Context, string) float64 { + return 1.0 + }}) + + assertEquals(t, 1.0, v(ctx), "") + assertEquals(t, 1.0, v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{FloatFunc: func(context.Context, string) float64 { + return 2.0 + }}) + + assertEquals(t, 2.0, v(ctx), "") + assertEquals(t, 2.0, v2(ctx), "") }) +} + +func TestFlagSet_Int(t *testing.T) { + t.Run("Duplicate", func(t *testing.T) { + var set feature.FlagSet + set.Float("test") - t.Run("Nil", func(t *testing.T) { - var set feature.Set - set.SetProvider(nil) - assertEquals(t, "", register(t, &set)(testCtx)) + assertPanic(t, feature.ErrDuplicateFlag, func() { + set.Int("test") + }) }) - t.Run("Panic", func(t *testing.T) { - testErr := errors.New("test") + t.Run("Register", func(t *testing.T) { + ctx := context.Background() - var set feature.Set - set.SetProvider(feature.LoaderFunc(func(context.Context, any, string, reflect.Type) { - panic(testErr) - })) + var set feature.FlagSet + v := set.Int("test") + v2 := mustLookup(t, &set, "test").Func.(func(context.Context) int) - assertPanic(t, testErr, func() { - register(t, &set)(testCtx) - }) + assertEquals(t, 0, v(ctx), "") + assertEquals(t, 0, v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{IntFunc: func(context.Context, string) int { + return 1 + }}) + + assertEquals(t, 1, v(ctx), "") + assertEquals(t, 1, v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{IntFunc: func(context.Context, string) int { + return 2 + }}) + + assertEquals(t, 2, v(ctx), "") + assertEquals(t, 2, v2(ctx), "") }) } -func TestResolverFunc(t *testing.T) { - r := feature.LoaderFunc(func(ctx context.Context, dst any, name string, typ reflect.Type) { - assertEquals(t, "test", name) - assertEquals(t, "int", typ.Name()) - v := dst.(*int) - *v = 5 +func TestFlagSet_String(t *testing.T) { + t.Run("Duplicate", func(t *testing.T) { + var set feature.FlagSet + set.Int("test") + + assertPanic(t, feature.ErrDuplicateFlag, func() { + set.String("test") + }) }) - var dst int + t.Run("Register", func(t *testing.T) { + ctx := context.Background() - r(testCtx, &dst, "test", reflect.TypeFor[int]()) + var set feature.FlagSet + v := set.String("test") + v2 := mustLookup(t, &set, "test").Func.(func(context.Context) string) - assertEquals(t, 5, dst) + assertEquals(t, "", v(ctx), "") + assertEquals(t, "", v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{StringFunc: func(context.Context, string) string { + return "one" + }}) + + assertEquals(t, "one", v(ctx), "") + assertEquals(t, "one", v2(ctx), "") + + set.SetRegistry(&feature.SimpleRegistry{StringFunc: func(context.Context, string) string { + return "two" + }}) + + assertEquals(t, "two", v(ctx), "") + assertEquals(t, "two", v2(ctx), "") + }) +} + +func TestLabels(t *testing.T) { + var s feature.FlagSet + + s.Bool("test", + feature.WithLabel("labelC", "C"), + feature.WithLabel("labelB", "B"), + feature.WithLabel("labelE", "E"), + feature.WithLabels(map[string]string{ + "labelA": "A", + "labelE": "E2", + "labelF": "F", + "labelD": "D", + })) + + f := mustLookup(t, &s, "test") + + keys := make([]string, 0, f.Labels.Len()) + labels := make(map[string]string, f.Labels.Len()) + + for key, value := range f.Labels.All { + keys = append(keys, key) + labels[key] = value + } + + assertEquals(t, 6, len(keys), "unexpected number of labels") + assertEquals(t, []string{"labelA", "labelB", "labelC", "labelD", "labelE", "labelF"}, keys, "labels not sorted") + assertEquals(t, map[string]string{ + "labelA": "A", + "labelB": "B", + "labelC": "C", + "labelD": "D", + "labelE": "E2", + "labelF": "F", + }, labels, "labels do not match") + assertEquals(t, 6, f.Labels.Len(), "wrong number of labels reported") } var globalBool bool -func BenchmarkValuer(b *testing.B) { +func BenchmarkFlagSet_Bool(b *testing.B) { ctx := context.Background() - var set feature.Set - set.SetProvider(feature.LoaderFunc(func(_ context.Context, dst any, _ string, _ reflect.Type) { - v := dst.(*bool) - *v = true - })) - - p := feature.Register[bool](&set, "test") + var set feature.FlagSet + set.SetRegistry(testRegistry) + f := set.Bool("test") b.ReportAllocs() - b.ResetTimer() for range b.N { - globalBool = p(ctx) + globalBool = f(ctx) } } -var testCtx = context.Background() +var globalFloat float64 -func assertEquals[T comparable](tb testing.TB, want, got T) { - tb.Helper() +func BenchmarkFlagSet_Float(b *testing.B) { + ctx := context.Background() + + var set feature.FlagSet + set.SetRegistry(testRegistry) + + f := set.Float("test") + b.ReportAllocs() - if got != want { - tb.Errorf("expected %v, got %v", want, got) + for range b.N { + globalFloat = f(ctx) } } -func assertEqualsf[T comparable](tb testing.TB, want, got T, msg string, args ...any) { - tb.Helper() +var globalInt int + +func BenchmarkFlagSet_Int(b *testing.B) { + ctx := context.Background() + + var set feature.FlagSet + set.SetRegistry(testRegistry) - if got != want { - tb.Errorf("%s: expected %v, got %v", fmt.Sprintf(msg, args...), want, got) + f := set.Int("test") + b.ReportAllocs() + + for range b.N { + globalInt = f(ctx) } } -func assertPanic(tb testing.TB, want error, f func()) { - tb.Helper() +var globalString string + +func BenchmarkFlagSet_String(b *testing.B) { + ctx := context.Background() - got := recoverFrom(tb, f) + var set feature.FlagSet + set.SetRegistry(testRegistry) - err, ok := got.(error) - if !ok { - tb.Fatalf("expected error %q, got %v", want, err) - } + f := set.String("test") + b.ReportAllocs() - if !errors.Is(err, want) { - tb.Errorf("expeted error that is %q, got %q", want, got) + for range b.N { + globalString = f(ctx) } } -func recoverFrom(tb testing.TB, f func()) (recovered any) { +func assertEquals[T any](tb testing.TB, want, got T, msg string) { tb.Helper() - defer func() { - tb.Helper() + if msg == "" { + msg = "result mismatch" + } + + labelsComparer := cmp.Comparer(func(x, y feature.Labels) bool { + return cmp.Equal(maps.Collect(x.All), maps.Collect(y.All)) + }) + + flagComparer := cmp.Comparer(func(x, y feature.Flag) bool { + return x.Name == y.Name && x.Description == y.Description && cmp.Equal(x.Labels, y.Labels, labelsComparer) + }) + + if diff := cmp.Diff(want, got, flagComparer, labelsComparer); diff != "" { + tb.Errorf("%s (-want +got):\n%s", msg, diff) + } +} - recovered = recover() - if recovered == nil { - tb.Error("no panic was caught") +func assertPanic(tb testing.TB, want error, f func()) { + defer func() { + got := recover() + if got == nil { + tb.Errorf("expected panic with error %q, call did not panic", want) + } + gotErr, ok := got.(error) + if !ok { + tb.Fatalf("recovered value is not an error: %#v", got) + } + if !errors.Is(gotErr, want) { + tb.Errorf("expected error %q, got %q", want, gotErr) } }() f() - - return } -func register(tb testing.TB, set *feature.Set, opts ...feature.Option) feature.Valuer[string] { - return feature.Register[string](set, tb.Name(), opts...) +func mustLookup(tb testing.TB, set *feature.FlagSet, name string) feature.Flag { + tb.Helper() + + f, ok := set.Lookup(name) + if !ok { + tb.Fatalf("flag %q not found", name) + } + + return f } diff --git a/go.mod b/go.mod index 1552b91..d7dc434 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/nussjustin/feature -go 1.22.2 +go 1.23rc1 + +require github.com/google/go-cmp v0.6.0 // indirect diff --git a/go.sum b/go.sum index e69de29..5a8d551 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/sorted_map.go b/sorted_map.go new file mode 100644 index 0000000..7eddea6 --- /dev/null +++ b/sorted_map.go @@ -0,0 +1,36 @@ +package feature + +import ( + "maps" + "slices" +) + +type sortedMap[T any] struct { + m map[string]T + keys []string +} + +func (s sortedMap[T]) add(key string, val T) sortedMap[T] { + return s.addMany(map[string]T{key: val}) +} + +func (s sortedMap[T]) addMany(m map[string]T) sortedMap[T] { + s2 := sortedMap[T]{m: maps.Clone(s.m), keys: slices.Clone(s.keys)} + + if s2.m == nil { + s2.m = make(map[string]T, 1) + } + + for key, val := range m { + if _, ok := s.m[key]; !ok { + s2.keys = append(s2.keys, key) + } + s2.m[key] = val + } + + if len(s.keys) != len(s2.keys) { + slices.Sort(s2.keys) + } + + return s2 +}