Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gofeatureflag): Provider refactor #313

Merged
merged 1 commit into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be?

Suggested change
_exporterMetadata.Add("provider", ".NET");
_exporterMetadata.Add("provider", "GoFeatureFlag .NET");

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is used by GOFF internally .NET is enough in that case.

_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