Skip to content

Commit 81bbce2

Browse files
feat(gofeatureflag): Support flag metadata
Signed-off-by: Thomas Poignant <[email protected]>
1 parent e603c08 commit 81bbce2

File tree

4 files changed

+102
-9
lines changed

4 files changed

+102
-9
lines changed

src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs

+16-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using OpenFeature.Constant;
1212
using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
13+
using OpenFeature.Contrib.Providers.GOFeatureFlag.helpers;
1314
using OpenFeature.Model;
1415

1516
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
@@ -97,7 +98,8 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
9798
{
9899
var resp = await CallApi(flagKey, defaultValue, context);
99100
return new ResolutionDetails<bool>(flagKey, bool.Parse(resp.value.ToString()), ErrorType.None,
100-
resp.reason, resp.variationType);
101+
resp.reason, resp.variationType, null,
102+
resp.metadata == null ? null : new ImmutableMetadata(resp.metadata));
101103
}
102104
catch (FormatException e)
103105
{
@@ -121,7 +123,8 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
121123
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
122124
/// <exception cref="GeneralError">If an unknown error happen</exception>
123125
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
124-
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue,
126+
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey,
127+
string defaultValue,
125128
EvaluationContext context = null, CancellationToken cancellationToken = default)
126129
{
127130
try
@@ -130,7 +133,7 @@ public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(st
130133
if (!(resp.value is JsonElement element && element.ValueKind == JsonValueKind.String))
131134
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
132135
return new ResolutionDetails<string>(flagKey, resp.value.ToString(), ErrorType.None, resp.reason,
133-
resp.variationType);
136+
resp.variationType, null, resp.metadata == null ? null : new ImmutableMetadata(resp.metadata));
134137
}
135138
catch (FormatException e)
136139
{
@@ -161,7 +164,8 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
161164
{
162165
var resp = await CallApi(flagKey, defaultValue, context);
163166
return new ResolutionDetails<int>(flagKey, int.Parse(resp.value.ToString()), ErrorType.None,
164-
resp.reason, resp.variationType);
167+
resp.reason, resp.variationType, null,
168+
resp.metadata == null ? null : new ImmutableMetadata(resp.metadata));
165169
}
166170
catch (FormatException e)
167171
{
@@ -185,15 +189,17 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
185189
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
186190
/// <exception cref="GeneralError">If an unknown error happen</exception>
187191
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
188-
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue,
192+
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey,
193+
double defaultValue,
189194
EvaluationContext context = null, CancellationToken cancellationToken = default)
190195
{
191196
try
192197
{
193198
var resp = await CallApi(flagKey, defaultValue, context);
194199
return new ResolutionDetails<double>(flagKey,
195200
double.Parse(resp.value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
196-
resp.reason, resp.variationType);
201+
resp.reason, resp.variationType, null,
202+
resp.metadata == null ? null : new ImmutableMetadata(resp.metadata));
197203
}
198204
catch (FormatException e)
199205
{
@@ -217,7 +223,8 @@ public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(st
217223
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
218224
/// <exception cref="GeneralError">If an unknown error happen</exception>
219225
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
220-
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue,
226+
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey,
227+
Value defaultValue,
221228
EvaluationContext context = null, CancellationToken cancellationToken = default)
222229
{
223230
try
@@ -227,7 +234,7 @@ public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(
227234
{
228235
var value = ConvertValue((JsonElement)resp.value);
229236
return new ResolutionDetails<Value>(flagKey, value, ErrorType.None, resp.reason,
230-
resp.variationType);
237+
resp.variationType, null, resp.metadata == null ? null : new ImmutableMetadata(resp.metadata));
231238
}
232239

233240
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
@@ -285,6 +292,7 @@ private async Task<GoFeatureFlagResponse> CallApi<T>(string flagKey, T defaultVa
285292
if ("FLAG_NOT_FOUND".Equals(goffResp.errorCode))
286293
throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration");
287294

295+
if (goffResp.metadata != null) goffResp.metadata = DictionaryConverter.ConvertDictionary(goffResp.metadata);
288296
return goffResp;
289297
}
290298

src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
24
{
35
/// <summary>
@@ -39,5 +41,10 @@ public class GoFeatureFlagResponse
3941
/// value contains the result of the flag.
4042
/// </summary>
4143
public object value { get; set; }
44+
45+
/// <summary>
46+
/// metadata contains the metadata of the flag.
47+
/// </summary>
48+
public Dictionary<string, object> metadata { get; set; }
4249
}
4350
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text.Json;
4+
5+
namespace OpenFeature.Contrib.Providers.GOFeatureFlag.helpers
6+
{
7+
public static class DictionaryConverter
8+
{
9+
public static Dictionary<string, object> ConvertDictionary(Dictionary<string, object> inputDictionary)
10+
{
11+
return inputDictionary.ToDictionary(
12+
kvp => kvp.Key,
13+
kvp => ConvertValue(kvp.Value)
14+
);
15+
}
16+
17+
public static object ConvertValue(object value)
18+
{
19+
if (value is JsonElement jsonElement)
20+
switch (jsonElement.ValueKind)
21+
{
22+
case JsonValueKind.String:
23+
return jsonElement.GetString();
24+
case JsonValueKind.Number:
25+
if (jsonElement.TryGetInt32(out var intValue)) return intValue;
26+
27+
if (jsonElement.TryGetDouble(out var doubleValue)) return doubleValue;
28+
return jsonElement.GetRawText(); // Fallback to string if not int or double
29+
case JsonValueKind.True:
30+
return true;
31+
case JsonValueKind.False:
32+
return false;
33+
case JsonValueKind.Null:
34+
return null;
35+
case JsonValueKind.Object:
36+
return ConvertDictionary(
37+
JsonSerializer
38+
.Deserialize<Dictionary<string, object>>(jsonElement
39+
.GetRawText())); //Recursive for nested objects
40+
case JsonValueKind.Array:
41+
var array = new List<object>();
42+
foreach (var element in jsonElement.EnumerateArray()) array.Add(ConvertValue(element));
43+
44+
return array;
45+
default:
46+
return jsonElement.GetRawText(); // Handle other types as needed
47+
}
48+
49+
return value; // Return original value if not a JsonElement
50+
}
51+
}
52+
}

test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs

+27-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ private static HttpMessageHandler InitMock()
5656
"{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"CUSTOM_REASON\",\"errorCode\":\"\",\"value\":true}");
5757
mockHttp.When($"{prefixEval}does_not_exists{suffixEval}").Respond(mediaType,
5858
"{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":true,\"version\":\"\",\"reason\":\"ERROR\",\"errorCode\":\"FLAG_NOT_FOUND\",\"value\":\"\"}");
59+
mockHttp.When($"{prefixEval}integer_with_metadata{suffixEval}").Respond(mediaType,
60+
"{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":100, \"metadata\": {\"key1\": \"key1\", \"key2\":1, \"key3\":1.345, \"key4\":true}}");
5961
return mockHttp;
6062
}
6163

@@ -560,4 +562,28 @@ public async Task should_use_object_default_value_if_flag_not_found()
560562
Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType);
561563
Assert.Equal("flag does_not_exists was not found in your configuration", res.Result.ErrorMessage);
562564
}
563-
}
565+
566+
[Fact]
567+
public async Task should_resolve_a_flag_with_metadata()
568+
{
569+
var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions
570+
{
571+
Endpoint = baseUrl,
572+
HttpMessageHandler = _mockHttp,
573+
Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond)
574+
});
575+
await Api.Instance.SetProviderAsync(g);
576+
var client = Api.Instance.GetClient("test-client");
577+
var res = client.GetIntegerDetailsAsync("integer_with_metadata", 1200, _defaultEvaluationCtx);
578+
Assert.NotNull(res.Result);
579+
Assert.Equal(100, res.Result.Value);
580+
Assert.Equal(ErrorType.None, res.Result.ErrorType);
581+
Assert.Equal(Reason.TargetingMatch, res.Result.Reason);
582+
Assert.Equal("True", res.Result.Variant);
583+
Assert.NotNull(res.Result.FlagMetadata);
584+
Assert.Equal("key1", res.Result.FlagMetadata.GetString("key1"));
585+
Assert.Equal(1, res.Result.FlagMetadata.GetInt("key2"));
586+
Assert.Equal(1.345, res.Result.FlagMetadata.GetDouble("key3"));
587+
Assert.True(res.Result.FlagMetadata.GetBool("key4"));
588+
}
589+
}

0 commit comments

Comments
 (0)