Skip to content

Commit 4a934e2

Browse files
authored
Merge pull request #546 from scothis/stasher
Type safe stashing of context values
2 parents 18cb6f4 + 5dcfc6a commit 4a934e2

File tree

4 files changed

+163
-32
lines changed

4 files changed

+163
-32
lines changed

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -915,19 +915,21 @@ To setup a Config for a test and make assertions that the expected behavior matc
915915

916916
The stash allows passing arbitrary state between sub reconcilers within the scope of a single reconciler request. Values are stored on the context by [`StashValue`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#StashValue) and accessed via [`RetrieveValue`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#RetrieveValue).
917917

918+
A [`Stasher`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#Stasher) provides a convenient way to interact with typed values. Create a [`NewStasher`](https://pkg.go.dev/reconciler.io/runtime/reconcilers#NewStasher) using the type of the value being stashed and a unique stash key. All operations through a stasher, including retrieval are type safe with options for handling missing values on retrieval.
919+
918920
For testing, given stashed values can be defined in a [SubReconcilerTests](#subreconcilertests) with [`GivenStashedValues`](https://pkg.go.dev/reconciler.io/runtime/testing#SubReconcilerTestCase.GivenStashedValues). Newly stashed or mutated values expectations are defined with [`ExpectStashedValues`](https://pkg.go.dev/reconciler.io/runtime/testing#SubReconcilerTestCase.ExpectStashedValues). An optional, custom function for asserting stashed values can be provided via [`VerifyStashedValue`](https://pkg.go.dev/reconciler.io/runtime/testing#SubReconcilerTestCase.VerifyStashedValue).
919921

920922
**Example:**
921923

922924
```go
923-
const exampleStashKey reconcilers.StashKey = "example"
925+
var exampleStasher = reconcilers.NewStasher[Example]("example")
924926

925927
func StashExampleSubReconciler(c reconcilers.Config) reconcilers.SubReconciler[*examplev1.MyExample] {
926928
return &reconcilers.SyncReconciler[*examplev1.MyExample]{
927929
Name: "StashExample",
928930
Sync: func(ctx context.Context, resource *examplev1.MyExample) error {
929931
value := Example{} // something we want to expose to a sub reconciler later in this chain
930-
reconcilers.StashValue(ctx, exampleStashKey, value)
932+
exampleStasher.Store(ctx, value)
931933
return nil
932934
},
933935
}
@@ -938,9 +940,9 @@ func StashExampleSubReconciler(c reconcilers.Config) reconcilers.SubReconciler[*
938940
return &reconcilers.SyncReconciler[*examplev1.MyExample]{
939941
Name: "StashExample",
940942
Sync: func(ctx context.Context, resource *examplev1.MyExample) error {
941-
value, ok := reconcilers.RetrieveValue(ctx, exampleStashKey).(Example)
942-
if !ok {
943-
return nil, fmt.Errorf("expected stashed value for key %q", exampleStashKey)
943+
value, err := exampleStasher.RetrieveOrError(ctx)
944+
if err == reconcilers.ErrStashValueNotFound {
945+
return nil, fmt.Errorf("%w for key %q", err, exampleStasher.Key())
944946
}
945947
... // do something with the value
946948
},

reconcilers/childset.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,13 @@ func (r *ChildSetReconciler[T, CT, CLT]) childReconcilerFor(desired CT, desiredE
232232
return desired, desiredErr
233233
},
234234
ReflectChildStatusOnParent: func(ctx context.Context, parent T, child CT, err error) {
235-
result := retrieveChildSetResult[CT](ctx)
235+
result := childSetResultStasher[CT]().RetrieveOrEmpty(ctx)
236236
result.Children = append(result.Children, ChildSetPartialResult[CT]{
237237
Id: id,
238238
Child: child,
239239
Err: err,
240240
})
241-
stashChildSetResult(ctx, result)
241+
childSetResultStasher[CT]().Store(ctx, result)
242242
},
243243
HarmonizeImmutableFields: r.HarmonizeImmutableFields,
244244
MergeBeforeUpdate: r.MergeBeforeUpdate,
@@ -370,7 +370,7 @@ func (r *ChildSetReconciler[T, CT, CLT]) composeChildReconcilers(ctx context.Con
370370
}
371371

372372
func (r *ChildSetReconciler[T, CT, CLT]) reflectStatus(ctx context.Context, parent T) {
373-
result := clearChildSetResult[CT](ctx)
373+
result := childSetResultStasher[CT]().Clear(ctx)
374374
r.ReflectChildrenStatusOnParent(ctx, parent, result)
375375
}
376376

@@ -392,26 +392,8 @@ func (r *ChildSetResult[T]) AggregateError() error {
392392
return utilerrors.NewAggregate(errs)
393393
}
394394

395-
const childSetResultStashKey StashKey = "reconciler.io/runtime:childSetResult"
396-
397-
func retrieveChildSetResult[T client.Object](ctx context.Context) ChildSetResult[T] {
398-
value := RetrieveValue(ctx, childSetResultStashKey)
399-
if result, ok := value.(ChildSetResult[T]); ok {
400-
return result
401-
}
402-
return ChildSetResult[T]{}
403-
}
404-
405-
func stashChildSetResult[T client.Object](ctx context.Context, result ChildSetResult[T]) {
406-
StashValue(ctx, childSetResultStashKey, result)
407-
}
408-
409-
func clearChildSetResult[T client.Object](ctx context.Context) ChildSetResult[T] {
410-
value := ClearValue(ctx, childSetResultStashKey)
411-
if result, ok := value.(ChildSetResult[T]); ok {
412-
return result
413-
}
414-
return ChildSetResult[T]{}
395+
func childSetResultStasher[T client.Object]() Stasher[ChildSetResult[T]] {
396+
return NewStasher[ChildSetResult[T]]("reconciler.io/runtime:childSetResult")
415397
}
416398

417399
const knownChildrenStashKey StashKey = "reconciler.io/runtime:knownChildren"

reconcilers/stash.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package reconcilers
1818

1919
import (
2020
"context"
21+
"errors"
2122
"fmt"
2223
)
2324

@@ -55,3 +56,72 @@ func ClearValue(ctx context.Context, key StashKey) interface{} {
5556
delete(stash, key)
5657
return value
5758
}
59+
60+
// Stasher stores and retrieves values from the stash context. The context which gets passed to its methods must be configured
61+
// with a stash via WithStash(). The stash is pre-configured for the context within a reconciler.
62+
type Stasher[T any] interface {
63+
// Key is the stash key used to store and retrieve the value
64+
Key() StashKey
65+
66+
// Store saves the value in the stash under the key
67+
Store(ctx context.Context, value T)
68+
69+
// Clear removes the key from the stash returning the previous value, if any.
70+
Clear(ctx context.Context) T
71+
72+
// RetrieveOrDie retrieves the value from the stash, or panics if the key is not in the stash
73+
RetrieveOrDie(ctx context.Context) T
74+
75+
// RetrieveOrEmpty retrieves the value from the stash, or an error if the key is not in the stash
76+
RetrieveOrError(ctx context.Context) (T, error)
77+
78+
// RetrieveOrEmpty retrieves the value from the stash, or the empty value if the key is not in the stash
79+
RetrieveOrEmpty(ctx context.Context) T
80+
}
81+
82+
// NewStasher creates a stasher for the value type
83+
func NewStasher[T any](key StashKey) Stasher[T] {
84+
return &stasher[T]{
85+
key: key,
86+
}
87+
}
88+
89+
type stasher[T any] struct {
90+
key StashKey
91+
}
92+
93+
func (s *stasher[T]) Key() StashKey {
94+
return s.key
95+
}
96+
97+
func (s *stasher[T]) Store(ctx context.Context, value T) {
98+
StashValue(ctx, s.Key(), value)
99+
}
100+
101+
func (s *stasher[T]) Clear(ctx context.Context) T {
102+
previous, _ := ClearValue(ctx, s.Key()).(T)
103+
return previous
104+
}
105+
106+
func (s *stasher[T]) RetrieveOrDie(ctx context.Context) T {
107+
value, err := s.RetrieveOrError(ctx)
108+
if err != nil {
109+
panic(err)
110+
}
111+
return value
112+
}
113+
114+
var ErrStashValueNotFound = errors.New("value not found in stash")
115+
116+
func (s *stasher[T]) RetrieveOrError(ctx context.Context) (T, error) {
117+
value, ok := RetrieveValue(ctx, s.Key()).(T)
118+
if !ok {
119+
return value, ErrStashValueNotFound
120+
}
121+
return value, nil
122+
}
123+
124+
func (s *stasher[T]) RetrieveOrEmpty(ctx context.Context) T {
125+
value, _ := s.RetrieveOrError(ctx)
126+
return value
127+
}

reconcilers/stash_test.go

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestStash(t *testing.T) {
4747
}
4848

4949
var key StashKey = "stash-key"
50-
ctx := WithStash(context.TODO())
50+
ctx := WithStash(context.Background())
5151
for _, c := range tests {
5252
t.Run(c.name, func(t *testing.T) {
5353
StashValue(ctx, key, c.value)
@@ -59,7 +59,7 @@ func TestStash(t *testing.T) {
5959
}
6060

6161
func TestStash_StashValue_UndecoratedContext(t *testing.T) {
62-
ctx := context.TODO()
62+
ctx := context.Background()
6363
var key StashKey = "stash-key"
6464
value := "value"
6565

@@ -72,7 +72,7 @@ func TestStash_StashValue_UndecoratedContext(t *testing.T) {
7272
}
7373

7474
func TestStash_RetrieveValue_UndecoratedContext(t *testing.T) {
75-
ctx := context.TODO()
75+
ctx := context.Background()
7676
var key StashKey = "stash-key"
7777

7878
defer func() {
@@ -84,10 +84,87 @@ func TestStash_RetrieveValue_UndecoratedContext(t *testing.T) {
8484
}
8585

8686
func TestStash_RetrieveValue_Undefined(t *testing.T) {
87-
ctx := WithStash(context.TODO())
87+
ctx := WithStash(context.Background())
8888
var key StashKey = "stash-key"
8989

9090
if value := RetrieveValue(ctx, key); value != nil {
9191
t.Error("expected RetrieveValue() to return nil for undefined key")
9292
}
9393
}
94+
95+
func TestStasher(t *testing.T) {
96+
ctx := WithStash(context.Background())
97+
stasher := NewStasher[string]("my-key")
98+
99+
if key := stasher.Key(); key != StashKey("my-key") {
100+
t.Errorf("expected key to be %q got %q", StashKey("my-key"), key)
101+
}
102+
103+
t.Run("no value", func(t *testing.T) {
104+
t.Run("RetrieveOrEmpty", func(t *testing.T) {
105+
if value := stasher.RetrieveOrEmpty(ctx); value != "" {
106+
t.Error("expected value to be empty")
107+
}
108+
})
109+
t.Run("RetrieveOrError", func(t *testing.T) {
110+
if value, err := stasher.RetrieveOrError(ctx); err == nil {
111+
t.Error("expected err")
112+
} else if value != "" {
113+
t.Error("expected value to be empty")
114+
}
115+
})
116+
t.Run("RetrieveOrDie", func(t *testing.T) {
117+
defer func() {
118+
if r := recover(); r != nil {
119+
return
120+
}
121+
t.Errorf("expected to recover")
122+
}()
123+
value := stasher.RetrieveOrDie(ctx)
124+
t.Errorf("expected to panic, got %q", value)
125+
})
126+
})
127+
128+
t.Run("has value", func(t *testing.T) {
129+
stasher.Store(ctx, "hello world")
130+
t.Run("RetrieveOrEmpty", func(t *testing.T) {
131+
if value := stasher.RetrieveOrEmpty(ctx); value != "hello world" {
132+
t.Errorf("expected value to be %q got %q", "hello world", value)
133+
}
134+
})
135+
t.Run("RetrieveOrError", func(t *testing.T) {
136+
if value, err := stasher.RetrieveOrError(ctx); err != nil {
137+
t.Errorf("unexpected err: %s", err)
138+
} else if value != "hello world" {
139+
t.Errorf("expected value to be %q got %q", "hello world", value)
140+
}
141+
})
142+
t.Run("RetrieveOrDie", func(t *testing.T) {
143+
if value := stasher.RetrieveOrDie(ctx); value != "hello world" {
144+
t.Errorf("expected value to be %q got %q", "hello world", value)
145+
}
146+
})
147+
})
148+
149+
t.Run("context scoped", func(t *testing.T) {
150+
stasher.Store(ctx, "hello world")
151+
if value := stasher.RetrieveOrEmpty(ctx); value != "hello world" {
152+
t.Errorf("expected value to be %q got %q", "hello world", value)
153+
}
154+
altCtx := WithStash(context.Background())
155+
if value := stasher.RetrieveOrEmpty(altCtx); value != "" {
156+
t.Error("expected value to be empty")
157+
}
158+
})
159+
160+
t.Run("Clear", func(t *testing.T) {
161+
stasher.Store(ctx, "hello world")
162+
if value := stasher.RetrieveOrEmpty(ctx); value != "hello world" {
163+
t.Errorf("expected value to be %q got %q", "hello world", value)
164+
}
165+
stasher.Clear(ctx)
166+
if value := stasher.RetrieveOrEmpty(ctx); value != "" {
167+
t.Error("expected value to be empty")
168+
}
169+
})
170+
}

0 commit comments

Comments
 (0)