Skip to content

Commit

Permalink
feat: add TranscationContext
Browse files Browse the repository at this point in the history
Signed-off-by: Kevin Schoonover <me@kschoon.me>
  • Loading branch information
kevinschoonover committed Aug 30, 2024
1 parent 80c0235 commit 4d08b89
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 11 deletions.
2 changes: 1 addition & 1 deletion openfeature/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions openfeature/evaluation_context.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
}
36 changes: 26 additions & 10 deletions openfeature/evaluation_context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()

Expand Down Expand Up @@ -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) {
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions openfeature/internal/context_key.go
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 4d08b89

Please # to comment.