Skip to content

Commit

Permalink
feat(gofeatureflag): Provider refactor (#313)
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Poignant <[email protected]>
  • Loading branch information
thomaspoignant authored Feb 10, 2025
1 parent e603c08 commit c30446e
Show file tree
Hide file tree
Showing 28 changed files with 1,347 additions and 764 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Net;
using System.Net.Http;
Expand All @@ -9,7 +10,11 @@
using System.Threading;
using System.Threading.Tasks;
using OpenFeature.Constant;
using OpenFeature.Contrib.Providers.GOFeatureFlag.converters;
using OpenFeature.Contrib.Providers.GOFeatureFlag.exception;
using OpenFeature.Contrib.Providers.GOFeatureFlag.extensions;
using OpenFeature.Contrib.Providers.GOFeatureFlag.hooks;
using OpenFeature.Contrib.Providers.GOFeatureFlag.models;
using OpenFeature.Model;

namespace OpenFeature.Contrib.Providers.GOFeatureFlag
Expand All @@ -20,8 +25,8 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag
public class GoFeatureFlagProvider : FeatureProvider
{
private const string ApplicationJson = "application/json";
private ExporterMetadata _exporterMetadata;
private HttpClient _httpClient;
private JsonSerializerOptions _serializerOptions;

/// <summary>
/// Constructor of the provider.
Expand All @@ -34,6 +39,17 @@ public GoFeatureFlagProvider(GoFeatureFlagProviderOptions options)
InitializeProvider(options);
}

/// <summary>
/// List of hooks to use for this provider
/// </summary>
/// <returns></returns>
public override IImmutableList<Hook> GetProviderHooks()
{
var hooks = ImmutableArray.CreateBuilder<Hook>();
hooks.Add(new EnrichEvaluationContextHook(_exporterMetadata));
return hooks.ToImmutable();
}

/// <summary>
/// validateInputOptions is validating the different options provided when creating the provider.
/// </summary>
Expand All @@ -53,6 +69,10 @@ private void ValidateInputOptions(GoFeatureFlagProviderOptions options)
/// <param name="options">Options used while creating the provider</param>
private void InitializeProvider(GoFeatureFlagProviderOptions options)
{
_exporterMetadata = options.ExporterMetadata ?? new ExporterMetadata();
_exporterMetadata.Add("provider", ".NET");
_exporterMetadata.Add("openfeature", true);

_httpClient = options.HttpMessageHandler != null
? new HttpClient(options.HttpMessageHandler)
: new HttpClient
Expand All @@ -63,7 +83,6 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options)
};
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
_httpClient.BaseAddress = new Uri(options.Endpoint);
_serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

if (options.ApiKey != null)
_httpClient.DefaultRequestHeaders.Authorization =
Expand Down Expand Up @@ -96,8 +115,8 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
try
{
var resp = await CallApi(flagKey, defaultValue, context);
return new ResolutionDetails<bool>(flagKey, bool.Parse(resp.value.ToString()), ErrorType.None,
resp.reason, resp.variationType);
return new ResolutionDetails<bool>(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None,
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
Expand All @@ -121,16 +140,17 @@ public override async Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(str
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
/// <exception cref="GeneralError">If an unknown error happen</exception>
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue,
public override async Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey,
string defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
if (!(resp.value is JsonElement element && element.ValueKind == JsonValueKind.String))
if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String))
throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
return new ResolutionDetails<string>(flagKey, resp.value.ToString(), ErrorType.None, resp.reason,
resp.variationType);
return new ResolutionDetails<string>(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason,
resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
Expand Down Expand Up @@ -160,8 +180,8 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
try
{
var resp = await CallApi(flagKey, defaultValue, context);
return new ResolutionDetails<int>(flagKey, int.Parse(resp.value.ToString()), ErrorType.None,
resp.reason, resp.variationType);
return new ResolutionDetails<int>(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None,
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
Expand All @@ -185,15 +205,16 @@ public override async Task<ResolutionDetails<int>> ResolveIntegerValueAsync(stri
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
/// <exception cref="GeneralError">If an unknown error happen</exception>
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue,
public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey,
double defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
return new ResolutionDetails<double>(flagKey,
double.Parse(resp.value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
resp.reason, resp.variationType);
double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None,
resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}
catch (FormatException e)
{
Expand All @@ -217,17 +238,18 @@ public override async Task<ResolutionDetails<double>> ResolveDoubleValueAsync(st
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
/// <exception cref="GeneralError">If an unknown error happen</exception>
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue,
public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey,
Value defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default)
{
try
{
var resp = await CallApi(flagKey, defaultValue, context);
if (resp.value is JsonElement)
if (resp.Value is JsonElement)
{
var value = ConvertValue((JsonElement)resp.value);
return new ResolutionDetails<Value>(flagKey, value, ErrorType.None, resp.reason,
resp.variationType);
var value = ConvertValue((JsonElement)resp.Value);
return new ResolutionDetails<Value>(flagKey, value, ErrorType.None, resp.Reason,
resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata());
}

throw new TypeMismatchError($"flag value {flagKey} had unexpected type");
Expand All @@ -253,39 +275,40 @@ public override async Task<ResolutionDetails<Value>> ResolveStructureValueAsync(
/// <exception cref="FlagNotFoundError">If the flag does not exists</exception>
/// <exception cref="GeneralError">If an unknown error happen</exception>
/// <exception cref="FlagDisabled">If the flag is disabled</exception>
private async Task<GoFeatureFlagResponse> CallApi<T>(string flagKey, T defaultValue,
private async Task<OfrepResponse> CallApi<T>(string flagKey, T defaultValue,
EvaluationContext context = null)
{
var request = new GOFeatureFlagRequest<T>
{
User = context,
DefaultValue = defaultValue
};
var goffRequest = JsonSerializer.Serialize(request, _serializerOptions);

var response = await _httpClient.PostAsync($"v1/feature/{flagKey}/eval",
new StringContent(goffRequest, Encoding.UTF8, ApplicationJson));
var request = new OfrepRequest(context);
var response = await _httpClient.PostAsync($"ofrep/v1/evaluate/flags/{flagKey}",
new StringContent(request.AsJsonString(), Encoding.UTF8, ApplicationJson));

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

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

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

var responseBody = await response.Content.ReadAsStringAsync();
var goffResp =
JsonSerializer.Deserialize<GoFeatureFlagResponse>(responseBody);
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var ofrepResp =
JsonSerializer.Deserialize<OfrepResponse>(responseBody, options);

if (goffResp != null && Reason.Disabled.Equals(goffResp.reason))
if (Reason.Disabled.Equals(ofrepResp?.Reason))
throw new FlagDisabled();

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

return goffResp;
if (ofrepResp?.Metadata != null)
ofrepResp.Metadata = DictionaryConverter.ConvertDictionary(ofrepResp.Metadata);

return ofrepResp;
}

/// <summary>
Expand Down Expand Up @@ -337,4 +360,4 @@ private Value ConvertValue(JsonElement value)
throw new ImpossibleToConvertTypeError($"impossible to convert the object {value}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net.Http;
using OpenFeature.Contrib.Providers.GOFeatureFlag.models;

namespace OpenFeature.Contrib.Providers.GOFeatureFlag
{
Expand Down Expand Up @@ -34,5 +35,11 @@ public class GoFeatureFlagProviderOptions
/// Default: null
/// </Summary>
public string ApiKey { get; set; }

/// <summary>
/// (optional) ExporterMetadata are static information you can set that will be available in the
/// evaluation data sent to the exporter.
/// </summary>
public ExporterMetadata ExporterMetadata { get; set; }
}
}
}

This file was deleted.

This file was deleted.

Loading

0 comments on commit c30446e

Please sign in to comment.