Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 45 additions & 37 deletions feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@
// ctx = MaxItemsKey.WithValue(ctx, 100)
// limit := MaxItemsKey.Get(ctx) // Returns 100
//
// # Inspecting Values
//
// Use Inspect to retrieve both the value and whether it was set in one call:
//
// var MaxItems = feature.NewNamed[int]("max-items")
// inspection := MaxItems.Inspect(ctx)
// fmt.Println(inspection) // Output: "max-items: 100" or "max-items: <not set>"
// fmt.Println(inspection.IsSet()) // Output: true or false
//
// # Key Properties
//
// - Type-safe: Uses generics to ensure type safety at compile time
Expand Down Expand Up @@ -93,13 +102,14 @@ type Key[V any] interface {
// It is equivalent to !IsSet(ctx).
IsNotSet(ctx context.Context) bool

// DebugValue returns a string representation combining the key name and its value from the context.
// This is useful for debugging and logging purposes.
// Format: "<key-name>: <value>" or "<key-name>: <not set>".
DebugValue(ctx context.Context) string
// Inspect retrieves the value from the context and returns an Inspection
// that provides convenient methods for working with the result.
Inspect(ctx context.Context) Inspection[V]

fmt.Stringer

fmt.GoStringer

// downcast is an internal method used to retrieve the underlying key implementation.
// also used for sealing the interface.
downcast() key[V]
Expand Down Expand Up @@ -131,6 +141,10 @@ type BoolKey interface {
// WithDisabled returns a new context with this feature flag disabled (set to false).
// The original context is not modified.
WithDisabled(ctx context.Context) context.Context

// InspectBool retrieves the value from the context and returns a BoolInspection
// that provides convenience methods for working with boolean feature flags.
InspectBool(ctx context.Context) BoolInspection
}

// Option is a function that configures the behavior of a feature flag key.
Expand All @@ -145,7 +159,7 @@ type options struct {
}

// WithName returns an option that sets a debug name for the key.
// This name is included in the String() output and used in DebugValue() for easier debugging.
// This name is included in the String() output for easier debugging.
//
// Example:
//
Expand Down Expand Up @@ -284,22 +298,26 @@ type boolKey struct {
}

// String returns the debug name of the key.
// This implements fmt.Stringer.
func (k key[V]) String() string {
return k.name
}

// DebugValue returns a string representation combining the key name and its value from the context.
// This is useful for debugging and logging purposes.
// Format: "<key-name>: <value>" or "<key-name>: <not set>".
func (k key[V]) DebugValue(ctx context.Context) string {
keyName := k.String()
// GoString returns a Go syntax representation of the key.
// This implements fmt.GoStringer.
func (k key[V]) GoString() string {
return fmt.Sprintf("feature.Key[%T]{name: %q}", *new(V), k.name)
}

// Inspect retrieves the value from the context and returns an Inspection.
func (k key[V]) Inspect(ctx context.Context) Inspection[V] {
val, ok := k.TryGet(ctx)

if !ok {
return keyName + ": <not set>"
return Inspection[V]{
Key: k,
Value: val,
Ok: ok,
}

return fmt.Sprintf("%s: %v", keyName, val)
}

func (k key[V]) downcast() key[V] {
Expand All @@ -314,9 +332,7 @@ func (k key[V]) WithValue(ctx context.Context, value V) context.Context {
// Get retrieves the value associated with this key from the context.
// If the key is not set in the context, it returns the zero value of type V.
func (k key[V]) Get(ctx context.Context) V {
val, _ := k.TryGet(ctx)

return val
return k.Inspect(ctx).Get()
}

// TryGet attempts to retrieve the value associated with this key from the context.
Expand All @@ -330,51 +346,43 @@ func (k key[V]) TryGet(ctx context.Context) (V, bool) {
// GetOrDefault retrieves the value associated with this key from the context.
// If the key is not set, it returns the provided default value.
func (k key[V]) GetOrDefault(ctx context.Context, defaultValue V) V {
if val, ok := k.TryGet(ctx); ok {
return val
}

return defaultValue
return k.Inspect(ctx).GetOrDefault(defaultValue)
}

// MustGet retrieves the value associated with this key from the context.
// If the key is not set, it panics with a descriptive error message.
func (k key[V]) MustGet(ctx context.Context) V {
val, ok := k.TryGet(ctx)
if !ok {
panic(fmt.Sprintf("key %s is not set in context", k.String()))
}

return val
return k.Inspect(ctx).MustGet()
}

// IsSet returns true if this key has been set in the context.
func (k key[V]) IsSet(ctx context.Context) bool {
_, ok := k.TryGet(ctx)

return ok
return k.Inspect(ctx).IsSet()
}

// IsNotSet returns true if this key has not been set in the context.
func (k key[V]) IsNotSet(ctx context.Context) bool {
return !k.IsSet(ctx)
return k.Inspect(ctx).IsNotSet()
}

// InspectBool retrieves the value from the context and returns a BoolInspection.
func (k boolKey) InspectBool(ctx context.Context) BoolInspection {
return BoolInspection{Inspection: k.Inspect(ctx)}
}

// Enabled returns true if the feature flag is set to true in the context.
func (k boolKey) Enabled(ctx context.Context) bool {
return k.Get(ctx)
return k.InspectBool(ctx).Enabled()
}

// Disabled returns true if the feature flag is either not set or set to false.
func (k boolKey) Disabled(ctx context.Context) bool {
return !k.Enabled(ctx)
return k.InspectBool(ctx).Disabled()
}

// ExplicitlyDisabled returns true if the feature flag is explicitly set to false.
func (k boolKey) ExplicitlyDisabled(ctx context.Context) bool {
val, ok := k.TryGet(ctx)

return ok && !val
return k.InspectBool(ctx).ExplicitlyDisabled()
}

// WithEnabled returns a new context with this feature flag enabled (set to true).
Expand Down
129 changes: 32 additions & 97 deletions feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,122 +491,52 @@ func TestString(t *testing.T) {
})
}

// TestDebugValue tests the DebugValue method.
func TestDebugValue(t *testing.T) {
// TestGoString tests the GoString method for keys.
func TestGoString(t *testing.T) {
t.Parallel()

t.Run("unset key shows not set", func(t *testing.T) {
t.Run("Key GoString includes package name and type", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
key := feature.NewNamed[string]("test-key")
goStr := key.GoString()

debugValue := key.DebugValue(ctx)

want := "test-key: <not set>"

if debugValue != want {
t.Errorf("DebugValue() = %q, want %q", debugValue, want)
}
})

t.Run("set key shows name and value", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
key := feature.NewNamed[int]("max-retries")
ctx = key.WithValue(ctx, 5)
debugValue := key.DebugValue(ctx)
want := "max-retries: 5"

if debugValue != want {
t.Errorf("DebugValue() = %q, want %q", debugValue, want)
if !strings.Contains(goStr, "feature.Key[string]") {
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[string]")
}
})

t.Run("bool key shows name and value when unset", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
flag := feature.NewNamedBool("enable-feature")
debugValue := flag.DebugValue(ctx)
want := "enable-feature: <not set>"

if debugValue != want {
t.Errorf("DebugValue() unset = %q, want %q", debugValue, want)
if !strings.Contains(goStr, "name:") {
t.Errorf("GoString() = %q, want to contain field name %q", goStr, "name:")
}
})

t.Run("bool key shows name and value when enabled", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
flag := feature.NewNamedBool("enable-feature")
ctx = flag.WithEnabled(ctx)
debugValue := flag.DebugValue(ctx)
want := "enable-feature: true"

if debugValue != want {
t.Errorf("DebugValue() enabled = %q, want %q", debugValue, want)
if !strings.Contains(goStr, "test-key") {
t.Errorf("GoString() = %q, want to contain %q", goStr, "test-key")
}
})

t.Run("bool key shows name and value when disabled", func(t *testing.T) {
t.Run("Key GoString with int type", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
flag := feature.NewNamedBool("enable-feature")
ctx = flag.WithDisabled(ctx)
debugValue := flag.DebugValue(ctx)
want := "enable-feature: false"
key := feature.NewNamed[int]("max-retries")
goStr := key.GoString()

if debugValue != want {
t.Errorf("DebugValue() disabled = %q, want %q", debugValue, want)
if !strings.Contains(goStr, "feature.Key[int]") {
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[int]")
}
})

t.Run("anonymous key shows call site info in name", func(t *testing.T) {
t.Run("BoolKey GoString includes bool type", func(t *testing.T) {
t.Parallel()

ctx := context.Background()
key := feature.New[string]()
ctx = key.WithValue(ctx, "value")

debugValue := key.DebugValue(ctx)
// Should contain "anonymous(" (call site info) and "@0x" (address) and ": value"
if !strings.Contains(debugValue, "anonymous(") {
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "anonymous(")
}

if !strings.Contains(debugValue, "@0x") {
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "@0x")
}

if !strings.Contains(debugValue, ": value") {
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, ": value")
}
})

t.Run("complex value types are formatted", func(t *testing.T) {
t.Parallel()
flag := feature.NewNamedBool("my-feature")
goStr := flag.GoString()

type Config struct {
MaxRetries int
Timeout string
if !strings.Contains(goStr, "feature.Key[bool]") {
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[bool]")
}

ctx := context.Background()
key := feature.NewNamed[Config]("config")
ctx = key.WithValue(ctx, Config{MaxRetries: 3, Timeout: "30s"})

debugValue := key.DebugValue(ctx)
// Should contain the key name and struct representation
if !strings.Contains(debugValue, "config:") {
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "config:")
}
// Check that it contains the struct values
if !strings.Contains(debugValue, "3") || !strings.Contains(debugValue, "30s") {
t.Errorf("DebugValue() = %q, want to contain struct values", debugValue)
if !strings.Contains(goStr, "my-feature") {
t.Errorf("GoString() = %q, want to contain %q", goStr, "my-feature")
}
})
}
Expand Down Expand Up @@ -978,22 +908,27 @@ func ExampleKey_IsNotSet() {
// Using cache size: 1024
}

func ExampleKey_DebugValue() {
func ExampleKey_Inspect() {
ctx := context.Background()

// Create a named key for better debug output
var MaxRetries = feature.NewNamed[int]("max-retries")

// Check debug value when not set
fmt.Println(MaxRetries.DebugValue(ctx))
// Inspect when not set
fmt.Println(MaxRetries.Inspect(ctx))

// Set a value and check again
// Set a value and inspect again
ctx = MaxRetries.WithValue(ctx, 5)
fmt.Println(MaxRetries.DebugValue(ctx))
inspection := MaxRetries.Inspect(ctx)
fmt.Println(inspection)
fmt.Println("Value:", inspection.Get())
fmt.Println("Is set:", inspection.IsSet())

// Output:
// max-retries: <not set>
// max-retries: 5
// Value: 5
// Is set: true
}

func ExampleKey_String() {
Expand Down
Loading
Loading