From 4d08b89f6ea2cb2202ec5d7c57e28b6722c33dad Mon Sep 17 00:00:00 2001 From: Kevin Schoonover Date: Fri, 30 Aug 2024 10:46:24 -0700 Subject: [PATCH] feat: add TranscationContext Signed-off-by: Kevin Schoonover --- openfeature/client.go | 2 +- openfeature/evaluation_context.go | 29 +++++++++++++++++++++ openfeature/evaluation_context_test.go | 36 +++++++++++++++++++------- openfeature/internal/context_key.go | 10 +++++++ 4 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 openfeature/internal/context_key.go diff --git a/openfeature/client.go b/openfeature/client.go index 337d5657..b2c7fb27 100644 --- a/openfeature/client.go +++ b/openfeature/client.go @@ -677,7 +677,7 @@ func (c *Client) evaluate( // ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour provider, globalHooks, globalCtx := c.api.ForEvaluation(c.metadata.name) - evalCtx = mergeContexts(evalCtx, c.evaluationContext, globalCtx) // API (global) -> client -> invocation + evalCtx = mergeContexts(evalCtx, c.evaluationContext, TranscationContext(ctx), globalCtx) // API (global) -> transaction -> client -> invocation apiClientInvocationProviderHooks := append(append(append(globalHooks, c.hooks...), options.hooks...), provider.Hooks()...) // API, Client, Invocation, Provider providerInvocationClientApiHooks := append(append(append(provider.Hooks(), options.hooks...), c.hooks...), globalHooks...) // Provider, Invocation, Client, API diff --git a/openfeature/evaluation_context.go b/openfeature/evaluation_context.go index 19135553..54b80b10 100644 --- a/openfeature/evaluation_context.go +++ b/openfeature/evaluation_context.go @@ -1,5 +1,11 @@ package openfeature +import ( + "context" + + "github.com/open-feature/go-sdk/openfeature/internal" +) + // EvaluationContext provides ambient information for the purposes of flag evaluation // The use of the constructor, NewEvaluationContext, is enforced to set EvaluationContext's fields in order // to enforce immutability. @@ -53,3 +59,26 @@ func NewEvaluationContext(targetingKey string, attributes map[string]interface{} func NewTargetlessEvaluationContext(attributes map[string]interface{}) EvaluationContext { return NewEvaluationContext("", attributes) } + +// NewTranscationContext constructs a TranscationContext +// +// ctx - the context to embed the EvaluationContext in +// ec - the EvaluationContext to embed into the context +func WithTranscationContext(ctx context.Context, ec EvaluationContext) context.Context { + return context.WithValue(ctx, internal.TranscationContextKey, ec) +} + +// TranscationContext extracts a EvaluationContext from the current +// golang.org/x/net/context. if no EvaluationContext exist, it will construct +// an empty EvaluationContext +// +// ctx - the context to pull EvaluationContext from +func TranscationContext(ctx context.Context) EvaluationContext { + ec, ok := ctx.Value(internal.TranscationContextKey).(EvaluationContext) + + if !ok { + return EvaluationContext{} + } + + return ec +} diff --git a/openfeature/evaluation_context_test.go b/openfeature/evaluation_context_test.go index 550c3bfb..0cb4a003 100644 --- a/openfeature/evaluation_context_test.go +++ b/openfeature/evaluation_context_test.go @@ -83,7 +83,7 @@ func TestRequirement_3_2_1(t *testing.T) { }) } -// Evaluation context MUST be merged in the order: API (global) - client - invocation, +// Evaluation context MUST be merged in the order: API (global) - transaction - client - invocation, // with duplicate values being overwritten. func TestRequirement_3_2_2(t *testing.T) { defer t.Cleanup(initSingleton) @@ -93,12 +93,22 @@ func TestRequirement_3_2_2(t *testing.T) { targetingKey: "API", attributes: map[string]interface{}{ "invocationEvalCtx": true, - "foo": 2, - "user": 2, + "foo": 3, + "user": 3, }, } SetEvaluationContext(apiEvalCtx) + transactionEvalCtx := EvaluationContext{ + targetingKey: "Transcation", + attributes: map[string]interface{}{ + "transactionEvalCtx": true, + "foo": 2, + "user": 2, + }, + } + transactionCtx := WithTranscationContext(context.Background(), transactionEvalCtx) + mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() @@ -130,21 +140,21 @@ func TestRequirement_3_2_2(t *testing.T) { expectedMergedEvalCtx := EvaluationContext{ targetingKey: "Client", attributes: map[string]interface{}{ - "apiEvalCtx": true, - "invocationEvalCtx": true, - "clientEvalCtx": true, - "foo": "bar", - "user": 1, + "apiEvalCtx": true, + "transactionEvalCtx": true, + "invocationEvalCtx": true, + "clientEvalCtx": true, + "foo": "bar", + "user": 1, }, } flatCtx := flattenContext(expectedMergedEvalCtx) mockProvider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), flatCtx) - _, err = client.StringValue(context.Background(), "foo", "bar", invocationEvalCtx) + _, err = client.StringValue(transactionCtx, "foo", "bar", invocationEvalCtx) if err != nil { t.Error(err) } - } func TestEvaluationContext_AttributesNotPassedByReference(t *testing.T) { @@ -160,6 +170,12 @@ func TestEvaluationContext_AttributesNotPassedByReference(t *testing.T) { } } +func TestRequirement_3_3_1(t *testing.T) { + t.Run("The API MUST have a method for setting the evaluation context of the transaction context propagator for the current transaction.", func(t *testing.T) { + WithTranscationContext(context.Background(), EvaluationContext{}) + }) +} + func TestEvaluationContext_AttributesFuncNotPassedByReference(t *testing.T) { evalCtx := NewEvaluationContext("foo", map[string]interface{}{ "foo": "bar", diff --git a/openfeature/internal/context_key.go b/openfeature/internal/context_key.go new file mode 100644 index 00000000..40bf84e1 --- /dev/null +++ b/openfeature/internal/context_key.go @@ -0,0 +1,10 @@ +package internal + +// ContextKey is just an empty struct. It exists so TranscationContext can be +// an immutable public variable with a unique type. It's immutable +// because nobody else can create a ContextKey, being unexported. +type ContextKey struct{} + +// TranscationContext is the context key to use with golang.org/x/net/context's +// WithValue function to associate an EvaluationContext value with a context. +var TranscationContextKey ContextKey