Skip to content

Commit e3b299d

Browse files
feat: add flag metadata field (#178)
1 parent 1d1e3cd commit e3b299d

File tree

3 files changed

+262
-1
lines changed

3 files changed

+262
-1
lines changed

pkg/openfeature/client.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,78 @@ type ResolutionDetail struct {
164164
Reason Reason
165165
ErrorCode ErrorCode
166166
ErrorMessage string
167+
FlagMetadata FlagMetadata
168+
}
169+
170+
// A structure which supports definition of arbitrary properties, with keys of type string, and values of type boolean, string, int64 or float64.
171+
//
172+
// This structure is populated by a provider for use by an Application Author (via the Evaluation API) or an Application Integrator (via hooks).
173+
type FlagMetadata map[string]interface{}
174+
175+
// Fetch string value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
176+
func (f FlagMetadata) GetString(key string) (string, error) {
177+
v, ok := f[key]
178+
if !ok {
179+
return "", fmt.Errorf("key %s does not exist in FlagMetadata", key)
180+
}
181+
switch t := v.(type) {
182+
case string:
183+
return v.(string), nil
184+
default:
185+
return "", fmt.Errorf("wrong type for key %s, expected string, got %T", key, t)
186+
}
187+
}
188+
189+
// Fetch bool value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
190+
func (f FlagMetadata) GetBool(key string) (bool, error) {
191+
v, ok := f[key]
192+
if !ok {
193+
return false, fmt.Errorf("key %s does not exist in FlagMetadata", key)
194+
}
195+
switch t := v.(type) {
196+
case bool:
197+
return v.(bool), nil
198+
default:
199+
return false, fmt.Errorf("wrong type for key %s, expected bool, got %T", key, t)
200+
}
201+
}
202+
203+
// Fetch int64 value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
204+
func (f FlagMetadata) GetInt(key string) (int64, error) {
205+
v, ok := f[key]
206+
if !ok {
207+
return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key)
208+
}
209+
switch t := v.(type) {
210+
case int:
211+
return int64(v.(int)), nil
212+
case int8:
213+
return int64(v.(int8)), nil
214+
case int16:
215+
return int64(v.(int16)), nil
216+
case int32:
217+
return int64(v.(int32)), nil
218+
case int64:
219+
return v.(int64), nil
220+
default:
221+
return 0, fmt.Errorf("wrong type for key %s, expected integer, got %T", key, t)
222+
}
223+
}
224+
225+
// Fetch float64 value from FlagMetadata, returns an error if the key does not exist, or, the value is of the wrong type
226+
func (f FlagMetadata) GetFloat(key string) (float64, error) {
227+
v, ok := f[key]
228+
if !ok {
229+
return 0, fmt.Errorf("key %s does not exist in FlagMetadata", key)
230+
}
231+
switch t := v.(type) {
232+
case float32:
233+
return float64(v.(float32)), nil
234+
case float64:
235+
return v.(float64), nil
236+
default:
237+
return 0, fmt.Errorf("wrong type for key %s, expected float, got %T", key, t)
238+
}
167239
}
168240

169241
// Option applies a change to EvaluationOptions

pkg/openfeature/client_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,75 @@ func TestRequirement_1_4_12(t *testing.T) {
630630
}
631631
}
632632

633+
// Requirement_1_4_13
634+
// If the `flag metadata` field in the `flag resolution` structure returned by the configured `provider` is set,
635+
// the `evaluation details` structure's `flag metadata` field MUST contain that value. Otherwise,
636+
// it MUST contain an empty record.
637+
func TestRequirement_1_4_13(t *testing.T) {
638+
client := NewClient("test-client")
639+
flagKey := "flag-key"
640+
evalCtx := EvaluationContext{}
641+
flatCtx := flattenContext(evalCtx)
642+
643+
ctrl := gomock.NewController(t)
644+
t.Run("No Metadata", func(t *testing.T) {
645+
defer t.Cleanup(initSingleton)
646+
mockProvider := NewMockFeatureProvider(ctrl)
647+
defaultValue := true
648+
mockProvider.EXPECT().Metadata().AnyTimes()
649+
mockProvider.EXPECT().Hooks().AnyTimes()
650+
mockProvider.EXPECT().BooleanEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
651+
Return(BoolResolutionDetail{
652+
Value: true,
653+
ProviderResolutionDetail: ProviderResolutionDetail{
654+
FlagMetadata: nil,
655+
},
656+
}).Times(1)
657+
SetProvider(mockProvider)
658+
659+
evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{})
660+
if err != nil {
661+
t.Error(err)
662+
}
663+
if !reflect.DeepEqual(evDetails.FlagMetadata, FlagMetadata{}) {
664+
t.Errorf(
665+
"flag metadata is not as expected in EvaluationDetail, got %v, expected %v",
666+
evDetails.FlagMetadata, FlagMetadata{},
667+
)
668+
}
669+
})
670+
671+
t.Run("Metadata present", func(t *testing.T) {
672+
defer t.Cleanup(initSingleton)
673+
mockProvider := NewMockFeatureProvider(ctrl)
674+
defaultValue := true
675+
metadata := FlagMetadata{
676+
"bing": "bong",
677+
}
678+
mockProvider.EXPECT().Metadata().AnyTimes()
679+
mockProvider.EXPECT().Hooks().AnyTimes()
680+
mockProvider.EXPECT().BooleanEvaluation(context.Background(), flagKey, defaultValue, flatCtx).
681+
Return(BoolResolutionDetail{
682+
Value: true,
683+
ProviderResolutionDetail: ProviderResolutionDetail{
684+
FlagMetadata: metadata,
685+
},
686+
}).Times(1)
687+
SetProvider(mockProvider)
688+
689+
evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{})
690+
if err != nil {
691+
t.Error(err)
692+
}
693+
if !reflect.DeepEqual(metadata, evDetails.FlagMetadata) {
694+
t.Errorf(
695+
"flag metadata is not as expected in EvaluationDetail, got %v, expected %v",
696+
evDetails.FlagMetadata, metadata,
697+
)
698+
}
699+
})
700+
}
701+
633702
// Requirement_1_5_1
634703
// The `evaluation options` structure's `hooks` field denotes an ordered collection of hooks that the client MUST
635704
// execute for the respective flag evaluation, in addition to those already configured.
@@ -908,3 +977,117 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) {
908977
t.Error("not supposed to have an error code")
909978
}
910979
}
980+
981+
func TestFlagMetadataAccessors(t *testing.T) {
982+
983+
t.Run("bool", func(t *testing.T) {
984+
expectedValue := true
985+
key := "bool"
986+
key2 := "not-bool"
987+
metadata := FlagMetadata{
988+
key: expectedValue,
989+
key2: "12",
990+
}
991+
val, err := metadata.GetBool(key)
992+
if err != nil {
993+
t.Error("unexpected error value, expected nil", err)
994+
}
995+
if val != expectedValue {
996+
t.Errorf("wrong value returned from FlagMetadata, expected %t, got %t", val, expectedValue)
997+
}
998+
_, err = metadata.GetBool(key2)
999+
if err == nil {
1000+
t.Error("unexpected error value", err)
1001+
}
1002+
_, err = metadata.GetBool("not-in-map")
1003+
if err == nil {
1004+
t.Error("unexpected error value", err)
1005+
}
1006+
})
1007+
1008+
t.Run("string", func(t *testing.T) {
1009+
expectedValue := "string"
1010+
key := "string"
1011+
key2 := "not-string"
1012+
metadata := FlagMetadata{
1013+
key: expectedValue,
1014+
key2: true,
1015+
}
1016+
val, err := metadata.GetString(key)
1017+
if err != nil {
1018+
t.Error("unexpected error value, expected nil", err)
1019+
}
1020+
if val != expectedValue {
1021+
t.Errorf("wrong value returned from FlagMetadata, expected %s, got %s", val, expectedValue)
1022+
}
1023+
_, err = metadata.GetString(key2)
1024+
if err == nil {
1025+
t.Error("unexpected error value", err)
1026+
}
1027+
_, err = metadata.GetString("not-in-map")
1028+
if err == nil {
1029+
t.Error("unexpected error value", err)
1030+
}
1031+
})
1032+
1033+
t.Run("int", func(t *testing.T) {
1034+
expectedValue := int64(12)
1035+
metadata := FlagMetadata{
1036+
"int": int(12),
1037+
"int8": int8(12),
1038+
"int16": int16(12),
1039+
"int32": int32(12),
1040+
"int164": int32(12),
1041+
}
1042+
for k := range metadata {
1043+
val, err := metadata.GetInt(k)
1044+
if err != nil {
1045+
t.Error("unexpected error value, expected nil", err)
1046+
}
1047+
if val != expectedValue {
1048+
t.Errorf("wrong value returned from FlagMetadata, expected %b, got %b", val, expectedValue)
1049+
}
1050+
}
1051+
1052+
metadata = FlagMetadata{
1053+
"not-int": true,
1054+
}
1055+
_, err := metadata.GetInt("not-int")
1056+
if err == nil {
1057+
t.Error("unexpected error value", err)
1058+
}
1059+
_, err = metadata.GetInt("not-in-map")
1060+
if err == nil {
1061+
t.Error("unexpected error value", err)
1062+
}
1063+
})
1064+
1065+
t.Run("float", func(t *testing.T) {
1066+
expectedValue := float64(12)
1067+
metadata := FlagMetadata{
1068+
"float32": float32(12),
1069+
"float64": float64(12),
1070+
}
1071+
for k := range metadata {
1072+
val, err := metadata.GetFloat(k)
1073+
if err != nil {
1074+
t.Error("unexpected error value, expected nil", err)
1075+
}
1076+
if val != expectedValue {
1077+
t.Errorf("wrong value returned from FlagMetadata, expected %b, got %b", val, expectedValue)
1078+
}
1079+
}
1080+
1081+
metadata = FlagMetadata{
1082+
"not-float": true,
1083+
}
1084+
_, err := metadata.GetInt("not-float")
1085+
if err == nil {
1086+
t.Error("unexpected error value", err)
1087+
}
1088+
_, err = metadata.GetInt("not-in-map")
1089+
if err == nil {
1090+
t.Error("unexpected error value", err)
1091+
}
1092+
})
1093+
}

pkg/openfeature/provider.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const (
1818
StaticReason Reason = "STATIC"
1919
// CachedReason - the resolved value was retrieved from cache
2020
CachedReason Reason = "CACHED"
21-
// UnknownReason - the reason for the resolved value could not be determined.
21+
// UnknownReason - the reason for the resolved value could not be determined.
2222
UnknownReason Reason = "UNKNOWN"
2323
// ErrorReason - the resolved value was the result of an error.
2424
ErrorReason Reason = "ERROR"
@@ -54,14 +54,20 @@ type ProviderResolutionDetail struct {
5454
ResolutionError ResolutionError
5555
Reason Reason
5656
Variant string
57+
FlagMetadata FlagMetadata
5758
}
5859

5960
func (p ProviderResolutionDetail) ResolutionDetail() ResolutionDetail {
61+
metadata := FlagMetadata{}
62+
if p.FlagMetadata != nil {
63+
metadata = p.FlagMetadata
64+
}
6065
return ResolutionDetail{
6166
Variant: p.Variant,
6267
Reason: p.Reason,
6368
ErrorCode: p.ResolutionError.code,
6469
ErrorMessage: p.ResolutionError.message,
70+
FlagMetadata: metadata,
6571
}
6672
}
6773

0 commit comments

Comments
 (0)