Skip to content

Commit

Permalink
handle and test for mismatched types and nils internally
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob ODwyer committed Oct 26, 2023
1 parent 22b70b6 commit b946d9b
Show file tree
Hide file tree
Showing 2 changed files with 319 additions and 12 deletions.
122 changes: 110 additions & 12 deletions openfeature_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import (

// DevCycleProvider implements the FeatureProvider interface and provides functions for evaluating flags
type DevCycleProvider struct {
Client *Client
Client ClientImpl
}

type ClientImpl interface {
Variable(userdata User, key string, defaultValue interface{}) (Variable, error)
}

// Metadata returns the metadata of the provider
Expand Down Expand Up @@ -47,10 +51,32 @@ func (p DevCycleProvider) BooleanEvaluation(ctx context.Context, flag string, de
}

if variable.IsDefaulted {
return openfeature.BoolResolutionDetail{Value: defaultValue, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason}}
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
}

return openfeature.BoolResolutionDetail{Value: variable.Value.(bool), ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason}}
switch variable.Value.(type) {
case bool:
return openfeature.BoolResolutionDetail{
Value: variable.Value.(bool),
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason},
}
case nil:
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
default:
return openfeature.BoolResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
}
}
}

// StringEvaluation returns a string flag
Expand All @@ -76,10 +102,32 @@ func (p DevCycleProvider) StringEvaluation(ctx context.Context, flag string, def
}

if variable.IsDefaulted {
return openfeature.StringResolutionDetail{Value: defaultValue, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason}}
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
}

return openfeature.StringResolutionDetail{Value: variable.Value.(string), ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason}}
switch variable.Value.(type) {
case string:
return openfeature.StringResolutionDetail{
Value: variable.Value.(string),
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason},
}
case nil:
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
default:
return openfeature.StringResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
}
}
}

// FloatEvaluation returns a float flag
Expand All @@ -105,10 +153,32 @@ func (p DevCycleProvider) FloatEvaluation(ctx context.Context, flag string, defa
}

if variable.IsDefaulted {
return openfeature.FloatResolutionDetail{Value: defaultValue, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason}}
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
}

return openfeature.FloatResolutionDetail{Value: variable.Value.(float64), ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason}}
switch castValue := variable.Value.(type) {
case float64:
return openfeature.FloatResolutionDetail{
Value: castValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason},
}
case nil:
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
default:
return openfeature.FloatResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
}
}
}

// IntEvaluation returns an int flag
Expand All @@ -134,10 +204,32 @@ func (p DevCycleProvider) IntEvaluation(ctx context.Context, flag string, defaul
}

if variable.IsDefaulted {
return openfeature.IntResolutionDetail{Value: defaultValue, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason}}
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
}

return openfeature.IntResolutionDetail{Value: int64(variable.Value.(float64)), ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason}}
switch castValue := variable.Value.(type) {
case float64:
return openfeature.IntResolutionDetail{
Value: int64(castValue),
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason},
}
case nil:
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
default:
return openfeature.IntResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
}
}
}

// ObjectEvaluation returns an object flag
Expand All @@ -163,11 +255,17 @@ func (p DevCycleProvider) ObjectEvaluation(ctx context.Context, flag string, def
}
}

if variable.IsDefaulted {
return openfeature.InterfaceResolutionDetail{Value: defaultValue, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason}}
if variable.IsDefaulted || variable.Value == nil {
return openfeature.InterfaceResolutionDetail{
Value: defaultValue,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.DefaultReason},
}
}

return openfeature.InterfaceResolutionDetail{Value: variable.Value, ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason}}
return openfeature.InterfaceResolutionDetail{
Value: variable.Value,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{Reason: openfeature.TargetingMatchReason},
}
}

// Hooks returns hooks
Expand Down
209 changes: 209 additions & 0 deletions openfeature_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,212 @@ func Test_ObjectEvaluation_TargetMatchInvalidType(t *testing.T) {
require.Equal(t, defaultValue, resolutionDetail.Value)
require.Equal(t, openfeature.DefaultReason, resolutionDetail.ProviderResolutionDetail.Reason)
}

type StubClient struct {
variable Variable
err error
}

func (c StubClient) Variable(userdata User, key string, defaultValue interface{}) (Variable, error) {
return c.variable, c.err
}

func TestEvaluationValueHandling(t *testing.T) {
evalCtx := openfeature.FlattenedContext{"userId": "1234"}
testCases := []struct {
name string
method string
variable Variable
errorResult error
expected any
}{
{
name: "BooleanEvaluation default",
method: "BooleanEvaluation",
variable: Variable{IsDefaulted: true},
expected: openfeature.BoolResolutionDetail{
Value: false,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "BooleanEvaluation nil",
method: "BooleanEvaluation",
variable: Variable{},
expected: openfeature.BoolResolutionDetail{
Value: false,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "BooleanEvaluation unexpected type",
method: "BooleanEvaluation",
variable: Variable{BaseVariable: BaseVariable{Value: "not a bool"}},
expected: openfeature.BoolResolutionDetail{
Value: false,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
},
},
{
name: "StringEvaluation default",
method: "StringEvaluation",
variable: Variable{IsDefaulted: true},
expected: openfeature.StringResolutionDetail{
Value: "default",
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "StringEvaluation nil",
method: "StringEvaluation",
variable: Variable{},
expected: openfeature.StringResolutionDetail{
Value: "default",
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "StringEvaluation unexpected type",
method: "StringEvaluation",
variable: Variable{BaseVariable: BaseVariable{Value: 1234}},
expected: openfeature.StringResolutionDetail{
Value: "default",
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
},
},
{
name: "FloatEvaluation default",
method: "FloatEvaluation",
variable: Variable{IsDefaulted: true},
expected: openfeature.FloatResolutionDetail{
Value: 1.23,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "FloatEvaluation nil",
method: "FloatEvaluation",
variable: Variable{},
expected: openfeature.FloatResolutionDetail{
Value: 1.23,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "FloatEvaluation unexpected type",
method: "FloatEvaluation",
variable: Variable{BaseVariable: BaseVariable{Value: "not a float64"}},
expected: openfeature.FloatResolutionDetail{
Value: 1.23,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
},
},
{
name: "IntEvaluation default",
method: "IntEvaluation",
variable: Variable{IsDefaulted: true},
expected: openfeature.IntResolutionDetail{
Value: 123,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "IntEvaluation nil",
method: "IntEvaluation",
variable: Variable{},
expected: openfeature.IntResolutionDetail{
Value: 123,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "IntEvaluation unexpected type",
method: "IntEvaluation",
variable: Variable{BaseVariable: BaseVariable{Value: "not a int64"}},
expected: openfeature.IntResolutionDetail{
Value: 123,
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.ErrorReason,
ResolutionError: openfeature.NewTypeMismatchResolutionError("Variable result is nil"),
},
},
},
{
name: "ObjectEvaluation default",
method: "ObjectEvaluation",
variable: Variable{IsDefaulted: true},
expected: openfeature.InterfaceResolutionDetail{
Value: map[string]bool{"default": true},
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
{
name: "ObjectEvaluation nil",
method: "ObjectEvaluation",
variable: Variable{},
expected: openfeature.InterfaceResolutionDetail{
Value: map[string]bool{"default": true},
ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
Reason: openfeature.DefaultReason,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a DevCycleProvider with the mock client
provider := DevCycleProvider{
Client: StubClient{
variable: tc.variable,
err: tc.errorResult,
},
}

// Call BooleanEvaluation with the test case inputs

var result any
switch tc.method {
case "BooleanEvaluation":
result = provider.BooleanEvaluation(context.Background(), "example", false, evalCtx)
case "StringEvaluation":
result = provider.StringEvaluation(context.Background(), "example", "default", evalCtx)
case "FloatEvaluation":
result = provider.FloatEvaluation(context.Background(), "example", float64(1.23), evalCtx)
case "IntEvaluation":
result = provider.IntEvaluation(context.Background(), "example", int64(123), evalCtx)
case "ObjectEvaluation":
result = provider.ObjectEvaluation(context.Background(), "example", map[string]bool{"default": true}, evalCtx)
}

require.Equalf(t, tc.expected, result, tc.name)
})
}
}

0 comments on commit b946d9b

Please sign in to comment.