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 #91

Closed
Closed
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
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
208 changes: 208 additions & 0 deletions src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
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;

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");
internal readonly IFlagsmithClient _flagsmithClient;
/// <summary>
/// Creates new instance of <see cref="FlagsmithProvider"/>
/// </summary>
/// <param name="options">Flagsmith client options. You can just use <see cref="FlagsmithConfiguration"/> class</param>
public FlagsmithProvider(IFlagsmithConfiguration options)
{
_flagsmithClient = new FlagsmithClient(options);
}

/// <summary>
/// Creates new instance of <see cref="FlagsmithProvider"/>
/// </summary>
/// <param name="options">Flagsmith client options. You can just use <see cref="FlagsmithConfiguration"/> 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(IFlagsmithConfiguration options, HttpClient httpClient)
{
_flagsmithClient = new FlagsmithClient(options, httpClient);
}


/// <summary>
/// Creates new instance of <see cref="FlagsmithProvider"/>
/// </summary>
/// <param name="flagsmithClient">Precreated Flagsmith client. You can just use <see cref="FlagsmithClient"/> class.</param>
public FlagsmithProvider(IFlagsmithClient flagsmithClient)
{
_flagsmithClient = flagsmithClient;
}

private Task<IFlags> GetFlags(EvaluationContext ctx = null)
{
var key = ctx?.GetValue("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());
}


/// <inheritdoc/>
public override Metadata GetMetadata() => Metadata;

/// <inheritdoc/>

public override async Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null)
{
var flags = await GetFlags(context);
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey);
if (!isFlagEnabled)
{
return new ResolutionDetails<bool>(flagKey, defaultValue, reason: Reason.Disabled);
}

var stringValue = await flags.GetFeatureValue(flagKey);
if (bool.TryParse(stringValue, out var parsedValue))
{
return new ResolutionDetails<bool>(flagKey, parsedValue);
}
throw new TypeMismatchException("Failed to parse value in boolean type");

}

/// <inheritdoc/>

public override async Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null)
{

var flags = await GetFlags(context);
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey);
if (!isFlagEnabled)
{
return new ResolutionDetails<string>(flagKey, defaultValue, reason: Reason.Disabled);
}

var stringValue = await flags.GetFeatureValue(flagKey);
return new ResolutionDetails<string>(flagKey, stringValue);
}

/// <inheritdoc/>
public override async Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null)
{

var flags = await GetFlags(context);
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey);
if (!isFlagEnabled)
{
return new ResolutionDetails<int>(flagKey, defaultValue, reason: Reason.Disabled);
}

var stringValue = await flags.GetFeatureValue(flagKey);
if(int.TryParse(stringValue, out var parsedValue))
{
return new ResolutionDetails<int>(flagKey, parsedValue);
}
throw new TypeMismatchException("Failed to parse value in int type");
}

/// <inheritdoc/>
public override async Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null)
{

var flags = await GetFlags(context);
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey);
if (!isFlagEnabled)
{
return new ResolutionDetails<double>(flagKey, defaultValue, reason: Reason.Disabled);
}

var stringValue = await flags.GetFeatureValue(flagKey);
if (double.TryParse(stringValue, out var parsedValue))
{
return new ResolutionDetails<double>(flagKey, parsedValue);
}
throw new TypeMismatchException("Failed to parse value in double type");
}


/// <inheritdoc/>
public override async Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue, EvaluationContext context = null)
{

var flags = await GetFlags(context);
var isFlagEnabled = await flags.IsFeatureEnabled(flagKey);
if (!isFlagEnabled)
{
return new ResolutionDetails<Value>(flagKey, defaultValue, reason: Reason.Disabled);
}
var stringValue = await flags.GetFeatureValue(flagKey);

try
{
var mappedValue = JsonNode.Parse(stringValue);
var value = ConvertValue(mappedValue);
if (value is not null)
{
return new ResolutionDetails<Value>(flagKey, value);

}
}
catch(Exception ex)
{
throw new TypeMismatchException("Failed to parse value in structure type", ex);
}
throw new TypeMismatchException("Failed to parse value in structure type");
}

/// <summary>
/// convertValue is converting the object return by the proxy response in the right type.
/// </summary>
/// <param name="node">The value we have received</param>
/// <returns>A converted object</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 Value(arr);
}

if (node is JsonObject jsonObject)
{
var dict = jsonObject.ToDictionary(x => x.Key, x => ConvertValue(x.Value));

return new Value(new Structure(dict));
}

if (node.AsValue().TryGetValue<JsonElement>(out var jsonElement))
{
if (jsonElement.ValueKind == JsonValueKind.False || jsonElement.ValueKind == JsonValueKind.True)
return new Value(jsonElement.GetBoolean());
if (jsonElement.ValueKind == JsonValueKind.Number)
return new Value(jsonElement.GetDouble());

if (jsonElement.ValueKind == JsonValueKind.String)
return new Value(jsonElement.ToString());
}
return null;
}
}
}
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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading