Skip to content

Commit

Permalink
feat: Add Flagsmith provider (#89)
Browse files Browse the repository at this point in the history
Signed-off-by: Vladimir Petrusevici <[email protected]>
Co-authored-by: Matthew Elwell <[email protected]>
Co-authored-by: Michael Beemer <[email protected]>
  • Loading branch information
3 people authored Oct 24, 2023
1 parent d8cac7f commit b7ba62e
Show file tree
Hide file tree
Showing 12 changed files with 876 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .github/component_owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ components:
- toddbaert
src/OpenFeature.Contrib.Providers.GOFeatureFlag:
- thomaspoignant
src/OpenFeature.Contrib.Providers.Flagsmith:
- vpetrusevici
- matthewelwell

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

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}"
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

0 comments on commit b7ba62e

Please sign in to comment.