From b7ba62e4f88f23fba9daeaf487465834846ae532 Mon Sep 17 00:00:00 2001 From: vpetrusevici Date: Tue, 24 Oct 2023 23:26:23 +0300 Subject: [PATCH] feat: Add Flagsmith provider (#89) Signed-off-by: Vladimir Petrusevici Co-authored-by: Matthew Elwell Co-authored-by: Michael Beemer --- .github/component_owners.yml | 6 + .release-please-manifest.json | 3 +- DotnetSdkContrib.sln | 32 +- build/Common.props | 2 +- release-please-config.json | 10 + .../FlagsmithProvider.cs | 184 +++++++ .../FlagsmithProviderConfiguration.cs | 15 + .../IFlagsmithProviderConfiguration.cs | 22 + ...Feature.Contrib.Providers.Flagsmith.csproj | 32 ++ .../README.md | 114 +++++ .../FlagsmithProviderTest.cs | 453 ++++++++++++++++++ ...re.Contrib.Providers.Flagsmith.Test.csproj | 14 + 12 files changed, 876 insertions(+), 11 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj create mode 100644 src/OpenFeature.Contrib.Providers.Flagsmith/README.md create mode 100644 test/OpenFeature.Contrib.Providers.Flagsmith.Test/FlagsmithProviderTest.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flagsmith.Test/OpenFeature.Contrib.Providers.Flagsmith.Test.csproj diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 9fabb907..db652298 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -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: @@ -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 \ No newline at end of file diff --git a/.release-please-manifest.json b/.release-please-manifest.json index f3c91174..e503230b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -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" } \ No newline at end of file diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index dea93120..91f68f30 100644 --- a/DotnetSdkContrib.sln +++ b/DotnetSdkContrib.sln @@ -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 @@ -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} @@ -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 diff --git a/build/Common.props b/build/Common.props index 0cdf34da..fc34c474 100644 --- a/build/Common.props +++ b/build/Common.props @@ -25,6 +25,6 @@ --> [1.0.0,2.0) - [0.5,) + [1.2,) \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index 430ab3bb..c7415e3e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -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": [ diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs b/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs new file mode 100644 index 00000000..d937464e --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProvider.cs @@ -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 +{ + /// + /// FlagsmithProvider is the .NET provider implementation for the feature flag solution Flagsmith. + /// + public class FlagsmithProvider : FeatureProvider + { + private readonly static Metadata Metadata = new("Flagsmith Provider"); + delegate bool TryParseDelegate(string value, out T x); + internal readonly IFlagsmithClient _flagsmithClient; + + /// + /// Settings for Flagsmith Open feature provider + /// + public IFlagsmithProviderConfiguration Configuration { get; } + + + /// + /// Creates new instance of + /// + /// Open feature provider options. You can just use class + /// Flagsmith client options. You can just use class + public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithConfiguration flagsmithOptions) + { + Configuration = providerOptions; + _flagsmithClient = new FlagsmithClient(flagsmithOptions); + } + + /// + /// Creates new instance of + /// + /// Flagsmith client options. You can just use class + /// Open feature provider options. You can just use class + /// Http client that will be used for flagsmith requests. You also can use it to register as Typed HttpClient with as abstraction + public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithConfiguration flagsmithOptions, HttpClient httpClient) + { + Configuration = providerOptions; + _flagsmithClient = new FlagsmithClient(flagsmithOptions, httpClient); + } + + + /// + /// Creates new instance of + /// + /// Open feature provider options. You can just use class + /// Precreated Flagsmith client. You can just use class. + public FlagsmithProvider(IFlagsmithProviderConfiguration providerOptions, IFlagsmithClient flagsmithClient) + { + Configuration = providerOptions; + _flagsmithClient = flagsmithClient; + } + + private Task 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> ResolveValue(string flagKey, T defaultValue, TryParseDelegate 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> IsFeatureEnabled(string flagKey, EvaluationContext context) + { + var flags = await GetFlags(context); + var isFeatureEnabled = await flags.IsFeatureEnabled(flagKey); + return new(flagKey, isFeatureEnabled); + } + + + /// + public override Metadata GetMetadata() => Metadata; + + /// + + public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null) + => Configuration.UsingBooleanConfigValue + ? ResolveValue(flagKey, defaultValue, bool.TryParse, context) + : IsFeatureEnabled(flagKey, context); + + /// + public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null) + => ResolveValue(flagKey, defaultValue, int.TryParse, context); + + /// + public override Task> 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); + + + /// + public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null) + => ResolveValue(flagKey, defaultValue, (string x, out string y) => { y = x; return true; }, context); + + + /// + public override Task> 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; + } + + /// + /// convertValue is converting the dynamically typed object received from Flagsmith into the correct type + /// + /// The dynamically typed value we received from Flagsmith + /// A correctly typed object representing the flag value + private Value ConvertValue(JsonNode node) + { + if (node == null) + return null; + if (node is JsonArray jsonArray) + { + var arr = new List(); + 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(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; + } + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs b/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs new file mode 100644 index 00000000..13e1d237 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/FlagsmithProviderConfiguration.cs @@ -0,0 +1,15 @@ +namespace OpenFeature.Contrib.Providers.Flagsmith; + +/// +/// Settings for Flagsmith open feature provider +/// +public class FlagsmithProviderConfiguration : IFlagsmithProviderConfiguration +{ + /// + /// Key that will be used as identity for Flagsmith requests. Default: "targetingKey" + /// + public string TargetingKey { get; set; } = "targetingKey"; + + /// + public bool UsingBooleanConfigValue { get; set; } +} diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs b/src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs new file mode 100644 index 00000000..4f594a30 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/IFlagsmithProviderConfiguration.cs @@ -0,0 +1,22 @@ +using Flagsmith; + +namespace OpenFeature.Contrib.Providers.Flagsmith; + +/// +/// Settings for Flagsmith Open feature provider +/// +public interface IFlagsmithProviderConfiguration +{ + /// + /// Key that will be used as identity for Flagsmith requests. + /// + public string TargetingKey { get; } + + /// + /// 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 + /// + public bool UsingBooleanConfigValue { get; } +} diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj new file mode 100644 index 00000000..12698004 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj @@ -0,0 +1,32 @@ + + + + netstandard20 + OpenFeature.Contrib.Providers.Flagsmith + 0.1.0 + + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + Flagsmith provider for .NET + https://openfeature.dev + https://github.com/open-feature/dotnet-sdk-contrib + Vladimir Petrusevici + + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + latest + + diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/README.md b/src/OpenFeature.Contrib.Providers.Flagsmith/README.md new file mode 100644 index 00000000..e8619e1c --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/README.md @@ -0,0 +1,114 @@ +# Flagsmith .NET Provider + +The Flagsmith provider allows you to connect to your Flagsmith instance through the OpenFeature SDK + +# .Net SDK usage + +## Install dependencies + +The first things we will do is install the **Open Feature SDK** and the **Flagsmith Feature Flag provider**. + +### .NET Cli +```shell +dotnet add package OpenFeature.Contrib.Providers.Flagsmith +``` +### Package Manager + +```shell +NuGet\Install-Package OpenFeature.Contrib.Providers.Flagsmith +``` +### Package Reference + +```xml + +``` +### Packet cli + +```shell +packet add OpenFeature.Contrib.Providers.Flagsmith +``` + +### Cake + +```shell +// Install OpenFeature.Contrib.Providers.Flagsmith as a Cake Addin +#addin nuget:?package=OpenFeature.Contrib.Providers.Flagsmith + +// Install OpenFeature.Contrib.Providers.Flagsmith as a Cake Tool +#tool nuget:?package=OpenFeature.Contrib.Providers.Flagsmith +``` + +## Using the Flagsmith Provider with the OpenFeature SDK + +To create a Flagmith provider you should define provider and Flagsmith settings. + +```csharp +using OpenFeature.Contrib.Providers.Flagd; + +namespace OpenFeatureTestApp +{ + class Hello { + static void Main(string[] args) { + + // Additional configs for provider + var providerConfig = new FlagsmithProviderConfiguration(); + + //Flagsmith client configuration + var flagsmithConfig = new FlagsmithConfiguration + { + ApiUrl = "https://edge.api.flagsmith.com/api/v1/", + EnvironmentKey = string.Empty, + EnableClientSideEvaluation = false, + EnvironmentRefreshIntervalSeconds = 60, + EnableAnalytics = false, + Retries = 1 + }; + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithConfig);\ + + // Set the flagsmithProvider as the provider for the OpenFeature SDK + OpenFeature.Api.Instance.SetProvider(flagsmithProvider); + + var client = OpenFeature.Api.Instance.GetClient("my-app"); + + var val = client.GetBooleanValue("myBoolFlag", false, null); + + // Print the value of the 'myBoolFlag' feature flag + System.Console.WriteLine(val.Result.ToString()); + } + } +} +``` + +You also can create Flagsmith provider using ```HttpClient``` or precreated ```FlagsmithClient``` + +```csharp +using var httpClient = new HttpClient(); +var flagsmithProvider = new FlagsmithProvider(providerConfig, config, httpClient); +``` +```csharp +using var flagsmithClient = new FlagsmithClient(flagsmithOptions); +var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); +``` +### Configuring the FlagsmithProvider + +To configure FlagsmithConfiguration just use [an example](https://github.com/Flagsmith/flagsmith-dotnet-client/tree/main/Example) from Flagsmith GitHub. +For FlagsmithProviderConfiguration you can configure next parameters using custom implementation or just ```FlagsmithProviderConfiguration```: +```csharp +public interface IFlagsmithProviderConfiguration +{ + /// + /// Key that will be used as identity for Flagsmith requests. + /// + public string TargetingKey { get; } + + /// + /// 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 + /// + public bool UsingBooleanConfigValue { get; } +} +``` + + diff --git a/test/OpenFeature.Contrib.Providers.Flagsmith.Test/FlagsmithProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flagsmith.Test/FlagsmithProviderTest.cs new file mode 100644 index 00000000..29a4e608 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagsmith.Test/FlagsmithProviderTest.cs @@ -0,0 +1,453 @@ +using Xunit; +using System; +using Flagsmith; +using System.Net.Http; +using NSubstitute; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Model; +using System.Linq; +using OpenFeature.Error; +using System.Collections.Generic; + +namespace OpenFeature.Contrib.Providers.Flagsmith.Test +{ + public class UnitTestFlagsmithProvider + { + private static FlagsmithConfiguration GetDefaultFlagsmithConfiguration() => new() + { + ApiUrl = "https://edge.api.flagsmith.com/api/v1/", + EnvironmentKey = string.Empty, + EnableClientSideEvaluation = false, + EnvironmentRefreshIntervalSeconds = 60, + EnableAnalytics = false, + Retries = 1 + }; + + private static FlagsmithProviderConfiguration GetDefaultFlagsmithProviderConfigurationConfiguration() => new(); + + [Fact] + public void CreateFlagmithProvider_WithValidCredentials_CreatesProviderInstanceSuccessfully() + { + // Arrange + var config = GetDefaultFlagsmithConfiguration(); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + // Act + var flagsmithProvider = new FlagsmithProvider(providerConfig, config); + + + // Assert + Assert.NotNull(flagsmithProvider._flagsmithClient); + } + + [Fact] + public void CreateFlagmithProvider_WithValidCredentialsAndCustomHttpClient_CreatesProviderInstanceSuccessfully() + { + // Arrange + var config = GetDefaultFlagsmithConfiguration(); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + using var httpClient = new HttpClient(); + // Act + var flagsmithProvider = new FlagsmithProvider(providerConfig, config, httpClient); + + + // Assert + Assert.NotNull(flagsmithProvider._flagsmithClient); + } + + [Fact] + public async Task GetValue_ForEnabledFeatureWithEvaluationContext_ReturnCorrectValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + var date = DateTime.Now; + flags.GetFeatureValue("example-feature").Returns("true"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetIdentityFlags("233", Arg.Is>(x => x.Count == 7 && x.Any(c => c.GetTraitKey() == "key1"))).Returns(flags); + + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + var contextBuilder = EvaluationContext.Builder() + .Set("key1", "value") + .Set("key2", 1) + .Set("key3", true) + .Set("key4", date) + .Set("key5", Structure.Empty) + .Set("key6", 1.0) + .Set("targetingKey", "233"); + // Act + var result = await flagsmithProvider.ResolveBooleanValue("example-feature", false, contextBuilder.Build()); + + // Assert + Assert.True(result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Null(result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + + [Theory] + [InlineData(true, true, "true", true, null, true)] + [InlineData(false, true, "true", true, null, true)] + [InlineData(true, false, "true", true, null, true)] + [InlineData(false, false, "true", true, null, true)] + [InlineData(true, true, "false", true, null, false)] + [InlineData(false, true, "false", true, null, false)] + [InlineData(true, false, "false", true, null, true)] + [InlineData(false, false, "false", true, null, true)] + + [InlineData(true, true, "true", false, "DISABLED", true)] + [InlineData(false, true, "true", false, "DISABLED", false)] + [InlineData(true, false, "true", false, null, false)] + [InlineData(false, false, "true", false, null, false)] + [InlineData(true, true, "false", false, "DISABLED", true)] + [InlineData(false, true, "false", false, "DISABLED", false)] + [InlineData(true, false, "false", false, null, false)] + [InlineData(false, false, "false", false, null, false)] + public async Task GetBooleanValue_ForEnabledFeatureWithValidFormatAndSettedConfigValue_ReturnExpectedResult( + bool defaultValue, + bool enabledValueConfig, + string settedValue, + bool featureEnabled, + string expectedReason, + bool expectedResult) + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns(settedValue); + flags.IsFeatureEnabled("example-feature").Returns(featureEnabled); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + providerConfig.UsingBooleanConfigValue = enabledValueConfig; + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveBooleanValue("example-feature", defaultValue); + + // Assert + Assert.Equal(expectedResult, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Equal(expectedReason, result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetBooleanValue_ForEnabledFeatureWithWrongFormatValue_ThrowsTypeMismatch() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("hreni"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + providerConfig.UsingBooleanConfigValue = true; + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act and Assert + await Assert.ThrowsAsync(() => flagsmithProvider.ResolveBooleanValue("example-feature", true)); + } + + + [Fact] + public async Task GetDoubleValue_ForEnabledFeatureWithValidFormat_ReturnCorrectValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("32.334"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveDoubleValue("example-feature", 32.22); + + // Assert + Assert.Equal(32.334, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Null(result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + + [Fact] + public async Task GetDoubleValue_ForDisabledFeatureWithValidFormat_ReturnDefaultValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("4112"); + flags.IsFeatureEnabled("example-feature").Returns(false); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveDoubleValue("example-feature", -32.22); + + // Assert + Assert.Equal(-32.22, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Equal(Reason.Disabled, result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetDoubleValue_ForEnabledFeatureWithWrongFormatValue_ThrowsTypeMismatch() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("hreni"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act and Assert + await Assert.ThrowsAsync(() => flagsmithProvider.ResolveDoubleValue("example-feature", 2222.22133)); + } + + + + [Fact] + public async Task GetStringValue_ForEnabledFeatureWithValidFormat_ReturnCorrectValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("example"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveStringValue("example-feature", "example"); + + // Assert + Assert.Equal("example", result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Null(result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + + [Fact] + public async Task GetStringValue_ForDisabledFeatureWithValidFormat_ReturnDefaultValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("4112"); + flags.IsFeatureEnabled("example-feature").Returns(false); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveStringValue("example-feature", "3333a"); + + // Assert + Assert.Equal("3333a", result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Equal(Reason.Disabled, result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + + [Fact] + public async Task GetIntValue_ForEnabledFeatureWithValidFormat_ReturnCorrectValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("232"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveIntegerValue("example-feature", 32); + + // Assert + Assert.Equal(232, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Null(result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetIntValue_ForDisabledFeatureWithValidFormat_ReturnDefaultValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("4112"); + flags.IsFeatureEnabled("example-feature").Returns(false); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveIntegerValue("example-feature", -32); + + // Assert + Assert.Equal(-32, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Equal(Reason.Disabled, result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetIntValue_ForEnabledFeatureWithWrongFormatValue_ThrowsTypeMismatch() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("hreni"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act and Assert + await Assert.ThrowsAsync(() => flagsmithProvider.ResolveIntegerValue("example-feature", 2222)); + } + + [Fact] + public async Task GetStructureValue_ForEnabledFeatureWithValidFormat_ReturnCorrectValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + + #pragma warning disable format + var expectedValue = + """ + { + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": [ + "GML", + "XML" + ] + }, + "GlossSee": "markup" + } + } + } + } + } + """; + #pragma warning restore format + + flags.GetFeatureValue("example-feature").Returns(expectedValue); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var defaultObject = new Value(Structure.Empty); + + var result = await flagsmithProvider.ResolveStructureValue("example-feature", defaultObject); + + // Assert + var glossary = result.Value.AsStructure.GetValue("glossary"); + Assert.True(glossary.IsStructure); + Assert.Equal("example glossary", glossary.AsStructure.GetValue("title").AsString); + var glossDiv = glossary.AsStructure.GetValue("GlossDiv"); + Assert.True(glossDiv.IsStructure); + var glossList = glossDiv.AsStructure.GetValue("GlossList"); + Assert.True(glossList.IsStructure); + var glossEntry = glossList.AsStructure.GetValue("GlossEntry"); + Assert.True(glossEntry.IsStructure); + Assert.Equal("SGML", glossEntry.AsStructure.GetValue("SortAs").AsString); + var glossDef = glossEntry.AsStructure.GetValue("GlossDef"); + Assert.True(glossDef.IsStructure); + var glossSeeAlso = glossDef.AsStructure.GetValue("GlossSeeAlso"); + Assert.True(glossSeeAlso.IsList); + Assert.Equal(2, glossSeeAlso.AsList.Count); + Assert.Equal("GML", glossSeeAlso.AsList.First().AsString); + Assert.Equal("XML", glossSeeAlso.AsList.Last().AsString); + + Assert.Equal("example-feature", result.FlagKey); + Assert.Null(result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetStructureValue_ForDisabledFeatureWithValidFormat_ReturnDefaultValue() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("4112"); + flags.IsFeatureEnabled("example-feature").Returns(false); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + + var defaultObject = new Value("default"); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act + var result = await flagsmithProvider.ResolveStructureValue("example-feature", defaultObject); + + // Assert + Assert.Equal(defaultObject, result.Value); + Assert.Equal("example-feature", result.FlagKey); + Assert.Equal(Reason.Disabled, result.Reason); + Assert.Equal(ErrorType.None, result.ErrorType); + } + + [Fact] + public async Task GetStructureValue_ForEnabledFeatureWithWrongFormatValue_ThrowsTypeMismatch() + { + // Arrange + var flagsmithClient = Substitute.For(); + var flags = Substitute.For(); + flags.GetFeatureValue("example-feature").Returns("hreni"); + flags.IsFeatureEnabled("example-feature").Returns(true); + flagsmithClient.GetEnvironmentFlags().Returns(flags); + + var defaultObject = new Value("default"); + var providerConfig = GetDefaultFlagsmithProviderConfigurationConfiguration(); + var flagsmithProvider = new FlagsmithProvider(providerConfig, flagsmithClient); + + // Act and Assert + await Assert.ThrowsAsync(() => flagsmithProvider.ResolveStructureValue("example-feature", defaultObject)); + } + } +} diff --git a/test/OpenFeature.Contrib.Providers.Flagsmith.Test/OpenFeature.Contrib.Providers.Flagsmith.Test.csproj b/test/OpenFeature.Contrib.Providers.Flagsmith.Test/OpenFeature.Contrib.Providers.Flagsmith.Test.csproj new file mode 100644 index 00000000..e35f8e7e --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagsmith.Test/OpenFeature.Contrib.Providers.Flagsmith.Test.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + latest + + \ No newline at end of file