Skip to content

Commit

Permalink
WIP3
Browse files Browse the repository at this point in the history
  • Loading branch information
nussjustin committed Aug 12, 2024
1 parent ff91c34 commit 9234c08
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 283 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
@@ -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
276 changes: 168 additions & 108 deletions feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,78 +3,60 @@ 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

// 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()
Expand All @@ -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].
Expand All @@ -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() {}
Loading

0 comments on commit 9234c08

Please sign in to comment.