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: Add Flagsmith provider #89

Merged
merged 13 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
8 changes: 8 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ components:
- toddbaert
src/OpenFeature.Contrib.Providers.GOFeatureFlag:
- thomaspoignant
src/OpenFeature.Contrib.Providers.Flagsmith:
- vpetrusevici
- matthewelwell
- novakzaballa
vpetrusevici marked this conversation as resolved.
Show resolved Hide resolved

# test/
test/OpenFeature.Contrib.Hooks.Otel.Test:
Expand All @@ -19,6 +23,10 @@ components:
- toddbaert
test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test:
- thomaspoignant
test/OpenFeature.Contrib.Providers.Flagsmith.Test:
- vpetrusevici
- matthewelwell
- novakzaballa

ignored-authors:
- renovate-bot
3 changes: 2 additions & 1 deletion .release-please-manifest.json
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"
}
32 changes: 23 additions & 9 deletions DotnetSdkContrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@ VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0E563821-BD08-4B7F-BF9D-395CAD80F026}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagd", "src\OpenFeature.Contrib.Providers.Flagd\OpenFeature.Contrib.Providers.Flagd.csproj", "{6F8FF25A-F22B-4083-B3F9-B4B9BB6FB699}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Providers.Flagd", "src\OpenFeature.Contrib.Providers.Flagd\OpenFeature.Contrib.Providers.Flagd.csproj", "{6F8FF25A-F22B-4083-B3F9-B4B9BB6FB699}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Hooks.Otel", "src\OpenFeature.Contrib.Hooks.Otel\OpenFeature.Contrib.Hooks.Otel.csproj", "{82D10BAE-F1EE-432A-BD5D-DECAD07A84FE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Hooks.Otel", "src\OpenFeature.Contrib.Hooks.Otel\OpenFeature.Contrib.Hooks.Otel.csproj", "{82D10BAE-F1EE-432A-BD5D-DECAD07A84FE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Hooks.Otel.Test", "test\OpenFeature.Contrib.Hooks.Otel.Test\OpenFeature.Contrib.Hooks.Otel.Test.csproj", "{199FA48A-06EF-4E15-8206-C095D1455A99}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Hooks.Otel.Test", "test\OpenFeature.Contrib.Hooks.Otel.Test\OpenFeature.Contrib.Hooks.Otel.Test.csproj", "{199FA48A-06EF-4E15-8206-C095D1455A99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagd.Test", "test\OpenFeature.Contrib.Providers.Flagd.Test\OpenFeature.Contrib.Providers.Flagd.Test.csproj", "{206323A0-7334-4723-8394-C31C150B95DC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Providers.Flagd.Test", "test\OpenFeature.Contrib.Providers.Flagd.Test\OpenFeature.Contrib.Providers.Flagd.Test.csproj", "{206323A0-7334-4723-8394-C31C150B95DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.GOFeatureFlag", "src\OpenFeature.Contrib.Providers.GOFeatureFlag\OpenFeature.Contrib.Providers.GOFeatureFlag.csproj", "{F7BE205B-0375-4EC5-9B18-FAFEF7A78D71}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Providers.GOFeatureFlag", "src\OpenFeature.Contrib.Providers.GOFeatureFlag\OpenFeature.Contrib.Providers.GOFeatureFlag.csproj", "{F7BE205B-0375-4EC5-9B18-FAFEF7A78D71}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.GOFeatureFlag.Test", "test\OpenFeature.Contrib.Providers.GOFeatureFlag.Test\OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj", "{4041B63F-9CF6-4886-8FC7-BD1A7E45F859}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Providers.GOFeatureFlag.Test", "test\OpenFeature.Contrib.Providers.GOFeatureFlag.Test\OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj", "{4041B63F-9CF6-4886-8FC7-BD1A7E45F859}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenFeature.Contrib.Providers.Flagsmith", "src\OpenFeature.Contrib.Providers.Flagsmith\OpenFeature.Contrib.Providers.Flagsmith.csproj", "{47008BEE-7888-4B9B-8884-712A922C3F9B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flagsmith.Test", "test\OpenFeature.Contrib.Providers.Flagsmith.Test\OpenFeature.Contrib.Providers.Flagsmith.Test.csproj", "{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}"
vpetrusevici marked this conversation as resolved.
Show resolved Hide resolved
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F8FF25A-F22B-4083-B3F9-B4B9BB6FB699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F8FF25A-F22B-4083-B3F9-B4B9BB6FB699}.Debug|Any CPU.Build.0 = Debug|Any CPU
Expand All @@ -52,6 +53,17 @@ Global
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859}.Release|Any CPU.Build.0 = Release|Any CPU
{47008BEE-7888-4B9B-8884-712A922C3F9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47008BEE-7888-4B9B-8884-712A922C3F9B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47008BEE-7888-4B9B-8884-712A922C3F9B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47008BEE-7888-4B9B-8884-712A922C3F9B}.Release|Any CPU.Build.0 = Release|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6F8FF25A-F22B-4083-B3F9-B4B9BB6FB699} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
Expand All @@ -60,5 +72,7 @@ Global
{206323A0-7334-4723-8394-C31C150B95DC} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{F7BE205B-0375-4EC5-9B18-FAFEF7A78D71} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{4041B63F-9CF6-4886-8FC7-BD1A7E45F859} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
{47008BEE-7888-4B9B-8884-712A922C3F9B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026}
{C3BA23C2-BEC3-4683-A64A-C914C3D8037E} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE}
EndGlobalSection
EndGlobal
2 changes: 1 addition & 1 deletion build/Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
-->
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
<!-- 0.5+ -->
<OpenFeatureVer>[0.5,)</OpenFeatureVer>
<OpenFeatureVer>[1.2,)</OpenFeatureVer>
</PropertyGroup>
</Project>
10 changes: 10 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@
"extra-files": [
"OpenFeature.Contrib.Providers.GOFeatureFlag.csproj"
]
},
"src/OpenFeature.Contrib.Providers.Flagsmith": {
"package-name": "OpenFeature.Contrib.Providers.Flagsmith",
"release-type": "simple",
"bump-minor-pre-major": true,
"bump-patch-for-minor-pre-major": true,
"versioning": "default",
"extra-files": [
"OpenFeature.Contrib.Providers.Flagsmith.csproj"
]
}
},
"changelog-sections": [
Expand Down
184 changes: 184 additions & 0 deletions src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs
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;
}
}
}
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; }
}
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; }
}
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>
Loading
Loading