Skip to content

Commit 375ee78

Browse files
mpywclaude
andauthored
Add Inspection pattern for ergonomic value retrieval (#2)
This commit introduces a new Inspection pattern that separates context value retrieval from display logic, providing a more ergonomic API. ## Breaking Changes - Remove DebugValue/DebugString methods (replaced by Inspection.String()) ## New Features ### Inspection[V] struct - Captures Key, Value, and Ok (whether set) in one call - Provides helper methods without requiring context: - Get() / TryGet() / GetOrDefault() / MustGet() - IsSet() / IsNotSet() - Implements fmt.Stringer: "key-name: value" or "key-name: <not set>" - Implements fmt.GoStringer with package qualification ### BoolInspection struct - Specialized wrapper for boolean feature flags - Provides Enabled() / Disabled() / ExplicitlyDisabled() - Embeds Inspection[bool] for full functionality ### Key[V] interface additions - Inspect(ctx) Inspection[V] - retrieves value with metadata - fmt.GoStringer - returns "feature.Key[T]{name: \"...\"}" ### BoolKey interface additions - InspectBool(ctx) BoolInspection ## Internal Changes - Refactor existing Key methods to use Inspection internally - Split inspection-related code into inspection.go - Split inspection tests into inspection_test.go - Achieve 100% test coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent f138f17 commit 375ee78

File tree

4 files changed

+541
-134
lines changed

4 files changed

+541
-134
lines changed

feature.go

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@
4242
// ctx = MaxItemsKey.WithValue(ctx, 100)
4343
// limit := MaxItemsKey.Get(ctx) // Returns 100
4444
//
45+
// # Inspecting Values
46+
//
47+
// Use Inspect to retrieve both the value and whether it was set in one call:
48+
//
49+
// var MaxItems = feature.NewNamed[int]("max-items")
50+
// inspection := MaxItems.Inspect(ctx)
51+
// fmt.Println(inspection) // Output: "max-items: 100" or "max-items: <not set>"
52+
// fmt.Println(inspection.IsSet()) // Output: true or false
53+
//
4554
// # Key Properties
4655
//
4756
// - Type-safe: Uses generics to ensure type safety at compile time
@@ -93,13 +102,14 @@ type Key[V any] interface {
93102
// It is equivalent to !IsSet(ctx).
94103
IsNotSet(ctx context.Context) bool
95104

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

101109
fmt.Stringer
102110

111+
fmt.GoStringer
112+
103113
// downcast is an internal method used to retrieve the underlying key implementation.
104114
// also used for sealing the interface.
105115
downcast() key[V]
@@ -131,6 +141,10 @@ type BoolKey interface {
131141
// WithDisabled returns a new context with this feature flag disabled (set to false).
132142
// The original context is not modified.
133143
WithDisabled(ctx context.Context) context.Context
144+
145+
// InspectBool retrieves the value from the context and returns a BoolInspection
146+
// that provides convenience methods for working with boolean feature flags.
147+
InspectBool(ctx context.Context) BoolInspection
134148
}
135149

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

147161
// WithName returns an option that sets a debug name for the key.
148-
// This name is included in the String() output and used in DebugValue() for easier debugging.
162+
// This name is included in the String() output for easier debugging.
149163
//
150164
// Example:
151165
//
@@ -284,22 +298,26 @@ type boolKey struct {
284298
}
285299

286300
// String returns the debug name of the key.
301+
// This implements fmt.Stringer.
287302
func (k key[V]) String() string {
288303
return k.name
289304
}
290305

291-
// DebugValue returns a string representation combining the key name and its value from the context.
292-
// This is useful for debugging and logging purposes.
293-
// Format: "<key-name>: <value>" or "<key-name>: <not set>".
294-
func (k key[V]) DebugValue(ctx context.Context) string {
295-
keyName := k.String()
306+
// GoString returns a Go syntax representation of the key.
307+
// This implements fmt.GoStringer.
308+
func (k key[V]) GoString() string {
309+
return fmt.Sprintf("feature.Key[%T]{name: %q}", *new(V), k.name)
310+
}
311+
312+
// Inspect retrieves the value from the context and returns an Inspection.
313+
func (k key[V]) Inspect(ctx context.Context) Inspection[V] {
296314
val, ok := k.TryGet(ctx)
297315

298-
if !ok {
299-
return keyName + ": <not set>"
316+
return Inspection[V]{
317+
Key: k,
318+
Value: val,
319+
Ok: ok,
300320
}
301-
302-
return fmt.Sprintf("%s: %v", keyName, val)
303321
}
304322

305323
func (k key[V]) downcast() key[V] {
@@ -314,9 +332,7 @@ func (k key[V]) WithValue(ctx context.Context, value V) context.Context {
314332
// Get retrieves the value associated with this key from the context.
315333
// If the key is not set in the context, it returns the zero value of type V.
316334
func (k key[V]) Get(ctx context.Context) V {
317-
val, _ := k.TryGet(ctx)
318-
319-
return val
335+
return k.Inspect(ctx).Get()
320336
}
321337

322338
// TryGet attempts to retrieve the value associated with this key from the context.
@@ -330,51 +346,43 @@ func (k key[V]) TryGet(ctx context.Context) (V, bool) {
330346
// GetOrDefault retrieves the value associated with this key from the context.
331347
// If the key is not set, it returns the provided default value.
332348
func (k key[V]) GetOrDefault(ctx context.Context, defaultValue V) V {
333-
if val, ok := k.TryGet(ctx); ok {
334-
return val
335-
}
336-
337-
return defaultValue
349+
return k.Inspect(ctx).GetOrDefault(defaultValue)
338350
}
339351

340352
// MustGet retrieves the value associated with this key from the context.
341353
// If the key is not set, it panics with a descriptive error message.
342354
func (k key[V]) MustGet(ctx context.Context) V {
343-
val, ok := k.TryGet(ctx)
344-
if !ok {
345-
panic(fmt.Sprintf("key %s is not set in context", k.String()))
346-
}
347-
348-
return val
355+
return k.Inspect(ctx).MustGet()
349356
}
350357

351358
// IsSet returns true if this key has been set in the context.
352359
func (k key[V]) IsSet(ctx context.Context) bool {
353-
_, ok := k.TryGet(ctx)
354-
355-
return ok
360+
return k.Inspect(ctx).IsSet()
356361
}
357362

358363
// IsNotSet returns true if this key has not been set in the context.
359364
func (k key[V]) IsNotSet(ctx context.Context) bool {
360-
return !k.IsSet(ctx)
365+
return k.Inspect(ctx).IsNotSet()
366+
}
367+
368+
// InspectBool retrieves the value from the context and returns a BoolInspection.
369+
func (k boolKey) InspectBool(ctx context.Context) BoolInspection {
370+
return BoolInspection{Inspection: k.Inspect(ctx)}
361371
}
362372

363373
// Enabled returns true if the feature flag is set to true in the context.
364374
func (k boolKey) Enabled(ctx context.Context) bool {
365-
return k.Get(ctx)
375+
return k.InspectBool(ctx).Enabled()
366376
}
367377

368378
// Disabled returns true if the feature flag is either not set or set to false.
369379
func (k boolKey) Disabled(ctx context.Context) bool {
370-
return !k.Enabled(ctx)
380+
return k.InspectBool(ctx).Disabled()
371381
}
372382

373383
// ExplicitlyDisabled returns true if the feature flag is explicitly set to false.
374384
func (k boolKey) ExplicitlyDisabled(ctx context.Context) bool {
375-
val, ok := k.TryGet(ctx)
376-
377-
return ok && !val
385+
return k.InspectBool(ctx).ExplicitlyDisabled()
378386
}
379387

380388
// WithEnabled returns a new context with this feature flag enabled (set to true).

feature_test.go

Lines changed: 32 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -491,122 +491,52 @@ func TestString(t *testing.T) {
491491
})
492492
}
493493

494-
// TestDebugValue tests the DebugValue method.
495-
func TestDebugValue(t *testing.T) {
494+
// TestGoString tests the GoString method for keys.
495+
func TestGoString(t *testing.T) {
496496
t.Parallel()
497497

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

501-
ctx := context.Background()
502501
key := feature.NewNamed[string]("test-key")
502+
goStr := key.GoString()
503503

504-
debugValue := key.DebugValue(ctx)
505-
506-
want := "test-key: <not set>"
507-
508-
if debugValue != want {
509-
t.Errorf("DebugValue() = %q, want %q", debugValue, want)
510-
}
511-
})
512-
513-
t.Run("set key shows name and value", func(t *testing.T) {
514-
t.Parallel()
515-
516-
ctx := context.Background()
517-
key := feature.NewNamed[int]("max-retries")
518-
ctx = key.WithValue(ctx, 5)
519-
debugValue := key.DebugValue(ctx)
520-
want := "max-retries: 5"
521-
522-
if debugValue != want {
523-
t.Errorf("DebugValue() = %q, want %q", debugValue, want)
504+
if !strings.Contains(goStr, "feature.Key[string]") {
505+
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[string]")
524506
}
525-
})
526-
527-
t.Run("bool key shows name and value when unset", func(t *testing.T) {
528-
t.Parallel()
529507

530-
ctx := context.Background()
531-
flag := feature.NewNamedBool("enable-feature")
532-
debugValue := flag.DebugValue(ctx)
533-
want := "enable-feature: <not set>"
534-
535-
if debugValue != want {
536-
t.Errorf("DebugValue() unset = %q, want %q", debugValue, want)
508+
if !strings.Contains(goStr, "name:") {
509+
t.Errorf("GoString() = %q, want to contain field name %q", goStr, "name:")
537510
}
538-
})
539-
540-
t.Run("bool key shows name and value when enabled", func(t *testing.T) {
541-
t.Parallel()
542-
543-
ctx := context.Background()
544-
flag := feature.NewNamedBool("enable-feature")
545-
ctx = flag.WithEnabled(ctx)
546-
debugValue := flag.DebugValue(ctx)
547-
want := "enable-feature: true"
548511

549-
if debugValue != want {
550-
t.Errorf("DebugValue() enabled = %q, want %q", debugValue, want)
512+
if !strings.Contains(goStr, "test-key") {
513+
t.Errorf("GoString() = %q, want to contain %q", goStr, "test-key")
551514
}
552515
})
553516

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

557-
ctx := context.Background()
558-
flag := feature.NewNamedBool("enable-feature")
559-
ctx = flag.WithDisabled(ctx)
560-
debugValue := flag.DebugValue(ctx)
561-
want := "enable-feature: false"
520+
key := feature.NewNamed[int]("max-retries")
521+
goStr := key.GoString()
562522

563-
if debugValue != want {
564-
t.Errorf("DebugValue() disabled = %q, want %q", debugValue, want)
523+
if !strings.Contains(goStr, "feature.Key[int]") {
524+
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[int]")
565525
}
566526
})
567527

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

571-
ctx := context.Background()
572-
key := feature.New[string]()
573-
ctx = key.WithValue(ctx, "value")
574-
575-
debugValue := key.DebugValue(ctx)
576-
// Should contain "anonymous(" (call site info) and "@0x" (address) and ": value"
577-
if !strings.Contains(debugValue, "anonymous(") {
578-
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "anonymous(")
579-
}
580-
581-
if !strings.Contains(debugValue, "@0x") {
582-
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "@0x")
583-
}
584-
585-
if !strings.Contains(debugValue, ": value") {
586-
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, ": value")
587-
}
588-
})
589-
590-
t.Run("complex value types are formatted", func(t *testing.T) {
591-
t.Parallel()
531+
flag := feature.NewNamedBool("my-feature")
532+
goStr := flag.GoString()
592533

593-
type Config struct {
594-
MaxRetries int
595-
Timeout string
534+
if !strings.Contains(goStr, "feature.Key[bool]") {
535+
t.Errorf("GoString() = %q, want to contain %q", goStr, "feature.Key[bool]")
596536
}
597537

598-
ctx := context.Background()
599-
key := feature.NewNamed[Config]("config")
600-
ctx = key.WithValue(ctx, Config{MaxRetries: 3, Timeout: "30s"})
601-
602-
debugValue := key.DebugValue(ctx)
603-
// Should contain the key name and struct representation
604-
if !strings.Contains(debugValue, "config:") {
605-
t.Errorf("DebugValue() = %q, want to contain %q", debugValue, "config:")
606-
}
607-
// Check that it contains the struct values
608-
if !strings.Contains(debugValue, "3") || !strings.Contains(debugValue, "30s") {
609-
t.Errorf("DebugValue() = %q, want to contain struct values", debugValue)
538+
if !strings.Contains(goStr, "my-feature") {
539+
t.Errorf("GoString() = %q, want to contain %q", goStr, "my-feature")
610540
}
611541
})
612542
}
@@ -978,22 +908,27 @@ func ExampleKey_IsNotSet() {
978908
// Using cache size: 1024
979909
}
980910

981-
func ExampleKey_DebugValue() {
911+
func ExampleKey_Inspect() {
982912
ctx := context.Background()
983913

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

987-
// Check debug value when not set
988-
fmt.Println(MaxRetries.DebugValue(ctx))
917+
// Inspect when not set
918+
fmt.Println(MaxRetries.Inspect(ctx))
989919

990-
// Set a value and check again
920+
// Set a value and inspect again
991921
ctx = MaxRetries.WithValue(ctx, 5)
992-
fmt.Println(MaxRetries.DebugValue(ctx))
922+
inspection := MaxRetries.Inspect(ctx)
923+
fmt.Println(inspection)
924+
fmt.Println("Value:", inspection.Get())
925+
fmt.Println("Is set:", inspection.IsSet())
993926

994927
// Output:
995928
// max-retries: <not set>
996929
// max-retries: 5
930+
// Value: 5
931+
// Is set: true
997932
}
998933

999934
func ExampleKey_String() {

0 commit comments

Comments
 (0)