From eb2ffb14f5f3b79d99e46b2dc5c19e885afa1296 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:53:30 -0700 Subject: [PATCH] chore(refactor): Remove AddHooks, refactor execution, add contract tests. (#131) This is a "chore" because I plan on rebase merging this feature branch and this is not a release-note change. --- internal/hooks/evaluation_execution.go | 68 ++++ internal/hooks/evaluation_execution_test.go | 305 ++++++++++++++++ internal/hooks/runner.go | 131 ++----- internal/hooks/runner_test.go | 381 +++++--------------- ldclient.go | 135 +++---- ldclient_events.go | 52 ++- ldclient_hooks_test.go | 21 -- testservice/go.sum | 3 + testservice/sdk_client_entity.go | 10 + testservice/service.go | 1 + testservice/servicedef/command_params.go | 21 ++ testservice/servicedef/sdk_config.go | 13 + testservice/servicedef/service_params.go | 1 + testservice/test_hook.go | 107 ++++++ 14 files changed, 747 insertions(+), 502 deletions(-) create mode 100644 internal/hooks/evaluation_execution.go create mode 100644 internal/hooks/evaluation_execution_test.go create mode 100644 testservice/test_hook.go diff --git a/internal/hooks/evaluation_execution.go b/internal/hooks/evaluation_execution.go new file mode 100644 index 00000000..2046a0da --- /dev/null +++ b/internal/hooks/evaluation_execution.go @@ -0,0 +1,68 @@ +package hooks + +import ( + gocontext "context" + + "github.com/launchdarkly/go-sdk-common/v3/ldlog" + "github.com/launchdarkly/go-sdk-common/v3/ldreason" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" +) + +// EvaluationExecution represents the state of a running series of evaluation stages. +type EvaluationExecution struct { + hooks []ldhooks.Hook + data []ldhooks.EvaluationSeriesData + context ldhooks.EvaluationSeriesContext + loggers *ldlog.Loggers +} + +// BeforeEvaluation executes the BeforeEvaluation stage of registered hooks. +func (e *EvaluationExecution) BeforeEvaluation(ctx gocontext.Context) { + e.executeStage( + false, + "BeforeEvaluation", + func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { + return hook.BeforeEvaluation(ctx, e.context, data) + }) +} + +// AfterEvaluation executes the AfterEvaluation stage of registered hooks. +func (e *EvaluationExecution) AfterEvaluation( + ctx gocontext.Context, + detail ldreason.EvaluationDetail, +) { + e.executeStage( + true, + "AfterEvaluation", + func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { + return hook.AfterEvaluation(ctx, e.context, data, detail) + }) +} + +func (e *EvaluationExecution) executeStage( + reverse bool, + stageName string, + fn func( + hook ldhooks.Hook, + data ldhooks.EvaluationSeriesData, + ) (ldhooks.EvaluationSeriesData, error)) { + returnData := make([]ldhooks.EvaluationSeriesData, len(e.hooks)) + iterator := newIterator(reverse, e.hooks) + for iterator.hasNext() { + i, hook := iterator.getNext() + + outData, err := fn(hook, e.data[i]) + if err != nil { + returnData[i] = e.data[i] + e.loggers.Errorf( + "During evaluation of flag \"%s\", an error was encountered in \"%s\" of the \"%s\" hook: %s", + e.context.FlagKey(), + stageName, + hook.Metadata().Name(), + err.Error()) + continue + } + returnData[i] = outData + } + e.data = returnData +} diff --git a/internal/hooks/evaluation_execution_test.go b/internal/hooks/evaluation_execution_test.go new file mode 100644 index 00000000..0c186f28 --- /dev/null +++ b/internal/hooks/evaluation_execution_test.go @@ -0,0 +1,305 @@ +package hooks + +import ( + "context" + "errors" + "testing" + + "github.com/launchdarkly/go-sdk-common/v3/ldlog" + "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" + "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" + + "github.com/launchdarkly/go-sdk-common/v3/ldcontext" + "github.com/launchdarkly/go-sdk-common/v3/ldreason" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" + "github.com/stretchr/testify/assert" +) + +func emptyExecutionAssertions(t *testing.T, res *EvaluationExecution, ldContext ldcontext.Context) { + assert.Empty(t, res.hooks) + assert.Empty(t, res.data) + assert.Equal(t, ldContext, res.context.Context()) + assert.Equal(t, "test-flag", res.context.FlagKey()) + assert.Equal(t, "testMethod", res.context.Method()) + assert.Equal(t, ldvalue.Bool(false), res.context.DefaultValue()) +} + +type orderTracker struct { + orderBefore []string + orderAfter []string +} + +func newOrderTracker() *orderTracker { + return &orderTracker{ + orderBefore: make([]string, 0), + orderAfter: make([]string, 0), + } +} + +func TestEvaluationExecution(t *testing.T) { + falseValue := ldvalue.Bool(false) + ldContext := ldcontext.New("test-context") + + t.Run("with no hooks", func(t *testing.T) { + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{}) + + t.Run("run before evaluation", func(t *testing.T) { + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + execution.BeforeEvaluation(context.Background()) + emptyExecutionAssertions(t, execution, ldContext) + }) + + t.Run("run after evaluation", func(t *testing.T) { + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + execution.AfterEvaluation(context.Background(), + ldreason.NewEvaluationDetail(falseValue, 0, + ldreason.NewEvalReasonFallthrough())) + emptyExecutionAssertions(t, execution, ldContext) + }) + }) + + t.Run("with hooks", func(t *testing.T) { + t.Run("prepare evaluation series", func(t *testing.T) { + hookA := sharedtest.NewTestHook("a") + hookB := sharedtest.NewTestHook("b") + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) + + ldContext := ldcontext.New("test-context") + res := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, "testMethod") + + assert.Len(t, res.hooks, 2) + assert.Len(t, res.data, 2) + assert.Equal(t, ldContext, res.context.Context()) + assert.Equal(t, "test-flag", res.context.FlagKey()) + assert.Equal(t, "testMethod", res.context.Method()) + assert.Equal(t, falseValue, res.context.DefaultValue()) + assert.Equal(t, res.data[0], ldhooks.EmptyEvaluationSeriesData()) + assert.Equal(t, res.data[1], ldhooks.EmptyEvaluationSeriesData()) + }) + + t.Run("verify execution order", func(t *testing.T) { + testCases := []struct { + name string + method func(execution *EvaluationExecution) + expectedBeforeOrder []string + expectedAfterOrder []string + }{ + {name: "BeforeEvaluation", + method: func(execution *EvaluationExecution) { + execution.BeforeEvaluation(context.Background()) + }, + expectedBeforeOrder: []string{"a", "b"}, + expectedAfterOrder: make([]string, 0), + }, + {name: "AfterEvaluation", + method: func(execution *EvaluationExecution) { + detail := ldreason.NewEvaluationDetail(falseValue, 0, + ldreason.NewEvalReasonFallthrough()) + execution.AfterEvaluation(context.Background(), detail) + }, + expectedBeforeOrder: make([]string, 0), + expectedAfterOrder: []string{"b", "a"}}, + } + + t.Run("with hooks registered at config time", func(t *testing.T) { + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + tracker := newOrderTracker() + hookA := createOrderTrackingHook("a", tracker) + hookB := createOrderTrackingHook("b", tracker) + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) + + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + testCase.method(execution) + + // BeforeEvaluation should execute in registration order. + assert.Equal(t, testCase.expectedBeforeOrder, tracker.orderBefore) + assert.Equal(t, testCase.expectedAfterOrder, tracker.orderAfter) + }) + } + }) + + t.Run("run before evaluation", func(t *testing.T) { + hookA := sharedtest.NewTestHook("a") + hookB := sharedtest.NewTestHook("b") + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) + + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + execution.BeforeEvaluation(context.Background()) + + hookA.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageBeforeEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, + falseValue, "testMethod"), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + GoContext: context.Background(), + }}) + + hookB.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageBeforeEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, + falseValue, "testMethod"), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + GoContext: context.Background(), + }}) + }) + + t.Run("run after evaluation", func(t *testing.T) { + hookA := sharedtest.NewTestHook("a") + hookB := sharedtest.NewTestHook("b") + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) + + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + detail := ldreason.NewEvaluationDetail(falseValue, 0, + ldreason.NewEvalReasonFallthrough()) + execution.AfterEvaluation(context.Background(), detail) + + hookA.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageAfterEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, + falseValue, "testMethod"), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + Detail: detail, + GoContext: context.Background(), + }}) + + hookB.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageAfterEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, + falseValue, "testMethod"), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + Detail: detail, + GoContext: context.Background(), + }}) + }) + + t.Run("run before evaluation with an error", func(t *testing.T) { + mockLog := ldlogtest.NewMockLog() + hookA := sharedtest.NewTestHook("a") + hookA.BeforeInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + ) (ldhooks.EvaluationSeriesData, error) { + return ldhooks.NewEvaluationSeriesBuilder(data). + Set("testA", "A"). + Build(), errors.New("something bad") + } + hookB := sharedtest.NewTestHook("b") + hookB.BeforeInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + ) (ldhooks.EvaluationSeriesData, error) { + return ldhooks.NewEvaluationSeriesBuilder(data). + Set("testB", "testB"). + Build(), nil + } + + runner := NewRunner(mockLog.Loggers, []ldhooks.Hook{hookA, hookB}) + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + + execution.BeforeEvaluation(context.Background()) + assert.Len(t, execution.hooks, 2) + assert.Len(t, execution.data, 2) + assert.Equal(t, ldContext, execution.context.Context()) + assert.Equal(t, "test-flag", execution.context.FlagKey()) + assert.Equal(t, "testMethod", execution.context.Method()) + assert.Equal(t, ldhooks.EmptyEvaluationSeriesData(), execution.data[0]) + assert.Equal(t, + ldhooks.NewEvaluationSeriesBuilder( + ldhooks.EmptyEvaluationSeriesData()). + Set("testB", "testB"). + Build(), execution.data[1]) + assert.Equal(t, falseValue, execution.context.DefaultValue()) + + assert.Equal(t, []string{"During evaluation of flag \"test-flag\", an error was encountered in \"BeforeEvaluation\" of the \"a\" hook: something bad"}, + mockLog.GetOutput(ldlog.Error)) + }) + + t.Run("run after evaluation with an error", func(t *testing.T) { + mockLog := ldlogtest.NewMockLog() + hookA := sharedtest.NewTestHook("a") + // The hooks execute in reverse order, so we have an error in B and check that A still executes. + hookA.AfterInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + detail ldreason.EvaluationDetail, + ) (ldhooks.EvaluationSeriesData, error) { + return ldhooks.NewEvaluationSeriesBuilder(data). + Set("testA", "testA"). + Build(), nil + } + hookB := sharedtest.NewTestHook("b") + hookB.AfterInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + detail ldreason.EvaluationDetail, + ) (ldhooks.EvaluationSeriesData, error) { + return ldhooks.NewEvaluationSeriesBuilder(data). + Set("testB", "B"). + Build(), errors.New("something bad") + + } + + runner := NewRunner(mockLog.Loggers, []ldhooks.Hook{hookA, hookB}) + execution := runner.prepareEvaluationSeries("test-flag", ldContext, falseValue, + "testMethod") + detail := ldreason.NewEvaluationDetail(falseValue, 0, + ldreason.NewEvalReasonFallthrough()) + + execution.AfterEvaluation(context.Background(), detail) + assert.Len(t, execution.hooks, 2) + assert.Len(t, execution.data, 2) + assert.Equal(t, ldContext, execution.context.Context()) + assert.Equal(t, "test-flag", execution.context.FlagKey()) + assert.Equal(t, "testMethod", execution.context.Method()) + assert.Equal(t, ldhooks.EmptyEvaluationSeriesData(), execution.data[1]) + assert.Equal(t, + ldhooks.NewEvaluationSeriesBuilder( + ldhooks.EmptyEvaluationSeriesData()). + Set("testA", "testA"). + Build(), execution.data[0]) + assert.Equal(t, falseValue, execution.context.DefaultValue()) + assert.Equal(t, []string{"During evaluation of flag \"test-flag\", an error was encountered in \"AfterEvaluation\" of the \"b\" hook: something bad"}, + mockLog.GetOutput(ldlog.Error)) + }) + }) + }) +} + +func createOrderTrackingHook(name string, tracker *orderTracker) sharedtest.TestHook { + h := sharedtest.NewTestHook(name) + h.BeforeInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + ) (ldhooks.EvaluationSeriesData, error) { + tracker.orderBefore = append(tracker.orderBefore, name) + return data, nil + } + h.AfterInject = func( + ctx context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + detail ldreason.EvaluationDetail, + ) (ldhooks.EvaluationSeriesData, error) { + tracker.orderAfter = append(tracker.orderAfter, name) + return data, nil + } + + return h +} diff --git a/internal/hooks/runner.go b/internal/hooks/runner.go index 27053292..59f54aef 100644 --- a/internal/hooks/runner.go +++ b/internal/hooks/runner.go @@ -1,13 +1,13 @@ package hooks import ( - "context" - "sync" + gocontext "context" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldlog" "github.com/launchdarkly/go-sdk-common/v3/ldreason" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" "github.com/launchdarkly/go-server-sdk/v7/ldhooks" ) @@ -15,22 +15,6 @@ import ( type Runner struct { hooks []ldhooks.Hook loggers ldlog.Loggers - mutex *sync.RWMutex -} - -// EvaluationExecution represents the state of a running series of evaluation stages. -type EvaluationExecution struct { - hooks []ldhooks.Hook - data []ldhooks.EvaluationSeriesData - context ldhooks.EvaluationSeriesContext -} - -func (e EvaluationExecution) withData(data []ldhooks.EvaluationSeriesData) EvaluationExecution { - return EvaluationExecution{ - hooks: e.hooks, - context: e.context, - data: data, - } } // NewRunner creates a new hook runner. @@ -38,104 +22,43 @@ func NewRunner(loggers ldlog.Loggers, hooks []ldhooks.Hook) *Runner { return &Runner{ loggers: loggers, hooks: hooks, - mutex: &sync.RWMutex{}, } } -// AddHooks adds hooks to the runner. -func (h *Runner) AddHooks(hooks ...ldhooks.Hook) { - h.mutex.Lock() - defer h.mutex.Unlock() - - h.hooks = append(h.hooks, hooks...) -} - -// getHooks returns a copy of the hooks. This copy is suitable for use when executing a series. This keeps the set -// of hooks stable for the duration of the series. This prevents things like calling the AfterEvaluation method for -// a hook that didn't have the BeforeEvaluation method called. -func (h *Runner) getHooks() []ldhooks.Hook { - h.mutex.RLock() - defer h.mutex.RUnlock() - copiedHooks := make([]ldhooks.Hook, len(h.hooks)) - copy(copiedHooks, h.hooks) - return copiedHooks +// RunEvaluation runs the evaluation series surrounding the given evaluation function. +func (h *Runner) RunEvaluation( + ctx gocontext.Context, + flagKey string, + evalContext ldcontext.Context, + defaultVal ldvalue.Value, + method string, + fn func() (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error), +) (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { + if len(h.hooks) == 0 { + return fn() + } + e := h.prepareEvaluationSeries(flagKey, evalContext, defaultVal, method) + e.BeforeEvaluation(ctx) + detail, flag, err := fn() + e.AfterEvaluation(ctx, detail) + return detail, flag, err } -// PrepareEvaluationSeries creates an EvaluationExecution suitable for executing evaluation stages and gets a copy -// of hooks to use during series execution. -// -// For an invocation of a series the same set of hooks should be used. For instance a hook added mid-evaluation should -// not be executed during the "AfterEvaluation" stage of that evaluation. -func (h *Runner) PrepareEvaluationSeries( +// PrepareEvaluationSeries creates an EvaluationExecution suitable for executing evaluation stages. +func (h *Runner) prepareEvaluationSeries( flagKey string, evalContext ldcontext.Context, defaultVal ldvalue.Value, method string, -) EvaluationExecution { - hooksForEval := h.getHooks() - - returnData := make([]ldhooks.EvaluationSeriesData, len(hooksForEval)) - for i := range hooksForEval { +) *EvaluationExecution { + returnData := make([]ldhooks.EvaluationSeriesData, len(h.hooks)) + for i := range h.hooks { returnData[i] = ldhooks.EmptyEvaluationSeriesData() } - return EvaluationExecution{ - hooks: hooksForEval, + return &EvaluationExecution{ + hooks: h.hooks, data: returnData, context: ldhooks.NewEvaluationSeriesContext(flagKey, evalContext, defaultVal, method), + loggers: &h.loggers, } } - -// BeforeEvaluation executes the BeforeEvaluation stage of registered hooks. -func (h *Runner) BeforeEvaluation(ctx context.Context, execution EvaluationExecution) EvaluationExecution { - return h.executeStage( - execution, - false, - "BeforeEvaluation", - func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { - return hook.BeforeEvaluation(ctx, execution.context, data) - }) -} - -// AfterEvaluation executes the AfterEvaluation stage of registered hooks. -func (h *Runner) AfterEvaluation( - ctx context.Context, - execution EvaluationExecution, - detail ldreason.EvaluationDetail, -) EvaluationExecution { - return h.executeStage( - execution, - true, - "AfterEvaluation", - func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { - return hook.AfterEvaluation(ctx, execution.context, data, detail) - }) -} - -func (h *Runner) executeStage( - execution EvaluationExecution, - reverse bool, - stageName string, - fn func( - hook ldhooks.Hook, - data ldhooks.EvaluationSeriesData, - ) (ldhooks.EvaluationSeriesData, error)) EvaluationExecution { - returnData := make([]ldhooks.EvaluationSeriesData, len(execution.hooks)) - iterator := newIterator(reverse, execution.hooks) - for iterator.hasNext() { - i, hook := iterator.getNext() - - outData, err := fn(hook, execution.data[i]) - if err != nil { - returnData[i] = execution.data[i] - h.loggers.Errorf( - "During evaluation of flag \"%s\", an error was encountered in \"%s\" of the \"%s\" hook: %s", - execution.context.FlagKey(), - stageName, - hook.Metadata().Name(), - err.Error()) - continue - } - returnData[i] = outData - } - return execution.withData(returnData) -} diff --git a/internal/hooks/runner_test.go b/internal/hooks/runner_test.go index 33604faa..8915eec0 100644 --- a/internal/hooks/runner_test.go +++ b/internal/hooks/runner_test.go @@ -1,330 +1,123 @@ package hooks import ( - "context" - "errors" + gocontext "context" "testing" - "github.com/launchdarkly/go-sdk-common/v3/ldlog" - "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" - "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" - "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldreason" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" + "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" "github.com/launchdarkly/go-server-sdk/v7/ldhooks" "github.com/stretchr/testify/assert" ) -func emptyExecutionAssertions(t *testing.T, res EvaluationExecution, ldContext ldcontext.Context) { - assert.Empty(t, res.hooks) - assert.Empty(t, res.data) - assert.Equal(t, ldContext, res.context.Context()) - assert.Equal(t, "test-flag", res.context.FlagKey()) - assert.Equal(t, "testMethod", res.context.Method()) - assert.Equal(t, ldvalue.Bool(false), res.context.DefaultValue()) -} - -type orderTracker struct { - orderBefore []string - orderAfter []string -} - -func newOrderTracker() *orderTracker { - return &orderTracker{ - orderBefore: make([]string, 0), - orderAfter: make([]string, 0), - } -} - func TestHookRunner(t *testing.T) { falseValue := ldvalue.Bool(false) ldContext := ldcontext.New("test-context") + flagKey := "test-flag" + testMethod := "testMethod" + defaultDetail := ldreason.NewEvaluationDetail(falseValue, 0, ldreason.NewEvalReasonFallthrough()) + basicResult := func() (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { + return defaultDetail, nil, nil + } t.Run("with no hooks", func(t *testing.T) { runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{}) t.Run("prepare evaluation series", func(t *testing.T) { - res := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, "testMethod") - emptyExecutionAssertions(t, res, ldContext) - }) - - t.Run("run before evaluation", func(t *testing.T) { - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - res := runner.BeforeEvaluation(context.Background(), execution) + res := runner.prepareEvaluationSeries(flagKey, ldContext, falseValue, testMethod) emptyExecutionAssertions(t, res, ldContext) }) - t.Run("run after evaluation", func(t *testing.T) { - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - res := runner.AfterEvaluation(context.Background(), execution, - ldreason.NewEvaluationDetail(falseValue, 0, - ldreason.NewEvalReasonFallthrough())) - emptyExecutionAssertions(t, res, ldContext) + t.Run("run execution", func(t *testing.T) { + detail, flag, err := runner.RunEvaluation( + gocontext.Background(), + flagKey, + ldContext, + falseValue, + testMethod, + basicResult, + ) + assert.Equal(t, defaultDetail, detail) + assert.Nil(t, flag) + assert.Nil(t, err) }) }) - t.Run("with hooks", func(t *testing.T) { - t.Run("prepare evaluation series", func(t *testing.T) { - hookA := sharedtest.NewTestHook("a") - hookB := sharedtest.NewTestHook("b") - runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) - - ldContext := ldcontext.New("test-context") - res := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, "testMethod") - - assert.Len(t, res.hooks, 2) - assert.Len(t, res.data, 2) - assert.Equal(t, ldContext, res.context.Context()) - assert.Equal(t, "test-flag", res.context.FlagKey()) - assert.Equal(t, "testMethod", res.context.Method()) - assert.Equal(t, falseValue, res.context.DefaultValue()) - assert.Equal(t, res.data[0], ldhooks.EmptyEvaluationSeriesData()) - assert.Equal(t, res.data[1], ldhooks.EmptyEvaluationSeriesData()) - }) - - t.Run("verify execution order", func(t *testing.T) { - testCases := []struct { - name string - method func(runner *Runner, execution EvaluationExecution) - expectedBeforeOrder []string - expectedAfterOrder []string - }{ - {name: "BeforeEvaluation", - method: func(runner *Runner, execution EvaluationExecution) { - _ = runner.BeforeEvaluation(context.Background(), execution) - }, - expectedBeforeOrder: []string{"a", "b"}, - expectedAfterOrder: make([]string, 0), - }, - {name: "AfterEvaluation", - method: func(runner *Runner, execution EvaluationExecution) { - detail := ldreason.NewEvaluationDetail(falseValue, 0, - ldreason.NewEvalReasonFallthrough()) - _ = runner.AfterEvaluation(context.Background(), execution, detail) - }, - expectedBeforeOrder: make([]string, 0), - expectedAfterOrder: []string{"b", "a"}}, - } - - t.Run("with hooks registered at config time", func(t *testing.T) { - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - tracker := newOrderTracker() - hookA := createOrderTrackingHook("a", tracker) - hookB := createOrderTrackingHook("b", tracker) - runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) - - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - testCase.method(runner, execution) - - // BeforeEvaluation should execute in registration order. - assert.Equal(t, testCase.expectedBeforeOrder, tracker.orderBefore) - assert.Equal(t, testCase.expectedAfterOrder, tracker.orderAfter) - }) - } - }) - - t.Run("with hooks registered at run time", func(t *testing.T) { - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - tracker := newOrderTracker() - hookA := createOrderTrackingHook("a", tracker) - hookB := createOrderTrackingHook("b", tracker) - runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA}) - runner.AddHooks(hookB) - - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - testCase.method(runner, execution) - - // BeforeEvaluation should execute in registration order. - assert.Equal(t, testCase.expectedBeforeOrder, tracker.orderBefore) - assert.Equal(t, testCase.expectedAfterOrder, tracker.orderAfter) - }) - } - }) - }) - - t.Run("run before evaluation", func(t *testing.T) { - hookA := sharedtest.NewTestHook("a") - hookB := sharedtest.NewTestHook("b") - runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) - - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - _ = runner.BeforeEvaluation(context.Background(), execution) - - hookA.Verify(t, sharedtest.HookExpectedCall{ - HookStage: sharedtest.HookStageBeforeEvaluation, - EvalCapture: sharedtest.HookEvalCapture{ - EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, - falseValue, "testMethod"), - EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), - GoContext: context.Background(), - }}) - - hookB.Verify(t, sharedtest.HookExpectedCall{ - HookStage: sharedtest.HookStageBeforeEvaluation, - EvalCapture: sharedtest.HookEvalCapture{ - EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, - falseValue, "testMethod"), - EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), - GoContext: context.Background(), - }}) - }) - - t.Run("run after evaluation", func(t *testing.T) { - hookA := sharedtest.NewTestHook("a") - hookB := sharedtest.NewTestHook("b") - runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) - - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - detail := ldreason.NewEvaluationDetail(falseValue, 0, - ldreason.NewEvalReasonFallthrough()) - _ = runner.AfterEvaluation(context.Background(), execution, detail) - - hookA.Verify(t, sharedtest.HookExpectedCall{ + t.Run("verify execution and order", func(t *testing.T) { + tracker := newOrderTracker() + hookA := createOrderTrackingHook("a", tracker) + hookB := createOrderTrackingHook("b", tracker) + runner := NewRunner(sharedtest.NewTestLoggers(), []ldhooks.Hook{hookA, hookB}) + + _, _, _ = runner.RunEvaluation( + gocontext.Background(), + flagKey, + ldContext, + falseValue, + testMethod, + basicResult, + ) + + hookA.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageBeforeEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext( + flagKey, + ldContext, + falseValue, + testMethod, + ), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + GoContext: gocontext.Background(), + }, + }, + sharedtest.HookExpectedCall{ HookStage: sharedtest.HookStageAfterEvaluation, EvalCapture: sharedtest.HookEvalCapture{ - EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, - falseValue, "testMethod"), + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext( + flagKey, + ldContext, + falseValue, + testMethod, + ), EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), - Detail: detail, - GoContext: context.Background(), - }}) + GoContext: gocontext.Background(), + Detail: defaultDetail, + }, + }) - hookB.Verify(t, sharedtest.HookExpectedCall{ + hookB.Verify(t, sharedtest.HookExpectedCall{ + HookStage: sharedtest.HookStageBeforeEvaluation, + EvalCapture: sharedtest.HookEvalCapture{ + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext( + flagKey, + ldContext, + falseValue, + testMethod, + ), + EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), + GoContext: gocontext.Background(), + }}, + sharedtest.HookExpectedCall{ HookStage: sharedtest.HookStageAfterEvaluation, EvalCapture: sharedtest.HookEvalCapture{ - EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext("test-flag", ldContext, - falseValue, "testMethod"), + EvaluationSeriesContext: ldhooks.NewEvaluationSeriesContext( + flagKey, + ldContext, + falseValue, + testMethod, + ), EvaluationSeriesData: ldhooks.EmptyEvaluationSeriesData(), - Detail: detail, - GoContext: context.Background(), - }}) - }) - - t.Run("run before evaluation with an error", func(t *testing.T) { - mockLog := ldlogtest.NewMockLog() - hookA := sharedtest.NewTestHook("a") - hookA.BeforeInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - ) (ldhooks.EvaluationSeriesData, error) { - return ldhooks.NewEvaluationSeriesBuilder(data). - Set("testA", "A"). - Build(), errors.New("something bad") - } - hookB := sharedtest.NewTestHook("b") - hookB.BeforeInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - ) (ldhooks.EvaluationSeriesData, error) { - return ldhooks.NewEvaluationSeriesBuilder(data). - Set("testB", "testB"). - Build(), nil - } - - runner := NewRunner(mockLog.Loggers, []ldhooks.Hook{hookA, hookB}) - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - - res := runner.BeforeEvaluation(context.Background(), execution) - assert.Len(t, res.hooks, 2) - assert.Len(t, res.data, 2) - assert.Equal(t, ldContext, res.context.Context()) - assert.Equal(t, "test-flag", res.context.FlagKey()) - assert.Equal(t, "testMethod", res.context.Method()) - assert.Equal(t, ldhooks.EmptyEvaluationSeriesData(), res.data[0]) - assert.Equal(t, - ldhooks.NewEvaluationSeriesBuilder( - ldhooks.EmptyEvaluationSeriesData()). - Set("testB", "testB"). - Build(), res.data[1]) - assert.Equal(t, falseValue, res.context.DefaultValue()) - - assert.Equal(t, []string{"During evaluation of flag \"test-flag\", an error was encountered in \"BeforeEvaluation\" of the \"a\" hook: something bad"}, - mockLog.GetOutput(ldlog.Error)) - }) - - t.Run("run after evaluation with an error", func(t *testing.T) { - mockLog := ldlogtest.NewMockLog() - hookA := sharedtest.NewTestHook("a") - // The hooks execute in reverse order, so we have an error in B and check that A still executes. - hookA.AfterInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - detail ldreason.EvaluationDetail, - ) (ldhooks.EvaluationSeriesData, error) { - return ldhooks.NewEvaluationSeriesBuilder(data). - Set("testA", "testA"). - Build(), nil - } - hookB := sharedtest.NewTestHook("b") - hookB.AfterInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - detail ldreason.EvaluationDetail, - ) (ldhooks.EvaluationSeriesData, error) { - return ldhooks.NewEvaluationSeriesBuilder(data). - Set("testB", "B"). - Build(), errors.New("something bad") - - } - - runner := NewRunner(mockLog.Loggers, []ldhooks.Hook{hookA, hookB}) - execution := runner.PrepareEvaluationSeries("test-flag", ldContext, falseValue, - "testMethod") - detail := ldreason.NewEvaluationDetail(falseValue, 0, - ldreason.NewEvalReasonFallthrough()) + GoContext: gocontext.Background(), + Detail: defaultDetail, + }, + }) - res := runner.AfterEvaluation(context.Background(), execution, detail) - assert.Len(t, res.hooks, 2) - assert.Len(t, res.data, 2) - assert.Equal(t, ldContext, res.context.Context()) - assert.Equal(t, "test-flag", res.context.FlagKey()) - assert.Equal(t, "testMethod", res.context.Method()) - assert.Equal(t, ldhooks.EmptyEvaluationSeriesData(), res.data[1]) - assert.Equal(t, - ldhooks.NewEvaluationSeriesBuilder( - ldhooks.EmptyEvaluationSeriesData()). - Set("testA", "testA"). - Build(), res.data[0]) - assert.Equal(t, falseValue, res.context.DefaultValue()) - assert.Equal(t, []string{"During evaluation of flag \"test-flag\", an error was encountered in \"AfterEvaluation\" of the \"b\" hook: something bad"}, - mockLog.GetOutput(ldlog.Error)) - }) + // BeforeEvaluation should execute in registration order. + assert.Equal(t, []string{"a", "b"}, tracker.orderBefore) + assert.Equal(t, []string{"b", "a"}, tracker.orderAfter) }) } - -func createOrderTrackingHook(name string, tracker *orderTracker) sharedtest.TestHook { - h := sharedtest.NewTestHook(name) - h.BeforeInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - ) (ldhooks.EvaluationSeriesData, error) { - tracker.orderBefore = append(tracker.orderBefore, name) - return data, nil - } - h.AfterInject = func( - ctx context.Context, - seriesContext ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - detail ldreason.EvaluationDetail, - ) (ldhooks.EvaluationSeriesData, error) { - tracker.orderAfter = append(tracker.orderAfter, name) - return data, nil - } - - return h -} diff --git a/ldclient.go b/ldclient.go index 430b86e1..1ad50390 100644 --- a/ldclient.go +++ b/ldclient.go @@ -27,7 +27,6 @@ import ( "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" "github.com/launchdarkly/go-server-sdk/v7/internal/hooks" "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" - "github.com/launchdarkly/go-server-sdk/v7/ldhooks" "github.com/launchdarkly/go-server-sdk/v7/subsystems" "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl" ) @@ -399,27 +398,39 @@ func (client *LDClient) migrationVariation( ctx gocontext.Context, key string, context ldcontext.Context, defaultStage ldmigration.Stage, eventsScope eventsScope, method string, ) (ldmigration.Stage, interfaces.LDMigrationOpTracker, error) { - stageValue := ldvalue.String(string(defaultStage)) - hookExecution := client.hookRunner.PrepareEvaluationSeries(key, context, stageValue, method) - hookExecution = client.hookRunner.BeforeEvaluation(ctx, hookExecution) - detail, flag, err := client.variationAndFlag(key, context, stageValue, true, - eventsScope) - tracker := NewMigrationOpTracker(key, flag, context, detail, defaultStage) + defaultStageAsValue := ldvalue.String(string(defaultStage)) + + detail, flag, err := client.hookRunner.RunEvaluation( + ctx, + key, + context, + defaultStageAsValue, + method, + func() (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { + detail, flag, err := client.variationAndFlag(key, context, defaultStageAsValue, true, + eventsScope) + + if err != nil { + // Detail will already contain the default. + // We do not have an error on the flag-not-found case. + return detail, flag, nil + } - if err != nil { - return defaultStage, tracker, nil - } + _, err = ldmigration.ParseStage(detail.Value.StringValue()) + if err != nil { + detail = ldreason.NewEvaluationDetailForError(ldreason.EvalErrorWrongType, ldvalue.String(string(defaultStage))) + return detail, flag, fmt.Errorf("%s; returning default stage %s", err, defaultStage) + } - stage, err := ldmigration.ParseStage(detail.Value.StringValue()) - if err != nil { - detail = ldreason.NewEvaluationDetailForError(ldreason.EvalErrorWrongType, ldvalue.String(string(defaultStage))) - client.hookRunner.AfterEvaluation(ctx, hookExecution, detail) - tracker := NewMigrationOpTracker(key, flag, context, detail, defaultStage) - return defaultStage, tracker, fmt.Errorf("%s; returning default stage %s", err, defaultStage) - } + return detail, flag, err + }, + ) + + tracker := NewMigrationOpTracker(key, flag, context, detail, defaultStage) + // Stage will have already been parsed and defaulted. + stage, _ := ldmigration.ParseStage(detail.Value.StringValue()) - client.hookRunner.AfterEvaluation(ctx, hookExecution, detail) - return stage, tracker, nil + return stage, tracker, err } // Identify reports details about an evaluation context. @@ -723,7 +734,7 @@ func (client *LDClient) AllFlagsState(context ldcontext.Context, options ...flag // // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go func (client *LDClient) BoolVariation(key string, context ldcontext.Context, defaultVal bool) (bool, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, client.eventsDefault, boolVarFuncName) return detail.Value.BoolValue(), err } @@ -737,7 +748,7 @@ func (client *LDClient) BoolVariationDetail( context ldcontext.Context, defaultVal bool, ) (bool, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, client.eventsWithReasons, boolVarDetailFuncName) return detail.Value.BoolValue(), detail, err } @@ -757,7 +768,7 @@ func (client *LDClient) BoolVariationCtx( context ldcontext.Context, defaultVal bool, ) (bool, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, client.eventsDefault, boolVarExFuncName) return detail.Value.BoolValue(), err } @@ -775,7 +786,7 @@ func (client *LDClient) BoolVariationDetailCtx( context ldcontext.Context, defaultVal bool, ) (bool, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, client.eventsWithReasons, boolVarDetailExFuncName) return detail.Value.BoolValue(), detail, err } @@ -790,7 +801,7 @@ func (client *LDClient) BoolVariationDetailCtx( // // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go func (client *LDClient) IntVariation(key string, context ldcontext.Context, defaultVal int) (int, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, client.eventsDefault, intVarFuncName) return detail.Value.IntValue(), err } @@ -804,7 +815,7 @@ func (client *LDClient) IntVariationDetail( context ldcontext.Context, defaultVal int, ) (int, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, client.eventsWithReasons, intVarDetailFuncName) return detail.Value.IntValue(), detail, err } @@ -826,7 +837,7 @@ func (client *LDClient) IntVariationCtx( context ldcontext.Context, defaultVal int, ) (int, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, client.eventsDefault, intVarExFuncName) return detail.Value.IntValue(), err } @@ -844,7 +855,7 @@ func (client *LDClient) IntVariationDetailCtx( context ldcontext.Context, defaultVal int, ) (int, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, client.eventsWithReasons, intVarDetailExFuncName) return detail.Value.IntValue(), detail, err } @@ -857,7 +868,7 @@ func (client *LDClient) IntVariationDetailCtx( // // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go func (client *LDClient) Float64Variation(key string, context ldcontext.Context, defaultVal float64) (float64, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), true, client.eventsDefault, floatVarFuncName) return detail.Value.Float64Value(), err } @@ -871,7 +882,7 @@ func (client *LDClient) Float64VariationDetail( context ldcontext.Context, defaultVal float64, ) (float64, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), true, client.eventsWithReasons, floatVarDetailFuncName) return detail.Value.Float64Value(), detail, err } @@ -891,7 +902,7 @@ func (client *LDClient) Float64VariationCtx( context ldcontext.Context, defaultVal float64, ) (float64, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, client.eventsDefault, floatVarExFuncName) return detail.Value.Float64Value(), err } @@ -909,7 +920,7 @@ func (client *LDClient) Float64VariationDetailCtx( context ldcontext.Context, defaultVal float64, ) (float64, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, client.eventsWithReasons, floatVarDetailExFuncName) return detail.Value.Float64Value(), detail, err } @@ -922,7 +933,7 @@ func (client *LDClient) Float64VariationDetailCtx( // // For more information, see the Reference Guide: https://docs.launchdarkly.com/sdk/features/evaluating#go func (client *LDClient) StringVariation(key string, context ldcontext.Context, defaultVal string) (string, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, client.eventsDefault, stringVarFuncName) return detail.Value.StringValue(), err } @@ -936,7 +947,7 @@ func (client *LDClient) StringVariationDetail( context ldcontext.Context, defaultVal string, ) (string, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, client.eventsWithReasons, stringVarDetailFuncName) return detail.Value.StringValue(), detail, err } @@ -956,7 +967,7 @@ func (client *LDClient) StringVariationCtx( context ldcontext.Context, defaultVal string, ) (string, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, client.eventsDefault, stringVarExFuncName) return detail.Value.StringValue(), err } @@ -974,7 +985,7 @@ func (client *LDClient) StringVariationDetailCtx( context ldcontext.Context, defaultVal string, ) (string, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, + detail, _, err := client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, client.eventsWithReasons, stringVarDetailExFuncName) return detail.Value.StringValue(), detail, err } @@ -1007,7 +1018,7 @@ func (client *LDClient) JSONVariation( context ldcontext.Context, defaultVal ldvalue.Value, ) (ldvalue.Value, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, defaultVal, false, client.eventsDefault, + detail, _, err := client.variationWithHooks(gocontext.TODO(), key, context, defaultVal, false, client.eventsDefault, jsonVarFuncName) return detail.Value, err } @@ -1021,8 +1032,15 @@ func (client *LDClient) JSONVariationDetail( context ldcontext.Context, defaultVal ldvalue.Value, ) (ldvalue.Value, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(gocontext.TODO(), key, context, defaultVal, false, client.eventsWithReasons, - jsonVarDetailFuncName) + detail, _, err := client.variationWithHooks( + gocontext.TODO(), + key, + context, + defaultVal, + false, + client.eventsWithReasons, + jsonVarDetailFuncName, + ) return detail.Value, detail, err } @@ -1057,7 +1075,7 @@ func (client *LDClient) JSONVariationCtx( context ldcontext.Context, defaultVal ldvalue.Value, ) (ldvalue.Value, error) { - detail, err := client.variationWithHooks(ctx, key, context, defaultVal, false, client.eventsDefault, + detail, _, err := client.variationWithHooks(ctx, key, context, defaultVal, false, client.eventsDefault, jsonVarExFuncName) return detail.Value, err } @@ -1075,7 +1093,7 @@ func (client *LDClient) JSONVariationDetailCtx( context ldcontext.Context, defaultVal ldvalue.Value, ) (ldvalue.Value, ldreason.EvaluationDetail, error) { - detail, err := client.variationWithHooks(ctx, key, context, defaultVal, false, client.eventsWithReasons, + detail, _, err := client.variationWithHooks(ctx, key, context, defaultVal, false, client.eventsWithReasons, jsonVarDetailExFuncName) return detail.Value, detail, err } @@ -1144,18 +1162,6 @@ func (client *LDClient) WithEventsDisabled(disabled bool) interfaces.LDClientInt return client.withEventsDisabled } -// Generic method for evaluating a feature flag for a given evaluation context. -func (client *LDClient) variation( - key string, - context ldcontext.Context, - defaultVal ldvalue.Value, - checkType bool, - eventsScope eventsScope, -) (ldreason.EvaluationDetail, error) { - detail, _, err := client.variationAndFlag(key, context, defaultVal, checkType, eventsScope) - return detail, err -} - func (client *LDClient) variationWithHooks( context gocontext.Context, key string, @@ -1164,12 +1170,18 @@ func (client *LDClient) variationWithHooks( checkType bool, eventsScope eventsScope, method string, -) (ldreason.EvaluationDetail, error) { - execution := client.hookRunner.PrepareEvaluationSeries(key, evalContext, defaultVal, method) - execution = client.hookRunner.BeforeEvaluation(context, execution) - detail, err := client.variation(key, evalContext, defaultVal, checkType, eventsScope) - client.hookRunner.AfterEvaluation(context, execution, detail) - return detail, err +) (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { + detail, flag, err := client.hookRunner.RunEvaluation( + context, + key, + evalContext, + defaultVal, + method, + func() (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { + return client.variationAndFlag(key, evalContext, defaultVal, checkType, eventsScope) + }, + ) + return detail, flag, err } // Generic method for evaluating a feature flag for a given evaluation context, @@ -1297,13 +1309,6 @@ func (client *LDClient) evaluateInternal( return result, feature, nil } -// AddHooks allows adding a hook after creating the LDClient. -// -// If a hook can be registered at LDClient initialization, then the Hooks field of Config can be used instead. -func (client *LDClient) AddHooks(hooks ...ldhooks.Hook) { - client.hookRunner.AddHooks(hooks...) -} - func newEvaluationError(jsonValue ldvalue.Value, errorKind ldreason.EvalErrorKind) ldreason.EvaluationDetail { return ldreason.EvaluationDetail{ Value: jsonValue, diff --git a/ldclient_events.go b/ldclient_events.go index 936268e5..cb2d0de4 100644 --- a/ldclient_events.go +++ b/ldclient_events.go @@ -106,14 +106,14 @@ func (c *clientEventsDisabledDecorator) BoolVariation( context ldcontext.Context, defaultVal bool, ) (bool, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, c.scope, boolVarFuncName) return detail.Value.BoolValue(), err } func (c *clientEventsDisabledDecorator) BoolVariationDetail(key string, context ldcontext.Context, defaultVal bool) ( bool, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Bool(defaultVal), true, c.scope, boolVarDetailFuncName) return detail.Value.BoolValue(), detail, err } @@ -124,7 +124,7 @@ func (c *clientEventsDisabledDecorator) BoolVariationCtx( context ldcontext.Context, defaultVal bool, ) (bool, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, c.scope, boolVarExFuncName) return detail.Value.BoolValue(), err } @@ -136,7 +136,7 @@ func (c *clientEventsDisabledDecorator) BoolVariationDetailCtx( defaultVal bool, ) ( bool, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Bool(defaultVal), true, c.scope, boolVarDetailExFuncName) return detail.Value.BoolValue(), detail, err } @@ -146,14 +146,14 @@ func (c *clientEventsDisabledDecorator) IntVariation( context ldcontext.Context, defaultVal int, ) (int, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, c.scope, intVarFuncName) return detail.Value.IntValue(), err } func (c *clientEventsDisabledDecorator) IntVariationDetail(key string, context ldcontext.Context, defaultVal int) ( int, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Int(defaultVal), true, c.scope, intVarDetailFuncName) return detail.Value.IntValue(), detail, err } @@ -164,7 +164,7 @@ func (c *clientEventsDisabledDecorator) IntVariationCtx( context ldcontext.Context, defaultVal int, ) (int, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, c.scope, intVarExFuncName) return detail.Value.IntValue(), err } @@ -176,14 +176,14 @@ func (c *clientEventsDisabledDecorator) IntVariationDetailCtx( defaultVal int, ) ( int, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Int(defaultVal), true, c.scope, intVarDetailExFuncName) return detail.Value.IntValue(), detail, err } func (c *clientEventsDisabledDecorator) Float64Variation(key string, context ldcontext.Context, defaultVal float64) ( float64, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), true, c.scope, floatVarFuncName) return detail.Value.Float64Value(), err } @@ -194,7 +194,7 @@ func (c *clientEventsDisabledDecorator) Float64VariationDetail( defaultVal float64, ) ( float64, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.Float64(defaultVal), true, c.scope, floatVarDetailFuncName) return detail.Value.Float64Value(), detail, err } @@ -206,7 +206,7 @@ func (c *clientEventsDisabledDecorator) Float64VariationCtx( defaultVal float64, ) ( float64, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, c.scope, floatVarExFuncName) return detail.Value.Float64Value(), err } @@ -218,14 +218,14 @@ func (c *clientEventsDisabledDecorator) Float64VariationDetailCtx( defaultVal float64, ) ( float64, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.Float64(defaultVal), true, c.scope, floatVarDetailExFuncName) return detail.Value.Float64Value(), detail, err } func (c *clientEventsDisabledDecorator) StringVariation(key string, context ldcontext.Context, defaultVal string) ( string, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, c.scope, stringVarExFuncName) return detail.Value.StringValue(), err } @@ -236,7 +236,7 @@ func (c *clientEventsDisabledDecorator) StringVariationDetail( defaultVal string, ) ( string, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), + detail, _, err := c.client.variationWithHooks(gocontext.TODO(), key, context, ldvalue.String(defaultVal), true, c.scope, stringVarDetailFuncName) return detail.Value.StringValue(), detail, err } @@ -248,7 +248,7 @@ func (c *clientEventsDisabledDecorator) StringVariationCtx( defaultVal string, ) ( string, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, c.scope, + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, c.scope, stringVarExFuncName) return detail.Value.StringValue(), err } @@ -260,7 +260,7 @@ func (c *clientEventsDisabledDecorator) StringVariationDetailCtx( defaultVal string, ) ( string, ldreason.EvaluationDetail, error) { - detail, err := c.client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, c.scope, + detail, _, err := c.client.variationWithHooks(ctx, key, context, ldvalue.String(defaultVal), true, c.scope, stringVarDetailExFuncName) return detail.Value.StringValue(), detail, err } @@ -282,7 +282,15 @@ func (c *clientEventsDisabledDecorator) MigrationVariationCtx( func (c *clientEventsDisabledDecorator) JSONVariation(key string, context ldcontext.Context, defaultVal ldvalue.Value) ( ldvalue.Value, error) { - detail, err := c.client.variation(key, context, defaultVal, true, c.scope) + detail, _, err := c.client.variationWithHooks( + gocontext.TODO(), + key, + context, + defaultVal, + true, + c.scope, + jsonVarFuncName, + ) return detail.Value, err } @@ -292,7 +300,15 @@ func (c *clientEventsDisabledDecorator) JSONVariationDetail( defaultVal ldvalue.Value, ) ( ldvalue.Value, ldreason.EvaluationDetail, error) { - detail, err := c.client.variation(key, context, defaultVal, true, c.scope) + detail, _, err := c.client.variationWithHooks( + gocontext.TODO(), + key, + context, + defaultVal, + true, + c.scope, + jsonVarDetailFuncName, + ) return detail.Value, detail, err } diff --git a/ldclient_hooks_test.go b/ldclient_hooks_test.go index 092e6e54..da69edae 100644 --- a/ldclient_hooks_test.go +++ b/ldclient_hooks_test.go @@ -268,24 +268,3 @@ func TestHooksAreExecutedForAllVariationMethods(t *testing.T) { }) } } - -func TestUsesAStableSetOfHooksDuringEvaluation(t *testing.T) { - client, _ := MakeCustomClient("", Config{Offline: true, Hooks: []ldhooks.Hook{}}, 0) - - hook := sharedtest.NewTestHook("test-hook") - sneaky := sharedtest.NewTestHook("sneaky") - hook.BeforeInject = func( - ctx gocontext.Context, - context ldhooks.EvaluationSeriesContext, - data ldhooks.EvaluationSeriesData, - ) (ldhooks.EvaluationSeriesData, error) { - client.AddHooks(sneaky) - return ldhooks.NewEvaluationSeriesBuilder(data).Set("test-key", "test-value").Build(), nil - } - - client.AddHooks(hook) - - _, _ = client.BoolVariation("flag-key", ldcontext.New("test-context"), false) - - sneaky.VerifyNoCalls(t) -} diff --git a/testservice/go.sum b/testservice/go.sum index 7039a8ed..8dfcc99e 100644 --- a/testservice/go.sum +++ b/testservice/go.sum @@ -29,6 +29,7 @@ github.com/launchdarkly/go-test-helpers/v2 v2.2.0/go.mod h1:L7+th5govYp5oKU9iN7T github.com/launchdarkly/go-test-helpers/v2 v2.3.1 h1:KXUAQVTeHNcWVDVQ94uEkybI+URXI9rEd7E553EsZFw= github.com/launchdarkly/go-test-helpers/v2 v2.3.1/go.mod h1:L7+th5govYp5oKU9iN7To5PgznBuIjBPn+ejqKR0avw= github.com/launchdarkly/go-test-helpers/v3 v3.0.2 h1:rh0085g1rVJM5qIukdaQ8z1XTWZztbJ49vRZuveqiuU= +github.com/launchdarkly/go-test-helpers/v3 v3.0.2/go.mod h1:u2ZvJlc/DDJTFrshWW50tWMZHLVYXofuSHUfTU/eIwM= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= @@ -40,6 +41,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= golang.org/x/exp v0.0.0-20220823124025-807a23277127 h1:S4NrSKDfihhl3+4jSTgwoIevKxX9p7Iv9x++OEIptDo= @@ -51,3 +53,4 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/testservice/sdk_client_entity.go b/testservice/sdk_client_entity.go index f66cd9af..3a979360 100644 --- a/testservice/sdk_client_entity.go +++ b/testservice/sdk_client_entity.go @@ -14,6 +14,7 @@ import ( "github.com/launchdarkly/go-server-sdk/v7/interfaces" "github.com/launchdarkly/go-server-sdk/v7/interfaces/flagstate" "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" "github.com/launchdarkly/go-server-sdk/v7/testservice/servicedef" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" @@ -431,6 +432,15 @@ func makeSDKConfig(config servicedef.SDKConfigParams, sdkLog ldlog.Loggers) ld.C } } + if config.Hooks != nil { + hooks := make([]ldhooks.Hook, 0) + for _, hookConfig := range config.Hooks.Hooks { + hookInstance := newTestHook(hookConfig.Name, hookConfig.CallbackURI, hookConfig.Data) + hooks = append(hooks, hookInstance) + } + ret.Hooks = hooks + } + return ret } diff --git a/testservice/service.go b/testservice/service.go index 91978db9..59973a40 100644 --- a/testservice/service.go +++ b/testservice/service.go @@ -39,6 +39,7 @@ var capabilities = []string{ servicedef.CapabilityEventSampling, servicedef.CapabilityInlineContext, servicedef.CapabilityAnonymousRedaction, + servicedef.CapabilityEvaluationHooks, } // gets the specified environment variable, or the default if not set diff --git a/testservice/servicedef/command_params.go b/testservice/servicedef/command_params.go index 37f8c70b..9b5f0978 100644 --- a/testservice/servicedef/command_params.go +++ b/testservice/servicedef/command_params.go @@ -146,3 +146,24 @@ type MigrationOperationParams struct { type MigrationOperationResponse struct { Result interface{} `json:"result"` } + +type HookStage string + +const ( + BeforeEvaluation HookStage = "beforeEvaluation" + AfterEvaluation HookStage = "afterEvaluation" +) + +type EvaluationSeriesContext struct { + FlagKey string `json:"flagKey"` + Context ldcontext.Context `json:"context"` + DefaultValue ldvalue.Value `json:"defaultValue"` + Method string `json:"method"` +} + +type HookExecutionPayload struct { + EvaluationSeriesContext EvaluationSeriesContext `json:"evaluationSeriesContext,omitempty"` + EvaluationSeriesData map[string]ldvalue.Value `json:"evaluationSeriesData,omitempty"` + EvaluationDetail EvaluateFlagResponse `json:"evaluationDetail,omitempty"` + Stage HookStage `json:"stage,omitempty"` +} diff --git a/testservice/servicedef/sdk_config.go b/testservice/servicedef/sdk_config.go index 47c9eef5..cf2e93e0 100644 --- a/testservice/servicedef/sdk_config.go +++ b/testservice/servicedef/sdk_config.go @@ -16,6 +16,7 @@ type SDKConfigParams struct { PersistentDataStore *SDKConfigPersistentDataStoreParams `json:"persistentDataStore,omitempty"` BigSegments *SDKConfigBigSegmentsParams `json:"bigSegments,omitempty"` Tags *SDKConfigTagsParams `json:"tags,omitempty"` + Hooks *SDKConfigHooksParams `json:"hooks,omitempty"` } type SDKConfigServiceEndpointsParams struct { @@ -61,3 +62,15 @@ type SDKConfigTagsParams struct { ApplicationID ldvalue.OptionalString `json:"applicationId,omitempty"` ApplicationVersion ldvalue.OptionalString `json:"applicationVersion,omitempty"` } + +type SDKConfigEvaluationHookData map[string]ldvalue.Value + +type SDKConfigHookInstance struct { + Name string `json:"name"` + CallbackURI string `json:"callbackUri"` + Data map[HookStage]SDKConfigEvaluationHookData `json:"data,omitempty"` +} + +type SDKConfigHooksParams struct { + Hooks []SDKConfigHookInstance `json:"hooks"` +} diff --git a/testservice/servicedef/service_params.go b/testservice/servicedef/service_params.go index 095ee476..567bf3ad 100644 --- a/testservice/servicedef/service_params.go +++ b/testservice/servicedef/service_params.go @@ -20,6 +20,7 @@ const ( CapabilityEventSampling = "event-sampling" CapabilityInlineContext = "inline-context" CapabilityAnonymousRedaction = "anonymous-redaction" + CapabilityEvaluationHooks = "evaluation-hooks" ) type StatusRep struct { diff --git a/testservice/test_hook.go b/testservice/test_hook.go new file mode 100644 index 00000000..afd48e25 --- /dev/null +++ b/testservice/test_hook.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + + "github.com/launchdarkly/go-sdk-common/v3/ldreason" + "github.com/launchdarkly/go-sdk-common/v3/ldvalue" + "github.com/launchdarkly/go-server-sdk/v7/ldhooks" + "github.com/launchdarkly/go-server-sdk/v7/testservice/servicedef" +) + +type testHook struct { + ldhooks.Unimplemented + metadata ldhooks.Metadata + dataPayloads map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData + callbackService callbackService +} + +func newTestHook( + name string, + endpoint string, + data map[servicedef.HookStage]servicedef.SDKConfigEvaluationHookData, +) testHook { + return testHook{ + metadata: ldhooks.NewMetadata(name), + dataPayloads: data, + callbackService: callbackService{baseURL: endpoint}, + } +} + +func (t testHook) Metadata() ldhooks.Metadata { + return t.metadata +} + +func (t testHook) BeforeEvaluation( + _ context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, +) (ldhooks.EvaluationSeriesData, error) { + err := t.callbackService.post("", servicedef.HookExecutionPayload{ + EvaluationSeriesContext: evaluationSeriesContextToService(seriesContext), + EvaluationSeriesData: evaluationSeriesDataToService(data), + Stage: servicedef.BeforeEvaluation, + }, nil) + if err != nil { + return ldhooks.EmptyEvaluationSeriesData(), err + } + dataBuilder := ldhooks.NewEvaluationSeriesBuilder(data) + stageData := t.dataPayloads[servicedef.BeforeEvaluation] + for key, value := range stageData { + dataBuilder.Set(key, value) + } + return dataBuilder.Build(), nil +} + +func (t testHook) AfterEvaluation( + _ context.Context, + seriesContext ldhooks.EvaluationSeriesContext, + data ldhooks.EvaluationSeriesData, + detail ldreason.EvaluationDetail, +) (ldhooks.EvaluationSeriesData, error) { + err := t.callbackService.post("", servicedef.HookExecutionPayload{ + EvaluationSeriesContext: evaluationSeriesContextToService(seriesContext), + EvaluationSeriesData: evaluationSeriesDataToService(data), + Stage: servicedef.AfterEvaluation, + EvaluationDetail: *detailToService(detail), + }, nil) + if err != nil { + return ldhooks.EmptyEvaluationSeriesData(), err + } + dataBuilder := ldhooks.NewEvaluationSeriesBuilder(data) + stageData := t.dataPayloads[servicedef.AfterEvaluation] + for key, value := range stageData { + dataBuilder.Set(key, value) + } + return data, nil +} + +func evaluationSeriesContextToService( + seriesContext ldhooks.EvaluationSeriesContext, +) servicedef.EvaluationSeriesContext { + return servicedef.EvaluationSeriesContext{ + FlagKey: seriesContext.FlagKey(), + Context: seriesContext.Context(), + DefaultValue: seriesContext.DefaultValue(), + Method: seriesContext.Method(), + } +} + +func evaluationSeriesDataToService(seriesData ldhooks.EvaluationSeriesData) map[string]ldvalue.Value { + ret := make(map[string]ldvalue.Value) + for key, value := range seriesData.AsAnyMap() { + ret[key] = ldvalue.CopyArbitraryValue(value) + } + return ret +} + +func detailToService(detail ldreason.EvaluationDetail) *servicedef.EvaluateFlagResponse { + rep := &servicedef.EvaluateFlagResponse{ + Value: detail.Value, + VariationIndex: detail.VariationIndex.AsPointer(), + } + if detail.Reason.IsDefined() { + rep.Reason = &detail.Reason + } + return rep +}