-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Vladimir Petrusevici <[email protected]> Co-authored-by: Matthew Elwell <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
- Loading branch information
1 parent
d8cac7f
commit b7ba62e
Showing
12 changed files
with
876 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
{ | ||
"src/OpenFeature.Contrib.Hooks.Otel": "0.1.1", | ||
"src/OpenFeature.Contrib.Providers.Flagd": "0.1.7", | ||
"src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.4" | ||
"src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.1.4", | ||
"src/OpenFeature.Contrib.Providers.Flagsmith": "0.1.0" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
184 changes: 184 additions & 0 deletions
184
src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
using Flagsmith; | ||
using OpenFeature.Constant; | ||
using OpenFeature.Model; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Text.Json; | ||
using System.Text.Json.Nodes; | ||
using System.Threading.Tasks; | ||
using Trait = Flagsmith.Trait; | ||
using OpenFeature.Error; | ||
using System.Globalization; | ||
|
||
namespace OpenFeature.Contrib.Providers.Flagsmith | ||
{ | ||
/// <summary> | ||
/// FlagsmithProvider is the .NET provider implementation for the feature flag solution Flagsmith. | ||
/// </summary> | ||
public class FlagsmithProvider : FeatureProvider | ||
{ | ||
private readonly static Metadata Metadata = new("Flagsmith Provider"); | ||
delegate bool TryParseDelegate<T>(string value, out T x); | ||
internal readonly IFlagsmithClient _flagsmithClient; | ||
|
||
/// <summary> | ||
/// Settings for Flagsmith Open feature provider | ||
/// </summary> | ||
public IFlagsmithProviderConfiguration Configuration { get; } | ||
|
||
|
||
/// <summary> | ||
/// Creates new instance of <see cref="FlagsmithProvider"/> | ||
/// </summary> | ||
/// <param name="providerOptions">Open feature provider options. You can just use <see cref="FlagsmithProviderConfiguration"/> class </param> | ||
/// <param name="flagsmithOptions">Flagsmith client options. You can just use <see cref="FlagsmithConfiguration"/> class</param> | ||
public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithConfiguration flagsmithOptions) | ||
{ | ||
Configuration = providerOptions; | ||
_flagsmithClient = new FlagsmithClient(flagsmithOptions); | ||
} | ||
|
||
/// <summary> | ||
/// Creates new instance of <see cref="FlagsmithProvider"/> | ||
/// </summary> | ||
/// <param name="flagsmithOptions">Flagsmith client options. You can just use <see cref="FlagsmithConfiguration"/> class</param> | ||
/// <param name="providerOptions">Open feature provider options. You can just use <see cref="FlagsmithProviderConfiguration"/> class </param> | ||
/// <param name="httpClient">Http client that will be used for flagsmith requests. You also can use it to register <see cref="FeatureProvider"/> as Typed HttpClient with <see cref="FeatureProvider"> as abstraction</see></param> | ||
public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithConfiguration flagsmithOptions, HttpClient httpClient) | ||
{ | ||
Configuration = providerOptions; | ||
_flagsmithClient = new FlagsmithClient(flagsmithOptions, httpClient); | ||
} | ||
|
||
|
||
/// <summary> | ||
/// Creates new instance of <see cref="FlagsmithProvider"/> | ||
/// </summary> | ||
/// <param name="providerOptions">Open feature provider options. You can just use <see cref="FlagsmithProviderConfiguration"/> class </param> | ||
/// <param name="flagsmithClient">Precreated Flagsmith client. You can just use <see cref="FlagsmithClient"/> class.</param> | ||
public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithClient flagsmithClient) | ||
{ | ||
Configuration = providerOptions; | ||
_flagsmithClient = flagsmithClient; | ||
} | ||
|
||
private Task<IFlags> GetFlags(EvaluationContext ctx) | ||
{ | ||
var key = ctx?.GetValue(Configuration.TargetingKey)?.AsString; | ||
return string.IsNullOrEmpty(key) | ||
? _flagsmithClient.GetEnvironmentFlags() | ||
: _flagsmithClient.GetIdentityFlags(key, ctx.AsDictionary().Select(x => new Trait(x.Key, x.Value.AsObject) as ITrait).ToList()); | ||
} | ||
|
||
private async Task<ResolutionDetails<T>> ResolveValue<T>(string flagKey, T defaultValue, TryParseDelegate<T> tryParse, EvaluationContext context) | ||
{ | ||
|
||
var flags = await GetFlags(context); | ||
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey); | ||
if (!isFlagEnabled) | ||
{ | ||
return new(flagKey, defaultValue, reason: Reason.Disabled); | ||
} | ||
|
||
var stringValue = await flags.GetFeatureValue(flagKey); | ||
|
||
if (tryParse(stringValue, out var parsedValue)) | ||
{ | ||
return new(flagKey, parsedValue); | ||
} | ||
throw new TypeMismatchException("Failed to parse value in the expected type"); | ||
|
||
} | ||
|
||
private async Task<ResolutionDetails<bool>> IsFeatureEnabled(string flagKey, EvaluationContext context) | ||
{ | ||
var flags = await GetFlags(context); | ||
var isFeatureEnabled = await flags.IsFeatureEnabled(flagKey); | ||
return new(flagKey, isFeatureEnabled); | ||
} | ||
|
||
|
||
/// <inheritdoc/> | ||
public override Metadata GetMetadata() => Metadata; | ||
|
||
/// <inheritdoc/> | ||
|
||
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) | ||
=> Configuration.UsingBooleanConfigValue | ||
? ResolveValue(flagKey, defaultValue, bool.TryParse, context) | ||
: IsFeatureEnabled(flagKey, context); | ||
|
||
/// <inheritdoc/> | ||
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) | ||
=> ResolveValue(flagKey, defaultValue, int.TryParse, context); | ||
|
||
/// <inheritdoc/> | ||
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null) | ||
=> ResolveValue(flagKey, defaultValue, (string x, out double y) => double.TryParse(x, NumberStyles.Any, CultureInfo.InvariantCulture, out y), context); | ||
|
||
|
||
/// <inheritdoc/> | ||
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) | ||
=> ResolveValue(flagKey, defaultValue, (string x, out string y) => { y = x; return true; }, context); | ||
|
||
|
||
/// <inheritdoc/> | ||
public override Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null) | ||
=> ResolveValue(flagKey, defaultValue, TryParseValue, context); | ||
|
||
private bool TryParseValue(string stringValue, out Value result) | ||
{ | ||
try | ||
{ | ||
var mappedValue = JsonNode.Parse(stringValue); | ||
result = ConvertValue(mappedValue); | ||
} | ||
catch | ||
{ | ||
result = null; | ||
} | ||
return result is not null; | ||
} | ||
|
||
/// <summary> | ||
/// convertValue is converting the dynamically typed object received from Flagsmith into the correct type | ||
/// </summary> | ||
/// <param name="node">The dynamically typed value we received from Flagsmith</param> | ||
/// <returns>A correctly typed object representing the flag value</returns> | ||
private Value ConvertValue(JsonNode node) | ||
{ | ||
if (node == null) | ||
return null; | ||
if (node is JsonArray jsonArray) | ||
{ | ||
var arr = new List<Value>(); | ||
foreach (var item in jsonArray) | ||
{ | ||
var convertedValue = ConvertValue(item); | ||
if (convertedValue != null) arr.Add(convertedValue); | ||
} | ||
return new(arr); | ||
} | ||
|
||
if (node is JsonObject jsonObject) | ||
{ | ||
var dict = jsonObject.ToDictionary(x => x.Key, x => ConvertValue(x.Value)); | ||
|
||
return new(new Structure(dict)); | ||
} | ||
|
||
if (node.AsValue().TryGetValue<JsonElement>(out var jsonElement)) | ||
{ | ||
if (jsonElement.ValueKind == JsonValueKind.False || jsonElement.ValueKind == JsonValueKind.True) | ||
return new(jsonElement.GetBoolean()); | ||
if (jsonElement.ValueKind == JsonValueKind.Number) | ||
return new(jsonElement.GetDouble()); | ||
|
||
if (jsonElement.ValueKind == JsonValueKind.String) | ||
return new(jsonElement.ToString()); | ||
} | ||
return null; | ||
} | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
namespace OpenFeature.Contrib.Providers.Flagsmith; | ||
|
||
/// <summary> | ||
/// Settings for Flagsmith open feature provider | ||
/// </summary> | ||
public class FlagsmithProviderConfiguration : IFlagsmithProviderConfiguration | ||
{ | ||
/// <summary> | ||
/// Key that will be used as identity for Flagsmith requests. Default: "targetingKey" | ||
/// </summary> | ||
public string TargetingKey { get; set; } = "targetingKey"; | ||
|
||
/// <inheritdoc/> | ||
public bool UsingBooleanConfigValue { get; set; } | ||
} |
22 changes: 22 additions & 0 deletions
22
src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using Flagsmith; | ||
|
||
namespace OpenFeature.Contrib.Providers.Flagsmith; | ||
|
||
/// <summary> | ||
/// Settings for Flagsmith Open feature provider | ||
/// </summary> | ||
public interface IFlagsmithProviderConfiguration | ||
{ | ||
/// <summary> | ||
/// Key that will be used as identity for Flagsmith requests. | ||
/// </summary> | ||
public string TargetingKey { get; } | ||
|
||
/// <summary> | ||
/// Determines whether to resolve a feature value as a boolean or use | ||
/// the isFeatureEnabled as the flag itself. These values will be false | ||
/// and true respectively. | ||
/// Default: false | ||
/// </summary> | ||
public bool UsingBooleanConfigValue { get; } | ||
} |
32 changes: 32 additions & 0 deletions
32
src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>netstandard20</TargetFrameworks> | ||
<PackageId>OpenFeature.Contrib.Providers.Flagsmith</PackageId> | ||
<VersionNumber>0.1.0</VersionNumber> | ||
<!--x-release-please-version --> | ||
<Version>$(VersionNumber)</Version> | ||
<AssemblyVersion>$(VersionNumber)</AssemblyVersion> | ||
<FileVersion>$(VersionNumber)</FileVersion> | ||
<Description>Flagsmith provider for .NET</Description> | ||
<PackageProjectUrl>https://openfeature.dev</PackageProjectUrl> | ||
<RepositoryUrl>https://github.com/open-feature/dotnet-sdk-contrib</RepositoryUrl> | ||
<Authors>Vladimir Petrusevici</Authors> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<!-- make the internal methods visble to our test project --> | ||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> | ||
<_Parameter1>$(MSBuildProjectName).Test</_Parameter1> | ||
</AssemblyAttribute> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Flagsmith" Version="5.1.0" /> | ||
<PackageReference Include="System.Text.Json" Version="7.0.3" /> | ||
</ItemGroup> | ||
|
||
<PropertyGroup> | ||
<LangVersion>latest</LangVersion> | ||
</PropertyGroup> | ||
</Project> |
Oops, something went wrong.