Skip to content

Commit

Permalink
feat: add flag metadata field (#178)
Browse files Browse the repository at this point in the history
  • Loading branch information
james-milligan authored May 24, 2023
1 parent 1d1e3cd commit e3b299d
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 1 deletion.
72 changes: 72 additions & 0 deletions pkg/openfeature/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,78 @@ type ResolutionDetail struct {
Reason Reason
ErrorCode ErrorCode
ErrorMessage string
FlagMetadata FlagMetadata
}

// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, int64 or float64.
//
// This structure is populated by a provider for use by an Application Author (via the Evaluation API) or an Application Integrator (via hooks).
type FlagMetadata map[string]interface{}

// Fetch string value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
func (f FlagMetadata) GetString(key string) (string, error) {
v, ok := f[key]
if !ok {
return "", fmt.Errorf("key %s does not exist in FlagMetadata", key)
}
switch t := v.(type) {
case string:
return v.(string), nil
default:
return "", fmt.Errorf("wrong type for key %s, expected string, got %T", key, t)
}
}

// Fetch bool value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
func (f FlagMetadata) GetBool(key string) (bool, error) {
v, ok := f[key]
if !ok {
return false, fmt.Errorf("key %s does not exist in FlagMetadata", key)
}
switch t := v.(type) {
case bool:
return v.(bool), nil
default:
return false, fmt.Errorf("wrong type for key %s, expected bool, got %T", key, t)
}
}

// Fetch int64 value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
func (f FlagMetadata) GetInt(key string) (int64, error) {
v, ok := f[key]
if !ok {
return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key)
}
switch t := v.(type) {
case int:
return int64(v.(int)), nil
case int8:
return int64(v.(int8)), nil
case int16:
return int64(v.(int16)), nil
case int32:
return int64(v.(int32)), nil
case int64:
return v.(int64), nil
default:
return 0, fmt.Errorf("wrong type for key %s, expected integer, got %T", key, t)
}
}

// Fetch float64 value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
func (f FlagMetadata) GetFloat(key string) (float64, error) {
v, ok := f[key]
if !ok {
return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key)
}
switch t := v.(type) {
case float32:
return float64(v.(float32)), nil
case float64:
return v.(float64), nil
default:
return 0, fmt.Errorf("wrong type for key %s, expected float, got %T", key, t)
}
}

// Option applies a change to EvaluationOptions
Expand Down
183 changes: 183 additions & 0 deletions pkg/openfeature/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,75 @@ func TestRequirement_1_4_12(t *testing.T) {
}
}

// Requirement_1_4_13
// If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set,
// the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise,
// it MUST contain an empty record.
func TestRequirement_1_4_13(t *testing.T) {
client := NewClient("test-client")
flagKey := "flag-key"
evalCtx := EvaluationContext{}
flatCtx := flattenContext(evalCtx)

ctrl := gomock.NewController(t)
t.Run("No Metadata", func(t *testing.T) {
defer t.Cleanup(initSingleton)
mockProvider := NewMockFeatureProvider(ctrl)
defaultValue := true
mockProvider.EXPECT().Metadata().AnyTimes()
mockProvider.EXPECT().Hooks().AnyTimes()
mockProvider.EXPECT().BooleanEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
Return(BoolResolutionDetail{
Value: true,
ProviderResolutionDetail: ProviderResolutionDetail{
FlagMetadata: nil,
},
}).Times(1)
SetProvider(mockProvider)

evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{})
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(evDetails.FlagMetadata, FlagMetadata{}) {
t.Errorf(
"flag metadata is not as expected in EvaluationDetail, got %v, expected %v",
evDetails.FlagMetadata, FlagMetadata{},
)
}
})

t.Run("Metadata present", func(t *testing.T) {
defer t.Cleanup(initSingleton)
mockProvider := NewMockFeatureProvider(ctrl)
defaultValue := true
metadata := FlagMetadata{
"bing": "bong",
}
mockProvider.EXPECT().Metadata().AnyTimes()
mockProvider.EXPECT().Hooks().AnyTimes()
mockProvider.EXPECT().BooleanEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
Return(BoolResolutionDetail{
Value: true,
ProviderResolutionDetail: ProviderResolutionDetail{
FlagMetadata: metadata,
},
}).Times(1)
SetProvider(mockProvider)

evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{})
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(metadata, evDetails.FlagMetadata) {
t.Errorf(
"flag metadata is not as expected in EvaluationDetail, got %v, expected %v",
evDetails.FlagMetadata, metadata,
)
}
})
}

// Requirement_1_5_1
// The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST
// execute for the respective flag evaluation, in addition to those already configured.
Expand Down Expand Up @@ -908,3 +977,117 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) {
t.Error("not supposed to have an error code")
}
}

func TestFlagMetadataAccessors(t *testing.T) {

t.Run("bool", func(t *testing.T) {
expectedValue := true
key := "bool"
key2 := "not-bool"
metadata := FlagMetadata{
key: expectedValue,
key2: "12",
}
val, err := metadata.GetBool(key)
if err != nil {
t.Error("unexpected error value, expected nil", err)
}
if val != expectedValue {
t.Errorf("wrong value returned from FlagMetadata, expected %t, got %t", val, expectedValue)
}
_, err = metadata.GetBool(key2)
if err == nil {
t.Error("unexpected error value", err)
}
_, err = metadata.GetBool("not-in-map")
if err == nil {
t.Error("unexpected error value", err)
}
})

t.Run("string", func(t *testing.T) {
expectedValue := "string"
key := "string"
key2 := "not-string"
metadata := FlagMetadata{
key: expectedValue,
key2: true,
}
val, err := metadata.GetString(key)
if err != nil {
t.Error("unexpected error value, expected nil", err)
}
if val != expectedValue {
t.Errorf("wrong value returned from FlagMetadata, expected %s, got %s", val, expectedValue)
}
_, err = metadata.GetString(key2)
if err == nil {
t.Error("unexpected error value", err)
}
_, err = metadata.GetString("not-in-map")
if err == nil {
t.Error("unexpected error value", err)
}
})

t.Run("int", func(t *testing.T) {
expectedValue := int64(12)
metadata := FlagMetadata{
"int": int(12),
"int8": int8(12),
"int16": int16(12),
"int32": int32(12),
"int164": int32(12),
}
for k := range metadata {
val, err := metadata.GetInt(k)
if err != nil {
t.Error("unexpected error value, expected nil", err)
}
if val != expectedValue {
t.Errorf("wrong value returned from FlagMetadata, expected %b, got %b", val, expectedValue)
}
}

metadata = FlagMetadata{
"not-int": true,
}
_, err := metadata.GetInt("not-int")
if err == nil {
t.Error("unexpected error value", err)
}
_, err = metadata.GetInt("not-in-map")
if err == nil {
t.Error("unexpected error value", err)
}
})

t.Run("float", func(t *testing.T) {
expectedValue := float64(12)
metadata := FlagMetadata{
"float32": float32(12),
"float64": float64(12),
}
for k := range metadata {
val, err := metadata.GetFloat(k)
if err != nil {
t.Error("unexpected error value, expected nil", err)
}
if val != expectedValue {
t.Errorf("wrong value returned from FlagMetadata, expected %b, got %b", val, expectedValue)
}
}

metadata = FlagMetadata{
"not-float": true,
}
_, err := metadata.GetInt("not-float")
if err == nil {
t.Error("unexpected error value", err)
}
_, err = metadata.GetInt("not-in-map")
if err == nil {
t.Error("unexpected error value", err)
}
})
}
8 changes: 7 additions & 1 deletion pkg/openfeature/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
StaticReason Reason = "STATIC"
// CachedReason - the resolved value was retrieved from cache
CachedReason Reason = "CACHED"
// UnknownReason - the reason for the resolved value could not be determined.
// UnknownReason - the reason for the resolved value could not be determined.
UnknownReason Reason = "UNKNOWN"
// ErrorReason - the resolved value was the result of an error.
ErrorReason Reason = "ERROR"
Expand Down Expand Up @@ -54,14 +54,20 @@ type ProviderResolutionDetail struct {
ResolutionError ResolutionError
Reason Reason
Variant string
FlagMetadata FlagMetadata
}

func (p ProviderResolutionDetail) ResolutionDetail() ResolutionDetail {
metadata := FlagMetadata{}
if p.FlagMetadata != nil {
metadata = p.FlagMetadata
}
return ResolutionDetail{
Variant: p.Variant,
Reason: p.Reason,
ErrorCode: p.ResolutionError.code,
ErrorMessage: p.ResolutionError.message,
FlagMetadata: metadata,
}
}

Expand Down

0 comments on commit e3b299d

Please sign in to comment.