Skip to content

Commit a0af470

Browse files
feat: GO Feature Flag provider refactor
Signed-off-by: Thomas Poignant <[email protected]>
1 parent e603c08 commit a0af470

30 files changed

+1365
-764
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*.cs]
4+
indent_style = space
5+
indent_size = 4
6+
tab_width = 4
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
end_of_line = lf

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

-19
This file was deleted.

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

+58-35
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Globalization;
45
using System.Net;
56
using System.Net.Http;
@@ -9,7 +10,11 @@
910
using System.Threading;
1011
using System.Threading.Tasks;
1112
using OpenFeature.Constant;
13+
using OpenFeature.Contrib.Providers.GOFeatureFlag.converters;
1214
using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
15+
using OpenFeature.Contrib.Providers.GOFeatureFlag.extensions;
16+
using OpenFeature.Contrib.Providers.GOFeatureFlag.hooks;
17+
using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
1318
using OpenFeature.Model;
1419

1520
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
@@ -20,8 +25,8 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag
2025
public class GoFeatureFlagProvider : FeatureProvider
2126
{
2227
private const string ApplicationJson = "application/json";
28+
private ExporterMetadata _exporterMetadata;
2329
private HttpClient _httpClient;
24-
private JsonSerializerOptions _serializerOptions;
2530

2631
/// <summary>
2732
/// Constructor of the provider.
@@ -34,6 +39,17 @@ public GoFeatureFlagProvider(GoFeatureFlagProviderOptions options)
3439
InitializeProvider(options);
3540
}
3641

42+
/// <summary>
43+
/// List of hooks to use for this provider
44+
/// </summary>
45+
/// <returns></returns>
46+
public override IImmutableList<Hook> GetProviderHooks()
47+
{
48+
var hooks = ImmutableArray.CreateBuilder<Hook>();
49+
hooks.Add(new EnrichEvaluationContextHook(_exporterMetadata));
50+
return hooks.ToImmutable();
51+
}
52+
3753
/// <summary>
3854
/// validateInputOptions is validating the different options provided when creating the provider.
3955
/// </summary>
@@ -53,6 +69,10 @@ private void ValidateInputOptions(GoFeatureFlagProviderOptions options)
5369
/// <param name="options">Options used while creating the provider</param>
5470
private void InitializeProvider(GoFeatureFlagProviderOptions options)
5571
{
72+
_exporterMetadata = options.ExporterMetadata ?? new ExporterMetadata();
73+
_exporterMetadata.Add("provider", ".NET");
74+
_exporterMetadata.Add("openfeature", true);
75+
5676
_httpClient = options.HttpMessageHandler != null
5777
? new HttpClient(options.HttpMessageHandler)
5878
: new HttpClient
@@ -63,7 +83,6 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options)
6383
};
6484
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
6585
_httpClient.BaseAddress = new Uri(options.Endpoint);
66-
_serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
6786

6887
if (options.ApiKey != null)
6988
_httpClient.DefaultRequestHeaders.Authorization =
@@ -96,8 +115,8 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
96115
try
97116
{
98117
var resp = await CallApi(flagKey, defaultValue, context);
99-
return new ResolutionDetails<bool>(flagKey, bool.Parse(resp.value.ToString()), ErrorType.None,
100-
resp.reason, resp.variationType);
118+
return new ResolutionDetails<bool>(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None,
119+
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
101120
}
102121
catch (FormatException e)
103122
{
@@ -121,16 +140,17 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
121140
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
122141
/// <exception cref="GeneralError">If an unknown error happen</exception>
123142
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
124-
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue,
143+
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey,
144+
string defaultValue,
125145
EvaluationContext context = null, CancellationToken cancellationToken = default)
126146
{
127147
try
128148
{
129149
var resp = await CallApi(flagKey, defaultValue, context);
130-
if (!(resp.value is JsonElement element && element.ValueKind == JsonValueKind.String))
150+
if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String))
131151
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
132-
return new ResolutionDetails<string>(flagKey, resp.value.ToString(), ErrorType.None, resp.reason,
133-
resp.variationType);
152+
return new ResolutionDetails<string>(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason,
153+
resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
134154
}
135155
catch (FormatException e)
136156
{
@@ -160,8 +180,8 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
160180
try
161181
{
162182
var resp = await CallApi(flagKey, defaultValue, context);
163-
return new ResolutionDetails<int>(flagKey, int.Parse(resp.value.ToString()), ErrorType.None,
164-
resp.reason, resp.variationType);
183+
return new ResolutionDetails<int>(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None,
184+
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
165185
}
166186
catch (FormatException e)
167187
{
@@ -185,15 +205,16 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
185205
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
186206
/// <exception cref="GeneralError">If an unknown error happen</exception>
187207
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
188-
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue,
208+
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey,
209+
double defaultValue,
189210
EvaluationContext context = null, CancellationToken cancellationToken = default)
190211
{
191212
try
192213
{
193214
var resp = await CallApi(flagKey, defaultValue, context);
194215
return new ResolutionDetails<double>(flagKey,
195-
double.Parse(resp.value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
196-
resp.reason, resp.variationType);
216+
double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
217+
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
197218
}
198219
catch (FormatException e)
199220
{
@@ -217,17 +238,18 @@ public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(st
217238
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
218239
/// <exception cref="GeneralError">If an unknown error happen</exception>
219240
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
220-
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue,
241+
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey,
242+
Value defaultValue,
221243
EvaluationContext context = null, CancellationToken cancellationToken = default)
222244
{
223245
try
224246
{
225247
var resp = await CallApi(flagKey, defaultValue, context);
226-
if (resp.value is JsonElement)
248+
if (resp.Value is JsonElement)
227249
{
228-
var value = ConvertValue((JsonElement)resp.value);
229-
return new ResolutionDetails<Value>(flagKey, value, ErrorType.None, resp.reason,
230-
resp.variationType);
250+
var value = ConvertValue((JsonElement)resp.Value);
251+
return new ResolutionDetails<Value>(flagKey, value, ErrorType.None, resp.Reason,
252+
resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
231253
}
232254

233255
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
@@ -253,39 +275,40 @@ public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(
253275
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
254276
/// <exception cref="GeneralError">If an unknown error happen</exception>
255277
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
256-
private async Task<GoFeatureFlagResponse> CallApi<T>(string flagKey, T defaultValue,
278+
private async Task<OfrepResponse> CallApi<T>(string flagKey, T defaultValue,
257279
EvaluationContext context = null)
258280
{
259-
var request = new GOFeatureFlagRequest<T>
260-
{
261-
User = context,
262-
DefaultValue = defaultValue
263-
};
264-
var goffRequest = JsonSerializer.Serialize(request, _serializerOptions);
265-
266-
var response = await _httpClient.PostAsync($"v1/feature/{flagKey}/eval",
267-
new StringContent(goffRequest, Encoding.UTF8, ApplicationJson));
281+
var request = new OfrepRequest(context);
282+
var response = await _httpClient.PostAsync($"ofrep/v1/evaluate/flags/{flagKey}",
283+
new StringContent(request.AsJsonString(), Encoding.UTF8, ApplicationJson));
268284

269285
if (response.StatusCode == HttpStatusCode.NotFound)
270286
throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration");
271287

272-
if (response.StatusCode == HttpStatusCode.Unauthorized)
288+
if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden)
273289
throw new UnauthorizedError("invalid token used to contact GO Feature Flag relay proxy instance");
274290

275291
if (response.StatusCode >= HttpStatusCode.BadRequest)
276292
throw new GeneralError("impossible to contact GO Feature Flag relay proxy instance");
277293

278294
var responseBody = await response.Content.ReadAsStringAsync();
279-
var goffResp =
280-
JsonSerializer.Deserialize<GoFeatureFlagResponse>(responseBody);
295+
var options = new JsonSerializerOptions
296+
{
297+
PropertyNameCaseInsensitive = true
298+
};
299+
var ofrepResp =
300+
JsonSerializer.Deserialize<OfrepResponse>(responseBody, options);
281301

282-
if (goffResp != null && Reason.Disabled.Equals(goffResp.reason))
302+
if (Reason.Disabled.Equals(ofrepResp?.Reason))
283303
throw new FlagDisabled();
284304

285-
if ("FLAG_NOT_FOUND".Equals(goffResp.errorCode))
305+
if ("FLAG_NOT_FOUND".Equals(ofrepResp?.ErrorCode))
286306
throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration");
287307

288-
return goffResp;
308+
if (ofrepResp?.Metadata != null)
309+
ofrepResp.Metadata = DictionaryConverter.ConvertDictionary(ofrepResp.Metadata);
310+
311+
return ofrepResp;
289312
}
290313

291314
/// <summary>
@@ -337,4 +360,4 @@ private Value ConvertValue(JsonElement value)
337360
throw new ImpossibleToConvertTypeError($"impossible to convert the object {value}");
338361
}
339362
}
340-
}
363+
}

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Net.Http;
3+
using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
34

45
namespace OpenFeature.Contrib.Providers.GOFeatureFlag
56
{
@@ -34,5 +35,11 @@ public class GoFeatureFlagProviderOptions
3435
/// Default: null
3536
/// </Summary>
3637
public string ApiKey { get; set; }
38+
39+
/// <summary>
40+
/// (optional) ExporterMetadata are static information you can set that will be available in the
41+
/// evaluation data sent to the exporter.
42+
/// </summary>
43+
public ExporterMetadata ExporterMetadata { get; set; }
3744
}
38-
}
45+
}

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

-43
This file was deleted.

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

-64
This file was deleted.

0 commit comments

Comments
 (0)