Skip to content

Commit d61c744

Browse files
authored
fix(experimental): Functions for putting/getting an LDScopedClient from context.Context (#305)
This PR introduces a few more functions to the main `ld` package. This will allow Go developers to easily pass around a scoped client to any logic that already takes a `context.Context`. In particular, this will make writing HTTP middleware that establishes an LD context pretty easy. The middleware just needs to create a scoped client with the LD context it wants, then pass that scoped client into the `http.Request`'s built-in Go context. ```go func LDScopedClientMiddleware(client *LDClient) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { scopedClient := NewScopedClient(client, ldcontext.New("user-key")) ctx := GoContextWithScopedClient(r.Context(), scopedClient) next.ServeHTTP(w, r.WithContext(ctx)) }) } } func requestLogic(r *http.Request) { featureFlagEnabled := MustGetScopedClient(r.Context()).BoolVariation("my-flag", false) // use featureFlagEnabled... } ```
1 parent 2b96332 commit d61c744

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

ldclient_gocontext.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package ldclient
2+
3+
import "context"
4+
5+
type scopedClientKey struct{}
6+
7+
// GoContextWithScopedClient adds a scoped client to the Go context. This can be
8+
// used to pass a scoped client to a function or goroutine that might not
9+
// otherwise have access to it:
10+
//
11+
// scopedClient := ld.NewScopedClient(client, ldUserContext)
12+
// ctx := ld.GoContextWithScopedClient(context.Background(), scopedClient)
13+
// otherFunction(ctx)
14+
//
15+
// This function is not stable, and not subject to any backwards compatibility
16+
// guarantees or semantic versioning. It is not suitable for production usage. Do
17+
// not use it. You have been warned.
18+
func GoContextWithScopedClient(ctx context.Context, client *LDScopedClient) context.Context {
19+
return context.WithValue(ctx, scopedClientKey{}, client)
20+
}
21+
22+
// GetScopedClient retrieves a scoped client from the Go context that was set
23+
// with GoContextWithScopedClient, if present. If not present, returns nil and
24+
// false.
25+
//
26+
// func logicWithFeatureFlag(ctx context.Context) {
27+
// scopedClient, ok := ld.GetScopedClient(ctx)
28+
// isFeatureEnabled := false // default value if scoped client is not available
29+
// if ok {
30+
// isFeatureEnabled, err = scopedClient.BoolVariation("my-flag", false)
31+
// // handle err as appropriate...
32+
// }
33+
// }
34+
//
35+
// This function is not stable, and not subject to any backwards compatibility
36+
// guarantees or semantic versioning. It is not suitable for production usage. Do
37+
// not use it. You have been warned.
38+
func GetScopedClient(ctx context.Context) (*LDScopedClient, bool) {
39+
client, ok := ctx.Value(scopedClientKey{}).(*LDScopedClient)
40+
return client, ok
41+
}
42+
43+
// MustGetScopedClient retrieves a scoped client from the Go context that was set
44+
// with GoContextWithScopedClient, or panics if not present.
45+
//
46+
// func logicWithFeatureFlag(ctx context.Context) {
47+
// scopedClient := ld.MustGetScopedClient(ctx)
48+
// isFeatureEnabled, err := scopedClient.BoolVariation("my-flag", false)
49+
// // handle err as appropriate...
50+
// }
51+
//
52+
// This function is not stable, and not subject to any backwards compatibility
53+
// guarantees or semantic versioning. It is not suitable for production usage. Do
54+
// not use it. You have been warned.
55+
func MustGetScopedClient(ctx context.Context) *LDScopedClient {
56+
client, ok := GetScopedClient(ctx)
57+
if !ok {
58+
panic("No scoped client found in context")
59+
}
60+
return client
61+
}

ldclient_gocontext_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package ldclient
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGetScopedClient(t *testing.T) {
11+
t.Run("returns client from context", func(t *testing.T) {
12+
origCtx := context.Background()
13+
sc := &LDScopedClient{}
14+
15+
newCtx := GoContextWithScopedClient(origCtx, sc)
16+
retrieved, ok := GetScopedClient(newCtx)
17+
18+
assert.True(t, ok, "expected to find scoped client in context")
19+
assert.Equal(t, sc, retrieved, "retrieved client should match original")
20+
})
21+
22+
t.Run("returns nil when not present", func(t *testing.T) {
23+
retrieved, ok := GetScopedClient(context.Background())
24+
assert.False(t, ok, "should not find scoped client in empty context")
25+
assert.Nil(t, retrieved, "retrieved client should be nil when not present")
26+
})
27+
}
28+
29+
func TestMustGetScopedClient(t *testing.T) {
30+
sc := &LDScopedClient{}
31+
ctxWith := GoContextWithScopedClient(context.Background(), sc)
32+
33+
// Should return the client without panicking when present
34+
assert.Equal(t, sc, MustGetScopedClient(ctxWith))
35+
36+
// Should panic when the client is not present
37+
assert.Panics(t, func() {
38+
MustGetScopedClient(context.Background())
39+
})
40+
}

0 commit comments

Comments
 (0)