From 143b6bdb4ab7ce34c578bce050608ceaa074b3c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 09:42:15 -0400 Subject: [PATCH 01/56] chore(main): release OpenFeature.Contrib.Providers.ConfigCat 0.1.0 (#264) Signed-off-by: Todd Baert Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Todd Baert --- .release-please-manifest.json | 2 +- .../CHANGELOG.md | 11 +++++++++++ .../OpenFeature.Contrib.Providers.ConfigCat.csproj | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d9044fb6..fe26c012 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,7 +3,7 @@ "src/OpenFeature.Contrib.Providers.Flagd": "0.3.0", "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.0", "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0", - "src/OpenFeature.Contrib.Providers.ConfigCat": "0.0.5", + "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.0", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md index c380cf33..7d279c7c 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.5...OpenFeature.Contrib.Providers.ConfigCat-v0.1.0) (2024-08-22) + + +### ⚠ BREAKING CHANGES + +* use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) + +### ✨ New Features + +* use (and require) OpenFeature SDK v2 ([#262](https://github.com/open-feature/dotnet-sdk-contrib/issues/262)) ([f845134](https://github.com/open-feature/dotnet-sdk-contrib/commit/f84513438586457087ac47fd40629912f2ec473a)) + ## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.4...OpenFeature.Contrib.Providers.ConfigCat-v0.0.5) (2024-08-21) diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj index 6a9506d3..590b2244 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.ConfigCat - 0.0.5 + 0.1.0 $(VersionNumber) $(VersionNumber) $(VersionNumber) From 161fb638f22eecae2d4caa84c6c595878c8c48c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:15:21 -0400 Subject: [PATCH 02/56] chore(deps): update dependency grpc.tools to 2.66.0 (#271) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 049033b0..a84e4cfe 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -29,7 +29,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 829b5915e828f99a10255ead5f7cf108d9c9d94d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Sep 2024 08:52:46 -0400 Subject: [PATCH 03/56] chore(deps): update dependency flagsmith to 5.3.2 (#243) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagsmith.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj index e9453b9b..73234f32 100644 --- a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj @@ -18,7 +18,7 @@ - + From 0b2d5f29490ad16ee5efde55d31354e0322c6f86 Mon Sep 17 00:00:00 2001 From: adams85 <31276480+adams85@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:08:24 +0200 Subject: [PATCH 04/56] fix: Revise ConfigCat provider (#280) Signed-off-by: Adam Simon --- .../ConfigCatProvider.cs | 13 +++ ...Feature.Contrib.Providers.ConfigCat.csproj | 2 +- .../README.md | 90 ++++++++-------- .../UserBuilder.cs | 32 +++--- .../ConfigCatProviderTest.cs | 102 +++++++++++++++++- 5 files changed, 176 insertions(+), 63 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs index 48f966ed..c3889f5f 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/ConfigCatProvider.cs @@ -29,6 +29,19 @@ public ConfigCatProvider(string sdkKey, Action configBui Client = ConfigCatClient.Get(sdkKey, configBuilder); } + /// + public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + { + return Client.WaitForReadyAsync(cancellationToken); + } + + /// + public override Task ShutdownAsync(CancellationToken cancellationToken = default) + { + Client.Dispose(); + return Task.CompletedTask; + } + /// public override Metadata GetMetadata() { diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj index 590b2244..0094f252 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj @@ -16,6 +16,6 @@ - + diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/README.md b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md index b08e8826..227f7680 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/README.md +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/README.md @@ -2,7 +2,7 @@ The ConfigCat Flag provider allows you to connect to your ConfigCat instance. -# .Net SDK usage +# .NET SDK usage ## Requirements @@ -47,68 +47,72 @@ paket add OpenFeature.Contrib.Providers.ConfigCat The following example shows how to use the ConfigCat provider with the OpenFeature SDK. ```csharp -using OpenFeature.Contrib.Providers.ConfigCat; +using System; +using ConfigCat.Client; +using OpenFeature.Contrib.ConfigCat; -namespace OpenFeatureTestApp +var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#"); + +// Set the configCatProvider as the provider for the OpenFeature SDK +await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider); + +var client = OpenFeature.Api.Instance.GetClient(); + +var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false); +if (isAwesomeFeatureEnabled) +{ + doTheNewThing(); +} +else { - class Hello { - static void Main(string[] args) { - var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#"); - - // Set the configCatProvider as the provider for the OpenFeature SDK - OpenFeature.Api.Instance.SetProvider(configCatProvider); - - var client = OpenFeature.Api.Instance.GetClient(); - - var val = client.GetBooleanValueAsync("isMyAwesomeFeatureEnabled", false); - - if(isMyAwesomeFeatureEnabled) - { - doTheNewThing(); - } - else - { - doTheOldThing(); - } - } - } + doTheOldThing(); } ``` ### Customizing the ConfigCat Provider -The ConfigCat provider can be customized by passing a `ConfigCatClientOptions` object to the constructor. +The ConfigCat provider can be customized by passing a callback setting up a `ConfigCatClientOptions` object to the constructor. ```csharp -var configCatOptions = new ConfigCatClientOptions +Action configureConfigCatOptions = (options) => { - PollingMode = PollingModes.ManualPoll; - Logger = new ConsoleLogger(LogLevel.Info); + options.PollingMode = PollingModes.LazyLoad(cacheTimeToLive: TimeSpan.FromSeconds(10)); + options.Logger = new ConsoleLogger(LogLevel.Info); + // ... }; -var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configCatOptions); +var configCatProvider = new ConfigCatProvider("#YOUR-SDK-KEY#", configureConfigCatOptions); ``` For a full list of options see the [ConfigCat documentation](https://configcat.com/docs/sdk-reference/dotnet/). -## EvaluationContext and ConfigCat User relationship +### Cleaning up + +On application shutdown, clean up the OpenFeature provider and the underlying ConfigCat client. + +```csharp +await OpenFeature.Api.Instance.ShutdownAsync(); +``` + +## EvaluationContext and ConfigCat User Object relationship -ConfigCat has the concept of Users where you can evaluate a flag based on properties. The OpenFeature SDK has the concept of an EvaluationContext which is a dictionary of string keys and values. The ConfigCat provider will map the EvaluationContext to a ConfigCat User. +An evaluation context in the OpenFeature specification is a container for arbitrary contextual data that can be used as a basis for feature flag evaluation. +The ConfigCat provider translates these evaluation contexts to ConfigCat [User Objects](https://configcat.com/docs/targeting/user-object/). -The ConfigCat User has a few pre-defined parameters that can be used to evaluate a flag. These are: +The ConfigCat User Object has a few pre-defined attributes that can be used to evaluate a flag. These are: -| Parameter | Description | -|-----------|---------------------------------------------------------------------------------------------------------------------------------| -| `Id` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. | -| `Email` | Optional parameter for easier targeting rule definitions. | -| `Country` | Optional parameter for easier targeting rule definitions. | -| `Custom` | Optional dictionary for custom attributes of a user for advanced targeting rule definitions. E.g. User role, Subscription type. | +| Attribute | Description | +|--------------|----------------------------------------------------------------------------------------------------------------| +| `Identifier` | *REQUIRED*. Unique identifier of a user in your application. Can be any `string` value, even an email address. | +| `Email` | The email address of the user. | +| `Country` | The country of the user. | -Since EvaluationContext is a simple dictionary, the provider will try to match the keys to the ConfigCat User parameters following the table below in a case-insensitive manner. +Since `EvaluationContext` is a simple dictionary, the provider will try to match the keys to ConfigCat user attributes following the table below in a case-insensitive manner. -| EvaluationContext Key | ConfigCat User Parameter | +| EvaluationContext Key | ConfigCat User Attribute | |-----------------------|--------------------------| -| `id` | `Id` | -| `identifier` | `Id` | +| `id` | `Identifier` | +| `identifier` | `Identifier` | | `email` | `Email` | -| `country` | `Country` | \ No newline at end of file +| `country` | `Country` | +| Any other | `Custom` | \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs index ba8798ee..391e487b 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/UserBuilder.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using ConfigCat.Client; using OpenFeature.Model; @@ -17,35 +16,32 @@ internal static User BuildUser(this EvaluationContext context) return null; } - var user = context.TryGetValuesInsensitive(PossibleUserIds, out var pair) - ? new User(pair.Value.AsString) - : new User(Guid.NewGuid().ToString()); + var user = new User(context.GetUserId()); foreach (var value in context) { - switch (value.Key.ToUpperInvariant()) + if (StringComparer.OrdinalIgnoreCase.Equals("EMAIL", value.Key)) { - case "EMAIL": - user.Email = value.Value.AsString; - continue; - case "COUNTRY": - user.Country = value.Value.AsString; - continue; - default: - user.Custom.Add(value.Key, value.Value.AsString); - continue; + user.Email = value.Value.AsString; + } + else if (StringComparer.OrdinalIgnoreCase.Equals("COUNTRY", value.Key)) + { + user.Country = value.Value.AsString; + } + else + { + user.Custom.Add(value.Key, value.Value.AsString); } } return user; } - private static bool TryGetValuesInsensitive(this EvaluationContext context, string[] keys, - out KeyValuePair pair) + private static string GetUserId(this EvaluationContext context) { - pair = context.AsDictionary().FirstOrDefault(x => keys.Contains(x.Key.ToUpperInvariant())); + var pair = context.AsDictionary().FirstOrDefault(x => PossibleUserIds.Contains(x.Key, StringComparer.OrdinalIgnoreCase)); - return pair.Key != null; + return pair.Key != null ? pair.Value.AsString : ""; } } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs index 1a478f03..0bd0b44f 100644 --- a/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.ConfigCat.Test/ConfigCatProviderTest.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Threading; using System.Threading.Tasks; using AutoFixture.Xunit2; using ConfigCat.Client; @@ -12,14 +14,58 @@ namespace OpenFeature.Contrib.ConfigCat.Test { public class ConfigCatProviderTest { + const string TestConfigJson = +@" +{ + ""f"": { + ""isAwesomeFeatureEnabled"": { + ""t"": 0, + ""v"": { + ""b"": true + } + }, + ""isPOCFeatureEnabled"": { + ""t"": 0, + ""r"": [ + { + ""c"": [ + { + ""u"": { + ""a"": ""Email"", + ""c"": 2, + ""l"": [ + ""@example.com"" + ] + } + } + ], + ""s"": { + ""v"": { + ""b"": true + } + } + } + ], + ""v"": { + ""b"": false + } + } + } +} +"; + [Theory] [AutoData] - public void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey) + public async void CreateConfigCatProvider_WithSdkKey_CreatesProviderInstanceSuccessfully(string sdkKey) { var configCatProvider = new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(); }); + await configCatProvider.InitializeAsync(EvaluationContext.Empty); + Assert.NotNull(configCatProvider.Client); + + await configCatProvider.ShutdownAsync(); } [Theory] @@ -93,11 +139,40 @@ public async Task GetStructureValueAsync_ForFeature_ReturnExpectedResult(string var configCatProvider = new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", defaultValue.AsString)); }); + await configCatProvider.InitializeAsync(EvaluationContext.Empty); + var result = await configCatProvider.ResolveStructureValueAsync("example-feature", defaultValue); Assert.Equal(defaultValue.AsString, result.Value.AsString); Assert.Equal("example-feature", result.FlagKey); Assert.Equal(ErrorType.None, result.ErrorType); + + await configCatProvider.ShutdownAsync(); + } + + [Theory] + [InlineAutoData("alice@configcat.com", false)] + [InlineAutoData("bob@example.com", true)] + public async Task OpenFeatureAPI_EndToEnd_Test(string email, bool expectedValue) + { + var configCatProvider = new ConfigCatProvider("fake-67890123456789012/1234567890123456789012", options => + { options.ConfigFetcher = new FakeConfigFetcher(TestConfigJson); }); + + await OpenFeature.Api.Instance.SetProviderAsync(configCatProvider); + + var client = OpenFeature.Api.Instance.GetClient(); + + var evaluationContext = EvaluationContext.Builder() + .Set("email", email) + .Build(); + + var result = await client.GetBooleanDetailsAsync("isPOCFeatureEnabled", false, evaluationContext); + + Assert.Equal(expectedValue, result.Value); + Assert.Equal("isPOCFeatureEnabled", result.FlagKey); + Assert.Equal(ErrorType.None, result.ErrorType); + + await OpenFeature.Api.Instance.ShutdownAsync(); } private static async Task ExecuteResolveTest(object value, T defaultValue, T expectedValue, string sdkKey, Func>> resolveFunc) @@ -105,11 +180,15 @@ private static async Task ExecuteResolveTest(object value, T defaultValue, T var configCatProvider = new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); }); + await configCatProvider.InitializeAsync(EvaluationContext.Empty); + var result = await resolveFunc(configCatProvider, "example-feature", defaultValue); Assert.Equal(expectedValue, result.Value); Assert.Equal("example-feature", result.FlagKey); Assert.Equal(ErrorType.None, result.ErrorType); + + await configCatProvider.ShutdownAsync(); } private static async Task ExecuteResolveErrorTest(object value, T defaultValue, ErrorType expectedErrorType, string sdkKey, Func>> resolveFunc) @@ -117,9 +196,13 @@ private static async Task ExecuteResolveErrorTest(object value, T defaultValu var configCatProvider = new ConfigCatProvider(sdkKey, options => { options.FlagOverrides = BuildFlagOverrides(("example-feature", value)); }); + await configCatProvider.InitializeAsync(EvaluationContext.Empty); + var exception = await Assert.ThrowsAsync(() => resolveFunc(configCatProvider, "example-feature", defaultValue)); Assert.Equal(expectedErrorType, exception.ErrorType); + + await configCatProvider.ShutdownAsync(); } private static FlagOverrides BuildFlagOverrides(params (string key, object value)[] values) @@ -132,5 +215,22 @@ private static FlagOverrides BuildFlagOverrides(params (string key, object value return FlagOverrides.LocalDictionary(dictionary, OverrideBehaviour.LocalOnly); } + + private sealed class FakeConfigFetcher : IConfigCatConfigFetcher + { + private readonly string configJson; + + public FakeConfigFetcher(string configJson) + { + this.configJson = configJson; + } + + public void Dispose() { } + + public Task FetchAsync(FetchRequest request, CancellationToken cancellationToken) + { + return Task.FromResult(new FetchResponse(HttpStatusCode.OK, reasonPhrase: null, headers: Array.Empty>(), this.configJson)); + } + } } } \ No newline at end of file From 4016b60871e5cd7595ac8a0faea317522c37e2e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:03:05 -0400 Subject: [PATCH 05/56] chore(main): release OpenFeature.Contrib.Providers.ConfigCat 0.1.1 (#281) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md | 7 +++++++ .../OpenFeature.Contrib.Providers.ConfigCat.csproj | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fe26c012..42060071 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -3,7 +3,7 @@ "src/OpenFeature.Contrib.Providers.Flagd": "0.3.0", "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.0", "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0", - "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.0", + "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md index 7d279c7c..b152b20b 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.1.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.1.0...OpenFeature.Contrib.Providers.ConfigCat-v0.1.1) (2024-09-17) + + +### 🐛 Bug Fixes + +* Revise ConfigCat provider ([#280](https://github.com/open-feature/dotnet-sdk-contrib/issues/280)) ([0b2d5f2](https://github.com/open-feature/dotnet-sdk-contrib/commit/0b2d5f29490ad16ee5efde55d31354e0322c6f86)) + ## [0.1.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.ConfigCat-v0.0.5...OpenFeature.Contrib.Providers.ConfigCat-v0.1.0) (2024-08-22) diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj index 0094f252..67af26c1 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.ConfigCat - 0.1.0 + 0.1.1 $(VersionNumber) $(VersionNumber) $(VersionNumber) From 1c45c1a3578ddc814483ac83549c2be5579d403c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:32:37 -0400 Subject: [PATCH 06/56] chore(deps): update dependency google.protobuf to 3.28.2 (#272) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index a84e4cfe..5bae2ebe 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -27,7 +27,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From 04803d7cfcf739ea17c11dc576444ae75ba85192 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:11:44 -0400 Subject: [PATCH 07/56] chore(deps): update dependency grpc.net.client to 2.66.0 (#282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 5bae2ebe..63d32704 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -28,7 +28,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 0f87bc8642983ecc9aef16b5dd06654e82b5909a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:58:49 +0100 Subject: [PATCH 08/56] chore(deps): update opentelemetry-dotnet monorepo to 1.9.0 (#278) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Hooks.Otel.Test.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj b/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj index 7c9c2ba6..9c398354 100644 --- a/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj +++ b/test/OpenFeature.Contrib.Hooks.Otel.Test/OpenFeature.Contrib.Hooks.Otel.Test.csproj @@ -5,8 +5,8 @@ - - + + From 8cb79ab8e6d33adc9acb6d6b9795cc4b5e0cf81e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 12:04:52 +0100 Subject: [PATCH 09/56] chore(deps): update dependency system.text.json to 8.0.5 [security] (#287) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagsmith.csproj | 2 +- .../OpenFeature.Contrib.Providers.GOFeatureFlag.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj index 73234f32..7a1f1134 100644 --- a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj index 5d4319a3..3ac6dd47 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj @@ -11,7 +11,7 @@ - + From cbe61a90bcc1bc39e00f939d260384f3cb2b68bd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 18:47:50 +0100 Subject: [PATCH 10/56] chore(deps): update dotnet monorepo (#248) --- global.json | 2 +- ...penFeature.Contrib.Providers.FeatureManagement.Test.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/global.json b/global.json index bc3a25e7..a0d3f7be 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.400" + "version": "8.0.403" } } diff --git a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj index 07cfa947..b591fb4f 100644 --- a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/OpenFeature.Contrib.Providers.FeatureManagement.Test.csproj @@ -6,8 +6,8 @@ - - + + From 4d59bc35bd4c65c9989e8c980668d85242240eec Mon Sep 17 00:00:00 2001 From: Andrei de la Cruz Date: Thu, 17 Oct 2024 23:37:23 +0200 Subject: [PATCH 11/56] feat: Introduce flipt provider for dotnet (#293) Signed-off-by: Andrei de la Cruz --- .github.amrom.workers.devponent_owners.yml | 6 + .release-please-manifest.json | 3 +- DotnetSdkContrib.sln | 14 + nuget.config | 4 +- release-please-config.json | 10 + .../ClientWrapper/FliptClientWrapper.cs | 49 + .../ClientWrapper/IFliptClientWrapper.cs | 23 + .../Converters/JsonConverterExtensions.cs | 23 + .../OpenFeatureStructureConverter.cs | 30 + .../Converters/OpenFeatureValueConverter.cs | 102 + .../FliptExtensions.cs | 27 + .../FliptProvider.cs | 81 + .../FliptToOpenFeatureConverter.cs | 150 ++ ...OpenFeature.Contrib.Providers.Flipt.csproj | 41 + .../README.md | 134 + .../openapi.yaml | 2310 +++++++++++++++++ .../version.txt | 1 + .../FlipExtensionsTest.cs | 155 ++ .../FliptProviderTest.cs | 138 + .../FliptToOpenFeatureConverterTest.cs | 202 ++ ...eature.Contrib.Providers.Flipt.Test.csproj | 23 + 21 files changed, 3523 insertions(+), 3 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/README.md create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/version.txt create mode 100644 test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj diff --git a/.github.amrom.workers.devponent_owners.yml b/.github.amrom.workers.devponent_owners.yml index cae64618..4fdbf288 100644 --- a/.github.amrom.workers.devponent_owners.yml +++ b/.github.amrom.workers.devponent_owners.yml @@ -22,6 +22,9 @@ components: src/OpenFeature.Contrib.Providers.Statsig: - jenshenneberg - lattenborough + src/OpenFeature.Contrib.Providers.Flipt: + - jeandreidc + - markphelps # test/ test/OpenFeature.Contrib.Hooks.Otel.Test: @@ -45,6 +48,9 @@ components: test/src/OpenFeature.Contrib.Providers.Statsig.Test: - jenshenneberg - lattenborough + test/src/OpenFeature.Contrib.Providers.Flipt.Test: + - jeandreidc + - markphelps ignored-authors: - renovate-bot diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 42060071..60e3503a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -5,5 +5,6 @@ "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0", "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", - "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0" + "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.1" } \ No newline at end of file diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index 2c8566d1..57004386 100644 --- a/DotnetSdkContrib.sln +++ b/DotnetSdkContrib.sln @@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Statsig.Test", "test\OpenFeature.Contrib.Providers.Statsig.Test\OpenFeature.Contrib.Providers.Statsig.Test.csproj", "{F3080350-B0AB-4D59-B416-50CC38C99087}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt", "src\OpenFeature.Contrib.Providers.Flipt\OpenFeature.Contrib.Providers.Flipt.csproj", "{5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{B446D481-B5A3-4509-8933-C4CF6DA9B147}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -115,6 +119,14 @@ Global {F3080350-B0AB-4D59-B416-50CC38C99087}.Debug|Any CPU.Build.0 = Debug|Any CPU {F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.ActiveCfg = Release|Any CPU {F3080350-B0AB-4D59-B416-50CC38C99087}.Release|Any CPU.Build.0 = Release|Any CPU + {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B}.Release|Any CPU.Build.0 = Release|Any CPU + {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,5 +149,7 @@ Global {B8C5376B-BAFE-48B8-ABC1-111A93C033F2} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} {4C7C0E2D-6ECC-4D17-BC5D-18F6BA6F872A} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} {F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} + {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} + {B446D481-B5A3-4509-8933-C4CF6DA9B147} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} EndGlobalSection EndGlobal diff --git a/nuget.config b/nuget.config index 5a0edf43..c5f009d2 100644 --- a/nuget.config +++ b/nuget.config @@ -1,10 +1,10 @@ - + - + diff --git a/release-please-config.json b/release-please-config.json index 08143eca..cabbd73f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -72,6 +72,16 @@ "extra-files": [ "OpenFeature.Contrib.Providers.Statsig.csproj" ] + }, + "src/OpenFeature.Contrib.Providers.Flipt": { + "package-name": "OpenFeature.Contrib.Providers.Flipt", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "OpenFeature.Contrib.Providers.Flipt.csproj" + ] } }, "changelog-sections": [ diff --git a/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs new file mode 100644 index 00000000..ea332706 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs @@ -0,0 +1,49 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Flipt.Rest; + +namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper; + +/// +/// Wrapper for Flipt server sdk client for .net +/// +public class FliptClientWrapper : IFliptClientWrapper +{ + private readonly FliptRestClient _fliptRestClient; + + /// + /// + /// Url of flipt instance + /// Authentication access token + /// Timeout when calling flipt endpoints in seconds + public FliptClientWrapper(string fliptUrl, + string clientToken = "", + int timeoutInSeconds = 30) + { + _fliptRestClient = BuildClient(fliptUrl, clientToken, timeoutInSeconds); + } + + /// + public async Task EvaluateVariantAsync(EvaluationRequest evaluationRequest) + { + return await _fliptRestClient.EvaluateV1VariantAsync(evaluationRequest); + } + + /// + public async Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest) + { + return await _fliptRestClient.EvaluateV1BooleanAsync(evaluationRequest); + } + + private static FliptRestClient BuildClient(string fliptUrl, string clientToken, int timeoutInSeconds = 30) + { + var httpClient = new HttpClient + { + BaseAddress = new Uri(fliptUrl), + Timeout = TimeSpan.FromSeconds(timeoutInSeconds), + DefaultRequestHeaders = { { "Authorization", $"Bearer {clientToken}" } } + }; + return new FliptRestClient(httpClient); + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs new file mode 100644 index 00000000..bd0f0be9 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/IFliptClientWrapper.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Flipt.Rest; + +namespace OpenFeature.Contrib.Providers.Flipt.ClientWrapper; + +/// +/// +public interface IFliptClientWrapper +{ + /// + /// Wrapper to Flipt.io/EvaluateVariantAsync method + /// + /// + /// + Task EvaluateVariantAsync(EvaluationRequest evaluationRequest); + + /// + /// Wrapper to Flipt.io/EvaluateBooleanAsync method + /// + /// + /// + Task EvaluateBooleanAsync(EvaluationRequest evaluationRequest); +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs new file mode 100644 index 00000000..d1ebbbf4 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/JsonConverterExtensions.cs @@ -0,0 +1,23 @@ +using System.Text.Json; + +namespace OpenFeature.Contrib.Providers.Flipt.Converters; + +/// +/// Extensions for default JsonConverter behavior +/// +public static class JsonConverterExtensions +{ + /// + /// JsonConverter serializer settings for Flipt to OpenFeature model deserialization + /// + public static readonly JsonSerializerOptions DefaultSerializerSettings = new() + { + WriteIndented = true, + AllowTrailingCommas = true, + Converters = + { + new OpenFeatureStructureConverter(), + new OpenFeatureValueConverter() + } + }; +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs new file mode 100644 index 00000000..96da85b2 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureStructureConverter.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.Flipt.Converters; + +/// +/// JsonConverter for OpenFeature Structure type +/// +public class OpenFeatureStructureConverter : JsonConverter +{ + /// + public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options) + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(), + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + + /// + public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var jsonDocument = JsonDocument.ParseValue(ref reader); + var jsonText = jsonDocument.RootElement.GetRawText(); + return new Structure(JsonSerializer.Deserialize>(jsonText, + JsonConverterExtensions.DefaultSerializerSettings)); + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs new file mode 100644 index 00000000..6c638dad --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/Converters/OpenFeatureValueConverter.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.Flipt.Converters; + +/// +/// OpenFeature Value type converter +/// +public class OpenFeatureValueConverter : JsonConverter +{ + /// + public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = new Value(); + switch (reader.TokenType) + { + case JsonTokenType.String: + return reader.TryGetDateTime(out var dateTimeValue) + ? new Value(dateTimeValue) + : new Value(reader.GetString() ?? string.Empty); + case JsonTokenType.True: + case JsonTokenType.False: + return new Value(reader.GetBoolean()); + case JsonTokenType.Number: + if (reader.TryGetInt32(out var intValue)) return new Value(intValue); + if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue); + break; + case JsonTokenType.StartArray: + return new Value(GenerateValueArray(ref reader, typeToConvert, options)); + case JsonTokenType.StartObject: + return new Value(GetStructure(ref reader, typeToConvert, options)); + } + + return value; + } + + private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var startDepth = reader.CurrentDepth; + var structureDictionary = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var key = reader.GetString(); + reader.Read(); + var val = Read(ref reader, typeToConvert, options); + structureDictionary[key ?? string.Empty] = val; + } + + if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break; + } + + return new Structure(structureDictionary); + } + + + private IList GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + var valuesArray = new List(); + var startDepth = reader.CurrentDepth; + + while (reader.Read()) + switch (reader.TokenType) + { + case JsonTokenType.EndArray when reader.CurrentDepth == startDepth: + return valuesArray; + default: + valuesArray.Add(Read(ref reader, typeToConvert, options)); + break; + } + + return valuesArray; + } + + /// + public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) + { + if (value.IsList) + { + writer.WriteStartArray(); + foreach (var val in value.AsList!) + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject, + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + + writer.WriteEndArray(); + } + else + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject, + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs new file mode 100644 index 00000000..9f503533 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptExtensions.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using OpenFeature.Contrib.Providers.Flipt.Converters; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.Flipt; + +/// +/// Extension helper methods +/// +public static class FliptExtensions +{ + /// + /// Transforms openFeature EvaluationContext to a mutable Dictionary that flipt sdk accepts + /// + /// OpenFeature EvaluationContext + /// + public static Dictionary ToStringDictionary(this EvaluationContext evaluationContext) + { + return evaluationContext?.AsDictionary() + .ToDictionary(k => k.Key, + v => JsonSerializer.Serialize(v.Value.AsObject, + JsonConverterExtensions.DefaultSerializerSettings)) ?? + []; + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs new file mode 100644 index 00000000..6d0754ef --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptProvider.cs @@ -0,0 +1,81 @@ +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.Flipt; + +/// +/// FliptProvider is the .NET provider implementation for Flipt.io +/// +/// +/// Accepts an instantiated IFliptClientWrapper instance +/// +public class FliptProvider : FeatureProvider +{ + private static readonly Metadata Metadata = new("Flipt Provider"); + private readonly IFliptToOpenFeatureConverter _fliptToOpenFeatureConverter; + + /// + /// Instantiate a FliptProvider using configuration params + /// + /// Url of flipt instance + /// Namespace used for querying flags + /// Authentication access token + /// Timeout when calling flipt endpoints in seconds + public FliptProvider(string fliptUrl, string namespaceKey = "default", string clientToken = "", + int timeoutInSeconds = 30) : this(new FliptToOpenFeatureConverter(fliptUrl, namespaceKey, clientToken, + timeoutInSeconds)) + { + } + + internal FliptProvider(IFliptToOpenFeatureConverter fliptToOpenFeatureConverter) + { + _fliptToOpenFeatureConverter = fliptToOpenFeatureConverter; + } + + /// + public override Metadata GetMetadata() + { + return Metadata; + } + + /// + public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext context = null, + CancellationToken cancellationToken = new()) + { + return await _fliptToOpenFeatureConverter.EvaluateBooleanAsync(flagKey, defaultValue, context); + } + + /// + public override async Task> ResolveStringValueAsync(string flagKey, + string defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new()) + { + return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context); + } + + /// + public override async Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext context = null, + CancellationToken cancellationToken = new()) + { + return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context); + } + + /// + public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext context = null, + CancellationToken cancellationToken = new()) + { + return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context); + } + + /// + public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext context = null, + CancellationToken cancellationToken = new()) + { + return await _fliptToOpenFeatureConverter.EvaluateAsync(flagKey, defaultValue, context); + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs b/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs new file mode 100644 index 00000000..cf54c183 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/FliptToOpenFeatureConverter.cs @@ -0,0 +1,150 @@ +using System; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Flipt.Rest; +using OpenFeature.Constant; +using OpenFeature.Contrib.Providers.Flipt.ClientWrapper; +using OpenFeature.Contrib.Providers.Flipt.Converters; +using OpenFeature.Error; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.Flipt; + +/// +/// A wrapper of fliptClient to handle data casting and error mappings to OpenFeature models +/// +public class FliptToOpenFeatureConverter : IFliptToOpenFeatureConverter +{ + private readonly IFliptClientWrapper _fliptClientWrapper; + private readonly string _namespaceKey; + + /// + /// Wrapper that uses Flipt to OpenFeature compliant models + /// + /// Url of flipt instance + /// Namespace used for querying flags + /// Authentication access token + /// Timeout when calling flipt endpoints in seconds + public FliptToOpenFeatureConverter(string fliptUrl, + string namespaceKey = "default", + string clientToken = "", + int timeoutInSeconds = 30) : this(new FliptClientWrapper(fliptUrl, clientToken, timeoutInSeconds), + namespaceKey) + { + } + + internal FliptToOpenFeatureConverter(IFliptClientWrapper fliptClientWrapper, string namespaceKey = "default") + { + _fliptClientWrapper = fliptClientWrapper; + _namespaceKey = namespaceKey; + } + + /// + public async Task> EvaluateAsync(string flagKey, T defaultValue, + EvaluationContext context = null) + { + var evaluationRequest = new EvaluationRequest + { + NamespaceKey = _namespaceKey, + FlagKey = flagKey, + EntityId = context?.TargetingKey ?? "", + Context = context.ToStringDictionary() + }; + + try + { + var evaluationResponse = await _fliptClientWrapper.EvaluateVariantAsync(evaluationRequest); + + if (evaluationResponse.Reason == VariantEvaluationResponseReason.FLAG_DISABLED_EVALUATION_REASON) + return new ResolutionDetails(flagKey, defaultValue, ErrorType.None, + Reason.Disabled); + + if (!evaluationResponse.Match) + return new ResolutionDetails(flagKey, defaultValue, ErrorType.None, + Reason.Default); + try + { + if (string.IsNullOrEmpty(evaluationResponse.VariantAttachment)) + { + var convertedValue = (T)Convert.ChangeType(evaluationResponse.VariantKey, typeof(T)); + return new ResolutionDetails(flagKey, + convertedValue, ErrorType.None, + Reason.TargetingMatch, evaluationResponse.VariantKey); + } + + var deserializedValueObj = JsonSerializer.Deserialize(evaluationResponse.VariantAttachment, + JsonConverterExtensions.DefaultSerializerSettings); + + return new ResolutionDetails(flagKey, + (T)Convert.ChangeType(deserializedValueObj, typeof(T)), + ErrorType.None, Reason.TargetingMatch, evaluationResponse.VariantKey); + } + catch (Exception ex) + { + if (ex is InvalidCastException or FormatException) + throw new TypeMismatchException(ex.Message, ex); + } + } + catch (FliptRestException ex) + { + throw HttpRequestExceptionFromFliptRestException(ex); + } + + return new ResolutionDetails(flagKey, defaultValue, ErrorType.General, Reason.Unknown); + } + + /// + public async Task> EvaluateBooleanAsync(string flagKey, bool defaultValue, + EvaluationContext context = null) + { + try + { + var evaluationRequest = new EvaluationRequest + { + NamespaceKey = _namespaceKey, + FlagKey = flagKey, + EntityId = context?.TargetingKey ?? "", + Context = context.ToStringDictionary() + }; + var boolEvaluationResponse = await _fliptClientWrapper.EvaluateBooleanAsync(evaluationRequest); + return new ResolutionDetails(flagKey, boolEvaluationResponse.Enabled, ErrorType.None, + Reason.TargetingMatch); + } + catch (FliptRestException ex) + { + throw HttpRequestExceptionFromFliptRestException(ex); + } + } + + private static Exception HttpRequestExceptionFromFliptRestException(FliptRestException e) + { + return new HttpRequestException(e.Message, e); + } +} + +/// +/// Contract for fliptClient wrapper +/// +public interface IFliptToOpenFeatureConverter +{ + /// + /// Used for evaluating non-boolean flags. Flipt handles datatypes which is not boolean as variants + /// + /// + /// + /// + /// + /// OpenFeature ResolutionDetails object + Task> EvaluateAsync(string flagKey, T defaultValue, EvaluationContext context = null); + + /// + /// Used for evaluating boolean flags + /// + /// + /// + /// + /// OpenFeature ResolutionDetails object + Task> EvaluateBooleanAsync(string flagKey, bool defaultValue, + EvaluationContext context = null); +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj new file mode 100644 index 00000000..c2f50260 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -0,0 +1,41 @@ + + + + OpenFeature.Contrib.Providers.Flipt + 0.1.0 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + Flipt provider for .NET + Jean Andrei de la Cruz Austria + + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + latest + + + + + + diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md new file mode 100644 index 00000000..fdb3d38d --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md @@ -0,0 +1,134 @@ +# Flipt .NET Provider + +The flipt provider allows you to connect to your Flipt instance through the OpenFeature SDK + +# .Net SDK usage + +## Requirements + +- open-feature/dotnet-sdk v1.5.0 > v2.0.0 + +## Install dependencies + +The first things we will do is install the **Open Feature SDK** and the **Flipt Feature Flag provider**. + +### .NET Cli + +```shell +dotnet add package OpenFeature.Contrib.Providers.Flipt +``` + +### Package Manager + +```shell +NuGet\Install-Package OpenFeature.Contrib.Providers.Flipt +``` + +### Package Reference + +```xml + +``` + +### Packet cli + +```shell +packet add OpenFeature.Contrib.Providers.Flipt +``` + +### Cake + +```shell +// Install OpenFeature.Contrib.Providers.Flipt as a Cake Addin +#addin nuget:?package=OpenFeature.Contrib.Providers.Flipt + +// Install OpenFeature.Contrib.Providers.Flipt as a Cake Tool +#tool nuget:?package=OpenFeature.Contrib.Providers.Flipt +``` + +## Using the Flipt Provider with the OpenFeature SDK + +To create a Flipt provider you should define provider and pass in the instance `url` (required), `defaultNamespace` and +`token`. + +```csharp +using OpenFeature.Contrib.Providers.Flipt; +using OpenFeature.Model; + +// namespace and clientToken is optional +var featureProvider = new FliptProvider("http://localhost:8080", "default-namespace", "client-token"); + +// Set the featureProvider as the provider for the OpenFeature SDK +await OpenFeature.Api.Instance.SetProviderAsync(featureProvider); + +// Get an OpenFeature client +var client = OpenFeature.Api.Instance.GetClient(); + +// Optional: set EntityId and updated context +var context = EvaluationContext.Builder() + .SetTargetingKey("flipt EntityId") + .Set("extra-data-1", "extra-data-1-value") + .Build(); + +// Evaluate a flag +var val = await client.GetBooleanValueAsync("myBoolFlag", false, context); + +// Print the value of the 'myBoolFlag' feature flag +Console.WriteLine(val); +``` + +# Contribution + +## Code setup + +Since the official [flipt-csharp](https://github.com/flipt-io/flipt-server-sdks/tree/main/flipt-csharp) only supports +dotnet 8.0, it was not utilized in this provider as OpenFeature aims to support a bigger range of dotnet versions. + +### Rest Client using OpenAPI + +To work around this incompatibility, the openapi specification +of [Flipt](https://github.com/flipt-io/flipt/blob/main/openapi.yaml) was +used to generate a REST client using [nswag](https://github.com/RicoSuter/NSwag). + +## Updating the REST Client + +To generate or update the Flipt REST client **manually**, follow these steps: + +_The **Rest client is generated automatically during build time** using the committed `openapi.yaml` file and is saved +in the `/obj/` folder_ + +### 1. Download the OpenAPI Specification + +First, download the latest `openapi.yaml` file from the Flipt GitHub repository. This can be done manually or by using a +command like `curl` in the `/src/OpenFeature.Contrib.Providers.Flipt/`: + +``` +curl https://raw.githubusercontent.com/flipt-io/flipt/refs/heads/main/openapi.yaml -o openapi.yaml +``` + +### 2. Generate the Client Code + +With the `openapi.yml` file in your working directory, run the following `nswag` command to generate the REST client +code. Make sure to correct the command as shown below: + +``` +nswag openapi2csclient /className:FliptRestClient /namespace:Flipt.Rest /input:"openapi.yaml" /output:"./Flipt.Rest.Client.cs" /GenerateExceptionClasses:true /OperationGenerationMode:SingleClientFromPathSegments /JsonLibrary:SystemTextJson /GenerateOptionalParameters:true /GenerateDefaultValues:true /GenerateResponseClasses:true /GenerateClientInterfaces:true /GenerateClientClasses:true /GenerateDtoTypes:true /ExceptionClass:FliptRestException /GenerateNativeRecords:true /UseBaseUrl:false /GenerateBaseUrlProperty:false +``` + +#### Notes + +- Ensure the `nswag` CLI tool is correctly installed and accessible from your terminal or command prompt. +- The command provided generates a C# client for interacting with the Flipt API, leveraging the System.Text.Json library + for JSON serialization/deserialization. +- The generated client will include features such as exception classes, optional parameters, default values, response + classes, client interfaces, DTO types, and native records, according to the specified options. +- This process assumes you're working in a directory that contains the `openapi.yml` file and will generate the + `Flipt.Rest.Client.cs` file in the same directory. + +## Know issues and limitations + +-In `BuildClient()` method +from https://github.com/open-feature/dotnet-sdk-contrib/blob/204144f6df0dacf46e6d52d34dd6b5a223a853f4/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs#L41-L47 +a new `HttpClient` is created. In the future it would be better to allow passing of `HttpConnectionFactory` to avoid +problems regarding socket starvation + diff --git a/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml b/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml new file mode 100644 index 00000000..8e2c006f --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/openapi.yaml @@ -0,0 +1,2310 @@ +# Generated with protoc-gen-openapi +# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: api + version: 1.47.0 +servers: + - url: http://localhost:8080 +paths: + /api/v1/namespaces: + get: + tags: + - Flipt + - NamespacesService + operationId: listNamespaces + parameters: + - name: limit + in: query + schema: + type: integer + format: int32 + - name: offset + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/NamespaceList' + post: + tags: + - Flipt + - NamespacesService + operationId: createNamespace + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateNamespaceRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Namespace' + /api/v1/namespaces/{key}: + get: + tags: + - Flipt + - NamespacesService + operationId: getNamespace + parameters: + - name: key + in: path + required: true + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Namespace' + put: + tags: + - Flipt + - NamespacesService + operationId: updateNamespace + parameters: + - name: key + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateNamespaceRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Namespace' + delete: + tags: + - Flipt + - NamespacesService + operationId: deleteNamespace + parameters: + - name: key + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags: + get: + tags: + - Flipt + - FlagsService + operationId: listFlags + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + format: int32 + - name: offset + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/FlagList' + post: + tags: + - Flipt + - FlagsService + operationId: createFlag + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFlagRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Flag' + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts: + get: + tags: + - Flipt + - RolloutsService + operationId: listRollouts + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RolloutList' + post: + tags: + - Flipt + - RolloutsService + operationId: createRollout + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRolloutRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rollout' + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts/order: + put: + tags: + - Flipt + - RolloutsService + operationId: orderRollouts + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OrderRolloutsRequest' + required: true + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rollouts/{id}: + get: + tags: + - Flipt + - RolloutsService + operationId: getRollout + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rollout' + put: + tags: + - Flipt + - RolloutsService + operationId: updateRollout + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateRolloutRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rollout' + delete: + tags: + - Flipt + - RolloutsService + operationId: deleteRollout + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules: + get: + tags: + - Flipt + - RulesService + operationId: listRules + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + format: int32 + - name: offset + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/RuleList' + post: + tags: + - Flipt + - RulesService + operationId: createRule + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRuleRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/order: + put: + tags: + - Flipt + - RulesService + operationId: orderRules + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OrderRulesRequest' + required: true + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{id}: + get: + tags: + - Flipt + - RulesService + operationId: getRule + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + put: + tags: + - Flipt + - RulesService + operationId: updateRule + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateRuleRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + delete: + tags: + - Flipt + - RulesService + operationId: deleteRule + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{ruleId}/distributions: + post: + tags: + - Flipt + - DistributionsService + operationId: createDistribution + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: ruleId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateDistributionRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Distribution' + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/rules/{ruleId}/distributions/{id}: + put: + tags: + - Flipt + - DistributionsService + operationId: updateDistribution + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: ruleId + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateDistributionRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Distribution' + delete: + tags: + - Flipt + - DistributionsService + operationId: deleteDistribution + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: ruleId + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + - name: variantId + in: query + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/variants: + post: + tags: + - Flipt + - VariantsService + operationId: createVariant + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateVariantRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Variant' + /api/v1/namespaces/{namespaceKey}/flags/{flagKey}/variants/{id}: + put: + tags: + - Flipt + - VariantsService + operationId: updateVariant + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateVariantRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Variant' + delete: + tags: + - Flipt + - VariantsService + operationId: deleteVariant + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: flagKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/flags/{key}: + get: + tags: + - Flipt + - FlagsService + operationId: getFlag + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Flag' + put: + tags: + - Flipt + - FlagsService + operationId: updateFlag + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFlagRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Flag' + delete: + tags: + - Flipt + - FlagsService + operationId: deleteFlag + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/segments: + get: + tags: + - Flipt + - SegmentsService + operationId: listSegments + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: limit + in: query + schema: + type: integer + format: int32 + - name: offset + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SegmentList' + post: + tags: + - Flipt + - SegmentsService + operationId: createSegment + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSegmentRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Segment' + /api/v1/namespaces/{namespaceKey}/segments/{key}: + get: + tags: + - Flipt + - SegmentsService + operationId: getSegment + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + - name: reference + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Segment' + put: + tags: + - Flipt + - SegmentsService + operationId: updateSegment + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateSegmentRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Segment' + delete: + tags: + - Flipt + - SegmentsService + operationId: deleteSegment + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: key + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /api/v1/namespaces/{namespaceKey}/segments/{segmentKey}/constraints: + post: + tags: + - Flipt + - ConstraintsService + operationId: createConstraint + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: segmentKey + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateConstraintRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Constraint' + /api/v1/namespaces/{namespaceKey}/segments/{segmentKey}/constraints/{id}: + put: + tags: + - Flipt + - ConstraintsService + operationId: updateConstraint + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: segmentKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateConstraintRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Constraint' + delete: + tags: + - Flipt + - ConstraintsService + operationId: deleteConstraint + parameters: + - name: namespaceKey + in: path + required: true + schema: + type: string + - name: segmentKey + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /auth/v1/method/kubernetes/serviceaccount: + post: + tags: + - AuthenticationMethodKubernetesService + operationId: kubernetesVerifyServiceAccount + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyServiceAccountRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VerifyServiceAccountResponse' + /auth/v1/method/oidc/{provider}/authorize: + get: + tags: + - AuthenticationMethodOIDCService + operationId: oidcAuthorizeURL + parameters: + - name: provider + in: path + required: true + schema: + type: string + - name: state + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/AuthorizeURLResponse' + /auth/v1/method/oidc/{provider}/callback: + get: + tags: + - AuthenticationMethodOIDCService + operationId: oidcCallback + parameters: + - name: provider + in: path + required: true + schema: + type: string + - name: code + in: query + schema: + type: string + - name: state + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CallbackResponse' + /auth/v1/method/token: + post: + tags: + - AuthenticationMethodTokenService + operationId: createMethodToken + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTokenRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTokenResponse' + /auth/v1/self: + get: + tags: + - AuthenticationService + operationId: getAuthSelf + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Authentication' + /auth/v1/self/expire: + put: + tags: + - AuthenticationService + operationId: expireAuthSelf + parameters: + - name: expiresAt + in: query + schema: + type: string + format: date-time + responses: + "200": + description: OK + content: {} + /auth/v1/tokens: + get: + tags: + - AuthenticationService + operationId: listAuthTokens + parameters: + - name: method + in: query + schema: + enum: + - METHOD_NONE + - METHOD_TOKEN + - METHOD_OIDC + - METHOD_KUBERNETES + - METHOD_GITHUB + - METHOD_JWT + - METHOD_CLOUD + type: string + format: enum + - name: limit + in: query + schema: + type: integer + format: int32 + - name: pageToken + in: query + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ListAuthenticationsResponse' + /auth/v1/tokens/{id}: + get: + tags: + - AuthenticationService + operationId: getAuthToken + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Authentication' + delete: + tags: + - AuthenticationService + operationId: deleteAuthToken + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: {} + /evaluate/v1/batch: + post: + tags: + - EvaluationService + operationId: evaluateBatch + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BatchEvaluationRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/BatchEvaluationResponse' + /evaluate/v1/boolean: + post: + tags: + - EvaluationService + operationId: evaluateBoolean + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/BooleanEvaluationResponse' + /evaluate/v1/variant: + post: + tags: + - EvaluationService + operationId: evaluateVariant + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/VariantEvaluationResponse' + /ofrep/v1/configuration: + get: + tags: + - OFREPService + description: OFREP provider configuration + operationId: ofrep.configuration + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetProviderConfigurationResponse' + /ofrep/v1/evaluate/flags: + post: + tags: + - OFREPService + description: OFREP bulk flag evaluation + operationId: ofrep.evaluateBulk + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateBulkRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/BulkEvaluationResponse' + /ofrep/v1/evaluate/flags/{key}: + post: + tags: + - OFREPService + description: OFREP single flag evaluation + operationId: ofrep.evaluateFlag + parameters: + - name: key + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateFlagRequest' + required: true + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluatedFlag' +components: + schemas: + Authentication: + type: object + properties: + id: + type: string + method: + enum: + - METHOD_NONE + - METHOD_TOKEN + - METHOD_OIDC + - METHOD_KUBERNETES + - METHOD_GITHUB + - METHOD_JWT + - METHOD_CLOUD + type: string + format: enum + expiresAt: + type: string + format: date-time + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + metadata: + type: object + additionalProperties: + type: string + AuthorizeURLResponse: + type: object + properties: + authorizeUrl: + type: string + BatchEvaluationRequest: + required: + - requests + type: object + properties: + requestId: + type: string + requests: + type: array + items: + $ref: '#/components/schemas/EvaluationRequest' + reference: + type: string + BatchEvaluationResponse: + type: object + properties: + requestId: + type: string + responses: + type: array + items: + $ref: '#/components/schemas/EvaluationResponse' + requestDurationMillis: + type: number + format: double + BooleanEvaluationResponse: + type: object + properties: + enabled: + type: boolean + reason: + enum: + - UNKNOWN_EVALUATION_REASON + - FLAG_DISABLED_EVALUATION_REASON + - MATCH_EVALUATION_REASON + - DEFAULT_EVALUATION_REASON + type: string + format: enum + requestId: + type: string + requestDurationMillis: + type: number + format: double + timestamp: + type: string + format: date-time + flagKey: + type: string + BulkEvaluationResponse: + required: + - flags + type: object + properties: + flags: + type: array + items: + $ref: '#/components/schemas/EvaluatedFlag' + CacheInvalidation: + type: object + properties: + polling: + $ref: '#/components/schemas/Polling' + CallbackResponse: + type: object + properties: + clientToken: + type: string + authentication: + $ref: '#/components/schemas/Authentication' + Capabilities: + type: object + properties: + cacheInvalidation: + $ref: '#/components/schemas/CacheInvalidation' + flagEvaluation: + $ref: '#/components/schemas/FlagEvaluation' + Constraint: + type: object + properties: + id: + type: string + segmentKey: + type: string + type: + enum: + - UNKNOWN_COMPARISON_TYPE + - STRING_COMPARISON_TYPE + - NUMBER_COMPARISON_TYPE + - BOOLEAN_COMPARISON_TYPE + - DATETIME_COMPARISON_TYPE + - ENTITY_ID_COMPARISON_TYPE + type: string + format: enum + property: + type: string + operator: + type: string + value: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + namespaceKey: + type: string + description: + type: string + CreateConstraintRequest: + required: + - type + - property + - operator + type: object + properties: + segmentKey: + type: string + type: + enum: + - UNKNOWN_COMPARISON_TYPE + - STRING_COMPARISON_TYPE + - NUMBER_COMPARISON_TYPE + - BOOLEAN_COMPARISON_TYPE + - DATETIME_COMPARISON_TYPE + - ENTITY_ID_COMPARISON_TYPE + type: string + format: enum + property: + type: string + operator: + type: string + value: + type: string + namespaceKey: + type: string + description: + type: string + CreateDistributionRequest: + required: + - variantId + - rollout + type: object + properties: + flagKey: + type: string + ruleId: + type: string + variantId: + type: string + rollout: + type: number + format: float + namespaceKey: + type: string + CreateFlagRequest: + required: + - key + - name + - type + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + enabled: + type: boolean + namespaceKey: + type: string + type: + enum: + - VARIANT_FLAG_TYPE + - BOOLEAN_FLAG_TYPE + type: string + format: enum + metadata: + type: object + CreateNamespaceRequest: + required: + - key + - name + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + CreateRolloutRequest: + required: + - rank + type: object + properties: + namespaceKey: + type: string + flagKey: + type: string + rank: + type: integer + format: int32 + description: + type: string + segment: + $ref: '#/components/schemas/RolloutSegment' + threshold: + $ref: '#/components/schemas/RolloutThreshold' + CreateRuleRequest: + required: + - rank + type: object + properties: + flagKey: + type: string + segmentKey: + type: string + rank: + type: integer + format: int32 + namespaceKey: + type: string + segmentKeys: + type: array + items: + type: string + segmentOperator: + enum: + - OR_SEGMENT_OPERATOR + - AND_SEGMENT_OPERATOR + type: string + format: enum + CreateSegmentRequest: + required: + - key + - name + - matchType + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + matchType: + enum: + - ALL_MATCH_TYPE + - ANY_MATCH_TYPE + type: string + format: enum + namespaceKey: + type: string + CreateTokenRequest: + type: object + properties: + name: + type: string + description: + type: string + expiresAt: + type: string + format: date-time + namespaceKey: + type: string + metadata: + type: object + additionalProperties: + type: string + CreateTokenResponse: + type: object + properties: + clientToken: + type: string + authentication: + $ref: '#/components/schemas/Authentication' + CreateVariantRequest: + required: + - key + type: object + properties: + flagKey: + type: string + key: + type: string + name: + type: string + description: + type: string + attachment: + type: string + namespaceKey: + type: string + Distribution: + type: object + properties: + id: + type: string + ruleId: + type: string + variantId: + type: string + rollout: + type: number + format: float + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + ErrorEvaluationResponse: + type: object + properties: + flagKey: + type: string + namespaceKey: + type: string + reason: + enum: + - UNKNOWN_ERROR_EVALUATION_REASON + - NOT_FOUND_ERROR_EVALUATION_REASON + type: string + format: enum + EvaluateBulkRequest: + type: object + properties: + context: + type: object + additionalProperties: + type: string + EvaluateFlagRequest: + type: object + properties: + key: + type: string + context: + type: object + additionalProperties: + type: string + EvaluatedFlag: + type: object + properties: + key: + type: string + reason: + enum: + - UNKNOWN + - DISABLED + - TARGETING_MATCH + - DEFAULT + type: string + format: enum + variant: + type: string + metadata: + type: object + value: + $ref: '#/components/schemas/GoogleProtobufValue' + EvaluationRequest: + required: + - namespaceKey + - flagKey + - entityId + - context + type: object + properties: + requestId: + type: string + namespaceKey: + type: string + flagKey: + type: string + entityId: + type: string + context: + type: object + additionalProperties: + type: string + reference: + type: string + EvaluationResponse: + type: object + properties: + type: + enum: + - VARIANT_EVALUATION_RESPONSE_TYPE + - BOOLEAN_EVALUATION_RESPONSE_TYPE + - ERROR_EVALUATION_RESPONSE_TYPE + type: string + format: enum + booleanResponse: + $ref: '#/components/schemas/BooleanEvaluationResponse' + variantResponse: + $ref: '#/components/schemas/VariantEvaluationResponse' + errorResponse: + $ref: '#/components/schemas/ErrorEvaluationResponse' + Flag: + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + enabled: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + variants: + type: array + items: + $ref: '#/components/schemas/Variant' + namespaceKey: + type: string + type: + enum: + - VARIANT_FLAG_TYPE + - BOOLEAN_FLAG_TYPE + type: string + format: enum + defaultVariant: + $ref: '#/components/schemas/Variant' + metadata: + type: object + FlagEvaluation: + type: object + properties: + supportedTypes: + type: array + items: + type: string + FlagList: + type: object + properties: + flags: + type: array + items: + $ref: '#/components/schemas/Flag' + nextPageToken: + type: string + totalCount: + type: integer + format: int32 + GetProviderConfigurationResponse: + type: object + properties: + name: + type: string + capabilities: + $ref: '#/components/schemas/Capabilities' + GoogleProtobufValue: + description: Represents a dynamically typed value which can be either null, a number, a string, a boolean, a recursive struct value, or a list of values. + ListAuthenticationsResponse: + type: object + properties: + authentications: + type: array + items: + $ref: '#/components/schemas/Authentication' + nextPageToken: + type: string + Namespace: + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + protected: + type: boolean + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + NamespaceList: + type: object + properties: + namespaces: + type: array + items: + $ref: '#/components/schemas/Namespace' + nextPageToken: + type: string + totalCount: + type: integer + format: int32 + OrderRolloutsRequest: + required: + - rolloutIds + type: object + properties: + flagKey: + type: string + namespaceKey: + type: string + rolloutIds: + type: array + items: + type: string + OrderRulesRequest: + required: + - ruleIds + type: object + properties: + flagKey: + type: string + ruleIds: + type: array + items: + type: string + namespaceKey: + type: string + Polling: + type: object + properties: + enabled: + type: boolean + minPollingIntervalMs: + type: integer + format: uint32 + Rollout: + type: object + properties: + id: + type: string + namespaceKey: + type: string + flagKey: + type: string + type: + enum: + - UNKNOWN_ROLLOUT_TYPE + - SEGMENT_ROLLOUT_TYPE + - THRESHOLD_ROLLOUT_TYPE + type: string + format: enum + rank: + type: integer + format: int32 + description: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + segment: + $ref: '#/components/schemas/RolloutSegment' + threshold: + $ref: '#/components/schemas/RolloutThreshold' + RolloutList: + type: object + properties: + rules: + type: array + items: + $ref: '#/components/schemas/Rollout' + nextPageToken: + type: string + totalCount: + type: integer + format: int32 + RolloutSegment: + type: object + properties: + segmentKey: + type: string + value: + type: boolean + segmentKeys: + type: array + items: + type: string + segmentOperator: + enum: + - OR_SEGMENT_OPERATOR + - AND_SEGMENT_OPERATOR + type: string + format: enum + RolloutThreshold: + type: object + properties: + percentage: + type: number + format: float + value: + type: boolean + Rule: + type: object + properties: + id: + type: string + flagKey: + type: string + segmentKey: + type: string + distributions: + type: array + items: + $ref: '#/components/schemas/Distribution' + rank: + type: integer + format: int32 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + namespaceKey: + type: string + segmentKeys: + type: array + items: + type: string + segmentOperator: + enum: + - OR_SEGMENT_OPERATOR + - AND_SEGMENT_OPERATOR + type: string + format: enum + RuleList: + type: object + properties: + rules: + type: array + items: + $ref: '#/components/schemas/Rule' + nextPageToken: + type: string + totalCount: + type: integer + format: int32 + Segment: + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + constraints: + type: array + items: + $ref: '#/components/schemas/Constraint' + matchType: + enum: + - ALL_MATCH_TYPE + - ANY_MATCH_TYPE + type: string + format: enum + namespaceKey: + type: string + SegmentList: + type: object + properties: + segments: + type: array + items: + $ref: '#/components/schemas/Segment' + nextPageToken: + type: string + totalCount: + type: integer + format: int32 + UpdateConstraintRequest: + required: + - type + - property + - operator + type: object + properties: + id: + type: string + segmentKey: + type: string + type: + enum: + - UNKNOWN_COMPARISON_TYPE + - STRING_COMPARISON_TYPE + - NUMBER_COMPARISON_TYPE + - BOOLEAN_COMPARISON_TYPE + - DATETIME_COMPARISON_TYPE + - ENTITY_ID_COMPARISON_TYPE + type: string + format: enum + property: + type: string + operator: + type: string + value: + type: string + namespaceKey: + type: string + description: + type: string + UpdateDistributionRequest: + required: + - variantId + - rollout + type: object + properties: + id: + type: string + flagKey: + type: string + ruleId: + type: string + variantId: + type: string + rollout: + type: number + format: float + namespaceKey: + type: string + UpdateFlagRequest: + required: + - name + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + enabled: + type: boolean + namespaceKey: + type: string + defaultVariantId: + type: string + metadata: + type: object + UpdateNamespaceRequest: + required: + - name + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + UpdateRolloutRequest: + type: object + properties: + id: + type: string + namespaceKey: + type: string + flagKey: + type: string + description: + type: string + segment: + $ref: '#/components/schemas/RolloutSegment' + threshold: + $ref: '#/components/schemas/RolloutThreshold' + UpdateRuleRequest: + type: object + properties: + id: + type: string + flagKey: + type: string + segmentKey: + type: string + namespaceKey: + type: string + segmentKeys: + type: array + items: + type: string + segmentOperator: + enum: + - OR_SEGMENT_OPERATOR + - AND_SEGMENT_OPERATOR + type: string + format: enum + UpdateSegmentRequest: + required: + - name + - matchType + type: object + properties: + key: + type: string + name: + type: string + description: + type: string + matchType: + enum: + - ALL_MATCH_TYPE + - ANY_MATCH_TYPE + type: string + format: enum + namespaceKey: + type: string + UpdateVariantRequest: + required: + - key + type: object + properties: + id: + type: string + flagKey: + type: string + key: + type: string + name: + type: string + description: + type: string + attachment: + type: string + namespaceKey: + type: string + Variant: + type: object + properties: + id: + type: string + flagKey: + type: string + key: + type: string + name: + type: string + description: + type: string + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + attachment: + type: string + namespaceKey: + type: string + VariantEvaluationResponse: + type: object + properties: + match: + type: boolean + segmentKeys: + type: array + items: + type: string + reason: + enum: + - UNKNOWN_EVALUATION_REASON + - FLAG_DISABLED_EVALUATION_REASON + - MATCH_EVALUATION_REASON + - DEFAULT_EVALUATION_REASON + type: string + format: enum + variantKey: + type: string + variantAttachment: + type: string + requestId: + type: string + requestDurationMillis: + type: number + format: double + timestamp: + type: string + format: date-time + flagKey: + type: string + VerifyServiceAccountRequest: + type: object + properties: + serviceAccountToken: + type: string + VerifyServiceAccountResponse: + type: object + properties: + clientToken: + type: string + authentication: + $ref: '#/components/schemas/Authentication' + securitySchemes: + bearerAuth: + type: http + scheme: bearer + jwtAuth: + type: http + scheme: JWT +security: + - bearerAuth: [] +tags: + - name: AuthenticationMethodKubernetesService + - name: AuthenticationMethodOIDCService + - name: AuthenticationMethodTokenService + - name: AuthenticationService + - name: EvaluationService + - name: Flipt + - name: OFREPService diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt new file mode 100644 index 00000000..6c6aa7cb --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs new file mode 100644 index 00000000..21e9ec1d --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs @@ -0,0 +1,155 @@ +using System.Text.Json; +using FluentAssertions; +using OpenFeature.Contrib.Providers.Flipt.Converters; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Contrib.Providers.Flipt.Test; + +public class FlipExtensionsTest +{ + [Fact] + public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary() + { + var evaluationContext = EvaluationContext.Builder().Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + [Fact] + public void ToStringDictionary_WithContext_ShouldReturnADictionaryWithValues() + { + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("location", "somewhere") + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("location"); + } + + [Fact] + public void ToStringDictionary_WithContextAndIntegerValue_ShouldReturnADictionaryWithStringValues() + { + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("age", 23) + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("age"); + result["age"].Should().Be("23"); + } + + [Fact] + public void ToStringDictionary_WithContextAndValuesOfStrings_ShouldReturnADictionaryWithSerializedStringValues() + { + var testStructure = new Structure(new Dictionary + { + { "config1", new Value("value1") }, + { "config2", new Value("value2") } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("config", testStructure) + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("config"); + + JsonSerializer + .Deserialize(result["config"], + JsonConverterExtensions.DefaultSerializerSettings).Should() + .BeEquivalentTo(testStructure); + } + + [Fact] + public void ToStringDictionary_WithContextAndMixedValueTypes_ShouldReturnADictionaryWithSerializedValues() + { + var testStructure = new Structure(new Dictionary + { + { "config1", new Value(1) }, + { "config2", new Value("value2") }, + { "config3", new Value(DateTime.Now) } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("config", testStructure) + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("config"); + + var deserialized = JsonSerializer.Deserialize(result["config"], + JsonConverterExtensions.DefaultSerializerSettings); + deserialized.Should().BeEquivalentTo(testStructure); + } + + [Fact] + public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADictionaryWithSerializedValues() + { + var sampleDictionary = new Dictionary(); + sampleDictionary["config2"] = new Value([ + new Value([new Value("element1-1"), new Value("element1-2")]), new Value("element2"), + new Value("element3") + ]); + sampleDictionary["config3"] = new Value(DateTime.Now); + + var testStructure = new Structure(sampleDictionary); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("config", testStructure) + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("config"); + + var deserialized = JsonSerializer.Deserialize(result["config"], + JsonConverterExtensions.DefaultSerializerSettings); + deserialized.Should().BeEquivalentTo(testStructure); + } + + [Fact] + public void ToStringDictionary_WithContextWithNestedStructure_ShouldReturnADictionaryWithSerializedValues() + { + var testStructure = new Structure(new Dictionary + { + { + "config-value-struct", new Value(new Structure(new Dictionary + { + { "nested1", new Value(1) } + })) + }, + { "config-value-value", new Value(new Value(DateTime.Now)) } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey(Guid.NewGuid().ToString()) + .Set("config", testStructure) + .Build(); + var result = evaluationContext.ToStringDictionary(); + + result.Should().NotBeNull(); + result.Should().NotBeEmpty(); + result.Keys.Should().Contain("config"); + + var deserialized = JsonSerializer.Deserialize(result["config"], + JsonConverterExtensions.DefaultSerializerSettings); + deserialized.Should().BeEquivalentTo(testStructure); + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs new file mode 100644 index 00000000..07bfd62c --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs @@ -0,0 +1,138 @@ +using Flipt.Rest; +using FluentAssertions; +using Moq; +using OpenFeature.Contrib.Providers.Flipt.ClientWrapper; +using OpenFeature.Error; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Contrib.Providers.Flipt.Test; + +public class FliptProviderTest +{ + private readonly string _fliptUrl = "http://localhost:8080/"; + + [Fact] + public void CreateFliptProvider_ShouldReturnFliptProvider() + { + // Flipt library always returns a flipt instance + var fliptProvider = new FliptProvider(_fliptUrl); + Assert.NotNull(fliptProvider); + } + + [Fact] + public void CreateFliptProvider_GivenEmptyUrl_ShouldThrowInvalidOperationException() + { + var act = void() => new FliptProvider(""); + act.Should().Throw(); + } + + + [Fact] + public async Task + ResolveNonBooleansAsync_GivenFlagThatHasATypeMismatch_ShouldReturnDefaultValueWithTypeMismatchError() + { + var mockFliptClientWrapper = new Mock(); + const string flagKey = "iamnotadouble"; + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ReturnsAsync(new VariantEvaluationResponse + { + FlagKey = flagKey, + VariantKey = "variant-key", + RequestId = Guid.NewGuid() + .ToString(), + SegmentKeys = ["segment1"], + VariantAttachment = "", + Match = true, + Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON + }); + + var provider = new FliptProvider(new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object)); + + var resolution = async Task>() => + await provider.ResolveDoubleValueAsync(flagKey, 0.0); + await resolution.Should().ThrowAsync(); + } + + [Fact] + public async Task ResolveStringValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper() + { + const string flagKey = "feature-flag-key"; + var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey); + await provider.ResolveStringValueAsync(flagKey, ""); + mockFliptClientWrapper.Verify( + fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once); + } + + [Fact] + public async Task ResolveDoubleValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper() + { + const string flagKey = "feature-flag-key"; + var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "0.0"); + await provider.ResolveDoubleValueAsync(flagKey, 0.0); + mockFliptClientWrapper.Verify( + fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once); + } + + [Fact] + public async Task ResolveIntegerValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper() + { + const string flagKey = "feature-flag-key"; + var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "0"); + await provider.ResolveIntegerValueAsync(flagKey, 0); + mockFliptClientWrapper.Verify( + fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once); + } + + [Fact] + public async Task ResolveStructureValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper() + { + const string flagKey = "feature-flag-key"; + var (provider, mockFliptClientWrapper) = + GenerateFliptProviderWithMockedDependencies(flagKey, new Value().AsString!); + await provider.ResolveStructureValueAsync(flagKey, new Value()); + mockFliptClientWrapper.Verify( + fcw => fcw.EvaluateVariantAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once); + } + + [Fact] + public async Task ResolveBooleanValueAsync_WhenCalled_ShouldCallCorrectMethodFromFliptClientWrapper() + { + const string flagKey = "feature-flag-key"; + var (provider, mockFliptClientWrapper) = GenerateFliptProviderWithMockedDependencies(flagKey, "true"); + await provider.ResolveBooleanValueAsync(flagKey, false); + mockFliptClientWrapper.Verify( + fcw => fcw.EvaluateBooleanAsync(It.Is(er => er.FlagKey == flagKey)), Times.Once); + } + + private static (FliptProvider, Mock) GenerateFliptProviderWithMockedDependencies( + string flagKey, string variantKey = "variant-key") + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ReturnsAsync(new VariantEvaluationResponse + { + FlagKey = flagKey, + VariantKey = variantKey, + RequestId = Guid.NewGuid() + .ToString(), + SegmentKeys = ["segment1"], + VariantAttachment = "", + Match = true, + Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON + }); + + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny())) + .ReturnsAsync(new BooleanEvaluationResponse + { + FlagKey = flagKey, + RequestId = Guid.NewGuid() + .ToString(), + Enabled = true, + Reason = BooleanEvaluationResponseReason.MATCH_EVALUATION_REASON + }); + + return (new FliptProvider(new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object)), + mockFliptClientWrapper); + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs new file mode 100644 index 00000000..99573a06 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs @@ -0,0 +1,202 @@ +using System.Net; +using System.Net.Http; +using Flipt.Rest; +using FluentAssertions; +using Moq; +using OpenFeature.Constant; +using OpenFeature.Contrib.Providers.Flipt.ClientWrapper; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Contrib.Providers.Flipt.Test; + +public class FliptToOpenFeatureConverterTest +{ + // EvaluateBooleanAsync Tests + [Theory] + [InlineData(HttpStatusCode.NotFound, ErrorType.FlagNotFound, false)] + [InlineData(HttpStatusCode.BadRequest, ErrorType.TypeMismatch, false)] + [InlineData(HttpStatusCode.InternalServerError, ErrorType.ProviderNotReady, false)] + [InlineData(HttpStatusCode.Forbidden, ErrorType.ProviderNotReady, false)] + [InlineData(HttpStatusCode.Ambiguous, ErrorType.General, false)] + public async Task EvaluateBooleanAsync_GivenHttpRequestException_ShouldHandleHttpRequestException( + HttpStatusCode thrownStatusCode, ErrorType expectedOpenFeatureErrorType, bool fallbackValue) + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => + fcw.EvaluateBooleanAsync(It.IsAny())) + .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null)); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = async Task>() => + await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallbackValue); + + await resolution.Should().ThrowAsync(); + } + + [Theory] + [InlineData("show-feature", true)] + [InlineData("show-feature", false)] + public async Task EvaluateBooleanAsync_GivenExistingFlag_ShouldReturnFlagValue(string flagKey, + bool valueFromSrc) + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny())) + .ReturnsAsync(new BooleanEvaluationResponse + { + Enabled = valueFromSrc, + FlagKey = flagKey, + RequestId = Guid.NewGuid().ToString() + }); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = await fliptToOpenFeature.EvaluateBooleanAsync("show-feature", false); + + resolution.FlagKey.Should().Be(flagKey); + resolution.Value.Should().Be(valueFromSrc); + resolution.Reason.Should().Be(Reason.TargetingMatch); + } + + [Theory] + [InlineData("show-feature", false)] + [InlineData("show-feature", true)] + public async Task EvaluateBooleanAsync_GivenNonExistentFlag_ShouldReturnDefaultValueWithFlagNotFoundError( + string flagKey, bool fallBackValue) + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateBooleanAsync(It.IsAny())) + .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = async Task>() => + await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallBackValue); + + await resolution.Should().ThrowAsync(); + } + + // EvaluateAsync Tests + + [Theory] + [InlineData(HttpStatusCode.NotFound, ErrorType.FlagNotFound, 0.0)] + [InlineData(HttpStatusCode.BadRequest, ErrorType.TypeMismatch, 0.0)] + [InlineData(HttpStatusCode.InternalServerError, ErrorType.ProviderNotReady, 0.0)] + [InlineData(HttpStatusCode.Forbidden, ErrorType.ProviderNotReady, 0.0)] + [InlineData(HttpStatusCode.Ambiguous, ErrorType.General, 0.0)] + public async Task EvaluateAsync_GivenHttpRequestException_ShouldHandleHttpRequestException( + HttpStatusCode thrownStatusCode, ErrorType expectedOpenFeatureErrorType, double fallbackValue) + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => + fcw.EvaluateVariantAsync(It.IsAny())) + .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null)); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = async Task>() => + await fliptToOpenFeature.EvaluateAsync("flagKey", fallbackValue); + + await resolution.Should().ThrowAsync(); + } + + [Theory] + [InlineData("variant-flag", 1.0, 1.0)] + [InlineData("variant-flag", "thisisastring", "thisisastring")] + [InlineData("variant-flag", 1, 1)] + public async Task EvaluateAsync_GivenExistingVariantFlagWhichIsNotAnObject_ShouldReturnFlagValue(string flagKey, + object valueFromSrc, object? expectedValue = null, string variantAttachment = "") + { + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ReturnsAsync(new VariantEvaluationResponse + { + FlagKey = flagKey, + VariantKey = valueFromSrc.ToString() ?? string.Empty, + RequestId = Guid.NewGuid().ToString(), + SegmentKeys = ["segment1"], + VariantAttachment = variantAttachment, + Match = true, + Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON + }); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, valueFromSrc); + + resolution.FlagKey.Should().Be(flagKey); + resolution.Variant.Should().Be(valueFromSrc.ToString() ?? string.Empty); + resolution.Value.Should().BeEquivalentTo(expectedValue?.ToString()); + resolution.Reason.Should().Be(Reason.TargetingMatch); + } + + [Fact] + public async Task EvaluateAsync_GivenExistingVariantFlagAndWithAnObject_ShouldReturnFlagValue() + { + const string flagKey = "variant-flag"; + const string variantKey = "variant-A"; + const string valueFromSrc = """ + { + "name": "Mr. Robinson", + "age": 12, + } + """; + var expectedValue = new Value(new Structure(new Dictionary + { + { "name", new Value("Mr. Robinson") }, { "age", new Value(12) } + })); + + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ReturnsAsync(new VariantEvaluationResponse + { + FlagKey = flagKey, + VariantKey = variantKey, + RequestId = Guid.NewGuid().ToString(), + SegmentKeys = ["segment1"], + VariantAttachment = valueFromSrc, + Match = true, + Reason = VariantEvaluationResponseReason.MATCH_EVALUATION_REASON + }); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, new Value()); + + resolution.FlagKey.Should().Be(flagKey); + resolution.Variant.Should().Be(variantKey); + resolution.Value.Should().BeEquivalentTo(expectedValue); + } + + + [Fact] + public async Task + EvaluateVariantAsync_GivenNonExistentFlagWithNonNestedFallback_ShouldReturnDefaultValueWithFlagNotFoundError() + { + var fallbackValue = new Value(new Structure(new Dictionary + { + { "name", new Value("Mr. Robinson") }, { "age", new Value(12) } + })); + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = async Task>() => + await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue); + + await resolution.Should().ThrowAsync(); + } + + + [Fact] + public async Task + EvaluateVariantAsync_GivenNonExistentFlagWithNestedFallback_ShouldReturnDefaultValueWithFlagNotFoundError() + { + var fallbackValue = new Value(""); + var mockFliptClientWrapper = new Mock(); + mockFliptClientWrapper.Setup(fcw => fcw.EvaluateVariantAsync(It.IsAny())) + .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); + + var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); + var resolution = async Task>() => + await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue); + + await resolution.Should().ThrowAsync(); + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj new file mode 100644 index 00000000..2ea46b29 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj @@ -0,0 +1,23 @@ + + + + enable + enable + + false + true + latest + + + + + + + + + + + + + + From 90aac6af09756cab84f43a7b4edf9d0c34e1833d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:40:06 -0400 Subject: [PATCH 12/56] chore(main): release OpenFeature.Contrib.Providers.Flipt 0.0.2 (#294) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .../CHANGELOG.md | 8 ++ ...OpenFeature.Contrib.Providers.Flipt.csproj | 82 +++++++++---------- .../version.txt | 2 +- 4 files changed, 51 insertions(+), 43 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 60e3503a..c5bf2874 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,5 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.1" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.2" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md new file mode 100644 index 00000000..5175e30d --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.1...OpenFeature.Contrib.Providers.Flipt-v0.0.2) (2024-10-17) + + +### ✨ New Features + +* Introduce flipt provider for dotnet ([#293](https://github.com/open-feature/dotnet-sdk-contrib/issues/293)) ([4d59bc3](https://github.com/open-feature/dotnet-sdk-contrib/commit/4d59bc35bd4c65c9989e8c980668d85242240eec)) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index c2f50260..f9c5a1f0 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -1,41 +1,41 @@ - - - - OpenFeature.Contrib.Providers.Flipt - 0.1.0 - $(VersionNumber) - $(VersionNumber) - $(VersionNumber) - Flipt provider for .NET - Jean Andrei de la Cruz Austria - - - - - - <_Parameter1>$(MSBuildProjectName).Test - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - latest - - - - - - + + + + OpenFeature.Contrib.Providers.Flipt + 0.0.2 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + Flipt provider for .NET + Jean Andrei de la Cruz Austria + + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + latest + + + + + + diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt index 6c6aa7cb..4e379d2b 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.0.2 From 3095cafd90c1828b3a678b110d200be1480e4070 Mon Sep 17 00:00:00 2001 From: Andrei de la Cruz Date: Fri, 18 Oct 2024 15:12:34 +0200 Subject: [PATCH 13/56] fix: warnings in xunit tests due to unused theory params (#297) Signed-off-by: Andrei de la Cruz --- .../FliptToOpenFeatureConverterTest.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs index 99573a06..e57e4d18 100644 --- a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs @@ -1,3 +1,5 @@ +// ReSharper disable RedundantUsingDirective + using System.Net; using System.Net.Http; using Flipt.Rest; @@ -14,13 +16,13 @@ public class FliptToOpenFeatureConverterTest { // EvaluateBooleanAsync Tests [Theory] - [InlineData(HttpStatusCode.NotFound, ErrorType.FlagNotFound, false)] - [InlineData(HttpStatusCode.BadRequest, ErrorType.TypeMismatch, false)] - [InlineData(HttpStatusCode.InternalServerError, ErrorType.ProviderNotReady, false)] - [InlineData(HttpStatusCode.Forbidden, ErrorType.ProviderNotReady, false)] - [InlineData(HttpStatusCode.Ambiguous, ErrorType.General, false)] + [InlineData(HttpStatusCode.NotFound, false)] + [InlineData(HttpStatusCode.BadRequest, false)] + [InlineData(HttpStatusCode.InternalServerError, false)] + [InlineData(HttpStatusCode.Forbidden, false)] + [InlineData(HttpStatusCode.Ambiguous, false)] public async Task EvaluateBooleanAsync_GivenHttpRequestException_ShouldHandleHttpRequestException( - HttpStatusCode thrownStatusCode, ErrorType expectedOpenFeatureErrorType, bool fallbackValue) + HttpStatusCode thrownStatusCode, bool fallbackValue) { var mockFliptClientWrapper = new Mock(); mockFliptClientWrapper.Setup(fcw => @@ -69,7 +71,7 @@ public async Task EvaluateBooleanAsync_GivenNonExistentFlag_ShouldReturnDefaultV var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); var resolution = async Task>() => - await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallBackValue); + await fliptToOpenFeature.EvaluateBooleanAsync(flagKey, fallBackValue); await resolution.Should().ThrowAsync(); } @@ -77,13 +79,13 @@ public async Task EvaluateBooleanAsync_GivenNonExistentFlag_ShouldReturnDefaultV // EvaluateAsync Tests [Theory] - [InlineData(HttpStatusCode.NotFound, ErrorType.FlagNotFound, 0.0)] - [InlineData(HttpStatusCode.BadRequest, ErrorType.TypeMismatch, 0.0)] - [InlineData(HttpStatusCode.InternalServerError, ErrorType.ProviderNotReady, 0.0)] - [InlineData(HttpStatusCode.Forbidden, ErrorType.ProviderNotReady, 0.0)] - [InlineData(HttpStatusCode.Ambiguous, ErrorType.General, 0.0)] + [InlineData(HttpStatusCode.NotFound, 0.0)] + [InlineData(HttpStatusCode.BadRequest, 0.0)] + [InlineData(HttpStatusCode.InternalServerError, 0.0)] + [InlineData(HttpStatusCode.Forbidden, 0.0)] + [InlineData(HttpStatusCode.Ambiguous, 0.0)] public async Task EvaluateAsync_GivenHttpRequestException_ShouldHandleHttpRequestException( - HttpStatusCode thrownStatusCode, ErrorType expectedOpenFeatureErrorType, double fallbackValue) + HttpStatusCode thrownStatusCode, double fallbackValue) { var mockFliptClientWrapper = new Mock(); mockFliptClientWrapper.Setup(fcw => @@ -132,10 +134,10 @@ public async Task EvaluateAsync_GivenExistingVariantFlagAndWithAnObject_ShouldRe const string flagKey = "variant-flag"; const string variantKey = "variant-A"; const string valueFromSrc = """ - { + { "name": "Mr. Robinson", - "age": 12, - } + "age": 12, + } """; var expectedValue = new Value(new Structure(new Dictionary { From ad01db2991a147d527637afac30827f73a4cc40e Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 18 Oct 2024 09:43:51 -0400 Subject: [PATCH 14/56] fix: force a republish (#298) Signed-off-by: Michael Beemer --- src/OpenFeature.Contrib.Providers.Flipt/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md index fdb3d38d..c7f8d116 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/README.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md @@ -10,7 +10,7 @@ The flipt provider allows you to connect to your Flipt instance through the Open ## Install dependencies -The first things we will do is install the **Open Feature SDK** and the **Flipt Feature Flag provider**. +The first thing we will do is install the **OpenFeature SDK** and the **Flipt Feature Flag provider**. ### .NET Cli @@ -48,7 +48,7 @@ packet add OpenFeature.Contrib.Providers.Flipt ## Using the Flipt Provider with the OpenFeature SDK -To create a Flipt provider you should define provider and pass in the instance `url` (required), `defaultNamespace` and +To create a Flipt provider, you should define the provider and pass in the instance `url` (required), `defaultNamespace` and `token`. ```csharp @@ -129,6 +129,6 @@ nswag openapi2csclient /className:FliptRestClient /namespace:Flipt.Rest /input:" -In `BuildClient()` method from https://github.com/open-feature/dotnet-sdk-contrib/blob/204144f6df0dacf46e6d52d34dd6b5a223a853f4/src/OpenFeature.Contrib.Providers.Flipt/ClientWrapper/FliptClientWrapper.cs#L41-L47 -a new `HttpClient` is created. In the future it would be better to allow passing of `HttpConnectionFactory` to avoid +a new `HttpClient` is created. In the future, it would be better to allow passing of `HttpConnectionFactory` to avoid problems regarding socket starvation From ac1922721351c0e55d9c40b2cbfeed75712d831a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 09:48:18 -0400 Subject: [PATCH 15/56] chore(main): release OpenFeature.Contrib.Providers.Flipt 0.0.3 (#299) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md | 7 +++++++ .../OpenFeature.Contrib.Providers.Flipt.csproj | 2 +- src/OpenFeature.Contrib.Providers.Flipt/version.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c5bf2874..9635ccba 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,5 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.2" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.3" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md index 5175e30d..8e9ff225 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.2...OpenFeature.Contrib.Providers.Flipt-v0.0.3) (2024-10-18) + + +### 🐛 Bug Fixes + +* force a republish ([#298](https://github.com/open-feature/dotnet-sdk-contrib/issues/298)) ([ad01db2](https://github.com/open-feature/dotnet-sdk-contrib/commit/ad01db2991a147d527637afac30827f73a4cc40e)) + ## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.1...OpenFeature.Contrib.Providers.Flipt-v0.0.2) (2024-10-17) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index f9c5a1f0..40ed357d 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.Flipt - 0.0.2 + 0.0.3 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt index 4e379d2b..bcab45af 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -1 +1 @@ -0.0.2 +0.0.3 From 50fd738585567a39f6fd0b1db37b899cbae42ba5 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 18 Oct 2024 10:16:50 -0400 Subject: [PATCH 16/56] fix: update docs (#300) Signed-off-by: Michael Beemer --- src/OpenFeature.Contrib.Providers.Flipt/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md index c7f8d116..007a87d6 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/README.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md @@ -102,7 +102,7 @@ in the `/obj/` folder_ First, download the latest `openapi.yaml` file from the Flipt GitHub repository. This can be done manually or by using a command like `curl` in the `/src/OpenFeature.Contrib.Providers.Flipt/`: -``` +```shell curl https://raw.githubusercontent.com/flipt-io/flipt/refs/heads/main/openapi.yaml -o openapi.yaml ``` @@ -111,7 +111,7 @@ curl https://raw.githubusercontent.com/flipt-io/flipt/refs/heads/main/openapi.ya With the `openapi.yml` file in your working directory, run the following `nswag` command to generate the REST client code. Make sure to correct the command as shown below: -``` +```shell nswag openapi2csclient /className:FliptRestClient /namespace:Flipt.Rest /input:"openapi.yaml" /output:"./Flipt.Rest.Client.cs" /GenerateExceptionClasses:true /OperationGenerationMode:SingleClientFromPathSegments /JsonLibrary:SystemTextJson /GenerateOptionalParameters:true /GenerateDefaultValues:true /GenerateResponseClasses:true /GenerateClientInterfaces:true /GenerateClientClasses:true /GenerateDtoTypes:true /ExceptionClass:FliptRestException /GenerateNativeRecords:true /UseBaseUrl:false /GenerateBaseUrlProperty:false ``` From e947df5bd125658d41b8a5ec8c9e6f58e7e154d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:17:58 -0400 Subject: [PATCH 17/56] chore(main): release OpenFeature.Contrib.Providers.Flipt 0.0.4 (#301) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md | 7 +++++++ .../OpenFeature.Contrib.Providers.Flipt.csproj | 2 +- src/OpenFeature.Contrib.Providers.Flipt/version.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9635ccba..6cec9cd1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,5 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.3" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.4" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md index 8e9ff225..bd4c25af 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.3...OpenFeature.Contrib.Providers.Flipt-v0.0.4) (2024-10-18) + + +### 🐛 Bug Fixes + +* update docs ([#300](https://github.com/open-feature/dotnet-sdk-contrib/issues/300)) ([50fd738](https://github.com/open-feature/dotnet-sdk-contrib/commit/50fd738585567a39f6fd0b1db37b899cbae42ba5)) + ## [0.0.3](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.2...OpenFeature.Contrib.Providers.Flipt-v0.0.3) (2024-10-18) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index 40ed357d..a37c3f35 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.Flipt - 0.0.3 + 0.0.4 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt index bcab45af..81340c7e 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -1 +1 @@ -0.0.3 +0.0.4 From 1aaa3877ae3db884d401226b2138f8e3903a56c2 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Fri, 18 Oct 2024 10:31:08 -0400 Subject: [PATCH 18/56] fix: update readme Signed-off-by: Michael Beemer --- src/OpenFeature.Contrib.Providers.Flipt/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/README.md b/src/OpenFeature.Contrib.Providers.Flipt/README.md index 007a87d6..908769a9 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/README.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/README.md @@ -82,7 +82,7 @@ Console.WriteLine(val); ## Code setup Since the official [flipt-csharp](https://github.com/flipt-io/flipt-server-sdks/tree/main/flipt-csharp) only supports -dotnet 8.0, it was not utilized in this provider as OpenFeature aims to support a bigger range of dotnet versions. +dotnet 8.0 was not utilized by this provider as OpenFeature aims to support a bigger range of dotnet versions. ### Rest Client using OpenAPI From 2f4907eaca791a25cd61000ab81be87f1aa4c764 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:32:21 -0400 Subject: [PATCH 19/56] chore(main): release OpenFeature.Contrib.Providers.Flipt 0.0.5 (#302) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md | 7 +++++++ .../OpenFeature.Contrib.Providers.Flipt.csproj | 2 +- src/OpenFeature.Contrib.Providers.Flipt/version.txt | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6cec9cd1..910b4295 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,5 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.4" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md index bd4c25af..8756f502 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.Flipt/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.0.5](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.4...OpenFeature.Contrib.Providers.Flipt-v0.0.5) (2024-10-18) + + +### 🐛 Bug Fixes + +* update readme ([1aaa387](https://github.com/open-feature/dotnet-sdk-contrib/commit/1aaa3877ae3db884d401226b2138f8e3903a56c2)) + ## [0.0.4](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flipt-v0.0.3...OpenFeature.Contrib.Providers.Flipt-v0.0.4) (2024-10-18) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index a37c3f35..67d695d3 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.Flipt - 0.0.4 + 0.0.5 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.Flipt/version.txt b/src/OpenFeature.Contrib.Providers.Flipt/version.txt index 81340c7e..bbdeab62 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flipt/version.txt @@ -1 +1 @@ -0.0.4 +0.0.5 From e603c08df7c19f360b2d8896caef3e3a5bcdcefd Mon Sep 17 00:00:00 2001 From: chrfwow Date: Mon, 27 Jan 2025 15:07:00 +0100 Subject: [PATCH 20/56] feat: Update in-process resolver to support flag metadata #305 (#309) Signed-off-by: christian.lutnik --- .gitmodules | 2 +- .../Resolver/InProcess/JsonEvaluator.cs | 183 +++++++++++++----- .../JsonEvaluatorTest.cs | 116 +++++++++-- .../Utils.cs | 90 ++++++++- 4 files changed, 323 insertions(+), 68 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6940ffbf..b426c70e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"] path = src/OpenFeature.Contrib.Providers.Flagd/schemas - url = git@github.com:open-feature/schemas.git + url = https://github.com/open-feature/schemas.git [submodule "spec"] path = spec url = https://github.com/open-feature/spec.git diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 6154982e..5b449a0a 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.RegularExpressions; using JsonLogic.Net; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -9,31 +10,24 @@ using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using OpenFeature.Error; using OpenFeature.Model; -using System.Text.RegularExpressions; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess { - internal class FlagConfiguration { - [JsonProperty("state")] - internal string State { get; set; } - [JsonProperty("defaultVariant")] - internal string DefaultVariant { get; set; } - [JsonProperty("variants")] - internal Dictionary Variants { get; set; } - [JsonProperty("targeting")] - internal object Targeting { get; set; } - [JsonProperty("source")] - internal string Source { get; set; } + [JsonProperty("state")] internal string State { get; set; } + [JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; } + [JsonProperty("variants")] internal Dictionary Variants { get; set; } + [JsonProperty("targeting")] internal object Targeting { get; set; } + [JsonProperty("source")] internal string Source { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagSyncData { - [JsonProperty("flags")] - internal Dictionary Flags { get; set; } - [JsonProperty("$evaluators")] - internal Dictionary Evaluators { get; set; } + [JsonProperty("flags")] internal Dictionary Flags { get; set; } + [JsonProperty("$evaluators")] internal Dictionary Evaluators { get; set; } + [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } } internal class FlagConfigurationSync @@ -53,6 +47,7 @@ internal enum FlagConfigurationUpdateType internal class JsonEvaluator { private Dictionary _flags = new Dictionary(); + private Dictionary _flagSetMetadata = new Dictionary(); private string _selector; @@ -88,7 +83,57 @@ internal FlagSyncData Parse(string flagConfigurations) }); } - return JsonConvert.DeserializeObject(transformed); + + var data = JsonConvert.DeserializeObject(transformed); + if (data.Metadata == null) + { + data.Metadata = new Dictionary(); + } + else + { + foreach (var key in new List(data.Metadata.Keys)) + { + var value = data.Metadata[key]; + if (value is long longValue) + { + value = data.Metadata[key] = (int)longValue; + } + + VerifyMetadataValue(key, value); + } + } + + foreach (var flagConfig in data.Flags) + { + if (flagConfig.Value.Metadata == null) + { + continue; + } + + foreach (var key in new List(flagConfig.Value.Metadata.Keys)) + { + var value = flagConfig.Value.Metadata[key]; + if (value is long longValue) + { + value = flagConfig.Value.Metadata[key] = (int)longValue; + } + + VerifyMetadataValue(key, value); + } + } + + return data; + } + + private static void VerifyMetadataValue(string key, object value) + { + if (value is int || value is double || value is string || value is bool) + { + return; + } + + throw new ParseErrorException("Metadata entry for key " + key + " and value " + value + + " is of unknown type"); } internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations) @@ -99,55 +144,69 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat { case FlagConfigurationUpdateType.ALL: _flags = flagConfigsMap.Flags; + _flagSetMetadata = flagConfigsMap.Metadata; + break; case FlagConfigurationUpdateType.ADD: + case FlagConfigurationUpdateType.UPDATE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags[keyAndValue.Key] = keyAndValue.Value; } - break; - case FlagConfigurationUpdateType.UPDATE: - foreach (var keyAndValue in flagConfigsMap.Flags) + + foreach (var metadata in flagConfigsMap.Metadata) { - _flags[keyAndValue.Key] = keyAndValue.Value; + _flagSetMetadata[metadata.Key] = metadata.Value; } + break; case FlagConfigurationUpdateType.DELETE: foreach (var keyAndValue in flagConfigsMap.Flags) { _flags.Remove(keyAndValue.Key); } - break; + foreach (var keyValuePair in flagConfigsMap.Metadata) + { + _flagSetMetadata.Remove(keyValuePair.Key); + } + + break; } } - public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveBooleanValueAsync(string flagKey, bool defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStringValueAsync(string flagKey, string defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveIntegerValueAsync(string flagKey, int defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveDoubleValueAsync(string flagKey, double defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null) + public ResolutionDetails ResolveStructureValueAsync(string flagKey, Value defaultValue, + EvaluationContext context = null) { return ResolveValue(flagKey, defaultValue, context); } - private ResolutionDetails ResolveValue(string flagKey, T defaultValue, EvaluationContext context = null) + private ResolutionDetails ResolveValue(string flagKey, T defaultValue, + EvaluationContext context = null) { // check if we find the flag key var reason = Reason.Static; @@ -155,15 +214,30 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { if ("DISABLED" == flagConfiguration.State) { - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); + } + + Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); + if (flagConfiguration.Metadata != null) + { + foreach (var metadataEntry in flagConfiguration.Metadata) + { + combinedMetadata[metadataEntry.Key] = metadataEntry.Value; + } } + + var flagMetadata = new ImmutableMetadata(combinedMetadata); var variant = flagConfiguration.DefaultVariant; - if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}") + if (flagConfiguration.Targeting != null && + !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && + flagConfiguration.Targeting.ToString() != "{}") { reason = Reason.TargetingMatch; var flagdProperties = new Dictionary(); flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey)); - flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); + flagdProperties.Add(FlagdProperties.TimestampKey, + new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); if (context == null) { @@ -173,7 +247,7 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva var targetingContext = context.AsDictionary().Add( FlagdProperties.FlagdPropertiesKey, new Value(new Structure(flagdProperties)) - ); + ); var targetingString = flagConfiguration.Targeting.ToString(); // Parse json into hierarchical structure @@ -202,32 +276,39 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, Eva { // if variant is null, revert to default reason = Reason.Default; - flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue); + flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, + out var defaultVariantValue); if (defaultVariantValue == null) { - throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); + throw new FeatureProviderException(ErrorType.ParseError, + "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); } + var value = ExtractFoundVariant(defaultVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue)) { // if variant can be found, return it - this could be TARGETING_MATCH or STATIC. var value = ExtractFoundVariant(foundVariantValue, flagKey); return new ResolutionDetails( - flagKey: flagKey, - value, - reason: reason, - variant: variant - ); + flagKey: flagKey, + value, + reason: reason, + variant: variant, + flagMetadata: flagMetadata + ); } } - throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); + + throw new FeatureProviderException(ErrorType.FlagNotFound, + "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); } static T ExtractFoundVariant(object foundVariantValue, string flagKey) @@ -236,6 +317,7 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = Convert.ToInt32(foundVariantValue); } + if (typeof(T) == typeof(double)) { foundVariantValue = Convert.ToDouble(foundVariantValue); @@ -244,11 +326,14 @@ static T ExtractFoundVariant(object foundVariantValue, string flagKey) { foundVariantValue = ConvertJObjectToOpenFeatureValue(value); } + if (foundVariantValue is T castValue) { return castValue; } - throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); + + throw new FeatureProviderException(ErrorType.TypeMismatch, + "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); } static dynamic ConvertToDynamicObject(IImmutableDictionary dictionary) @@ -259,7 +344,9 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary dictio foreach (var kvp in dictionary) { expandoDict.Add(kvp.Key, - kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject); + kvp.Value.IsStructure + ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) + : kvp.Value.AsObject); } return expandoObject; @@ -302,4 +389,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue) return new Value(new Structure(result)); } } -} +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs index 930250a2..9348c16f 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Immutable; using AutoFixture; using OpenFeature.Constant; @@ -10,7 +11,6 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test { public class UnitTestJsonEvaluator { - [Fact] public void TestJsonEvaluatorAddFlagConfig() { @@ -23,7 +23,6 @@ public void TestJsonEvaluatorAddFlagConfig() var result = jsonEvaluator.ResolveBooleanValueAsync("validFlag", false); Assert.True(result.Value); - } [Fact] @@ -40,7 +39,6 @@ public void TestJsonEvaluatorAddStaticStringEvaluation() Assert.Equal("#CC0000", result.Value); Assert.Equal("red", result.Variant); Assert.Equal(Reason.Static, result.Reason); - } [Fact] @@ -57,7 +55,7 @@ public void TestJsonEvaluatorDynamicBoolEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlag", false, builder.Build()); @@ -80,7 +78,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyFlagKey() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -101,7 +100,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyTimestamp() var builder = EvaluationContext.Builder(); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, builder.Build()); + var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -119,7 +119,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluator() var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build()); Assert.True(result.Value); Assert.Equal("bool1", result.Variant); @@ -137,7 +138,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluatorReturningBoolTy var builder = EvaluationContext.Builder().Set("email", "test@faas.com"); - var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, builder.Build()); + var result = + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, + builder.Build()); Assert.True(result.Value); Assert.Equal("true", result.Variant); @@ -155,7 +158,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithMissingDefaultVariant() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, + builder.Build())); } [Fact] @@ -169,7 +174,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithUnexpectedVariantType() var builder = EvaluationContext.Builder(); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, + builder.Build())); } [Fact] @@ -186,7 +193,7 @@ public void TestJsonEvaluatorDynamicStringEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStringValueAsync("targetingStringFlag", "", builder.Build()); @@ -209,7 +216,7 @@ public void TestJsonEvaluatorDynamicFloatEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveDoubleValueAsync("targetingFloatFlag", 0, builder.Build()); @@ -232,7 +239,7 @@ public void TestJsonEvaluatorDynamicIntEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveIntegerValueAsync("targetingNumberFlag", 0, builder.Build()); @@ -255,7 +262,7 @@ public void TestJsonEvaluatorDynamicObjectEvaluation() var builder = EvaluationContext.Builder(); builder - .Set("color", "yellow"); + .Set("color", "yellow"); var result = jsonEvaluator.ResolveStructureValueAsync("targetingObjectFlag", null, builder.Build()); @@ -280,7 +287,8 @@ public void TestJsonEvaluatorDisabledBoolEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build())); } [Fact] @@ -299,7 +307,8 @@ public void TestJsonEvaluatorFlagNotFoundEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build())); } [Fact] @@ -318,7 +327,80 @@ public void TestJsonEvaluatorWrongTypeEvaluation() builder .Set("color", "yellow"); - Assert.Throws(() => jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + Assert.Throws(() => + jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build())); + } + + [Fact] + public void TestJsonEvaluatorReturnsFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.flags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false); + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.3", result.FlagMetadata.GetString("string")); + Assert.Equal(3, result.FlagMetadata.GetInt("integer")); + Assert.Equal(false, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.2, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags); + + var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); + + Assert.NotNull(result.FlagMetadata); + Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); + Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); + Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata)); + } + + [Fact] + public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata() + { + var fixture = new Fixture(); + + var jsonEvaluator = new JsonEvaluator(fixture.Create()); + + Assert.Throws(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata)); } } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs index c1db7aaa..de386478 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs @@ -301,10 +301,95 @@ public class Utils ""off"": false }, ""defaultVariant"": ""on"" + }, + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + } + } +}"; + + public static string metadataFlags = @"{ + ""flags"":{ + ""metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"":{ + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": 0.1, + } + }, + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" } + }, + ""metadata"": { + ""string"": ""1.0.3"", + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, } }"; + public static string invalidFlagSetMetadata = @"{ + ""flags"":{ + ""without-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"" + } + }, + ""metadata"": { + ""string"": {""in"": ""valid""}, + ""integer"": 3, + ""boolean"": false, + ""float"": 0.2, + } +}"; + public static string invalidFlagMetadata = @"{ + ""flags"":{ + ""invalid-metadata-flag"": { + ""state"": ""ENABLED"", + ""variants"": { + ""on"": true, + ""off"": false + }, + ""defaultVariant"": ""on"", + ""metadata"": { + ""string"": ""1.0.2"", + ""integer"": 2, + ""boolean"": true, + ""float"": {""in"": ""valid""}, + } + }, + } +}"; + + /// /// Repeatedly runs the supplied assertion until it doesn't throw, or the timeout is reached. /// @@ -312,11 +397,11 @@ public class Utils /// Timeout in millis (defaults to 1000) /// Poll interval (defaults to 100 /// - public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, int pollIntervalMillis = 100) + public static async Task AssertUntilAsync(Action assertionFunc, int timeoutMillis = 1000, + int pollIntervalMillis = 100) { using (var cts = CancellationTokenSource.CreateLinkedTokenSource(default(CancellationToken))) { - cts.CancelAfter(timeoutMillis); var exceptions = new List(); @@ -347,6 +432,7 @@ public static async Task AssertUntilAsync(Action assertionFun throw new AggregateException(message, exceptions); } } + throw new AggregateException(message, exceptions); } } From c30446eb51538b05378db7c4d56228f01ed1cb88 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 10 Feb 2025 11:50:34 +0100 Subject: [PATCH 21/56] feat(gofeatureflag): Provider refactor (#313) Signed-off-by: Thomas Poignant --- .../GOFeatureFlagRequest.cs | 19 - .../GoFeatureFlagProvider.cs | 93 +- .../GoFeatureFlagProviderOptions.cs | 9 +- .../GoFeatureFlagResponse.cs | 43 - .../GoFeatureFlagUser.cs | 64 - .../converters/DictionaryConverter.cs | 65 + .../converters/JsonConverterExtensions.cs | 24 + .../OpenFeatureStructureConverter.cs | 31 + .../converters/OpenFeatureValueConverter.cs | 103 ++ .../exception/FlagDisabled.cs | 2 +- .../exception/FlagNotFoundError.cs | 2 +- .../exception/GeneralError.cs | 2 +- .../exception/GoFeatureFlagException.cs | 2 +- .../exception/ImpossibleToConvertTypeError.cs | 2 +- .../exception/InvalidEvaluationContext.cs | 2 +- .../exception/InvalidOption.cs | 2 +- .../exception/InvalidTargetingKey.cs | 2 +- .../exception/TypeMismatchError.cs | 2 +- .../exception/UnauthorizedError.cs | 2 +- .../extensions/GoFeatureFlagExtensions.cs | 22 + .../hooks/EnrichEvaluationContextHook.cs | 42 + .../models/ExporterMetadata.cs | 61 + .../models/OfrepRequest.cs | 50 + .../models/OfrepResponse.cs | 50 + .../GoFeatureFlagProviderTest.cs | 1228 +++++++++-------- .../GoFeatureFlagUserTest.cs | 29 - .../OfrepSerializationTest.cs | 157 +++ ...ontrib.Providers.GOFeatureFlag.Test.csproj | 1 + 28 files changed, 1347 insertions(+), 764 deletions(-) delete mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs delete mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs delete mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/JsonConverterExtensions.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureStructureConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureValueConverter.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/extensions/GoFeatureFlagExtensions.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/hooks/EnrichEvaluationContextHook.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/ExporterMetadata.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepRequest.cs create mode 100644 src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepResponse.cs delete mode 100644 test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs create mode 100644 test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs deleted file mode 100644 index b9178d54..00000000 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GOFeatureFlagRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace OpenFeature.Contrib.Providers.GOFeatureFlag -{ - /// - /// GOFeatureFlagRequest is the object formatting the request to the relay proxy. - /// - /// Type of the default value. - public class GOFeatureFlagRequest - { - /// - /// GoFeatureFlagUser is the representation of the user. - /// - public GoFeatureFlagUser User { get; set; } - - /// - /// default value if we have an error. - /// - public T DefaultValue { get; set; } - } -} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs index 82b7994d..fddca3ec 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using System.Net; using System.Net.Http; @@ -9,7 +10,11 @@ using System.Threading; using System.Threading.Tasks; using OpenFeature.Constant; +using OpenFeature.Contrib.Providers.GOFeatureFlag.converters; using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; +using OpenFeature.Contrib.Providers.GOFeatureFlag.extensions; +using OpenFeature.Contrib.Providers.GOFeatureFlag.hooks; +using OpenFeature.Contrib.Providers.GOFeatureFlag.models; using OpenFeature.Model; namespace OpenFeature.Contrib.Providers.GOFeatureFlag @@ -20,8 +25,8 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag public class GoFeatureFlagProvider : FeatureProvider { private const string ApplicationJson = "application/json"; + private ExporterMetadata _exporterMetadata; private HttpClient _httpClient; - private JsonSerializerOptions _serializerOptions; /// /// Constructor of the provider. @@ -34,6 +39,17 @@ public GoFeatureFlagProvider(GoFeatureFlagProviderOptions options) InitializeProvider(options); } + /// + /// List of hooks to use for this provider + /// + /// + public override IImmutableList GetProviderHooks() + { + var hooks = ImmutableArray.CreateBuilder(); + hooks.Add(new EnrichEvaluationContextHook(_exporterMetadata)); + return hooks.ToImmutable(); + } + /// /// validateInputOptions is validating the different options provided when creating the provider. /// @@ -53,6 +69,10 @@ private void ValidateInputOptions(GoFeatureFlagProviderOptions options) /// Options used while creating the provider private void InitializeProvider(GoFeatureFlagProviderOptions options) { + _exporterMetadata = options.ExporterMetadata ?? new ExporterMetadata(); + _exporterMetadata.Add("provider", ".NET"); + _exporterMetadata.Add("openfeature", true); + _httpClient = options.HttpMessageHandler != null ? new HttpClient(options.HttpMessageHandler) : new HttpClient @@ -63,7 +83,6 @@ private void InitializeProvider(GoFeatureFlagProviderOptions options) }; _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson)); _httpClient.BaseAddress = new Uri(options.Endpoint); - _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; if (options.ApiKey != null) _httpClient.DefaultRequestHeaders.Authorization = @@ -96,8 +115,8 @@ public override async Task> ResolveBooleanValueAsync(str try { var resp = await CallApi(flagKey, defaultValue, context); - return new ResolutionDetails(flagKey, bool.Parse(resp.value.ToString()), ErrorType.None, - resp.reason, resp.variationType); + return new ResolutionDetails(flagKey, bool.Parse(resp.Value.ToString()), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); } catch (FormatException e) { @@ -121,16 +140,17 @@ public override async Task> ResolveBooleanValueAsync(str /// If the flag does not exists /// If an unknown error happen /// If the flag is disabled - public override async Task> ResolveStringValueAsync(string flagKey, string defaultValue, + public override async Task> ResolveStringValueAsync(string flagKey, + string defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { try { var resp = await CallApi(flagKey, defaultValue, context); - if (!(resp.value is JsonElement element && element.ValueKind == JsonValueKind.String)) + if (!(resp.Value is JsonElement element && element.ValueKind == JsonValueKind.String)) throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); - return new ResolutionDetails(flagKey, resp.value.ToString(), ErrorType.None, resp.reason, - resp.variationType); + return new ResolutionDetails(flagKey, resp.Value.ToString(), ErrorType.None, resp.Reason, + resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); } catch (FormatException e) { @@ -160,8 +180,8 @@ public override async Task> ResolveIntegerValueAsync(stri try { var resp = await CallApi(flagKey, defaultValue, context); - return new ResolutionDetails(flagKey, int.Parse(resp.value.ToString()), ErrorType.None, - resp.reason, resp.variationType); + return new ResolutionDetails(flagKey, int.Parse(resp.Value.ToString()), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); } catch (FormatException e) { @@ -185,15 +205,16 @@ public override async Task> ResolveIntegerValueAsync(stri /// If the flag does not exists /// If an unknown error happen /// If the flag is disabled - public override async Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, + public override async Task> ResolveDoubleValueAsync(string flagKey, + double defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { try { var resp = await CallApi(flagKey, defaultValue, context); return new ResolutionDetails(flagKey, - double.Parse(resp.value.ToString(), CultureInfo.InvariantCulture), ErrorType.None, - resp.reason, resp.variationType); + double.Parse(resp.Value.ToString(), CultureInfo.InvariantCulture), ErrorType.None, + resp.Reason, resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); } catch (FormatException e) { @@ -217,17 +238,18 @@ public override async Task> ResolveDoubleValueAsync(st /// If the flag does not exists /// If an unknown error happen /// If the flag is disabled - public override async Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, + public override async Task> ResolveStructureValueAsync(string flagKey, + Value defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = default) { try { var resp = await CallApi(flagKey, defaultValue, context); - if (resp.value is JsonElement) + if (resp.Value is JsonElement) { - var value = ConvertValue((JsonElement)resp.value); - return new ResolutionDetails(flagKey, value, ErrorType.None, resp.reason, - resp.variationType); + var value = ConvertValue((JsonElement)resp.Value); + return new ResolutionDetails(flagKey, value, ErrorType.None, resp.Reason, + resp.Variant, resp.ErrorDetails, resp.Metadata.ToImmutableMetadata()); } throw new TypeMismatchError($"flag value {flagKey} had unexpected type"); @@ -253,39 +275,40 @@ public override async Task> ResolveStructureValueAsync( /// If the flag does not exists /// If an unknown error happen /// If the flag is disabled - private async Task CallApi(string flagKey, T defaultValue, + private async Task CallApi(string flagKey, T defaultValue, EvaluationContext context = null) { - var request = new GOFeatureFlagRequest - { - User = context, - DefaultValue = defaultValue - }; - var goffRequest = JsonSerializer.Serialize(request, _serializerOptions); - - var response = await _httpClient.PostAsync($"v1/feature/{flagKey}/eval", - new StringContent(goffRequest, Encoding.UTF8, ApplicationJson)); + var request = new OfrepRequest(context); + var response = await _httpClient.PostAsync($"ofrep/v1/evaluate/flags/{flagKey}", + new StringContent(request.AsJsonString(), Encoding.UTF8, ApplicationJson)); if (response.StatusCode == HttpStatusCode.NotFound) throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration"); - if (response.StatusCode == HttpStatusCode.Unauthorized) + if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) throw new UnauthorizedError("invalid token used to contact GO Feature Flag relay proxy instance"); if (response.StatusCode >= HttpStatusCode.BadRequest) throw new GeneralError("impossible to contact GO Feature Flag relay proxy instance"); var responseBody = await response.Content.ReadAsStringAsync(); - var goffResp = - JsonSerializer.Deserialize(responseBody); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + var ofrepResp = + JsonSerializer.Deserialize(responseBody, options); - if (goffResp != null && Reason.Disabled.Equals(goffResp.reason)) + if (Reason.Disabled.Equals(ofrepResp?.Reason)) throw new FlagDisabled(); - if ("FLAG_NOT_FOUND".Equals(goffResp.errorCode)) + if ("FLAG_NOT_FOUND".Equals(ofrepResp?.ErrorCode)) throw new FlagNotFoundError($"flag {flagKey} was not found in your configuration"); - return goffResp; + if (ofrepResp?.Metadata != null) + ofrepResp.Metadata = DictionaryConverter.ConvertDictionary(ofrepResp.Metadata); + + return ofrepResp; } /// @@ -337,4 +360,4 @@ private Value ConvertValue(JsonElement value) throw new ImpossibleToConvertTypeError($"impossible to convert the object {value}"); } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs index e1e3b20a..3d3100cc 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagProviderOptions.cs @@ -1,5 +1,6 @@ using System; using System.Net.Http; +using OpenFeature.Contrib.Providers.GOFeatureFlag.models; namespace OpenFeature.Contrib.Providers.GOFeatureFlag { @@ -34,5 +35,11 @@ public class GoFeatureFlagProviderOptions /// Default: null /// public string ApiKey { get; set; } + + /// + /// (optional) ExporterMetadata are static information you can set that will be available in the + /// evaluation data sent to the exporter. + /// + public ExporterMetadata ExporterMetadata { get; set; } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs deleted file mode 100644 index c8de3dd6..00000000 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagResponse.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace OpenFeature.Contrib.Providers.GOFeatureFlag -{ - /// - /// GoFeatureFlagResponse is the response returned by the relay proxy. - /// - public class GoFeatureFlagResponse - { - /// - /// trackEvent is true when this call was tracked in GO Feature Flag. - /// - public bool trackEvents { get; set; } - - /// - /// variationType contains the name of the variation used for this flag. - /// - public string variationType { get; set; } - - /// - /// failed is true if GO Feature Flag had an issue. - /// - public bool failed { get; set; } - - /// - /// version of the flag used (optional) - /// - public string version { get; set; } - - /// - /// reason used to choose this variation. - /// - public string reason { get; set; } - - /// - /// errorCode is empty if everything went ok. - /// - public string errorCode { get; set; } - - /// - /// value contains the result of the flag. - /// - public object value { get; set; } - } -} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs deleted file mode 100644 index 65a013a8..00000000 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/GoFeatureFlagUser.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; -using OpenFeature.Model; - -namespace OpenFeature.Contrib.Providers.GOFeatureFlag -{ - /// - /// GOFeatureFlagUser is the representation of a User inside GO Feature Flag. - /// - public class GoFeatureFlagUser - { - private const string AnonymousField = "anonymous"; - private const string KeyField = "targetingKey"; - - /// - /// The targeting key for the user. - /// - public string Key { get; private set; } - - /// - /// Is the user Anonymous. - /// - public bool Anonymous { get; private set; } - - /// - /// Additional Custom Data to pass to GO Feature Flag. - /// - public Dictionary Custom { get; private set; } - - /** - * Convert the evaluation context into a GOFeatureFlagUser Object. - */ - public static implicit operator GoFeatureFlagUser(EvaluationContext ctx) - { - try - { - if (ctx is null) - throw new InvalidEvaluationContext("GO Feature Flag need an Evaluation context to work."); - if (!ctx.GetValue(KeyField).IsString) - throw new InvalidTargetingKey("targetingKey field MUST be a string."); - } - catch (KeyNotFoundException e) - { - throw new InvalidTargetingKey("targetingKey field is mandatory.", e); - } - - var anonymous = ctx.ContainsKey(AnonymousField) && ctx.GetValue(AnonymousField).IsBoolean - ? ctx.GetValue(AnonymousField).AsBoolean - : false; - - var custom = ctx.AsDictionary().ToDictionary(x => x.Key, x => x.Value.AsObject); - custom.Remove(AnonymousField); - custom.Remove(KeyField); - - return new GoFeatureFlagUser - { - Key = ctx.GetValue("targetingKey").AsString, - Anonymous = anonymous.Value, - Custom = custom - }; - } - } -} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs new file mode 100644 index 00000000..e7f9f319 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/DictionaryConverter.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters +{ + /// + /// DictionaryConverter is converting a json Dictionary to a Dictionary with real object. + /// + public static class DictionaryConverter + { + /// + /// Function that convert the dictionary to a Dictionary with real object. + /// + /// + /// A dictionary with real types. + public static Dictionary ConvertDictionary(Dictionary inputDictionary) + { + return inputDictionary.ToDictionary( + kvp => kvp.Key, + kvp => ConvertValue(kvp.Value) + ); + } + + /// + /// Function that convert a value to a object. + /// + /// + /// A value with real types. + public static object ConvertValue(object value) + { + if (value is JsonElement jsonElement) + switch (jsonElement.ValueKind) + { + case JsonValueKind.String: + return jsonElement.GetString(); + case JsonValueKind.Number: + if (jsonElement.TryGetInt32(out var intValue)) return intValue; + + if (jsonElement.TryGetDouble(out var doubleValue)) return doubleValue; + return jsonElement.GetRawText(); // Fallback to string if not int or double + case JsonValueKind.True: + return true; + case JsonValueKind.False: + return false; + case JsonValueKind.Null: + return null; + case JsonValueKind.Object: + return ConvertDictionary( + JsonSerializer + .Deserialize>(jsonElement + .GetRawText())); //Recursive for nested objects + case JsonValueKind.Array: + var array = new List(); + foreach (var element in jsonElement.EnumerateArray()) array.Add(ConvertValue(element)); + + return array; + default: + return jsonElement.GetRawText(); // Handle other types as needed + } + + return value; // Return original value if not a JsonElement + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/JsonConverterExtensions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/JsonConverterExtensions.cs new file mode 100644 index 00000000..2032d748 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/JsonConverterExtensions.cs @@ -0,0 +1,24 @@ +using System.Text.Json; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters +{ + /// + /// Extensions for default JsonConverter behavior + /// + public static class JsonConverterExtensions + { + /// + /// JsonConverter serializer settings for GO Feature Flag to OpenFeature model deserialization + /// + public static readonly JsonSerializerOptions DefaultSerializerSettings = new JsonSerializerOptions + { + WriteIndented = true, + AllowTrailingCommas = true, + Converters = + { + new OpenFeatureStructureConverter(), + new OpenFeatureValueConverter() + } + }; + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureStructureConverter.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureStructureConverter.cs new file mode 100644 index 00000000..fedfef08 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureStructureConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters +{ + /// + /// OpenFeatureStructureConverter + /// + public class OpenFeatureStructureConverter : JsonConverter + { + /// + public override void Write(Utf8JsonWriter writer, Structure value, JsonSerializerOptions options) + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsDictionary(), + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + + /// + public override Structure Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var jsonDocument = JsonDocument.ParseValue(ref reader); + var jsonText = jsonDocument.RootElement.GetRawText(); + return new Structure(JsonSerializer.Deserialize>(jsonText, + JsonConverterExtensions.DefaultSerializerSettings)); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureValueConverter.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureValueConverter.cs new file mode 100644 index 00000000..1363b173 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/converters/OpenFeatureValueConverter.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.converters +{ + /// + /// OpenFeature Value type converter + /// + public class OpenFeatureValueConverter : JsonConverter + { + /// + public override Value Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = new Value(); + switch (reader.TokenType) + { + case JsonTokenType.String: + return reader.TryGetDateTime(out var dateTimeValue) + ? new Value(dateTimeValue) + : new Value(reader.GetString() ?? string.Empty); + case JsonTokenType.True: + case JsonTokenType.False: + return new Value(reader.GetBoolean()); + case JsonTokenType.Number: + if (reader.TryGetInt32(out var intValue)) return new Value(intValue); + if (reader.TryGetDouble(out var dblValue)) return new Value(dblValue); + break; + case JsonTokenType.StartArray: + return new Value(GenerateValueArray(ref reader, typeToConvert, options)); + case JsonTokenType.StartObject: + return new Value(GetStructure(ref reader, typeToConvert, options)); + } + + return value; + } + + private Structure GetStructure(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var startDepth = reader.CurrentDepth; + var structureDictionary = new Dictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + var key = reader.GetString(); + reader.Read(); + var val = Read(ref reader, typeToConvert, options); + structureDictionary[key ?? string.Empty] = val; + } + + if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == startDepth) break; + } + + return new Structure(structureDictionary); + } + + + private IList GenerateValueArray(ref Utf8JsonReader reader, Type typeToConvert, + JsonSerializerOptions options) + { + var valuesArray = new List(); + var startDepth = reader.CurrentDepth; + + while (reader.Read()) + switch (reader.TokenType) + { + case JsonTokenType.EndArray when reader.CurrentDepth == startDepth: + return valuesArray; + default: + valuesArray.Add(Read(ref reader, typeToConvert, options)); + break; + } + + return valuesArray; + } + + /// + public override void Write(Utf8JsonWriter writer, Value value, JsonSerializerOptions options) + { + if (value.IsList) + { + writer.WriteStartArray(); + foreach (var val in value.AsList!) + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(val.AsObject, + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + + writer.WriteEndArray(); + } + else + { + var jsonDoc = JsonDocument.Parse(JsonSerializer.Serialize(value.AsObject, + JsonConverterExtensions.DefaultSerializerSettings)); + jsonDoc.WriteTo(writer); + } + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagDisabled.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagDisabled.cs index 4a4fc4ab..bd257978 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagDisabled.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagDisabled.cs @@ -6,4 +6,4 @@ namespace OpenFeature.Contrib.Providers.GOFeatureFlag.exception public class FlagDisabled : GoFeatureFlagException { } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagNotFoundError.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagNotFoundError.cs index cedc9b72..4f0e0ca8 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagNotFoundError.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/FlagNotFoundError.cs @@ -19,4 +19,4 @@ public FlagNotFoundError(string message, Exception innerException = null) : base { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GeneralError.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GeneralError.cs index 479ffc8a..47c0c799 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GeneralError.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GeneralError.cs @@ -19,4 +19,4 @@ public GeneralError(string message, Exception innerException = null) : base(Erro { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GoFeatureFlagException.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GoFeatureFlagException.cs index dabe0832..a6b87b39 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GoFeatureFlagException.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/GoFeatureFlagException.cs @@ -33,4 +33,4 @@ public GoFeatureFlagException(string message, Exception inner) { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/ImpossibleToConvertTypeError.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/ImpossibleToConvertTypeError.cs index d9e79259..c1134ba1 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/ImpossibleToConvertTypeError.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/ImpossibleToConvertTypeError.cs @@ -20,4 +20,4 @@ public ImpossibleToConvertTypeError(string message, Exception innerException = n { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidEvaluationContext.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidEvaluationContext.cs index a8a9202c..ebeb7b34 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidEvaluationContext.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidEvaluationContext.cs @@ -19,4 +19,4 @@ public InvalidEvaluationContext(string message, Exception innerException = null) { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidOption.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidOption.cs index 9e3d6ef4..dc72a244 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidOption.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidOption.cs @@ -13,4 +13,4 @@ public InvalidOption(string message) : base(message) { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidTargetingKey.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidTargetingKey.cs index cda1da91..7f6d9299 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidTargetingKey.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/InvalidTargetingKey.cs @@ -19,4 +19,4 @@ public InvalidTargetingKey(string message, Exception innerException = null) : ba { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/TypeMismatchError.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/TypeMismatchError.cs index 05d46754..75146a89 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/TypeMismatchError.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/TypeMismatchError.cs @@ -19,4 +19,4 @@ public TypeMismatchError(string message, Exception innerException = null) : base { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/UnauthorizedError.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/UnauthorizedError.cs index 80869f69..bcad6777 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/UnauthorizedError.cs +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/exception/UnauthorizedError.cs @@ -19,4 +19,4 @@ public UnauthorizedError(string message, Exception innerException = null) : base { } } -} \ No newline at end of file +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/extensions/GoFeatureFlagExtensions.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/extensions/GoFeatureFlagExtensions.cs new file mode 100644 index 00000000..8407a5cf --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/extensions/GoFeatureFlagExtensions.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.extensions +{ + /// + /// Extensions for GO Feature Flag provider. + /// + public static class GoFeatureFlagExtensions + { + /// + /// Convert a Dictionary to an ImmutableMetadata. + /// + /// + /// + public static ImmutableMetadata + ToImmutableMetadata(this Dictionary metadataDictionary) // 'this' keyword is crucial + { + return metadataDictionary != null ? new ImmutableMetadata(metadataDictionary) : null; + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/hooks/EnrichEvaluationContextHook.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/hooks/EnrichEvaluationContextHook.cs new file mode 100644 index 00000000..c0498288 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/hooks/EnrichEvaluationContextHook.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Contrib.Providers.GOFeatureFlag.models; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.hooks +{ + /// + /// Enrich the evaluation context with additional information + /// + public class EnrichEvaluationContextHook : Hook + { + private readonly Structure _metadata; + + /// + /// Constructor of the Hook + /// + /// metadata to use in order to enrich the evaluation context + public EnrichEvaluationContextHook(ExporterMetadata metadata) + { + _metadata = metadata.AsStructure(); + } + + /// + /// Enrich the evaluation context with additional information before the evaluation of the flag + /// + /// + /// + /// + /// + /// + public override ValueTask BeforeAsync(HookContext context, + IReadOnlyDictionary hints = null, CancellationToken cancellationToken = default) + { + var builder = EvaluationContext.Builder(); + builder.Merge(context.EvaluationContext); + builder.Set("gofeatureflag", _metadata); + return new ValueTask(builder.Build()); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/ExporterMetadata.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/ExporterMetadata.cs new file mode 100644 index 00000000..45a01b3b --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/ExporterMetadata.cs @@ -0,0 +1,61 @@ +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.models +{ + /// + /// This class represent the exporter metadata that will be sent in your evaluation data collectore + /// + public class ExporterMetadata + { + private readonly StructureBuilder _exporterMetadataBuilder = Structure.Builder(); + + /// + /// Add metadata to the exporter + /// + /// + /// + public void Add(string key, string value) + { + _exporterMetadataBuilder.Set(key, value); + } + + /// + /// Add metadata to the exporter + /// + /// + /// + public void Add(string key, bool value) + { + _exporterMetadataBuilder.Set(key, value); + } + + /// + /// Add metadata to the exporter + /// + /// + /// + public void Add(string key, double value) + { + _exporterMetadataBuilder.Set(key, value); + } + + /// + /// Add metadata to the exporter + /// + /// + /// + public void Add(string key, int value) + { + _exporterMetadataBuilder.Set(key, value); + } + + /// + /// Return the metadata as a structure + /// + /// + public Structure AsStructure() + { + return _exporterMetadataBuilder.Build(); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepRequest.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepRequest.cs new file mode 100644 index 00000000..609c096c --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepRequest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Text.Json; +using OpenFeature.Contrib.Providers.GOFeatureFlag.converters; +using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag +{ + /// + /// GO Feature Flag request to be sent to the evaluation API + /// + public class OfrepRequest + { + private const string KeyField = "targetingKey"; + private readonly EvaluationContext _ctx; + + /// + /// Create a new GO Feature Flag request to be sent to the evaluation API + /// + /// + /// + /// + public OfrepRequest(EvaluationContext ctx) + { + try + { + if (ctx is null) + throw new InvalidEvaluationContext("GO Feature Flag need an Evaluation context to work."); + if (!ctx.GetValue(KeyField).IsString) + throw new InvalidTargetingKey("targetingKey field MUST be a string."); + } + catch (KeyNotFoundException e) + { + throw new InvalidTargetingKey("targetingKey field is mandatory.", e); + } + + _ctx = ctx; + } + + /// + /// Returns the JSON request as string to be sent to the API + /// + /// JSON request as string to be sent to the API + public string AsJsonString() + { + var request = new Dictionary { { "context", _ctx.AsDictionary() } }; + return JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings); + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepResponse.cs b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepResponse.cs new file mode 100644 index 00000000..df232a38 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/models/OfrepResponse.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag +{ + /// + /// OfrepResponse is the response returned by the OFREP API. + /// + public class OfrepResponse + { + /// + /// value contains the result of the flag. + /// + public object Value { get; set; } + + /// + /// key contains the name of the feature flag. + /// + public string Key { get; set; } + + /// + /// reason used to choose this variation. + /// + public string Reason { get; set; } + + /// + /// variationType contains the name of the variation used for this flag. + /// + public string Variant { get; set; } + + /// + /// cacheable is true if the flag is cacheable. + /// + public bool Cacheable { get; set; } + + /// + /// errorCode is empty if everything went ok. + /// + public string ErrorCode { get; set; } + + /// + /// errorDetails is set only if errorCode is not empty. + /// + public string ErrorDetails { get; set; } + + /// + /// metadata contains the metadata of the flag. + /// + public Dictionary Metadata { get; set; } + } +} diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs index 7f433219..6ab3ce47 100644 --- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagProviderTest.cs @@ -1,563 +1,665 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using OpenFeature.Constant; -using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; -using OpenFeature.Model; -using RichardSzalay.MockHttp; -using Xunit; - -namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test; - -public class GoFeatureFlagProviderTest -{ - private static readonly string baseUrl = "http://gofeatureflag.org"; - private static readonly string prefixEval = baseUrl + "/v1/feature/"; - private static readonly string suffixEval = "/eval"; - private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx(); - private readonly HttpMessageHandler _mockHttp = InitMock(); - - private static HttpMessageHandler InitMock() - { - const string mediaType = "application/json"; - var mockHttp = new MockHttpMessageHandler(); - mockHttp.When($"{prefixEval}fail_500{suffixEval}").Respond(HttpStatusCode.InternalServerError); - mockHttp.When($"{prefixEval}api_key_missing{suffixEval}").Respond(HttpStatusCode.BadRequest); - mockHttp.When($"{prefixEval}invalid_api_key{suffixEval}").Respond(HttpStatusCode.Unauthorized); - mockHttp.When($"{prefixEval}flag_not_found{suffixEval}").Respond(HttpStatusCode.NotFound); - mockHttp.When($"{prefixEval}bool_targeting_match{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":true}"); - mockHttp.When($"{prefixEval}disabled{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":true}"); - mockHttp.When($"{prefixEval}disabled_double{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":100.25}"); - mockHttp.When($"{prefixEval}disabled_integer{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":100}"); - mockHttp.When($"{prefixEval}disabled_object{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":null}"); - mockHttp.When($"{prefixEval}disabled_string{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":false,\"version\":\"\",\"reason\":\"DISABLED\",\"errorCode\":\"\",\"value\":\"\"}"); - mockHttp.When($"{prefixEval}double_key{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":100.25}"); - mockHttp.When($"{prefixEval}flag_not_found{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"SdkDefault\",\"failed\":true,\"version\":\"\",\"reason\":\"ERROR\",\"errorCode\":\"FLAG_NOT_FOUND\",\"value\":\"false\"}"); - mockHttp.When($"{prefixEval}integer_key{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":100}"); - mockHttp.When($"{prefixEval}list_key{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":[\"test\",\"test1\",\"test2\",\"false\",\"test3\"]}"); - mockHttp.When($"{prefixEval}object_key{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":{\"test\":\"test1\",\"test2\":false,\"test3\":123.3,\"test4\":1,\"test5\":null}}"); - mockHttp.When($"{prefixEval}string_key{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"TARGETING_MATCH\",\"errorCode\":\"\",\"value\":\"CC0000\"}"); - mockHttp.When($"{prefixEval}unknown_reason{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"True\",\"failed\":false,\"version\":\"\",\"reason\":\"CUSTOM_REASON\",\"errorCode\":\"\",\"value\":true}"); - mockHttp.When($"{prefixEval}does_not_exists{suffixEval}").Respond(mediaType, - "{\"trackEvents\":true,\"variationType\":\"defaultSdk\",\"failed\":true,\"version\":\"\",\"reason\":\"ERROR\",\"errorCode\":\"FLAG_NOT_FOUND\",\"value\":\"\"}"); - return mockHttp; - } - - private static EvaluationContext InitDefaultEvaluationCtx() - { - return EvaluationContext.Builder() - .Set("targetingKey", "d45e303a-38c2-11ed-a261-0242ac120002") - .Set("email", "john.doe@gofeatureflag.org") - .Set("firstname", "john") - .Set("lastname", "doe") - .Set("anonymous", false) - .Set("professional", true) - .Set("rate", 3.14) - .Set("age", 30) - .Set("company_info", new Value(new Structure(new Dictionary - { - { "name", new Value("my_company") }, - { "size", new Value(120) } - }))) - .Set("labels", new Value(new List - { - new("pro"), - new("beta") - })) - .Build(); - } - - - [Fact] - public async Task getMetadata_validate_name() - { - var goFeatureFlagProvider = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Timeout = new TimeSpan(19 * TimeSpan.TicksPerHour), - Endpoint = baseUrl - }); - await Api.Instance.SetProviderAsync(goFeatureFlagProvider); - Assert.Equal("GO Feature Flag Provider", Api.Instance.GetProvider().GetMetadata().Name); - } - - - [Fact] - private void constructor_options_null() - { - Assert.Throws(() => new GoFeatureFlagProvider(null)); - } - - [Fact] - private void constructor_options_empty() - { - Assert.Throws(() => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions())); - } - - [Fact] - private void constructor_options_empty_endpoint() - { - Assert.Throws( - () => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = "" })); - } - - [Fact] - private void constructor_options_only_timeout() - { - Assert.Throws( - () => new GoFeatureFlagProvider( - new GoFeatureFlagProviderOptions { Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) } - ) - ); - } - - [Fact] - private void constructor_options_valid_endpoint() - { - var exception = Record.Exception(() => - new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = baseUrl })); - Assert.Null(exception); - } - - [Fact] - public async Task should_throw_an_error_if_endpoint_not_available() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("fail_500", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(ErrorType.General, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_have_bad_request_if_no_token() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("api_key_missing", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(Reason.Error, res.Result.Reason); - Assert.Equal(ErrorType.General, res.Result.ErrorType); - } - - [Fact] - public async Task should_have_unauthorized_if_invalid_token() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond), - ApiKey = "ff877c7a-4594-43b5-89a8-df44c9984bd8" - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("invalid_api_key", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(Reason.Error, res.Result.Reason); - Assert.Equal(ErrorType.General, res.Result.ErrorType); - } - - [Fact] - public async Task should_throw_an_error_if_flag_does_not_exists() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("flag_not_found", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_throw_an_error_if_we_expect_a_boolean_and_got_another_type() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("string_key", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_boolean_flag_with_TARGETING_MATCH_reason() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("bool_targeting_match", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.True(res.Result.Value); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_return_custom_reason_if_returned_by_relay_proxy() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("unknown_reason", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.True(res.Result.Value); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal("CUSTOM_REASON", res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_boolean_default_value_if_the_flag_is_disabled() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetBooleanDetailsAsync("disabled", false, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.False(res.Result.Value); - Assert.Equal(Reason.Disabled, res.Result.Reason); - } - - [Fact] - public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_type() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetStringDetailsAsync("bool_targeting_match", "default", _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal("default", res.Result.Value); - Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetStringDetailsAsync("string_key", "defaultValue", _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal("CC0000", res.Result.Value); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_string_default_value_if_the_flag_is_disabled() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetStringDetailsAsync("disabled_string", "defaultValue", _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal("defaultValue", res.Result.Value); - Assert.Equal(Reason.Disabled, res.Result.Reason); - } - - [Fact] - public async Task should_throw_an_error_if_we_expect_a_integer_and_got_another_type() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetIntegerDetailsAsync("string_key", 200, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(200, res.Result.Value); - Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_integer_flag_with_TARGETING_MATCH_reason() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetIntegerDetailsAsync("integer_key", 1200, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(100, res.Result.Value); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_integer_default_value_if_the_flag_is_disabled() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(1225, res.Result.Value); - Assert.Equal(Reason.Disabled, res.Result.Reason); - } - - [Fact] - public async Task should_throw_an_error_if_we_expect_a_integer_and_double_type() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetIntegerDetailsAsync("double_key", 200, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(200, res.Result.Value); - Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_double_flag_with_TARGETING_MATCH_reason() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetDoubleDetailsAsync("double_key", 1200.25, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(100.25, res.Result.Value); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_double_default_value_if_the_flag_is_disabled() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetDoubleDetailsAsync("disabled_double", 1225.34, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(1225.34, res.Result.Value); - Assert.Equal(Reason.Disabled, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_value_flag_with_TARGETING_MATCH_reason() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetObjectDetailsAsync("object_key", null, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - var want = JsonSerializer.Serialize(new Value(new Structure(new Dictionary - { - { "test", new Value("test1") }, { "test2", new Value(false) }, { "test3", new Value(123.3) }, - { "test4", new Value(1) } - }))); - Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value)); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_wrap_into_value_if_wrong_type() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetObjectDetailsAsync("string_key", null, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(new Value("CC0000").AsString, res.Result.Value.AsString); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_object_default_value_if_the_flag_is_disabled() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetObjectDetailsAsync("disabled_object", new Value("default"), _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(new Value("default").AsString, res.Result.Value.AsString); - Assert.Equal(Reason.Disabled, res.Result.Reason); - } - - - [Fact] - public async Task should_throw_an_error_if_no_targeting_key() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetStringDetailsAsync("list_key", "empty", EvaluationContext.Empty); - Assert.NotNull(res.Result); - Assert.Equal("empty", res.Result.Value); - Assert.Equal(ErrorType.InvalidContext, res.Result.ErrorType); - Assert.Equal(Reason.Error, res.Result.Reason); - } - - [Fact] - public async Task should_resolve_a_valid_value_flag_with_a_list() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetObjectDetailsAsync("list_key", null, _defaultEvaluationCtx); - Assert.NotNull(res.Result); - var want = JsonSerializer.Serialize(new Value(new List - { new("test"), new("test1"), new("test2"), new("false"), new("test3") })); - Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value)); - Assert.Equal(ErrorType.None, res.Result.ErrorType); - Assert.Equal(Reason.TargetingMatch, res.Result.Reason); - Assert.Equal("True", res.Result.Variant); - } - - [Fact] - public async Task should_use_object_default_value_if_flag_not_found() - { - var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions - { - Endpoint = baseUrl, - HttpMessageHandler = _mockHttp, - Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) - }); - await Api.Instance.SetProviderAsync(g); - var client = Api.Instance.GetClient("test-client"); - var res = client.GetObjectDetailsAsync("does_not_exists", new Value("default"), _defaultEvaluationCtx); - Assert.NotNull(res.Result); - Assert.Equal(new Value("default").AsString, res.Result.Value.AsString); - Assert.Equal(Reason.Error, res.Result.Reason); - Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType); - Assert.Equal("flag does_not_exists was not found in your configuration", res.Result.ErrorMessage); - } -} +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using OpenFeature.Constant; +using OpenFeature.Contrib.Providers.GOFeatureFlag.exception; +using OpenFeature.Contrib.Providers.GOFeatureFlag.models; +using OpenFeature.Model; +using RichardSzalay.MockHttp; +using Xunit; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test; + +public class GoFeatureFlagProviderTest +{ + private static readonly string baseUrl = "http://gofeatureflag.org"; + private static readonly string prefixEval = baseUrl + "/ofrep/v1/evaluate/flags/"; + private readonly EvaluationContext _defaultEvaluationCtx = InitDefaultEvaluationCtx(); + private readonly HttpMessageHandler _mockHttp = InitMock(); + + private static HttpMessageHandler InitMock() + { + const string mediaType = "application/json"; + var mockHttp = new MockHttpMessageHandler(); + mockHttp.When($"{prefixEval}fail_500").Respond(HttpStatusCode.InternalServerError); + mockHttp.When($"{prefixEval}api_key_missing").Respond(HttpStatusCode.BadRequest); + mockHttp.When($"{prefixEval}invalid_api_key").Respond(HttpStatusCode.Unauthorized); + mockHttp.When($"{prefixEval}flag_not_found").Respond(HttpStatusCode.NotFound); + mockHttp.When($"{prefixEval}bool_targeting_match").Respond(mediaType, + "{ \"value\":true, \"key\":\"bool_targeting_match\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true }"); + mockHttp.When($"{prefixEval}disabled").Respond(mediaType, + "{ \"value\":false, \"key\":\"disabled\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}disabled_double").Respond(mediaType, + "{ \"value\":100.25, \"key\":\"disabled_double\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}disabled_integer").Respond(mediaType, + "{ \"value\":100, \"key\":\"disabled_integer\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}disabled_object").Respond(mediaType, + "{ \"value\":null, \"key\":\"disabled_object\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}disabled_string").Respond(mediaType, + "{ \"value\":\"\", \"key\":\"disabled_string\", \"reason\":\"DISABLED\", \"variant\":\"defaultSdk\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}double_key").Respond(mediaType, + "{ \"value\":100.25, \"key\":\"double_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}flag_not_found").Respond(mediaType, + "{ \"value\":false, \"key\":\"flag_not_found\", \"reason\":\"FLAG_NOT_FOUND\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}integer_key").Respond(mediaType, + "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}list_key").Respond(mediaType, + "{ \"value\":[\"test\",\"test1\",\"test2\",\"false\",\"test3\"], \"key\":\"list_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}object_key").Respond(mediaType, + "{ \"value\":{\"test\":\"test1\",\"test2\":false,\"test3\":123.3,\"test4\":1,\"test5\":null}, \"key\":\"object_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}string_key").Respond(mediaType, + "{ \"value\":\"CC0000\", \"key\":\"string_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}unknown_reason").Respond(mediaType, + "{ \"value\":\"true\", \"key\":\"unknown_reason\", \"reason\":\"CUSTOM_REASON\", \"variant\":\"True\", \"cacheable\":true}"); + mockHttp.When($"{prefixEval}does_not_exists").Respond(mediaType, + "{ \"value\":\"\", \"key\":\"does_not_exists\", \"errorCode\":\"FLAG_NOT_FOUND\", \"variant\":\"defaultSdk\", \"cacheable\":true, \"errorDetails\":\"flag does_not_exists was not found in your configuration\"}"); + mockHttp.When($"{prefixEval}integer_with_metadata").Respond(mediaType, + "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true, \"metadata\":{\"key1\": \"key1\", \"key2\": 1, \"key3\": 1.345, \"key4\": true}}"); + return mockHttp; + } + + private static EvaluationContext InitDefaultEvaluationCtx() + { + return EvaluationContext.Builder() + .Set("targetingKey", "d45e303a-38c2-11ed-a261-0242ac120002") + .Set("email", "john.doe@gofeatureflag.org") + .Set("firstname", "john") + .Set("lastname", "doe") + .Set("anonymous", false) + .Set("professional", true) + .Set("rate", 3.14) + .Set("age", 30) + .Set("company_info", new Value(new Structure(new Dictionary + { + { "name", new Value("my_company") }, + { "size", new Value(120) } + }))) + .Set("labels", new Value(new List + { + new("pro"), + new("beta") + })) + .Build(); + } + + + [Fact] + public async Task getMetadata_validate_name() + { + var goFeatureFlagProvider = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Timeout = new TimeSpan(19 * TimeSpan.TicksPerHour), + Endpoint = baseUrl + }); + await Api.Instance.SetProviderAsync(goFeatureFlagProvider); + Assert.Equal("GO Feature Flag Provider", Api.Instance.GetProvider().GetMetadata().Name); + } + + + [Fact] + private void constructor_options_null() + { + Assert.Throws(() => new GoFeatureFlagProvider(null)); + } + + [Fact] + private void constructor_options_empty() + { + Assert.Throws(() => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions())); + } + + [Fact] + private void constructor_options_empty_endpoint() + { + Assert.Throws( + () => new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = "" })); + } + + [Fact] + private void constructor_options_only_timeout() + { + Assert.Throws( + () => new GoFeatureFlagProvider( + new GoFeatureFlagProviderOptions { Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) } + ) + ); + } + + [Fact] + private void constructor_options_valid_endpoint() + { + var exception = Record.Exception(() => + new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions { Endpoint = baseUrl })); + Assert.Null(exception); + } + + [Fact] + public async Task should_throw_an_error_if_endpoint_not_available() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("fail_500", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(ErrorType.General, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_have_bad_request_if_no_token() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("api_key_missing", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(Reason.Error, res.Result.Reason); + Assert.Equal(ErrorType.General, res.Result.ErrorType); + } + + [Fact] + public async Task should_have_unauthorized_if_invalid_token() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond), + ApiKey = "ff877c7a-4594-43b5-89a8-df44c9984bd8" + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("invalid_api_key", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(Reason.Error, res.Result.Reason); + Assert.Equal(ErrorType.General, res.Result.ErrorType); + } + + [Fact] + public async Task should_throw_an_error_if_flag_does_not_exists() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("flag_not_found", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_throw_an_error_if_we_expect_a_boolean_and_got_another_type() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("string_key", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_boolean_flag_with_TARGETING_MATCH_reason() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("bool_targeting_match", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.True(res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_return_custom_reason_if_returned_by_relay_proxy() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("unknown_reason", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.True(res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal("CUSTOM_REASON", res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_boolean_default_value_if_the_flag_is_disabled() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetBooleanDetailsAsync("disabled", false, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.False(res.Result.Value); + Assert.Equal(Reason.Disabled, res.Result.Reason); + } + + [Fact] + public async Task should_throw_an_error_if_we_expect_a_string_and_got_another_type() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetStringDetailsAsync("bool_targeting_match", "default", _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal("default", res.Result.Value); + Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_string_flag_with_TARGETING_MATCH_reason() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetStringDetailsAsync("string_key", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal("CC0000", res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_string_default_value_if_the_flag_is_disabled() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetStringDetailsAsync("disabled_string", "defaultValue", _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal("defaultValue", res.Result.Value); + Assert.Equal(Reason.Disabled, res.Result.Reason); + } + + [Fact] + public async Task should_throw_an_error_if_we_expect_a_integer_and_got_another_type() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetIntegerDetailsAsync("string_key", 200, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(200, res.Result.Value); + Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_integer_flag_with_TARGETING_MATCH_reason() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetIntegerDetailsAsync("integer_key", 1200, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(100, res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_integer_default_value_if_the_flag_is_disabled() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetIntegerDetailsAsync("disabled_integer", 1225, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(1225, res.Result.Value); + Assert.Equal(Reason.Disabled, res.Result.Reason); + } + + [Fact] + public async Task should_throw_an_error_if_we_expect_a_integer_and_double_type() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetIntegerDetailsAsync("double_key", 200, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(200, res.Result.Value); + Assert.Equal(ErrorType.TypeMismatch, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_double_flag_with_TARGETING_MATCH_reason() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetDoubleDetailsAsync("double_key", 1200.25, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(100.25, res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_double_default_value_if_the_flag_is_disabled() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetDoubleDetailsAsync("disabled_double", 1225.34, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(1225.34, res.Result.Value); + Assert.Equal(Reason.Disabled, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_value_flag_with_TARGETING_MATCH_reason() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("object_key", null, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + var want = JsonSerializer.Serialize(new Value(new Structure(new Dictionary + { + { "test", new Value("test1") }, { "test2", new Value(false) }, { "test3", new Value(123.3) }, + { "test4", new Value(1) } + }))); + Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value)); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_wrap_into_value_if_wrong_type() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("string_key", null, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(new Value("CC0000").AsString, res.Result.Value.AsString); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_object_default_value_if_the_flag_is_disabled() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("disabled_object", new Value("default"), _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(new Value("default").AsString, res.Result.Value.AsString); + Assert.Equal(Reason.Disabled, res.Result.Reason); + } + + + [Fact] + public async Task should_throw_an_error_if_no_targeting_key() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetStringDetailsAsync("list_key", "empty", EvaluationContext.Empty); + Assert.NotNull(res.Result); + Assert.Equal("empty", res.Result.Value); + Assert.Equal(ErrorType.InvalidContext, res.Result.ErrorType); + Assert.Equal(Reason.Error, res.Result.Reason); + } + + [Fact] + public async Task should_resolve_a_valid_value_flag_with_a_list() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("list_key", null, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + var want = JsonSerializer.Serialize(new Value(new List + { new("test"), new("test1"), new("test2"), new("false"), new("test3") })); + Assert.Equal(want, JsonSerializer.Serialize(res.Result.Value)); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + } + + [Fact] + public async Task should_use_object_default_value_if_flag_not_found() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("does_not_exists", new Value("default"), _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(new Value("default").AsString, res.Result.Value.AsString); + Assert.Equal(Reason.Error, res.Result.Reason); + Assert.Equal(ErrorType.FlagNotFound, res.Result.ErrorType); + Assert.Equal("flag does_not_exists was not found in your configuration", res.Result.ErrorMessage); + } + + [Fact] + public async Task should_have_default_exporter_metadata_in_context() + { + string capturedRequestBody = null; + var mock = new MockHttpMessageHandler(); + var mockedRequest = mock.When($"{prefixEval}integer_key").Respond( + async request => + { + capturedRequestBody = await request.Content.ReadAsStringAsync(); + return new HttpResponseMessage + { + Content = new StringContent( + "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}" + , Encoding.UTF8, "application/json") + }; + }); + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = mock, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("integer_key", new Value("default"), _defaultEvaluationCtx); + Assert.Equal(1, mock.GetMatchCount(mockedRequest)); + await Task.Delay(100); // time to wait to be sure body is extracted + var want = JObject.Parse( + "{\"context\":{\"labels\":[\"pro\",\"beta\"],\"gofeatureflag\":{\"openfeature\":true,\"provider\":\".NET\"},\"age\":30,\"firstname\":\"john\",\"professional\":true,\"company_info\":{\"name\":\"my_company\",\"size\":120},\"lastname\":\"doe\",\"anonymous\":false,\"rate\":3.14,\"email\":\"john.doe@gofeatureflag.org\",\"targetingKey\":\"d45e303a-38c2-11ed-a261-0242ac120002\"}}"); + var got = JObject.Parse(capturedRequestBody); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public async Task should_have_custom_exporter_metadata_in_context() + { + string capturedRequestBody = null; + var mock = new MockHttpMessageHandler(); + var mockedRequest = mock.When($"{prefixEval}integer_key").Respond( + async request => + { + capturedRequestBody = await request.Content.ReadAsStringAsync(); + return new HttpResponseMessage + { + Content = new StringContent( + "{ \"value\":100, \"key\":\"integer_key\", \"reason\":\"TARGETING_MATCH\", \"variant\":\"True\", \"cacheable\":true}" + , Encoding.UTF8, "application/json") + }; + }); + var exporterMetadata = new ExporterMetadata(); + exporterMetadata.Add("key1", "value1"); + exporterMetadata.Add("key2", 1.234); + exporterMetadata.Add("key3", 10); + exporterMetadata.Add("key4", false); + + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = mock, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond), + ExporterMetadata = exporterMetadata + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetObjectDetailsAsync("integer_key", new Value("default"), _defaultEvaluationCtx); + Assert.Equal(1, mock.GetMatchCount(mockedRequest)); + await Task.Delay(100); // time to wait to be sure body is extracted + var want = JObject.Parse( + "{\"context\":{\"labels\":[\"pro\",\"beta\"],\"gofeatureflag\":{\"openfeature\":true,\"provider\":\".NET\",\"key1\":\"value1\",\"key2\":1.234,\"key3\":10,\"key4\":false},\"age\":30,\"firstname\":\"john\",\"professional\":true,\"company_info\":{\"name\":\"my_company\",\"size\":120},\"lastname\":\"doe\",\"anonymous\":false,\"rate\":3.14,\"email\":\"john.doe@gofeatureflag.org\",\"targetingKey\":\"d45e303a-38c2-11ed-a261-0242ac120002\"}}"); + var got = JObject.Parse(capturedRequestBody); + + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public async Task should_resolve_a_flag_with_metadata() + { + var g = new GoFeatureFlagProvider(new GoFeatureFlagProviderOptions + { + Endpoint = baseUrl, + HttpMessageHandler = _mockHttp, + Timeout = new TimeSpan(1000 * TimeSpan.TicksPerMillisecond) + }); + await Api.Instance.SetProviderAsync(g); + var client = Api.Instance.GetClient("test-client"); + var res = client.GetIntegerDetailsAsync("integer_with_metadata", 1200, _defaultEvaluationCtx); + Assert.NotNull(res.Result); + Assert.Equal(100, res.Result.Value); + Assert.Equal(ErrorType.None, res.Result.ErrorType); + Assert.Equal(Reason.TargetingMatch, res.Result.Reason); + Assert.Equal("True", res.Result.Variant); + Assert.NotNull(res.Result.FlagMetadata); + Assert.Equal("key1", res.Result.FlagMetadata.GetString("key1")); + Assert.Equal(1, res.Result.FlagMetadata.GetInt("key2")); + Assert.Equal(1.345, res.Result.FlagMetadata.GetDouble("key3")); + Assert.True(res.Result.FlagMetadata.GetBool("key4")); + } +} diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs deleted file mode 100644 index 78767989..00000000 --- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/GoFeatureFlagUserTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json; -using OpenFeature.Model; -using Xunit; - -namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test; - -public class GoFeatureFlagUserTest -{ - [Fact] - public void GoFeatureFlagUserSerializesCorrectly() - { - var userContext = EvaluationContext.Builder() - .Set("targetingKey", "1d1b9238-2591-4a47-94cf-d2bc080892f1") - .Set("firstname", "john") - .Set("lastname", "doe") - .Set("email", "john.doe@gofeatureflag.org") - .Set("admin", true) - .Set("anonymous", false) - .Build(); - - GoFeatureFlagUser user = userContext; - - var userAsString = JsonSerializer.Serialize(user, - new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); - - Assert.Contains("{\"key\":\"1d1b9238-2591-4a47-94cf-d2bc080892f1\",\"anonymous\":false,\"custom\":{", - userAsString); - } -} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs new file mode 100644 index 00000000..6672e121 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OfrepSerializationTest.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using Newtonsoft.Json.Linq; +using OpenFeature.Contrib.Providers.GOFeatureFlag.converters; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Contrib.Providers.GOFeatureFlag.Test; + +public class OfrepSerializationTest +{ + [Fact] + public void OfrepSerializesCorrectly() + { + var ctx = EvaluationContext.Builder() + .Set("targetingKey", "1d1b9238-2591-4a47-94cf-d2bc080892f1") + .Set("firstname", "john") + .Set("lastname", "doe") + .Set("email", "john.doe@gofeatureflag.org") + .Set("admin", true) + .Set("anonymous", false) + .Build(); + + var ofrepReq = new OfrepRequest(ctx); + + var want = JObject.Parse("{\"context\":{\"firstname\":\"john\",\"email\":\"john.doe@gofeatureflag.org\",\"lastname\":\"doe\",\"targetingKey\":\"1d1b9238-2591-4a47-94cf-d2bc080892f1\",\"admin\":true,\"anonymous\":false}}"); + var got = JObject.Parse(ofrepReq.AsJsonString()); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary() + { + var evaluationContext = EvaluationContext.Builder().Build(); + var want = JObject.Parse("{\"context\":{}}"); + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContext_ShouldReturnADictionaryWithValues() + { + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("location", "somewhere") + .Build(); + + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"location\":\"somewhere\",\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContextAndIntegerValue_ShouldReturnADictionaryWithStringValues() + { + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("age", 23) + .Build(); + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"age\":23,\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContextAndValuesOfStrings_ShouldReturnADictionaryWithSerializedStringValues() + { + var testStructure = new Structure(new Dictionary + { + { "config1", new Value("value1") }, + { "config2", new Value("value2") } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("config", testStructure) + .Build(); + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"config\":{\"config1\":\"value1\", \"config2\":\"value2\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContextAndMixedValueTypes_ShouldReturnADictionaryWithSerializedValues() + { + var dateTime = new DateTime(2025, 9, 1); + var testStructure = new Structure(new Dictionary + { + { "config1", new Value(1) }, + { "config2", new Value("value2") }, + { "config3", new Value(dateTime) } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("config", testStructure) + .Build(); + + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"config\":{\"config3\":\"2025-09-01T00:00:00\",\"config2\":\"value2\",\"config1\":1},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADictionaryWithSerializedValues() + { + var sampleDictionary = new Dictionary(); + sampleDictionary["config2"] = new Value([ + new Value([new Value("element1-1"), new Value("element1-2")]), + new Value("element2"), + new Value("element3") + ]); + sampleDictionary["config3"] = new Value(new DateTime(2025, 9, 1)); + + var testStructure = new Structure(sampleDictionary); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("config", testStructure) + .Build(); + + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"config\":{\"config2\":[[\"element1-1\",\"element1-2\"],\"element2\",\"element3\"],\"config3\":\"2025-09-01T00:00:00\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } + + [Fact] + public void ToStringDictionary_WithContextWithNestedStructure_ShouldReturnADictionaryWithSerializedValues() + { + var testStructure = new Structure(new Dictionary + { + { + "config-value-struct", new Value(new Structure(new Dictionary + { + { "nested1", new Value(1) } + })) + }, + { "config-value-value", new Value(new Value(new DateTime(2025, 9, 1))) } + }); + + var evaluationContext = EvaluationContext.Builder() + .SetTargetingKey("828c9b62-94c4-4ef3-bddc-e024bfa51a67") + .Set("config", testStructure) + .Build(); + var request = new Dictionary { { "context", evaluationContext.AsDictionary() } }; + var got = JObject.Parse(JsonSerializer.Serialize(request, JsonConverterExtensions.DefaultSerializerSettings)); + var want = JObject.Parse("{\"context\":{\"config\":{\"config-value-struct\":{\"nested1\":1},\"config-value-value\":\"2025-09-01T00:00:00\"},\"targetingKey\":\"828c9b62-94c4-4ef3-bddc-e024bfa51a67\"}}"); + Assert.True(JToken.DeepEquals(want, got), "unexpected json"); + } +} diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj index 96b51275..615b3abc 100644 --- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj @@ -7,5 +7,6 @@ + From 9ae74e646da868eed4931e506841d651023c31d2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:54:37 +0100 Subject: [PATCH 22/56] chore(main): release OpenFeature.Contrib.Providers.GOFeatureFlag 0.2.1 (#288) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .../CHANGELOG.md | 12 ++++++++++++ ...penFeature.Contrib.Providers.GOFeatureFlag.csproj | 2 +- .../version.txt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 910b4295..c70f5e5d 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,7 +1,7 @@ { "src/OpenFeature.Contrib.Hooks.Otel": "0.2.0", "src/OpenFeature.Contrib.Providers.Flagd": "0.3.0", - "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.0", + "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.1", "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0", "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md index 85c6b384..7b0e6edc 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.2.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.0...OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.1) (2025-02-10) + + +### ✨ New Features + +* **gofeatureflag:** Provider refactor ([#313](https://github.com/open-feature/dotnet-sdk-contrib/issues/313)) ([c30446e](https://github.com/open-feature/dotnet-sdk-contrib/commit/c30446eb51538b05378db7c4d56228f01ed1cb88)) + + +### 🧹 Chore + +* **deps:** update dependency system.text.json to 8.0.5 [security] ([#287](https://github.com/open-feature/dotnet-sdk-contrib/issues/287)) ([8cb79ab](https://github.com/open-feature/dotnet-sdk-contrib/commit/8cb79ab8e6d33adc9acb6d6b9795cc4b5e0cf81e)) + ## [0.2.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.GOFeatureFlag-v0.1.10...OpenFeature.Contrib.Providers.GOFeatureFlag-v0.2.0) (2024-08-22) diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj index 3ac6dd47..3dc488c8 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/OpenFeature.Contrib.Providers.GOFeatureFlag.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.GOFeatureFlag - 0.2.0 + 0.2.1 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/version.txt b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/version.txt index 0ea3a944..0c62199f 100644 --- a/src/OpenFeature.Contrib.Providers.GOFeatureFlag/version.txt +++ b/src/OpenFeature.Contrib.Providers.GOFeatureFlag/version.txt @@ -1 +1 @@ -0.2.0 +0.2.1 From 2945301a4e5b0e2a3ec68c62c436c3bca87635fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 13:02:18 -0500 Subject: [PATCH 23/56] chore(deps): update dependency flagsmith to 5.4.3 (#290) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagsmith.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj index 7a1f1134..978c94e5 100644 --- a/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagsmith/OpenFeature.Contrib.Providers.Flagsmith.csproj @@ -18,7 +18,7 @@ - + From 4908000ed27a648ee7cf8823320ae7d7c8cd8c45 Mon Sep 17 00:00:00 2001 From: Michael Richardson Date: Wed, 12 Mar 2025 22:24:04 +1000 Subject: [PATCH 24/56] feat: Environment Variable Provider (#312) Signed-off-by: Michael Richardson --- .release-please-manifest.json | 3 +- DotnetSdkContrib.sln | 14 ++ release-please-config.json | 10 + .../EnvVarProvider.cs | 102 +++++++++ ...penFeature.Contrib.Providers.EnvVar.csproj | 13 ++ .../README.md | 41 ++++ .../version.txt | 1 + .../EnvVarProviderTests.cs | 195 ++++++++++++++++++ ...ature.Contrib.Providers.EnvVar.Test.csproj | 8 + 9 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs create mode 100644 src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj create mode 100644 src/OpenFeature.Contrib.Providers.EnvVar/README.md create mode 100644 src/OpenFeature.Contrib.Providers.EnvVar/version.txt create mode 100644 test/OpenFeature.Contrib.Providers.EnvVar.Test/EnvVarProviderTests.cs create mode 100644 test/OpenFeature.Contrib.Providers.EnvVar.Test/OpenFeature.Contrib.Providers.EnvVar.Test.csproj diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c70f5e5d..416b3586 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -6,5 +6,6 @@ "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", - "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5" + "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5", + "src/OpenFeature.Contrib.Providers.EnvVar": "0.0.1" } \ No newline at end of file diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index 57004386..2aaba701 100644 --- a/DotnetSdkContrib.sln +++ b/DotnetSdkContrib.sln @@ -45,6 +45,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Provide EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Flipt.Test", "test\OpenFeature.Contrib.Providers.Flipt.Test\OpenFeature.Contrib.Providers.Flipt.Test.csproj", "{B446D481-B5A3-4509-8933-C4CF6DA9B147}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.EnvVar", "src\OpenFeature.Contrib.Providers.EnvVar\OpenFeature.Contrib.Providers.EnvVar.csproj", "{F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.EnvVar.Test", "test\OpenFeature.Contrib.Providers.EnvVar.Test\OpenFeature.Contrib.Providers.EnvVar.Test.csproj", "{282AD5C5-099A-403D-B415-29AA88A701EC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -127,6 +131,14 @@ Global {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Debug|Any CPU.Build.0 = Debug|Any CPU {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.ActiveCfg = Release|Any CPU {B446D481-B5A3-4509-8933-C4CF6DA9B147}.Release|Any CPU.Build.0 = Release|Any CPU + {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB}.Release|Any CPU.Build.0 = Release|Any CPU + {282AD5C5-099A-403D-B415-29AA88A701EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {282AD5C5-099A-403D-B415-29AA88A701EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {282AD5C5-099A-403D-B415-29AA88A701EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {282AD5C5-099A-403D-B415-29AA88A701EC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -151,5 +163,7 @@ Global {F3080350-B0AB-4D59-B416-50CC38C99087} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} {5ECF7DBF-FE64-40A2-BF39-239DE173DA4B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} {B446D481-B5A3-4509-8933-C4CF6DA9B147} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} + {F7C6368F-29DC-4F70-AA0E-B3C340F9E1AB} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} + {282AD5C5-099A-403D-B415-29AA88A701EC} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} EndGlobalSection EndGlobal diff --git a/release-please-config.json b/release-please-config.json index cabbd73f..e9cbda48 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -82,6 +82,16 @@ "extra-files": [ "OpenFeature.Contrib.Providers.Flipt.csproj" ] + }, + "src/OpenFeature.Contrib.Providers.EnvVar": { + "package-name": "OpenFeature.Contrib.Providers.EnvVar", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "OpenFeature.Contrib.Providers.EnvVar.csproj" + ] } }, "changelog-sections": [ diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs b/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs new file mode 100644 index 00000000..36056958 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.EnvVar/EnvVarProvider.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; + +namespace OpenFeature.Contrib.Providers.EnvVar +{ + /// + /// An OpenFeature provider using environment variables. + /// + public sealed class EnvVarProvider : FeatureProvider + { + private const string Name = "Environment Variable Provider"; + private readonly string _prefix; + private delegate bool TryConvert(string value, out TResult result); + + /// + /// Creates a new instance of + /// + public EnvVarProvider() : this(string.Empty) + { + } + + /// + /// Creates a new instance of + /// + /// A prefix which will be used when evaluating environment variables + public EnvVarProvider(string prefix) + { + _prefix = prefix; + } + + /// + public override Metadata GetMetadata() + { + return new Metadata(Name); + } + + private Task> Resolve(string flagKey, T defaultValue, TryConvert tryConvert) + { + var envVarName = $"{_prefix}{flagKey}"; + var value = Environment.GetEnvironmentVariable(envVarName); + + if (value == null) + return Task.FromResult(new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, Reason.Error, string.Empty, $"Unable to find environment variable '{envVarName}'")); + + if (!tryConvert(value, out var convertedValue)) + throw new FeatureProviderException(ErrorType.TypeMismatch, $"Could not convert the value of environment variable '{envVarName}' to {typeof(T)}"); + + return Task.FromResult(new ResolutionDetails(flagKey, convertedValue, ErrorType.None, Reason.Static)); + } + + /// + public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return Resolve(flagKey, defaultValue, bool.TryParse); + } + + /// + public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return Resolve(flagKey, defaultValue, NoopTryParse); + + bool NoopTryParse(string value, out string result) + { + result = value; + return true; + } + } + + /// + public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return Resolve(flagKey, defaultValue, int.TryParse); + } + + /// + public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return Resolve(flagKey, defaultValue, double.TryParse); + } + + /// + public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null, + CancellationToken cancellationToken = new CancellationToken()) + { + return Resolve(flagKey, defaultValue, ConvertStringToValue); + + bool ConvertStringToValue(string s, out Value value) + { + value = new Value(s); + return true; + } + } + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj new file mode 100644 index 00000000..88360fa5 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj @@ -0,0 +1,13 @@ + + + + OpenFeature.Contrib.Providers.EnvVar + 0.0.1 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + Environment Variable Provider for .NET + Octopus Deploy + + + diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/README.md b/src/OpenFeature.Contrib.Providers.EnvVar/README.md new file mode 100644 index 00000000..7b1645b7 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.EnvVar/README.md @@ -0,0 +1,41 @@ +# .NET Environment Variable Provider + +This provider supports using the OpenFeature SDK to evaluate feature flags backed by environment variables. + +## Installation + +### .NET CLI + +```shell +dotnet add package OpenFeature.Contrib.Providers.EnvVar +``` + +## Using the ConfigCat Provider with the OpenFeature SDK + +The following example shows how to use the Environment Variable provider with the OpenFeature SDK. + +```csharp +using System; +using OpenFeature; +using OpenFeature.Contrib.EnvVar; + +// If you want to use a prefix for your environment variables, you can supply it in the constructor below. +// For example, if you all your feature flag environment variables will be prefixed with feature-flag- then +// you would use: +// var envVarProvider = new EnvVarProvider("feature-flag-"); +var envVarProvider = new EnvVarProvider(); + +// Set the Environment Variable provider as the provider for the OpenFeature SDK +await OpenFeature.Api.Instance.SetProviderAsync(envVarProvider); +var client = OpenFeature.Api.Instance.GetClient(); + +var isAwesomeFeatureEnabled = await client.GetBooleanValueAsync("isAwesomeFeatureEnabled", false); +if (isAwesomeFeatureEnabled) +{ + doTheNewThing(); +} +else +{ + doTheOldThing(); +} +``` diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/version.txt b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt new file mode 100644 index 00000000..8acdd82b --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt @@ -0,0 +1 @@ +0.0.1 diff --git a/test/OpenFeature.Contrib.Providers.EnvVar.Test/EnvVarProviderTests.cs b/test/OpenFeature.Contrib.Providers.EnvVar.Test/EnvVarProviderTests.cs new file mode 100644 index 00000000..88c91668 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.EnvVar.Test/EnvVarProviderTests.cs @@ -0,0 +1,195 @@ +using System; +using System.Threading.Tasks; +using AutoFixture.Xunit2; +using OpenFeature.Constant; +using OpenFeature.Error; +using OpenFeature.Model; +using Xunit; + +namespace OpenFeature.Contrib.Providers.EnvVar.Test +{ + public class EnvVarProviderTests + { + [Theory] + [AutoData] + public async Task ResolveBooleanValueAsync_WhenEnvironmentVariablePresent_ShouldReturnValue(string prefix, + string flagKey) + { + var value = true; + Environment.SetEnvironmentVariable(prefix + flagKey, value.ToString()); + + await ExecuteResolveValueTest(prefix, flagKey, false, value, Reason.Static, + (provider, key, defaultValue) => provider.ResolveBooleanValueAsync(key, defaultValue)); + } + + [Theory] + [AutoData] + public async Task ResolveBooleanValueAsync_WhenEnvironmentVariableMissing_ShouldReturnDefaultWithError(string prefix, + string flagKey, bool defaultValue) + { + // No matching environment value set + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, defaultValue, Reason.Error, ErrorType.FlagNotFound, + (provider, key, @default) => provider.ResolveBooleanValueAsync(key, @default)); + } + + [Theory] + [AutoData] + public async Task ResolveBooleanValueAsync_WhenEnvironmentVariableContainsInvalidValue_ShouldError( + string prefix, string flagKey, bool defaultValue) + { + var value = "xxxx"; // This value cannot be converted to a bool + Environment.SetEnvironmentVariable(prefix + flagKey, value); + + await ExecuteResolveErrorTest(prefix, flagKey, defaultValue, ErrorType.TypeMismatch, + (provider, key, @default) => provider.ResolveBooleanValueAsync(key, @default)); + + } + + [Theory] + [AutoData] + public async Task ResolveStringValueAsync_WhenEnvironmentVariablePresent_ShouldReturnValue(string prefix, + string flagKey, string value, string defaultValue) + { + Environment.SetEnvironmentVariable(prefix + flagKey, value); + + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, value, Reason.Static, + (provider, key, @default) => provider.ResolveStringValueAsync(key, defaultValue)); + } + + [Theory] + [AutoData] + public async Task ResolveStringValueAsync_WhenEnvironmentVariableMissing_ShouldReturnDefaultWithError(string prefix, + string flagKey, string defaultValue) + { + // No matching environment value set + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, defaultValue, Reason.Error, ErrorType.FlagNotFound, + (provider, key, @default) => provider.ResolveStringValueAsync(key, @default)); + } + + [Theory] + [AutoData] + public async Task ResolveIntegerValueAsync_WhenEnvironmentVariablePresent_ShouldReturnValue(string prefix, + string flagKey, int value, int defaultValue) + { + Environment.SetEnvironmentVariable(prefix + flagKey, value.ToString()); + + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, value, Reason.Static, + (provider, key, @default) => provider.ResolveIntegerValueAsync(key, @defaultValue)); + } + + [Theory] + [AutoData] + public async Task ResolveIntegerValueAsync_WhenEnvironmentVariableMissing_ShouldReturnDefaultWithError(string prefix, + string flagKey, int defaultValue) + { + // No matching environment value set + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, defaultValue, Reason.Error, ErrorType.FlagNotFound, + (provider, key, @default) => provider.ResolveIntegerValueAsync(key, @default)); + } + + [Theory] + [AutoData] + public async Task ResolveIntegerValueAsync_WhenEnvironmentVariableContainsInvalidValue_ShouldError( + string prefix, string flagKey, int defaultValue) + { + var value = "xxxx"; // This value cannot be converted to an int + Environment.SetEnvironmentVariable(prefix + flagKey, value); + + await ExecuteResolveErrorTest(prefix, flagKey, defaultValue, ErrorType.TypeMismatch, + (provider, key, @default) => provider.ResolveIntegerValueAsync(key, @default)); + + } + + [Theory] + [AutoData] + public async Task ResolveDoubleValueAsync_WhenEnvironmentVariablePresent_ShouldReturnValue(string prefix, + string flagKey, double value, double defaultValue) + { + Environment.SetEnvironmentVariable(prefix + flagKey, value.ToString()); + + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, value, Reason.Static, + (provider, key, @default) => provider.ResolveDoubleValueAsync(key, @defaultValue)); + } + + [Theory] + [AutoData] + public async Task ResolveDoubleValueAsync_WhenEnvironmentVariableMissing_ShouldReturnDefaultWithError(string prefix, + string flagKey, double defaultValue) + { + // No matching environment value set + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, defaultValue, Reason.Error, ErrorType.FlagNotFound, + (provider, key, @default) => provider.ResolveDoubleValueAsync(key, @default)); + } + + [Theory] + [AutoData] + public async Task ResolveDoubleValueAsync_WhenEnvironmentVariableContainsInvalidValue_ShouldError(string prefix, + string flagKey, double defaultValue) + { + var value = "xxxx"; // This value cannot be converted to a double + Environment.SetEnvironmentVariable(prefix + flagKey, value); + + await ExecuteResolveErrorTest(prefix, flagKey, defaultValue, ErrorType.TypeMismatch, + (provider, key, @default) => provider.ResolveDoubleValueAsync(key, @default)); + + } + + [Theory] + [AutoData] + public async Task ResolveStructureValueAsync_WhenEnvironmentVariablePresent_ShouldReturnValue(string prefix, + string flagKey, string value, string defaultValue) + { + Environment.SetEnvironmentVariable(prefix + flagKey, value); + + var provider = new EnvVarProvider(prefix); + var resolutionDetails = await provider.ResolveStructureValueAsync(flagKey, new Value(defaultValue)); + + Assert.Equal(value, resolutionDetails.Value.AsString); + Assert.Equal(Reason.Static, resolutionDetails.Reason); + Assert.Equal(ErrorType.None, resolutionDetails.ErrorType); + } + + [Theory] + [AutoData] + public async Task ResolveValueFromClient_WhenProviderConfigured_ShouldReturnValue(string prefix, string flagKey) + { + Environment.SetEnvironmentVariable(prefix + flagKey, true.ToString()); + + var provider = new EnvVarProvider(prefix); + await OpenFeature.Api.Instance.SetProviderAsync(provider); + var client = OpenFeature.Api.Instance.GetClient(); + + var receivedValue = await client.GetBooleanValueAsync(flagKey, false); + + Assert.True(receivedValue); + } + + private async Task ExecuteResolveValueTest(string prefix, string flagKey, T defaultValue, T expectedValue, + string expectedReason, Func>> resolve) + { + await ExecuteResolveValueTest(prefix, flagKey, defaultValue, expectedValue, expectedReason, ErrorType.None, resolve); + } + + private async Task ExecuteResolveValueTest(string prefix, string flagKey, T defaultValue, T expectedValue, + string expectedReason, ErrorType expectedErrorType, + Func>> resolve) + { + var provider = new EnvVarProvider(prefix); + var resolutionDetails = await resolve(provider, flagKey, defaultValue); + + Assert.Equal(expectedValue, resolutionDetails.Value); + Assert.Equal(expectedReason, resolutionDetails.Reason); + Assert.Equal(expectedErrorType, resolutionDetails.ErrorType); + } + + private async Task ExecuteResolveErrorTest(string prefix, string flagKey, T defaultValue, + ErrorType expectedErrorType, Func>> resolve) + { + var provider = new EnvVarProvider(prefix); + var exception = + await Assert.ThrowsAsync(() => resolve(provider, flagKey, defaultValue)); + + Assert.Equal(expectedErrorType, exception.ErrorType); + } + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.EnvVar.Test/OpenFeature.Contrib.Providers.EnvVar.Test.csproj b/test/OpenFeature.Contrib.Providers.EnvVar.Test/OpenFeature.Contrib.Providers.EnvVar.Test.csproj new file mode 100644 index 00000000..b5d8d9a0 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.EnvVar.Test/OpenFeature.Contrib.Providers.EnvVar.Test.csproj @@ -0,0 +1,8 @@ + + + + + + + + From 431ca453659d8c6fd6b1ef5d0cdf04ba8322e3ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 08:26:18 -0400 Subject: [PATCH 25/56] chore(main): release OpenFeature.Contrib.Providers.EnvVar 0.0.2 (#318) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md | 8 ++++++++ .../OpenFeature.Contrib.Providers.EnvVar.csproj | 2 +- src/OpenFeature.Contrib.Providers.EnvVar/version.txt | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 416b3586..1b3f81a7 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -7,5 +7,5 @@ "src/OpenFeature.Contrib.Providers.FeatureManagement": "0.1.0", "src/OpenFeature.Contrib.Providers.Statsig": "0.1.0", "src/OpenFeature.Contrib.Providers.Flipt": "0.0.5", - "src/OpenFeature.Contrib.Providers.EnvVar": "0.0.1" + "src/OpenFeature.Contrib.Providers.EnvVar": "0.0.2" } \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md new file mode 100644 index 00000000..a64575be --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.EnvVar/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [0.0.2](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.EnvVar-v0.0.1...OpenFeature.Contrib.Providers.EnvVar-v0.0.2) (2025-03-12) + + +### ✨ New Features + +* Environment Variable Provider ([#312](https://github.com/open-feature/dotnet-sdk-contrib/issues/312)) ([4908000](https://github.com/open-feature/dotnet-sdk-contrib/commit/4908000ed27a648ee7cf8823320ae7d7c8cd8c45)) diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj index 88360fa5..86f2754a 100644 --- a/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj +++ b/src/OpenFeature.Contrib.Providers.EnvVar/OpenFeature.Contrib.Providers.EnvVar.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.EnvVar - 0.0.1 + 0.0.2 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.EnvVar/version.txt b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt index 8acdd82b..4e379d2b 100644 --- a/src/OpenFeature.Contrib.Providers.EnvVar/version.txt +++ b/src/OpenFeature.Contrib.Providers.EnvVar/version.txt @@ -1 +1 @@ -0.0.1 +0.0.2 From a8ff0d294ea56d4ba585094fe2c6f9fa5eaa7f48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:45:35 +0100 Subject: [PATCH 26/56] chore(deps): update dyladan/component-owners digest to 58bd86e (#231) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/component-owners.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/component-owners.yml b/.github/workflows/component-owners.yml index befef350..359a7b81 100644 --- a/.github/workflows/component-owners.yml +++ b/.github/workflows/component-owners.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest name: Auto Assign Owners steps: - - uses: dyladan/component-owners@a0a1a67d6955b6efe190e9646e0ba536f882414a + - uses: dyladan/component-owners@58bd86e9814d23f1525d0a970682cead459fa783 with: config-file: .github.amrom.workers.devponent_owners.yml repo-token: ${{ secrets.GITHUB_TOKEN }} From 84acae2663677cf60c7e9691fb22fd250af6fd64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:20:48 +0100 Subject: [PATCH 27/56] chore(deps): update dependency grpc.tools to 2.71.0 (#286) --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 63d32704..f44b7d57 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -29,7 +29,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 6afad56da9be164b5a803a9c790a2586ad3a6b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:32:42 +0100 Subject: [PATCH 28/56] build: Cleanup sdk version (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 165 +++++++++--------- .github/workflows/release.yml | 8 +- build/Common.prod.props | 48 ++--- build/Common.props | 47 ++--- build/Common.tests.props | 94 +++++----- global.json | 11 +- ...OpenFeature.Contrib.Providers.Flipt.csproj | 19 +- 7 files changed, 195 insertions(+), 197 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae9c5e92..577b0c22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,48 +2,45 @@ name: CI on: push: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" pull_request: - branches: [ main ] + branches: [main] paths-ignore: - - '**.md' + - "**.md" jobs: build: strategy: matrix: - os: [ ubuntu-latest, windows-latest ] + os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --no-restore - - - name: Test - run: dotnet test --no-build --logger GitHubActions + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + global-json-file: global.json + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore + + - name: Test + run: dotnet test --no-build --logger GitHubActions e2e: runs-on: ubuntu-latest @@ -59,25 +56,22 @@ jobs: ports: - 9090:9090 steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json - - - name: Test - run: dotnet build && E2E=true dotnet test --logger GitHubActions + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + global-json-file: global.json + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Test + run: dotnet build && E2E=true dotnet test --logger GitHubActions packaging: needs: build @@ -89,41 +83,38 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - submodules: recursive - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 - env: - NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - source-url: https://nuget.pkg.github.com/open-feature/index.json - - - name: Restore - run: dotnet restore - - - name: Pack NuGet packages (CI versions) - if: startsWith(github.ref, 'refs/heads/') - run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - - - name: Pack NuGet packages (PR versions) - if: startsWith(github.ref, 'refs/pull/') - run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" - - - name: Publish NuGet packages (base) - if: github.event.pull_request.head.repo.fork == false - run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json - - - name: Publish NuGet packages (fork) - if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.6 - with: - name: nupkgs - path: src/**/*.nupkg + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + global-json-file: global.json + source-url: https://nuget.pkg.github.com/open-feature/index.json + + - name: Restore + run: dotnet restore + + - name: Pack NuGet packages (CI versions) + if: startsWith(github.ref, 'refs/heads/') + run: dotnet pack --no-restore --version-suffix "ci.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Pack NuGet packages (PR versions) + if: startsWith(github.ref, 'refs/pull/') + run: dotnet pack --no-restore --version-suffix "pr.$(date -u +%Y%m%dT%H%M%S)+sha.${GITHUB_SHA:0:9}" + + - name: Publish NuGet packages (base) + if: github.event.pull_request.head.repo.fork == false + run: dotnet nuget push "src/**/*.nupkg" --api-key "${{ secrets.GITHUB_TOKEN }}" --source https://nuget.pkg.github.com/open-feature/index.json + + - name: Publish NuGet packages (fork) + if: github.event.pull_request.head.repo.fork == true + uses: actions/upload-artifact@v4.3.6 + with: + name: nupkgs + path: src/**/*.nupkg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e107b87b..a65b2681 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: command: manifest token: ${{secrets.GITHUB_TOKEN}} default-branch: main + release-type: simple - uses: actions/checkout@v4 if: ${{ steps.release.outputs.releases_created }} @@ -28,10 +29,7 @@ jobs: env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x + global-json-file: global.json source-url: https://nuget.pkg.github.com/open-feature/index.json - name: Install dependencies @@ -45,7 +43,7 @@ jobs: - name: Pack if: ${{ steps.release.outputs.releases_created }} - run: | + run: | dotnet pack --configuration Release --no-build - name: Publish to Nuget diff --git a/build/Common.prod.props b/build/Common.prod.props index 6e4f1a30..cc1a257a 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -1,28 +1,30 @@ - + - - true - true - true - true - + + true + true + true + true + - - netstandard2.0;net462;net5.0;net6.0;net7.0;net8.0 - git - https://github.com/open-feature/dotnet-sdk-contrib - OpenFeature is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool or in-house solution. - Feature;OpenFeature;Flags; - openfeature-icon.png - https://openfeature.dev - Apache-2.0 - OpenFeature Authors - true - + + netstandard2.0;net462;net8.0 + git + https://github.com/open-feature/dotnet-sdk-contrib + OpenFeature is an open specification that provides a vendor-agnostic, + community-driven API for feature flagging that works with your favorite feature flag + management tool or in-house solution. + Feature;OpenFeature;Flags; + openfeature-icon.png + https://openfeature.dev + Apache-2.0 + OpenFeature Authors + true + - - - + + + - + \ No newline at end of file diff --git a/build/Common.props b/build/Common.props index 0bc9bed5..bdb03e49 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,33 +1,34 @@ - - - + + + - - 7.3 - true - + + latest + true + - - full - true - + + full + true + - - true - + + true + - - - - [2.0,3.0) - + + [2.0,3.0) + - - - - + + + + \ No newline at end of file diff --git a/build/Common.tests.props b/build/Common.tests.props index 9f94a8e4..88cb594b 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -1,55 +1,57 @@ - + - - false - net6.0 - $(TargetFrameworks);net462 - + + false + net8.0 + $(TargetFrameworks);net462 + - - true - + + true + - - - PreserveNewest - - + + + PreserveNewest + + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + - - - [4.17.0] - [3.1.2] - [6.7.0] - [2.3.3] - [17.3.2] - [5.0.0] - [2.4.3,3.0) - [2.4.1,3.0) - - + [4.17.0] + [3.1.2] + [6.7.0] + [2.3.3] + [17.3.2] + [5.0.0] + [2.4.3,3.0) + [2.4.1,3.0) + + \ No newline at end of file diff --git a/global.json b/global.json index a0d3f7be..80889bf5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,7 @@ { - "sdk": { - "rollForward": "latestFeature", - "version": "8.0.403" - } -} + "sdk": { + "rollForward": "latestFeature", + "version": "8.0.407", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj index 67d695d3..ab72f24d 100644 --- a/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj +++ b/src/OpenFeature.Contrib.Providers.Flipt/OpenFeature.Contrib.Providers.Flipt.csproj @@ -17,18 +17,20 @@ - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + @@ -36,6 +38,7 @@ - + - + \ No newline at end of file From 70a78f0ba613e7664fbc410c24aff119b7c2308a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Tue, 8 Apr 2025 07:49:02 +0100 Subject: [PATCH 29/56] chore: update Renovate configuration to extend community tooling (#321) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> Co-authored-by: Michael Beemer --- renovate.json | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/renovate.json b/renovate.json index 0d68c7be..e14ebdb5 100644 --- a/renovate.json +++ b/renovate.json @@ -1,10 +1,6 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:base" - ], - "semanticCommits": "enabled", - "labels": [ - "renovate" - ] -} + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "github>open-feature/community-tooling" + ] +} \ No newline at end of file From 99d3652a502afa3caf5a82c8d94f8d30e2a20ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:21:06 +0100 Subject: [PATCH 30/56] ci: Ensure conditional check for .NET SDK setup in release workflow (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a65b2681..5422c872 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,7 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 + if: ${{ steps.release.outputs.releases_created }} env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: From 294f34e6029c66ae1634dff910864e495fb3042c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:58:51 +0100 Subject: [PATCH 31/56] chore(deps): update actions/upload-artifact action to v4.6.2 (#274) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 577b0c22..a33c0f06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,7 +114,7 @@ jobs: - name: Publish NuGet packages (fork) if: github.event.pull_request.head.repo.fork == true - uses: actions/upload-artifact@v4.3.6 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: nupkgs path: src/**/*.nupkg From 69b9a33c08ca660c8f9ec3e18d25b9e6fb5ec0be Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:11:11 +0100 Subject: [PATCH 32/56] chore(deps): pin dependencies (#328) --- .github/workflows/ci.yml | 12 ++++++------ .github/workflows/dotnet-format.yml | 4 ++-- .github/workflows/lint-pr.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a33c0f06..8e95fc60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,13 +20,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -57,13 +57,13 @@ jobs: - 9090:9090 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -84,13 +84,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index fead00a7..992d368f 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -18,10 +18,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 with: global-json-file: global.json diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 6942f814..fb77800a 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -12,11 +12,11 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: marocchino/sticky-pull-request-comment@v2 + - uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 # When the previous steps fails, the workflow would stop. By adding this # condition you can continue the execution with the populated error message. if: always() && (steps.lint_pr_title.outputs.error_message != null) @@ -33,7 +33,7 @@ jobs: ``` # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 with: header: pr-title-lint-error delete: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5422c872..7a12d664 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: runs-on: windows-latest steps: - - uses: google-github-actions/release-please-action@v3 + - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 id: release with: command: manifest @@ -18,14 +18,14 @@ jobs: default-branch: main release-type: simple - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 if: ${{ steps.release.outputs.releases_created }} with: fetch-depth: 0 submodules: recursive - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 if: ${{ steps.release.outputs.releases_created }} env: NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 70056aa8f6b590bb4f91123f89812e57aea34f65 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:55:43 +0100 Subject: [PATCH 33/56] chore(deps): update dependency richardszalay.mockhttp to v7 (#279) --- .../OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj index 615b3abc..c29bafd5 100644 --- a/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test/OpenFeature.Contrib.Providers.GOFeatureFlag.Test.csproj @@ -6,7 +6,7 @@ - + From 58db25ee63a6a9ba72ba06032630197dea1bcbaf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:19:25 +0100 Subject: [PATCH 34/56] chore(deps): update dependency configcat.client to 9.3.2 (#289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Beemer Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.ConfigCat.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj index 67af26c1..81025918 100644 --- a/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj +++ b/src/OpenFeature.Contrib.Providers.ConfigCat/OpenFeature.Contrib.Providers.ConfigCat.csproj @@ -16,6 +16,6 @@ - + From 29553b252344057dc4eba7379b95acb085e9caa1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:44:48 +0100 Subject: [PATCH 35/56] chore(deps): update ghcr.io/open-feature/flagd-testbed docker tag to v0.5.21 (#291) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e95fc60..4274b3eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: services: # flagd-testbed for flagd RPC provider e2e tests flagd: - image: ghcr.io/open-feature/flagd-testbed:v0.5.6 + image: ghcr.io/open-feature/flagd-testbed:v0.5.21 ports: - 8013:8013 # sync-testbed for flagd in-process provider e2e tests diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml index f6b0c0a2..831feb79 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml @@ -1,6 +1,6 @@ services: flagd: - image: ghcr.io/open-feature/flagd-testbed:v0.5.6 + image: ghcr.io/open-feature/flagd-testbed:v0.5.21 ports: - 8013:8013 flagd-unstable: From 686cafc2c3b240736b61d0e32eec65b8449396c7 Mon Sep 17 00:00:00 2001 From: ericpattison Date: Wed, 9 Apr 2025 11:01:50 -0600 Subject: [PATCH 36/56] chore(deps): Upgrading to 4.0.0 of Microsoft.FeatureManagement (#303) --- ...Contrib.Providers.FeatureManagement.csproj | 2 +- .../appsettings.enabled.json | 402 ++++++------- .../appsettings.targeting.json | 542 ++++++++---------- 3 files changed, 423 insertions(+), 523 deletions(-) diff --git a/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj b/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj index 4f5c1578..3bdcb8ca 100644 --- a/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj +++ b/src/OpenFeature.Contrib.Providers.FeatureManagement/OpenFeature.Contrib.Providers.FeatureManagement.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json index 778394e6..d21b7915 100644 --- a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json +++ b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.enabled.json @@ -1,248 +1,210 @@ { - "FeatureManagement": { - "Flag_Boolean_AlwaysOn": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": true - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": false - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + "feature_management": { + "feature_flags": [ + { + "id": "Flag_Boolean_AlwaysOn", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": true + }, + { + "name": "FlagDisabled", + "configuration_value": false + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ] - }, - "Flag_Boolean_AlwaysOff": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": true - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": false + { + "id": "Flag_Boolean_AlwaysOff", + "enabled": false, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": true + }, + { + "name": "FlagDisabled", + "configuration_value": false + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [] - }, - - "Flag_Double_AlwaysOn": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1.0 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1.0 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + + { + "id": "Flag_Double_AlwaysOn", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1.0 + }, + { + "name": "FlagDisabled", + "configuration_value": -1.0 + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ] - }, - "Flag_Double_AlwaysOff": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1.0 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1.0 + { + "id": "Flag_Double_AlwaysOff", + "enabled": false, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1.0 + }, + { + "name": "FlagDisabled", + "configuration_value": -1.0 + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [] - }, - - "Flag_Integer_AlwaysOn": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + + { + "id": "Flag_Integer_AlwaysOn", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1 + }, + { + "name": "FlagDisabled", + "configuration_value": -1 + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ] - }, - "Flag_Integer_AlwaysOff": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1 + { + "id": "Flag_Integer_AlwaysOff", + "enabled": false, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1 + }, + { + "name": "FlagDisabled", + "configuration_value": -1 + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [] - }, - - "Flag_String_AlwaysOn": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": "FlagEnabled" - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": "FlagDisabled" - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + + { + "id": "Flag_String_AlwaysOn", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": "FlagEnabled" + }, + { + "name": "FlagDisabled", + "configuration_value": "FlagDisabled" + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ] - }, - "Flag_String_AlwaysOff": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": "FlagEnabled" - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": "FlagDisabled" + { + "id": "Flag_String_AlwaysOff", + "enabled": false, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": "FlagEnabled" + }, + { + "name": "FlagDisabled", + "configuration_value": "FlagDisabled" + } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [] - }, - - "Flag_Structure_AlwaysOn": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": { - "Field1": "Field1ValueOn", - "SubStructure": { - "Field2": 1 + + { + "id": "Flag_Structure_AlwaysOn", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": { + "Field1": "Field1ValueOn", + "SubStructure": { + "Field2": 1 + } } - } - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": { - "Field1": "Field1ValueOff", - "SubStructure": { - "Field2": -1 + }, + { + "name": "FlagDisabled", + "configuration_value": { + "Field1": "Field1ValueOff", + "SubStructure": { + "Field2": -1 + } } } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" - } - ] - }, - "Flag_Structure_AlwaysOff": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled" - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": { - "Field1": "Field1ValueOn", - "SubStructure": { - "Field2": 1 + { + "id": "Flag_Structure_AlwaysOff", + "enabled": false, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": { + "Field1": "Field1ValueOn", + "SubStructure": { + "Field2": 1 + } } - } - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": { - "Field1": "Field1ValueOff", - "SubStructure": { - "Field2": -1 + }, + { + "name": "FlagDisabled", + "configuration_value": { + "Field1": "Field1ValueOff", + "SubStructure": { + "Field2": -1 + } } } + ], + "allocation": { + "default_when_enabled": "FlagEnabled", + "default_when_disabled": "FlagDisabled" } - ], - "EnabledFor": [] - } + } + ] } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json index 2c75b500..7a4d3873 100644 --- a/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json +++ b/test/OpenFeature.Contrib.Providers.FeatureManagement.Test/appsettings.targeting.json @@ -1,348 +1,286 @@ { - "FeatureManagement": { - "Flag_Boolean_TargetingUserId": { - "Telemetry": { - "Enabled": true - }, - "Allocation": { - "DefaultWhenDisabled": "FlagDisabled", - "DefaultWhenEnabled": "FlagDisabled", - "User": [ + "feature_management": { + "feature_flags": [ + { + "id": "Flag_Boolean_TargetingUserId", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": true + }, { - "Variant": "FlagEnabled", - "Users": [ - "test.user@openfeature.dev" - ] + "name": "FlagDisabled", + "configuration_value": false } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": true - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": false - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "user": [ + { + "variant": "FlagEnabled", + "users": [ + "test.user@openfeature.dev" + ] + } + ] } - ] - }, - "Flag_Boolean_TargetingGroup": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "Group": [ + { + "id": "Flag_Boolean_TargetingGroup", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": true + }, { - "Variant": "FlagEnabled", - "Groups": [ - "test.group" - ] + "name": "FlagDisabled", + "configuration_value": false } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": true - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": false - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "group": [ + { + "variant": "FlagEnabled", + "groups": [ + "test.group" + ] + } + ] } - ] - }, - - "Flag_Double_TargetingUserId": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "User": [ + { + "id": "Flag_Double_TargetingUserId", + "enabled": true, + "variants": [ { - "Variant": "FlagEnabled", - "Users": [ - "test.user@openfeature.dev" - ] + "name": "FlagEnabled", + "configuration_value": 1.0 + }, + { + "name": "FlagDisabled", + "configuration_value": -1.0 } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1.0 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1.0 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "user": [ + { + "variant": "FlagEnabled", + "users": [ + "test.user@openfeature.dev" + ] + } + ] } - ] - }, - "Flag_Double_TargetingGroup": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "Group": [ + { + "id": "Flag_Double_TargetingGroup", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1.0 + }, { - "Variant": "FlagEnabled", - "Groups": [ - "test.group" - ] + "name": "FlagDisabled", + "configuration_value": -1.0 } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1.0 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1.0 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "group": [ + { + "variant": "FlagEnabled", + "groups": [ + "test.group" + ] + } + ] } - ] - }, - - "Flag_Integer_TargetingUserId": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "User": [ + { + "id": "Flag_Integer_TargetingUserId", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": 1 + }, { - "Variant": "FlagEnabled", - "Users": [ - "test.user@openfeature.dev" - ] + "name": "FlagDisabled", + "configuration_value": -1 } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "user": [ + { + "variant": "FlagEnabled", + "users": [ + "test.user@openfeature.dev" + ] + } + ] } - ] - }, - "Flag_Integer_TargetingGroup": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "Group": [ + { + "id": "Flag_Integer_TargetingGroup", + "enabled": true, + "variants": [ { - "Variant": "FlagEnabled", - "Groups": [ - "test.group" - ] + "name": "FlagEnabled", + "configuration_value": 1 + }, + { + "name": "FlagDisabled", + "configuration_value": -1 } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": 1 - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": -1 - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "group": [ + { + "variant": "FlagEnabled", + "groups": [ + "test.group" + ] + } + ] } - ] - }, - - "Flag_String_TargetingUserId": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "User": [ + { + "id": "Flag_String_TargetingUserId", + "enabled": true, + "variants": [ + { + "name": "FlagEnabled", + "configuration_value": "FlagEnabled" + }, { - "Variant": "FlagEnabled", - "Users": [ - "test.user@openfeature.dev" - ] + "name": "FlagDisabled", + "configuration_value": "FlagDisabled" } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": "FlagEnabled" - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": "FlagDisabled" - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "user": [ + { + "variant": "FlagEnabled", + "users": [ + "test.user@openfeature.dev" + ] + } + ] } - ] - }, - "Flag_String_TargetingGroup": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "Group": [ + { + "id": "Flag_String_TargetingGroup", + "enabled": true, + "variants": [ { - "Variant": "FlagEnabled", - "Groups": [ - "test.group" - ] + "name": "FlagEnabled", + "configuration_value": "FlagEnabled" + }, + { + "name": "FlagDisabled", + "configuration_value": "FlagDisabled" } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": "FlagEnabled" - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": "FlagDisabled" - } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "group": [ + { + "variant": "FlagEnabled", + "groups": [ + "test.group" + ] + } + ] } - ] - }, - - "Flag_Structure_TargetingUserId": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagEnabled", - "DefaultWhenDisabled": "FlagDisabled", - "User": [ + { + "id": "Flag_Structure_TargetingUserId", + "enabled": true, + "variants": [ { - "Variant": "FlagEnabled", - "Users": [ - "test.user@openfeature.dev" - ] - } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": { - "Field1": "Field1ValueOn", - "SubStructure": { - "Field2": 1 + "name": "FlagEnabled", + "configuration_value": { + "Field1": "Field1ValueOn", + "SubStructure": { + "Field2": 1 + } } - } - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": { - "Field1": "Field1ValueOff", - "SubStructure": { - "Field2": -1 + }, + { + "name": "FlagDisabled", + "configuration_value": { + "Field1": "Field1ValueOff", + "SubStructure": { + "Field2": -1 + } } } + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "user": [ + { + "variant": "FlagEnabled", + "users": [ + "test.user@openfeature.dev" + ] + } + ] } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" - } - ] - }, - "Flag_Structure_TargetingGroup": { - "Telemetry": { - "Enabled": true }, - "Allocation": { - "DefaultWhenEnabled": "FlagDisabled", - "DefaultWhenDisabled": "FlagDisabled", - "Group": [ + { + "id": "Flag_Structure_TargetingGroup", + "enabled": true, + "variants": [ { - "Variant": "FlagEnabled", - "Groups": [ - "test.group" - ] - } - ] - }, - "Variants": [ - { - "Name": "FlagEnabled", - "ConfigurationValue": { - "Field1": "Field1ValueOn", - "SubStructure": { - "Field2": 1 + "name": "FlagEnabled", + "configuration_value": { + "Field1": "Field1ValueOn", + "SubStructure": { + "Field2": 1 + } } - } - }, - { - "Name": "FlagDisabled", - "ConfigurationValue": { - "Field1": "Field1ValueOff", - "SubStructure": { - "Field2": -1 + }, + { + "name": "FlagDisabled", + "configuration_value": { + "Field1": "Field1ValueOff", + "SubStructure": { + "Field2": -1 + } } } + ], + "allocation": { + "default_when_disabled": "FlagDisabled", + "default_when_enabled": "FlagDisabled", + "group": [ + { + "variant": "FlagEnabled", + "groups": [ + "test.group" + ] + } + ] } - ], - "EnabledFor": [ - { - "Name": "AlwaysOn" - } - ] - } + } + ] } } \ No newline at end of file From d66adad35c30e4d6920315975fa0a897a513ae93 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:12:45 +0100 Subject: [PATCH 37/56] chore(deps): update dependency dotnet-sdk to v8.0.408 (#317) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 80889bf5..7b4ac0a9 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "rollForward": "latestFeature", - "version": "8.0.407", + "version": "8.0.408", "allowPrerelease": false } } \ No newline at end of file From faa44cc6db5b014069f3dd72b1bf34e3e5ada1df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:58:18 +0100 Subject: [PATCH 38/56] chore(deps): update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v0.5.21 (#323) --- src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml index 831feb79..dd75f1be 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml @@ -4,7 +4,7 @@ services: ports: - 8013:8013 flagd-unstable: - image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.6 + image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.21 ports: - 8014:8013 flagd-sync: From 1f7214d28f04e504fdf8f3dac7fa14ff613fa677 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:09:46 +0100 Subject: [PATCH 39/56] chore(deps): update src/openfeature.contrib.providers.flagd/schemas digest to 9b0ee43 (#332) --- src/OpenFeature.Contrib.Providers.Flagd/schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/schemas b/src/OpenFeature.Contrib.Providers.Flagd/schemas index 4b44704e..9b0ee43e 160000 --- a/src/OpenFeature.Contrib.Providers.Flagd/schemas +++ b/src/OpenFeature.Contrib.Providers.Flagd/schemas @@ -1 +1 @@ -Subproject commit 4b44704e447468fd74d8e95830627ce23b6ef16f +Subproject commit 9b0ee43ecc477e277d41770034fa495ec78838fe From 6cbf6563e7f5b2f4c4b8e0b557f978cfe12f79c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:17:57 +0100 Subject: [PATCH 40/56] chore(deps): update ghcr.io/open-feature/sync-testbed-unstable docker tag to v0.5.13 (#333) --- src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml index dd75f1be..6e4b2c57 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml @@ -12,6 +12,6 @@ services: ports: - 9090:9090 flagd-sync-unstable: - image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.6 + image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.13 ports: - 9091:9090 \ No newline at end of file From 3f63d35540979dfb42e1f9d80ba5d2bba0b4a509 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:25:40 +0100 Subject: [PATCH 41/56] chore(deps): update dependency google.protobuf to 3.30.2 (#335) --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index f44b7d57..b98db3a4 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -27,7 +27,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From cd4cd4f29bedebcca0a11085307bed72e6e7b794 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:33:03 +0100 Subject: [PATCH 42/56] chore(deps): update dependency grpc.net.client to 2.70.0 (#336) --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index b98db3a4..25347777 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -28,7 +28,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 308cc42afce6196ff4c4ffc89350454a44f1d1e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:58:34 +0100 Subject: [PATCH 43/56] chore(deps): update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v1 (#339) --- src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml index 6e4b2c57..061d89fa 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml @@ -4,7 +4,7 @@ services: ports: - 8013:8013 flagd-unstable: - image: ghcr.io/open-feature/flagd-testbed-unstable:v0.5.21 + image: ghcr.io/open-feature/flagd-testbed-unstable:v1.1.1 ports: - 8014:8013 flagd-sync: From bdd225e5973f182cf27c2d86cbaaeb9c50f3812c Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Tue, 15 Apr 2025 19:01:17 +0100 Subject: [PATCH 44/56] test: Remove FluentAssertions (#342) --- build/Common.tests.props | 2 - ...tensionsTest.cs => FliptExtensionsTest.cs} | 77 ++++++++++--------- .../FliptProviderTest.cs | 9 +-- .../FliptToOpenFeatureConverterTest.cs | 52 ++++++------- ...eature.Contrib.Providers.Flipt.Test.csproj | 1 - 5 files changed, 63 insertions(+), 78 deletions(-) rename test/OpenFeature.Contrib.Providers.Flipt.Test/{FlipExtensionsTest.cs => FliptExtensionsTest.cs} (68%) diff --git a/build/Common.tests.props b/build/Common.tests.props index 88cb594b..0344fc28 100644 --- a/build/Common.tests.props +++ b/build/Common.tests.props @@ -28,7 +28,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -47,7 +46,6 @@ --> [4.17.0] [3.1.2] - [6.7.0] [2.3.3] [17.3.2] [5.0.0] diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs similarity index 68% rename from test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs rename to test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs index 21e9ec1d..bb3e0ea9 100644 --- a/test/OpenFeature.Contrib.Providers.Flipt.Test/FlipExtensionsTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptExtensionsTest.cs @@ -1,12 +1,11 @@ -using System.Text.Json; -using FluentAssertions; using OpenFeature.Contrib.Providers.Flipt.Converters; using OpenFeature.Model; +using System.Text.Json; using Xunit; namespace OpenFeature.Contrib.Providers.Flipt.Test; -public class FlipExtensionsTest +public class FliptExtensionsTest { [Fact] public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary() @@ -14,8 +13,8 @@ public void ToStringDictionary_WithEmptyContext_ShouldReturnEmptyDictionary() var evaluationContext = EvaluationContext.Builder().Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().BeEmpty(); + Assert.NotNull(result); + Assert.Empty(result); } [Fact] @@ -27,9 +26,9 @@ public void ToStringDictionary_WithContext_ShouldReturnADictionaryWithValues() .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("location"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("location", result.Keys); } [Fact] @@ -41,10 +40,12 @@ public void ToStringDictionary_WithContextAndIntegerValue_ShouldReturnADictionar .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("age"); - result["age"].Should().Be("23"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("age", result.Keys); + + var actual = result["age"]; + Assert.Equal("23", actual); } [Fact] @@ -62,14 +63,13 @@ public void ToStringDictionary_WithContextAndValuesOfStrings_ShouldReturnADictio .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("config"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("config", result.Keys); - JsonSerializer - .Deserialize(result["config"], - JsonConverterExtensions.DefaultSerializerSettings).Should() - .BeEquivalentTo(testStructure); + var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings); + var actual = result["config"]; + Assert.Equal(expected, actual); } [Fact] @@ -88,13 +88,13 @@ public void ToStringDictionary_WithContextAndMixedValueTypes_ShouldReturnADictio .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("config"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("config", result.Keys); - var deserialized = JsonSerializer.Deserialize(result["config"], - JsonConverterExtensions.DefaultSerializerSettings); - deserialized.Should().BeEquivalentTo(testStructure); + var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings); + var actual = result["config"]; + Assert.Equal(expected, actual); } [Fact] @@ -102,7 +102,8 @@ public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADic { var sampleDictionary = new Dictionary(); sampleDictionary["config2"] = new Value([ - new Value([new Value("element1-1"), new Value("element1-2")]), new Value("element2"), + new Value([new Value("element1-1"), new Value("element1-2")]), + new Value("element2"), new Value("element3") ]); sampleDictionary["config3"] = new Value(DateTime.Now); @@ -115,13 +116,13 @@ public void ToStringDictionary_WithContextWithListAndNestedList_ShouldReturnADic .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("config"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("config", result.Keys); - var deserialized = JsonSerializer.Deserialize(result["config"], - JsonConverterExtensions.DefaultSerializerSettings); - deserialized.Should().BeEquivalentTo(testStructure); + var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings); + var actual = result["config"]; + Assert.Equal(expected, actual); } [Fact] @@ -144,12 +145,12 @@ public void ToStringDictionary_WithContextWithNestedStructure_ShouldReturnADicti .Build(); var result = evaluationContext.ToStringDictionary(); - result.Should().NotBeNull(); - result.Should().NotBeEmpty(); - result.Keys.Should().Contain("config"); + Assert.NotNull(result); + Assert.NotEmpty(result); + Assert.Contains("config", result.Keys); - var deserialized = JsonSerializer.Deserialize(result["config"], - JsonConverterExtensions.DefaultSerializerSettings); - deserialized.Should().BeEquivalentTo(testStructure); + var expected = JsonSerializer.Serialize(testStructure, JsonConverterExtensions.DefaultSerializerSettings); + var actual = result["config"]; + Assert.Equal(expected, actual); } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs index 07bfd62c..412a6294 100644 --- a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptProviderTest.cs @@ -1,5 +1,4 @@ using Flipt.Rest; -using FluentAssertions; using Moq; using OpenFeature.Contrib.Providers.Flipt.ClientWrapper; using OpenFeature.Error; @@ -23,11 +22,9 @@ public void CreateFliptProvider_ShouldReturnFliptProvider() [Fact] public void CreateFliptProvider_GivenEmptyUrl_ShouldThrowInvalidOperationException() { - var act = void() => new FliptProvider(""); - act.Should().Throw(); + Assert.Throws(() => new FliptProvider("")); } - [Fact] public async Task ResolveNonBooleansAsync_GivenFlagThatHasATypeMismatch_ShouldReturnDefaultValueWithTypeMismatchError() @@ -49,9 +46,7 @@ public async Task var provider = new FliptProvider(new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object)); - var resolution = async Task>() => - await provider.ResolveDoubleValueAsync(flagKey, 0.0); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await provider.ResolveDoubleValueAsync(flagKey, 0.0)); } [Fact] diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs index e57e4d18..4d6026d9 100644 --- a/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/FliptToOpenFeatureConverterTest.cs @@ -1,13 +1,14 @@ // ReSharper disable RedundantUsingDirective -using System.Net; -using System.Net.Http; using Flipt.Rest; -using FluentAssertions; using Moq; using OpenFeature.Constant; using OpenFeature.Contrib.Providers.Flipt.ClientWrapper; +using OpenFeature.Contrib.Providers.Flipt.Converters; using OpenFeature.Model; +using System.Net; +using System.Net.Http; +using System.Text.Json; using Xunit; namespace OpenFeature.Contrib.Providers.Flipt.Test; @@ -30,10 +31,8 @@ public async Task EvaluateBooleanAsync_GivenHttpRequestException_ShouldHandleHtt .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null)); var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); - var resolution = async Task>() => - await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallbackValue); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateBooleanAsync("flagKey", fallbackValue)); } [Theory] @@ -54,9 +53,9 @@ public async Task EvaluateBooleanAsync_GivenExistingFlag_ShouldReturnFlagValue(s var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); var resolution = await fliptToOpenFeature.EvaluateBooleanAsync("show-feature", false); - resolution.FlagKey.Should().Be(flagKey); - resolution.Value.Should().Be(valueFromSrc); - resolution.Reason.Should().Be(Reason.TargetingMatch); + Assert.Equal(flagKey, resolution.FlagKey); + Assert.Equal(valueFromSrc, resolution.Value); + Assert.Equal(Reason.TargetingMatch, resolution.Reason); } [Theory] @@ -70,10 +69,8 @@ public async Task EvaluateBooleanAsync_GivenNonExistentFlag_ShouldReturnDefaultV .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); - var resolution = async Task>() => - await fliptToOpenFeature.EvaluateBooleanAsync(flagKey, fallBackValue); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateBooleanAsync(flagKey, fallBackValue)); } // EvaluateAsync Tests @@ -93,10 +90,8 @@ public async Task EvaluateAsync_GivenHttpRequestException_ShouldHandleHttpReques .ThrowsAsync(new FliptRestException("", (int)thrownStatusCode, "", null, null)); var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); - var resolution = async Task>() => - await fliptToOpenFeature.EvaluateAsync("flagKey", fallbackValue); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("flagKey", fallbackValue)); } [Theory] @@ -122,10 +117,10 @@ public async Task EvaluateAsync_GivenExistingVariantFlagWhichIsNotAnObject_Shoul var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, valueFromSrc); - resolution.FlagKey.Should().Be(flagKey); - resolution.Variant.Should().Be(valueFromSrc.ToString() ?? string.Empty); - resolution.Value.Should().BeEquivalentTo(expectedValue?.ToString()); - resolution.Reason.Should().Be(Reason.TargetingMatch); + Assert.Equal(flagKey, resolution.FlagKey); + Assert.Equal(valueFromSrc.ToString() ?? string.Empty, resolution.Value); + Assert.Equal(expectedValue?.ToString(), resolution.Value); + Assert.Equal(Reason.TargetingMatch, resolution.Reason); } [Fact] @@ -160,11 +155,13 @@ public async Task EvaluateAsync_GivenExistingVariantFlagAndWithAnObject_ShouldRe var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); var resolution = await fliptToOpenFeature.EvaluateAsync(flagKey, new Value()); - resolution.FlagKey.Should().Be(flagKey); - resolution.Variant.Should().Be(variantKey); - resolution.Value.Should().BeEquivalentTo(expectedValue); - } + Assert.Equal(flagKey, resolution.FlagKey); + Assert.Equal(variantKey, resolution.Variant); + var expected = JsonSerializer.Serialize(expectedValue, JsonConverterExtensions.DefaultSerializerSettings); + var actual = JsonSerializer.Serialize(resolution.Value, JsonConverterExtensions.DefaultSerializerSettings); + Assert.Equal(expected, actual); + } [Fact] public async Task @@ -179,13 +176,10 @@ public async Task .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); - var resolution = async Task>() => - await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue)); } - [Fact] public async Task EvaluateVariantAsync_GivenNonExistentFlagWithNestedFallback_ShouldReturnDefaultValueWithFlagNotFoundError() @@ -196,9 +190,7 @@ public async Task .ThrowsAsync(new FliptRestException("", (int)HttpStatusCode.NotFound, "", null, null)); var fliptToOpenFeature = new FliptToOpenFeatureConverter(mockFliptClientWrapper.Object); - var resolution = async Task>() => - await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue); - await resolution.Should().ThrowAsync(); + await Assert.ThrowsAsync(async () => await fliptToOpenFeature.EvaluateAsync("non-existent-flag", fallbackValue)); } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj index 2ea46b29..2ab09f7a 100644 --- a/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj +++ b/test/OpenFeature.Contrib.Providers.Flipt.Test/OpenFeature.Contrib.Providers.Flipt.Test.csproj @@ -15,7 +15,6 @@ - From 0260158fcbf9e02b44285fcde689800d0428bae2 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Wed, 16 Apr 2025 12:01:33 -0400 Subject: [PATCH 45/56] chore: use publish environment Signed-off-by: Todd Baert --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a12d664..d8b1b132 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: jobs: release-package: + environment: publish runs-on: windows-latest steps: From 5d142fd798da9b668d8b45f5f6310a03b1424c36 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 12:27:59 +0100 Subject: [PATCH 46/56] chore(deps): update src/openfeature.contrib.providers.flagd/schemas digest to c707f56 (#343) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/OpenFeature.Contrib.Providers.Flagd/schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/schemas b/src/OpenFeature.Contrib.Providers.Flagd/schemas index 9b0ee43e..c707f563 160000 --- a/src/OpenFeature.Contrib.Providers.Flagd/schemas +++ b/src/OpenFeature.Contrib.Providers.Flagd/schemas @@ -1 +1 @@ -Subproject commit 9b0ee43ecc477e277d41770034fa495ec78838fe +Subproject commit c707f563d0a1b35ebea802568c6d3151633bde31 From 1173f4f1c0a06f191d4aa6b0353ac54f81889ec6 Mon Sep 17 00:00:00 2001 From: Kyle <38759683+kylejuliandev@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:10:58 +0100 Subject: [PATCH 47/56] chore: Use TestContainers instead of github services / docker for e2e tests (#345) Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> Signed-off-by: Todd Baert Co-authored-by: Todd Baert --- .github/workflows/ci.yml | 11 ------- .../docker-compose.yaml | 17 ----------- .../FlagdSyncTestBedContainer.cs | 18 ++++++++++++ ...rib.Providers.Flagd.E2e.ProcessTest.csproj | 1 + .../Steps/EvaluationStepDefinitionsProcess.cs | 17 +++++++++-- .../Steps/FlagdStepDefinitionsProcess.cs | 17 +++++++++-- .../Steps/TestHooks.cs | 29 +++++++++++++++++++ .../FlagdRpcTestBedContainer.cs | 18 ++++++++++++ ...Contrib.Providers.Flagd.E2e.RpcTest.csproj | 1 + .../Steps/EvaluationStepDefinitionsRpc.cs | 17 ++++++++--- .../Steps/FlagdStepDefinitionsRpc.cs | 18 ++++++++---- .../Steps/TestHooks.cs | 29 +++++++++++++++++++ 12 files changed, 150 insertions(+), 43 deletions(-) delete mode 100644 src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml create mode 100644 test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs create mode 100644 test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4274b3eb..55ee00b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,17 +44,6 @@ jobs: e2e: runs-on: ubuntu-latest - services: - # flagd-testbed for flagd RPC provider e2e tests - flagd: - image: ghcr.io/open-feature/flagd-testbed:v0.5.21 - ports: - - 8013:8013 - # sync-testbed for flagd in-process provider e2e tests - sync: - image: ghcr.io/open-feature/sync-testbed:v0.5.6 - ports: - - 9090:9090 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml b/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml deleted file mode 100644 index 061d89fa..00000000 --- a/src/OpenFeature.Contrib.Providers.Flagd/docker-compose.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - flagd: - image: ghcr.io/open-feature/flagd-testbed:v0.5.21 - ports: - - 8013:8013 - flagd-unstable: - image: ghcr.io/open-feature/flagd-testbed-unstable:v1.1.1 - ports: - - 8014:8013 - flagd-sync: - image: ghcr.io/open-feature/sync-testbed:v0.5.6 - ports: - - 9090:9090 - flagd-sync-unstable: - image: ghcr.io/open-feature/sync-testbed-unstable:v0.5.13 - ports: - - 9091:9090 \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs new file mode 100644 index 00000000..d5a7be03 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/FlagdSyncTestBedContainer.cs @@ -0,0 +1,18 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest +{ + public class FlagdSyncTestBedContainer + { + public IContainer Container { get; } + + public FlagdSyncTestBedContainer() + { + Container = new ContainerBuilder() + .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21") + .WithPortBinding(8015, true) + .Build(); + } + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj index a7d4c7f6..cc278ba0 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj @@ -27,6 +27,7 @@ + diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs index a9ea5f53..6863aef4 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/EvaluationStepDefinitionsProcess.cs @@ -1,16 +1,27 @@ -using System; +using OpenFeature.Contrib.Providers.Flagd.E2e.Test; using TechTalk.SpecFlow; -namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process +namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps { [Binding, Scope(Feature = "Flag evaluation")] public class EvaluationStepDefinitionsProcess : EvaluationStepDefinitionsBase { static EvaluationStepDefinitionsProcess() { - var flagdProvider = new FlagdProvider(FlagdConfig.Builder().WithPort(9090).WithResolverType(ResolverType.IN_PROCESS).Build()); + var host = TestHooks.FlagdSyncTestBed.Container.Hostname; + var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015); + + var flagdProvider = new FlagdProvider( + FlagdConfig.Builder() + .WithHost(host) + .WithPort(port) + .WithResolverType(ResolverType.IN_PROCESS) + .Build() + ); + Api.Instance.SetProviderAsync("process-test-evaluation", flagdProvider).Wait(5000); } + public EvaluationStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext) { client = Api.Instance.GetClient("process-test-evaluation"); diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs index 0392c43c..7dc81276 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/FlagdStepDefinitionsProcess.cs @@ -1,7 +1,7 @@ -using System; +using OpenFeature.Contrib.Providers.Flagd.E2e.Test; using TechTalk.SpecFlow; -namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process +namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps { [Binding] [Scope(Feature = "flagd providers")] @@ -10,9 +10,20 @@ public class FlagdStepDefinitionsProcess : FlagdStepDefinitionsBase { static FlagdStepDefinitionsProcess() { - var flagdProvider = new FlagdProvider(FlagdConfig.Builder().WithPort(9090).WithResolverType(ResolverType.IN_PROCESS).Build()); + var host = TestHooks.FlagdSyncTestBed.Container.Hostname; + var port = TestHooks.FlagdSyncTestBed.Container.GetMappedPublicPort(8015); + + var flagdProvider = new FlagdProvider( + FlagdConfig.Builder() + .WithHost(host) + .WithPort(port) + .WithResolverType(ResolverType.IN_PROCESS) + .Build() + ); + Api.Instance.SetProviderAsync("process-test-flagd", flagdProvider).Wait(5000); } + public FlagdStepDefinitionsProcess(ScenarioContext scenarioContext) : base(scenarioContext) { client = Api.Instance.GetClient("process-test-flagd"); diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs new file mode 100644 index 00000000..ac7b7503 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/Steps/TestHooks.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using TechTalk.SpecFlow; + +namespace OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.Steps +{ + [Binding] + public class TestHooks + { + public static FlagdSyncTestBedContainer FlagdSyncTestBed { get; private set; } + + [BeforeTestRun] + public static async Task StartContainerAsync() + { + FlagdSyncTestBed = new FlagdSyncTestBedContainer(); + + await FlagdSyncTestBed.Container.StartAsync(); + } + + [AfterTestRun] + public static async Task StopContainerAsync() + { + if (FlagdSyncTestBed != null) + { + await FlagdSyncTestBed.Container.StopAsync(); + await FlagdSyncTestBed.Container.DisposeAsync(); + } + } + } +} diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs new file mode 100644 index 00000000..7f3e3565 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/FlagdRpcTestBedContainer.cs @@ -0,0 +1,18 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; + +namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest +{ + public class FlagdRpcTestBedContainer + { + public IContainer Container { get; } + + public FlagdRpcTestBedContainer() + { + Container = new ContainerBuilder() + .WithImage("ghcr.io/open-feature/flagd-testbed:v0.5.21") + .WithPortBinding(8013, true) + .Build(); + } + } +} \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj index a7d4c7f6..cc278ba0 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj @@ -27,6 +27,7 @@ + diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs index e36e96b0..0e7e5e62 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/EvaluationStepDefinitionsRpc.cs @@ -1,17 +1,26 @@ - +using OpenFeature.Contrib.Providers.Flagd.E2e.Test; using TechTalk.SpecFlow; - -namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process +namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps { [Binding, Scope(Feature = "Flag evaluation")] public class EvaluationStepDefinitionsRpc : EvaluationStepDefinitionsBase { static EvaluationStepDefinitionsRpc() { - var flagdProvider = new FlagdProvider(); + var host = TestHooks.FlagdTestBed.Container.Hostname; + var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013); + + var flagdProvider = new FlagdProvider( + FlagdConfig.Builder() + .WithHost(host) + .WithPort(port) + .Build() + ); + Api.Instance.SetProviderAsync("rpc-test-evaluation", flagdProvider).Wait(5000); } + public EvaluationStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext) { client = Api.Instance.GetClient("rpc-test-evaluation"); diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs index a1cf9b68..831221ce 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/FlagdStepDefinitionsRpc.cs @@ -1,20 +1,28 @@ - +using OpenFeature.Contrib.Providers.Flagd.E2e.Test; using TechTalk.SpecFlow; - -namespace OpenFeature.Contrib.Providers.Flagd.E2e.Test.Process +namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps { [Binding] [Scope(Feature = "flagd providers")] [Scope(Feature = "flagd json evaluation")] public class FlagdStepDefinitionsRpc : FlagdStepDefinitionsBase { - static FlagdStepDefinitionsRpc() { - var flagdProvider = new FlagdProvider(); + var host = TestHooks.FlagdTestBed.Container.Hostname; + var port = TestHooks.FlagdTestBed.Container.GetMappedPublicPort(8013); + + var flagdProvider = new FlagdProvider( + FlagdConfig.Builder() + .WithHost(host) + .WithPort(port) + .Build() + ); + Api.Instance.SetProviderAsync("rpc-test-flagd", flagdProvider).Wait(5000); } + public FlagdStepDefinitionsRpc(ScenarioContext scenarioContext) : base(scenarioContext) { client = Api.Instance.GetClient("rpc-test-flagd"); diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs new file mode 100644 index 00000000..ed1a3a99 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/Steps/TestHooks.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using TechTalk.SpecFlow; + +namespace OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.Steps +{ + [Binding] + public class TestHooks + { + public static FlagdRpcTestBedContainer FlagdTestBed { get; private set; } + + [BeforeTestRun] + public static async Task StartContainerAsync() + { + FlagdTestBed = new FlagdRpcTestBedContainer(); + + await FlagdTestBed.Container.StartAsync(); + } + + [AfterTestRun] + public static async Task StopContainerAsync() + { + if (FlagdTestBed != null) + { + await FlagdTestBed.Container.StopAsync(); + await FlagdTestBed.Container.DisposeAsync(); + } + } + } +} From 9783b3ea5c636251578d663bd9bb4081e7205f38 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:47:09 +0100 Subject: [PATCH 48/56] chore(deps): update dependency testcontainers to 4.4.0 (#352) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj | 2 +- .../OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj index cc278ba0..67d45846 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest/OpenFeature.Contrib.Providers.Flagd.E2e.ProcessTest.csproj @@ -27,7 +27,7 @@ - + diff --git a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj index cc278ba0..67d45846 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj +++ b/test/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest/OpenFeature.Contrib.Providers.Flagd.E2e.RpcTest.csproj @@ -27,7 +27,7 @@ - + From bd4991596949e65cf1b7da72b7162b910a85bd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:17:37 +0100 Subject: [PATCH 49/56] ci: Add CodeQL analysis workflow configuration (#357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 100 ++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..bf41d7e2 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,100 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + schedule: + - cron: "15 3 * * 5" + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: "ubuntu-latest" + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: csharp + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From f186c705ac9081fb1cd15e43894111f7bab3391e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:22:42 +0100 Subject: [PATCH 50/56] ci: Change dotnet formatter (#348) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/dotnet-format.yml | 36 +++++++++---------- .../FlagdProvider.cs | 2 +- .../CustomEvaluators/StringEvaluator.cs | 4 +-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/.github/workflows/dotnet-format.yml b/.github/workflows/dotnet-format.yml index 992d368f..2557a95a 100644 --- a/.github/workflows/dotnet-format.yml +++ b/.github/workflows/dotnet-format.yml @@ -2,31 +2,29 @@ name: dotnet format on: push: - branches: [ main ] - paths: - - '**.cs' - - '.editorconfig' + branches: [main] pull_request: - branches: [ main ] - paths: - - '**.cs' - - '.editorconfig' + branches: [main] jobs: check-format: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + packages: read steps: - - name: Check out code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Check out code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Setup .NET - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 - with: - global-json-file: global.json + - name: Setup .NET + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + env: + NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + global-json-file: global.json + source-url: https://nuget.pkg.github.com/open-feature/index.json - - name: Install format tool - run: dotnet tool install -g dotnet-format - - - name: dotnet format - run: dotnet-format --folder --check + - name: dotnet format + run: dotnet format --verify-no-changes DotnetSdkContrib.sln diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs index 2b12223c..40a966f9 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs @@ -116,7 +116,7 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke if (t.IsFaulted) { throw t.Exception; - }; + } }); } diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs index f6119378..bbda204b 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs @@ -18,7 +18,7 @@ internal object StartsWith(IProcessJsonLogic p, JToken[] args, object data) if (!isValid(p, args, data, out string operandA, out string operandB)) { return false; - }; + } return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB)); } @@ -27,7 +27,7 @@ internal object EndsWith(IProcessJsonLogic p, JToken[] args, object data) if (!isValid(p, args, data, out string operandA, out string operandB)) { return false; - }; + } return operandA.EndsWith(operandB); } From 88b649a69b5cad57ab3905cc621824db4c13fb9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:37:39 +0100 Subject: [PATCH 51/56] chore(deps): pin dependencies (#358) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bf41d7e2..8af59f30 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -57,7 +57,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` @@ -67,7 +67,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -95,6 +95,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3 with: category: "/language:${{matrix.language}}" From 26e4ca68d7e2a7cce0e0728be3f85c4a2941219d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Mon, 21 Apr 2025 21:44:33 +0100 Subject: [PATCH 52/56] ci: Fix CI permissions (#359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +++++++++ .github/workflows/lint-pr.yml | 14 +++++++++----- .github/workflows/release.yml | 3 +++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55ee00b1..fdfae1f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,11 @@ jobs: runs-on: ${{ matrix.os }} + permissions: + contents: read + pull-requests: write + packages: read + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -44,6 +49,10 @@ jobs: e2e: runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + packages: read steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index fb77800a..e761bff6 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -1,4 +1,4 @@ -name: 'Lint PR' +name: "Lint PR" on: pull_request_target: @@ -10,9 +10,13 @@ on: jobs: main: name: Validate PR title + permissions: + contents: read + pull-requests: write runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 + - id: lint_pr_title + uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -24,16 +28,16 @@ jobs: header: pr-title-lint-error message: | Hey there and thank you for opening this pull request! 👋🏼 - + We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted. Details: - + ``` ${{ steps.lint_pr_title.outputs.error_message }} ``` # Delete a previous comment when the issue has been resolved - if: ${{ steps.lint_pr_title.outputs.error_message == null }} uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2 - with: + with: header: pr-title-lint-error delete: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8b1b132..cd00e230 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,9 @@ jobs: release-package: environment: publish runs-on: windows-latest + permissions: + contents: read + pull-requests: write steps: - uses: google-github-actions/release-please-action@db8f2c60ee802b3748b512940dde88eabd7b7e01 # v3 From ef9868688f0804e26a1b69b6ea25be5f105c26b5 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 22 Apr 2025 09:05:06 -0400 Subject: [PATCH 53/56] fix: migrate to System.Text.Json and JsonLogic (#347) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Todd Baert Co-authored-by: André Silva <2493377+askpt@users.noreply.github.com> --- .../FlagdConfig.cs | 2 - ...OpenFeature.Contrib.Providers.Flagd.csproj | 3 +- .../CustomEvaluators/FlagdProperties.cs | 43 ++- ...actionalEvaluator.cs => FractionalRule.cs} | 37 +- .../{SemVerEvaluator.cs => SemVerRule.cs} | 23 +- .../CustomEvaluators/StringEvaluator.cs | 59 ---- .../InProcess/CustomEvaluators/StringRule.cs | 59 ++++ .../Resolver/InProcess/JsonEvaluator.cs | 197 ++++++----- .../FractionalEvaluatorTest.cs | 101 +++--- .../JsonEvaluatorTest.cs | 6 +- .../SemVerEvaluatorTest.cs | 326 ++++++++---------- .../StringEvaluatorTest.cs | 174 +++++----- .../Utils.cs | 20 +- 13 files changed, 514 insertions(+), 536 deletions(-) rename src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/{FractionalEvaluator.cs => FractionalRule.cs} (70%) rename src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/{SemVerEvaluator.cs => SemVerRule.cs} (81%) delete mode 100644 src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs create mode 100644 src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs index b8512dd9..da45a056 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs @@ -1,6 +1,4 @@ using System; -using System.Numerics; -using JsonLogic.Net; namespace OpenFeature.Contrib.Providers.Flagd diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 25347777..19b06d34 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -22,7 +22,7 @@ - + @@ -33,5 +33,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs index ce336ac9..116e8a97 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs @@ -1,8 +1,12 @@ using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; +using Json.More; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators { - internal class FlagdProperties + internal sealed class FlagdProperties { internal const string FlagdPropertiesKey = "$flagd"; @@ -14,30 +18,23 @@ internal class FlagdProperties internal long Timestamp { get; set; } internal string TargetingKey { get; set; } - internal FlagdProperties(object from) + internal FlagdProperties(EvaluationContext from) { - //object value; - if (from is IDictionary dict) + + if (from.TryFind(TargetingKeyKey, out JsonNode targetingKeyValue) + && targetingKeyValue.GetValueKind() == JsonValueKind.String) + { + TargetingKey = targetingKeyValue.ToString(); + } + if (from.TryFind($"{FlagdPropertiesKey}.{FlagKeyKey}", out JsonNode flagKeyValue) + && flagKeyValue.GetValueKind() == JsonValueKind.String) + { + FlagKey = flagKeyValue.ToString(); + } + if (from.TryFind($"{FlagdPropertiesKey}.{TimestampKey}", out JsonNode timestampValue) + && timestampValue.GetValueKind() == JsonValueKind.Number) { - if (dict.TryGetValue(TargetingKeyKey, out object targetingKeyValue) - && targetingKeyValue is string targetingKeyString) - { - TargetingKey = targetingKeyString; - } - if (dict.TryGetValue(FlagdPropertiesKey, out object flagdPropertiesObj) - && flagdPropertiesObj is IDictionary flagdProperties) - { - if (flagdProperties.TryGetValue(FlagKeyKey, out object flagKeyObj) - && flagKeyObj is string flagKey) - { - FlagKey = flagKey; - } - if (flagdProperties.TryGetValue(TimestampKey, out object timestampObj) - && timestampObj is long timestamp) - { - Timestamp = timestamp; - } - } + Timestamp = timestampValue.GetValue(); } } } diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs similarity index 70% rename from src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs rename to src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs index f685d3c2..d59cab41 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalRule.cs @@ -2,47 +2,44 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using JsonLogic.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; using Murmur; -using Newtonsoft.Json.Linq; -using Semver; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators { /// - public class FractionalEvaluator + internal sealed class FractionalEvaluator : IRule { - internal FractionalEvaluator() - { - } - class FractionalEvaluationDistribution { public string variant; public int weight; } - internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) + /// + public JsonNode Apply(JsonNode args, EvaluationContext context) { // check if we have at least two arguments: // 1. the property value // 2. the array containing the buckets - if (args.Length == 0) + if (args.AsArray().Count == 0) { return null; } - var flagdProperties = new FlagdProperties(data); + var flagdProperties = new FlagdProperties(context); var bucketStartIndex = 0; - var arg0 = p.Apply(args[0], data); + var arg0 = JsonLogic.Apply(args[0], context); string propertyValue; - if (arg0 is string stringValue) + if (arg0.GetValueKind() == JsonValueKind.String) { - propertyValue = stringValue; + propertyValue = arg0.ToString(); bucketStartIndex = 1; } else @@ -53,16 +50,16 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) var distributions = new List(); var distributionSum = 0; - for (var i = bucketStartIndex; i < args.Length; i++) + for (var i = bucketStartIndex; i < args.AsArray().Count; i++) { - var bucket = p.Apply(args[i], data); + var bucket = JsonLogic.Apply(args[i], context); - if (!bucket.IsEnumerable()) + if (!(bucket.GetValueKind() == JsonValueKind.Array)) { continue; } - var bucketArr = bucket.MakeEnumerable().ToArray(); + var bucketArr = bucket.AsArray(); if (!bucketArr.Any()) { @@ -71,9 +68,9 @@ internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) var weight = 1; - if (bucketArr.Length >= 2 && bucketArr.ElementAt(1).IsNumeric()) + if (bucketArr.Count >= 2 && bucketArr.ElementAt(1).GetValueKind() == JsonValueKind.Number) { - weight = Convert.ToInt32(bucketArr.ElementAt(1)); + weight = bucketArr.ElementAt(1).GetValue(); } distributions.Add(new FractionalEvaluationDistribution diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs similarity index 81% rename from src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs rename to src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs index a25c968d..5d591259 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerRule.cs @@ -1,17 +1,12 @@ -using System; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; +using Json.Logic; using Semver; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators { /// - public class SemVerEvaluator + internal sealed class SemVerRule : IRule { - internal SemVerEvaluator() - { - } - const string OperatorEqual = "="; const string OperatorNotEqual = "!="; const string OperatorLess = "<"; @@ -21,21 +16,23 @@ internal SemVerEvaluator() const string OperatorMatchMajor = "^"; const string OperatorMatchMinor = "~"; - internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) + + /// + public JsonNode Apply(JsonNode args, EvaluationContext context) { // check if we have at least 3 arguments - if (args.Length < 3) + if (args.AsArray().Count < 3) { return false; } // get the value from the provided evaluation context - var versionString = p.Apply(args[0], data).ToString(); + var versionString = JsonLogic.Apply(args[0], context).ToString(); // get the operator - var semVerOperator = p.Apply(args[1], data).ToString(); + var semVerOperator = JsonLogic.Apply(args[1], context).ToString(); // get the target version - var targetVersionString = p.Apply(args[2], data).ToString(); + var targetVersionString = JsonLogic.Apply(args[2], context).ToString(); //convert to semantic versions if (!SemVersion.TryParse(versionString, SemVersionStyles.Strict, out var version) || diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs deleted file mode 100644 index bbda204b..00000000 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; -using OpenFeature.Error; -using OpenFeature.Model; - -namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators -{ - internal class StringEvaluator - { - internal StringEvaluator() - { - } - - internal object StartsWith(IProcessJsonLogic p, JToken[] args, object data) - { - if (!isValid(p, args, data, out string operandA, out string operandB)) - { - return false; - } - return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB)); - } - - internal object EndsWith(IProcessJsonLogic p, JToken[] args, object data) - { - if (!isValid(p, args, data, out string operandA, out string operandB)) - { - return false; - } - return operandA.EndsWith(operandB); - } - - private bool isValid(IProcessJsonLogic p, JToken[] args, object data, out string operandA, out string operandB) - { - // check if we have at least 2 arguments - operandA = null; - operandB = null; - - if (args.Length < 2) - { - return false; - } - operandA = p.Apply(args[0], data) as string; - operandB = p.Apply(args[1], data) as string; - - if (!(operandA is string) || !(operandB is string)) - { - // return false immediately if both operands are not strings. - return false; - } - - Convert.ToString(operandA); - Convert.ToString(operandB); - - return true; - } - } -} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs new file mode 100644 index 00000000..fd5eceed --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringRule.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; + +namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators +{ + internal sealed class StartsWithRule : IRule + { + public JsonNode Apply(JsonNode args, Json.Logic.EvaluationContext context) + { + if (!StringRule.isValid(args, context, out string operandA, out string operandB)) + { + return false; + } + return Convert.ToString(operandA).StartsWith(Convert.ToString(operandB)); + } + } + + internal sealed class EndsWithRule : IRule + { + public JsonNode Apply(JsonNode args, Json.Logic.EvaluationContext context) + { + if (!StringRule.isValid(args, context, out string operandA, out string operandB)) + { + return false; + } + return operandA.EndsWith(operandB); + } + } + + internal static class StringRule + { + internal static bool isValid(JsonNode args, Json.Logic.EvaluationContext context, out string argA, out string argB) + { + argA = null; + argB = null; + + // check if we have at least 2 arguments + if (args.AsArray().Count < 2) + { + return false; + } + + var nodeA = JsonLogic.Apply(args[0], context); + var nodeB = JsonLogic.Apply(args[1], context); + + // return false immediately if both operands are not strings + if (nodeA?.GetValueKind() != JsonValueKind.String || nodeB?.GetValueKind() != JsonValueKind.String) + { + return false; + } + + argA = nodeA.ToString(); + argB = nodeB.ToString(); + return true; + } + } +} \ No newline at end of file diff --git a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs index 5b449a0a..07117d23 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs @@ -2,32 +2,35 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using JsonLogic.Net; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Json.Logic; +using Json.More; using OpenFeature.Constant; using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using OpenFeature.Error; using OpenFeature.Model; +using EvaluationContext = OpenFeature.Model.EvaluationContext; namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess { internal class FlagConfiguration { - [JsonProperty("state")] internal string State { get; set; } - [JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; } - [JsonProperty("variants")] internal Dictionary Variants { get; set; } - [JsonProperty("targeting")] internal object Targeting { get; set; } - [JsonProperty("source")] internal string Source { get; set; } - [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } + [JsonPropertyName("state")] public string State { get; set; } + [JsonPropertyName("defaultVariant")] public string DefaultVariant { get; set; } + [JsonPropertyName("variants")] public Dictionary Variants { get; set; } + [JsonPropertyName("targeting")] public object Targeting { get; set; } + [JsonPropertyName("source")] public string Source { get; set; } + [JsonPropertyName("metadata")] public Dictionary Metadata { get; set; } } internal class FlagSyncData { - [JsonProperty("flags")] internal Dictionary Flags { get; set; } - [JsonProperty("$evaluators")] internal Dictionary Evaluators { get; set; } - [JsonProperty("metadata")] internal Dictionary Metadata { get; set; } + [JsonPropertyName("flags")] public Dictionary Flags { get; set; } + [JsonPropertyName("$evaluators")] public Dictionary Evaluators { get; set; } + [JsonPropertyName("metadata")] public Dictionary Metadata { get; set; } } internal class FlagConfigurationSync @@ -47,31 +50,25 @@ internal enum FlagConfigurationUpdateType internal class JsonEvaluator { private Dictionary _flags = new Dictionary(); - private Dictionary _flagSetMetadata = new Dictionary(); + private Dictionary _flagSetMetadata = new Dictionary(); private string _selector; - private readonly JsonLogicEvaluator _evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - internal JsonEvaluator(string selector) { _selector = selector; - var stringEvaluator = new StringEvaluator(); - var semVerEvaluator = new SemVerEvaluator(); - var fractionalEvaluator = new FractionalEvaluator(); - - EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.StartsWith); - EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); - EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + RuleRegistry.AddRule("starts_with", new StartsWithRule()); + RuleRegistry.AddRule("ends_with", new EndsWithRule()); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); + RuleRegistry.AddRule("fractional", new FractionalEvaluator()); } internal FlagSyncData Parse(string flagConfigurations) { - var parsed = JsonConvert.DeserializeObject(flagConfigurations); - var transformed = JsonConvert.SerializeObject(parsed); + var parsed = JsonSerializer.Deserialize(flagConfigurations); + var transformed = JsonSerializer.Serialize(parsed); // replace evaluators if (parsed.Evaluators != null && parsed.Evaluators.Count > 0) { @@ -84,21 +81,16 @@ internal FlagSyncData Parse(string flagConfigurations) } - var data = JsonConvert.DeserializeObject(transformed); + var data = JsonSerializer.Deserialize(transformed); if (data.Metadata == null) { - data.Metadata = new Dictionary(); + data.Metadata = new Dictionary(); } else { foreach (var key in new List(data.Metadata.Keys)) { var value = data.Metadata[key]; - if (value is long longValue) - { - value = data.Metadata[key] = (int)longValue; - } - VerifyMetadataValue(key, value); } } @@ -113,11 +105,6 @@ internal FlagSyncData Parse(string flagConfigurations) foreach (var key in new List(flagConfig.Value.Metadata.Keys)) { var value = flagConfig.Value.Metadata[key]; - if (value is long longValue) - { - value = flagConfig.Value.Metadata[key] = (int)longValue; - } - VerifyMetadataValue(key, value); } } @@ -125,9 +112,13 @@ internal FlagSyncData Parse(string flagConfigurations) return data; } - private static void VerifyMetadataValue(string key, object value) + private static void VerifyMetadataValue(string key, JsonElement value) { - if (value is int || value is double || value is string || value is bool) + //if (value is int || value is double || value is string || value is bool) + if (value.ValueKind == JsonValueKind.Number + || value.ValueKind == JsonValueKind.String + || value.ValueKind == JsonValueKind.True + || value.ValueKind == JsonValueKind.False) { return; } @@ -136,6 +127,23 @@ private static void VerifyMetadataValue(string key, object value) " is of unknown type"); } + private static object ExtractMetadataValue(string key, JsonElement value) + { + switch (value.ValueKind) + { + case JsonValueKind.Number: + return value.GetDouble(); + case JsonValueKind.String: + return value.GetString(); + case JsonValueKind.False: + case JsonValueKind.True: + return value.GetBoolean(); + + } + throw new ParseErrorException("Metadata entry for key " + key + " and value " + value + + " is of unknown type"); + } + internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations) { var flagConfigsMap = Parse(flagConfigurations); @@ -218,12 +226,15 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled"); } - Dictionary combinedMetadata = new Dictionary(_flagSetMetadata); + Dictionary combinedMetadata = _flagSetMetadata.ToDictionary( + entry => entry.Key, + entry => ExtractMetadataValue(entry.Key, entry.Value)); + if (flagConfiguration.Metadata != null) { foreach (var metadataEntry in flagConfiguration.Metadata) { - combinedMetadata[metadataEntry.Key] = metadataEntry.Value; + combinedMetadata[metadataEntry.Key] = ExtractMetadataValue(metadataEntry.Key, metadataEntry.Value); } } @@ -234,31 +245,31 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, flagConfiguration.Targeting.ToString() != "{}") { reason = Reason.TargetingMatch; - var flagdProperties = new Dictionary(); - flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey)); - flagdProperties.Add(FlagdProperties.TimestampKey, - new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds())); + var flagdProperties = new Dictionary + { + { FlagdProperties.FlagKeyKey, new Value(flagKey) }, + { FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()) } + }; if (context == null) { context = EvaluationContext.Builder().Build(); } - var targetingContext = context.AsDictionary().Add( - FlagdProperties.FlagdPropertiesKey, - new Value(new Structure(flagdProperties)) - ); + var contextDictionary = context.AsDictionary(); + contextDictionary = contextDictionary.Add(FlagdProperties.FlagdPropertiesKey, new Value(new Structure(flagdProperties))); + // TODO: all missing comments var targetingString = flagConfiguration.Targeting.ToString(); // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); // the JsonLogic evaluator will return the variant for the value // convert the EvaluationContext object into something the JsonLogic evaluator can work with - dynamic contextObj = (object)ConvertToDynamicObject(targetingContext); + var contextObj = JsonNode.Parse(JsonSerializer.Serialize(ConvertToDynamicObject(contextDictionary))); // convert whatever is returned to a string to try to use it as an index to Variants - var ruleResult = _evaluator.Apply(rule, contextObj); + var ruleResult = JsonLogic.Apply(rule, contextObj); if (ruleResult is bool) { // if this was a bool, convert from "True" to "true" to match JSON @@ -278,7 +289,7 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, reason = Reason.Default; flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue); - if (defaultVariantValue == null) + if (defaultVariantValue.ValueKind == JsonValueKind.Undefined || defaultVariantValue.ValueKind == JsonValueKind.Null) { throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant."); @@ -311,29 +322,47 @@ private ResolutionDetails ResolveValue(string flagKey, T defaultValue, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found"); } - static T ExtractFoundVariant(object foundVariantValue, string flagKey) + static T ExtractFoundVariant(JsonElement foundVariantValue, string flagKey) { - if (foundVariantValue is long) + try { - foundVariantValue = Convert.ToInt32(foundVariantValue); - } + if (typeof(T) == typeof(int)) + { + return (T)(object)foundVariantValue.GetInt32(); + } - if (typeof(T) == typeof(double)) - { - foundVariantValue = Convert.ToDouble(foundVariantValue); - } - else if (foundVariantValue is JObject value) - { - foundVariantValue = ConvertJObjectToOpenFeatureValue(value); - } + if (typeof(T) == typeof(double)) + { + return (T)(object)foundVariantValue.GetDouble(); + } + + if (typeof(T) == typeof(bool)) + { + return (T)(object)foundVariantValue.GetBoolean(); + } + + if (typeof(T) == typeof(string)) + { + return (T)(object)foundVariantValue.GetString(); + } + + if (foundVariantValue.ValueKind == JsonValueKind.Object || foundVariantValue.ValueKind == JsonValueKind.Array) + { + var converted = ConvertJsonObjectToOpenFeatureValue(foundVariantValue.AsNode().AsObject()); + if (converted is T castValue) + { + return castValue; + } + } + throw new Exception("Cannot cast flag value to expected type"); - if (foundVariantValue is T castValue) + } + catch (Exception e) { - return castValue; + throw new FeatureProviderException(ErrorType.TypeMismatch, + "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type", e); } - throw new FeatureProviderException(ErrorType.TypeMismatch, - "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type"); } static dynamic ConvertToDynamicObject(IImmutableDictionary dictionary) @@ -352,37 +381,35 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary dictio return expandoObject; } - static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue) + static Value ConvertJsonObjectToOpenFeatureValue(JsonObject jsonValue) { var result = new Dictionary(); - foreach (var property in jsonValue.Properties()) + foreach (var property in jsonValue.AsEnumerable()) { - switch (property.Value.Type) + switch (property.Value.GetValueKind()) { - case JTokenType.String: - result.Add(property.Name, new Value((string)property.Value)); - break; - - case JTokenType.Integer: - result.Add(property.Name, new Value((Int64)property.Value)); + case JsonValueKind.String: + result.Add(property.Key, new Value((string)property.Value)); break; - case JTokenType.Boolean: - result.Add(property.Name, new Value((bool)property.Value)); + case JsonValueKind.Number: + result.Add(property.Key, new Value((long)property.Value)); break; - case JTokenType.Float: - result.Add(property.Name, new Value((float)property.Value)); + case JsonValueKind.True: + case JsonValueKind.False: + result.Add(property.Key, new Value((bool)property.Value)); break; - case JTokenType.Object: - result.Add(property.Name, ConvertJObjectToOpenFeatureValue((JObject)property.Value)); + case JsonValueKind.Object: + case JsonValueKind.Array: + result.Add(property.Key, ConvertJsonObjectToOpenFeatureValue(property.Value.AsObject())); break; default: // Handle unknown data type or throw an exception - throw new InvalidOperationException($"Unsupported data type: {property.Value.Type}"); + throw new InvalidOperationException($"Unsupported data type: {property.Value.GetType()}"); } } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs index cd23b3ca..72fd85de 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/FractionalEvaluatorTest.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; +using Json.More; using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using Xunit; @@ -31,9 +33,7 @@ public class FractionalEvaluatorTest public void Evaluate(string email, string flagKey, string expected) { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var fractionalEvaluator = new FractionalEvaluator(); - EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + RuleRegistry.AddRule("fractional", new FractionalEvaluator()); var targetingString = @"{""fractional"": [ { @@ -42,19 +42,21 @@ public void Evaluate(string email, string flagKey, string expected) { ""var"":""email"" } ] }, - [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25], + [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25] ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { - { "email", email }, - {"$flagd", new Dictionary { {"flagKey", flagKey } } } - }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "email", email }, + { "$flagd", new Dictionary { { "flagKey", flagKey } } } + })); + + // Act + var result = JsonLogic.Apply(rule, data); - // Act & Assert - var result = evaluator.Apply(rule, data); + // Assert Assert.Equal(expected, result.ToString()); } @@ -63,9 +65,7 @@ public void Evaluate(string email, string flagKey, string expected) public void EvaluateUsingRelativeWeights(string email, string flagKey, string expected) { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var fractionalEvaluator = new FractionalEvaluator(); - EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + RuleRegistry.AddRule("fractional", new FractionalEvaluator()); var targetingString = @"{""fractional"": [ { @@ -74,19 +74,21 @@ public void EvaluateUsingRelativeWeights(string email, string flagKey, string ex { ""var"":""email"" } ] }, - [""red"", 5], [""blue"", 5], [""green"", 5], [""yellow"", 5], + [""red"", 5], [""blue"", 5], [""green"", 5], [""yellow"", 5] ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { { "email", email }, - {"$flagd", new Dictionary { {"flagKey", flagKey } } } - }; + { "$flagd", new Dictionary { { "flagKey", flagKey } } } + })); + + // Act + var result = JsonLogic.Apply(rule, data); - // Act & Assert - var result = evaluator.Apply(rule, data); + // Assert Assert.Equal(expected, result.ToString()); } @@ -95,9 +97,7 @@ public void EvaluateUsingRelativeWeights(string email, string flagKey, string ex public void EvaluateUsingDefaultWeights(string email, string flagKey, string expected) { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var fractionalEvaluator = new FractionalEvaluator(); - EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + RuleRegistry.AddRule("fractional", new FractionalEvaluator()); var targetingString = @"{""fractional"": [ { @@ -106,19 +106,21 @@ public void EvaluateUsingDefaultWeights(string email, string flagKey, string exp { ""var"":""email"" } ] }, - [""red""], [""blue""], [""green""], [""yellow""], + [""red""], [""blue""], [""green""], [""yellow""] ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { - { "email", email }, - {"$flagd", new Dictionary { {"flagKey", flagKey } } } - }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "email", email }, + { "$flagd", new Dictionary { { "flagKey", flagKey } } } + })); + + // Act + var result = JsonLogic.Apply(rule, data); - // Act & Assert - var result = evaluator.Apply(rule, data); + // Assert Assert.Equal(expected, result.ToString()); } @@ -127,26 +129,25 @@ public void EvaluateUsingDefaultWeights(string email, string flagKey, string exp public void EvaluateUsingTargetingKey(string flagKey, string expected) { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var fractionalEvaluator = new FractionalEvaluator(); - EvaluateOperators.Default.AddOperator("fractional", fractionalEvaluator.Evaluate); + RuleRegistry.AddRule("fractional", new FractionalEvaluator()); var targetingString = @"{""fractional"": [ - [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25], + [""red"", 25], [""blue"", 25], [""green"", 25], [""yellow"", 25] ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { - { "targetingKey", "myKey" }, - {"$flagd", new Dictionary { {"flagKey", flagKey } } } - }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "targetingKey", "myKey" }, + { "$flagd", new Dictionary { { "flagKey", flagKey } } } + })); - // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.Equal(expected, result.ToString()); + // Act + var result = JsonLogic.Apply(rule, data); + // Assert + Assert.Equal(expected, result.ToString()); } } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs index 9348c16f..7a84a913 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs @@ -343,7 +343,7 @@ public void TestJsonEvaluatorReturnsFlagMetadata() var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false); Assert.NotNull(result.FlagMetadata); Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); - Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(2, result.FlagMetadata.GetDouble("integer")); Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); } @@ -360,7 +360,7 @@ public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetadata() var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false); Assert.NotNull(result.FlagMetadata); Assert.Equal("1.0.3", result.FlagMetadata.GetString("string")); - Assert.Equal(3, result.FlagMetadata.GetInt("integer")); + Assert.Equal(3, result.FlagMetadata.GetDouble("integer")); Assert.Equal(false, result.FlagMetadata.GetBool("boolean")); Assert.Equal(.2, result.FlagMetadata.GetDouble("float")); } @@ -378,7 +378,7 @@ public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata() Assert.NotNull(result.FlagMetadata); Assert.Equal("1.0.2", result.FlagMetadata.GetString("string")); - Assert.Equal(2, result.FlagMetadata.GetInt("integer")); + Assert.Equal(2, result.FlagMetadata.GetDouble("integer")); Assert.Equal(true, result.FlagMetadata.GetBool("boolean")); Assert.Equal(.1, result.FlagMetadata.GetDouble("float")); } diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs index 46ad97a5..cad81516 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/SemVerEvaluatorTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using Xunit; @@ -8,344 +9,323 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test { public class SemVerEvaluatorTest { - [Fact] public void EvaluateVersionEqual() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""="", ""1.0.0"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.0" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.0" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.1"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.1" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionNotEqual() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""!="", ""1.0.0"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.0" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.0" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.1"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.1" } + })); - result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); } [Fact] public void EvaluateVersionLess() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""<"", ""1.0.2"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.1" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.1" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.2"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.2" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionLessOrEqual() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""<="", ""1.0.2"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.1" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.1" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.2"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.2" } + })); - result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.3"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.3" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionGreater() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, "">"", ""1.0.2"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.3" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.3" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.2"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.2" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionGreaterOrEqual() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, "">="", ""1.0.2"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.2" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.2" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.3"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.3" } + })); - result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "1.0.1"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.1" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionMatchMajor() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""^"", ""1.0.0"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.0.3" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.0.3" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "2.0.0"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "2.0.0" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionMatchMinor() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""~"", ""1.3.0"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.3.3" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.3.3" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("version", "2.3.0"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "2.3.0" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionTooFewArguments() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""~"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.3.3" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.3.3" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EvaluateVersionNotAValidVersion() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var semVerEvaluator = new SemVerEvaluator(); - EvaluateOperators.Default.AddOperator("sem_ver", semVerEvaluator.Evaluate); + RuleRegistry.AddRule("sem_ver", new SemVerRule()); var targetingString = @"{""sem_ver"": [ - { - ""var"": [ - ""version"" - ] - }, + { ""var"": ""version"" }, ""~"", ""test"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "version", "1.3.3" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "version", "1.3.3" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs index 6d59977d..ae3238bc 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/StringEvaluatorTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using JsonLogic.Net; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using Json.Logic; using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators; using Xunit; @@ -8,177 +9,156 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test { public class StringEvaluatorTest { - [Fact] public void StartsWith() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.StartsWith); + RuleRegistry.AddRule("starts_with", new StartsWithRule()); var targetingString = @"{""starts_with"": [ - { - ""var"": [ - ""color"" - ] - }, + { ""var"": ""color"" }, ""yellow"" ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", "yellowcolor" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", "yellowcolor" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("color", "blue"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", "blue" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EndsWith() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith); + RuleRegistry.AddRule("ends_with", new EndsWithRule()); var targetingString = @"{""ends_with"": [ - { - ""var"": [ - ""color"" - ] - }, - ""purple"" - ]}"; + { ""var"": ""color"" }, + ""purple"" + ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", "deep-purple" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", "deep-purple" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.True(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.True(result.GetValue()); - data.Clear(); - data.Add("color", "purple-nightmare"); + data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", "purple-nightmare" } + })); - result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void NonStringTypeInRule() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith); + RuleRegistry.AddRule("ends_with", new EndsWithRule()); var targetingString = @"{""ends_with"": [ - { - ""var"": [ - ""color"" - ] - }, - 1 - ]}"; + { ""var"": ""color"" }, + 1 + ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", "deep-purple" } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", "deep-purple" } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void NonStringTypeInData() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith); + RuleRegistry.AddRule("ends_with", new EndsWithRule()); var targetingString = @"{""ends_with"": [ - { - ""var"": [ - ""color"" - ] - }, - ""green"" - ]}"; + { ""var"": ""color"" }, + ""green"" + ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", 5 } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", 5 } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void EndsWithNotEnoughArguments() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("ends_with", stringEvaluator.EndsWith); + RuleRegistry.AddRule("ends_with", new EndsWithRule()); var targetingString = @"{""ends_with"": [ - { - ""var"": [ - ""color"" - ] - } - ]}"; + { ""var"": ""color"" } + ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", 5 } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", 5 } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } [Fact] public void StartsWithNotEnoughArguments() { // Arrange - var evaluator = new JsonLogicEvaluator(EvaluateOperators.Default); - var stringEvaluator = new StringEvaluator(); - EvaluateOperators.Default.AddOperator("starts_with", stringEvaluator.EndsWith); + RuleRegistry.AddRule("starts_with", new StartsWithRule()); var targetingString = @"{""starts_with"": [ - { - ""var"": [ - ""color"" - ] - } - ]}"; + { ""var"": ""color"" } + ]}"; - // Parse json into hierarchical structure - var rule = JObject.Parse(targetingString); + var rule = JsonNode.Parse(targetingString); - var data = new Dictionary { { "color", 5 } }; + var data = JsonNode.Parse(JsonSerializer.Serialize(new Dictionary + { + { "color", 5 } + })); // Act & Assert - var result = evaluator.Apply(rule, data); - Assert.False(result.IsTruthy()); + var result = JsonLogic.Apply(rule, data); + Assert.False(result.GetValue()); } } } \ No newline at end of file diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs index de386478..18aa3dff 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/Utils.cs @@ -162,7 +162,7 @@ public class Utils }, ""defaultVariant"": ""bool2"", ""targeting"": { - ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""] + ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""] } }, ""targetingBoolFlagUsingSharedEvaluatorReturningBoolType"": { @@ -173,7 +173,7 @@ public class Utils }, ""defaultVariant"": ""true"", ""targeting"": { - ""if"": [{ $ref: ""emailWithFaas"" }, true] + ""if"": [{ ""$ref"": ""emailWithFaas"" }, true] } }, ""targetingBoolFlagWithMissingDefaultVariant"": { @@ -184,7 +184,7 @@ public class Utils }, ""defaultVariant"": ""true"", ""targeting"": { - ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""] + ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""] } }, ""targetingBoolFlagWithUnexpectedVariantType"": { @@ -195,7 +195,7 @@ public class Utils }, ""defaultVariant"": ""true"", ""targeting"": { - ""if"": [{ $ref: ""emailWithFaas"" }, ""bool1""] + ""if"": [{ ""$ref"": ""emailWithFaas"" }, ""bool1""] } }, ""targetingStringFlag"": { @@ -313,7 +313,7 @@ public class Utils ""string"": ""1.0.2"", ""integer"": 2, ""boolean"": true, - ""float"": 0.1, + ""float"": 0.1 } } } @@ -332,7 +332,7 @@ public class Utils ""string"": ""1.0.2"", ""integer"": 2, ""boolean"": true, - ""float"": 0.1, + ""float"": 0.1 } }, ""without-metadata-flag"": { @@ -348,7 +348,7 @@ public class Utils ""string"": ""1.0.3"", ""integer"": 3, ""boolean"": false, - ""float"": 0.2, + ""float"": 0.2 } }"; @@ -367,7 +367,7 @@ public class Utils ""string"": {""in"": ""valid""}, ""integer"": 3, ""boolean"": false, - ""float"": 0.2, + ""float"": 0.2 } }"; public static string invalidFlagMetadata = @"{ @@ -383,9 +383,9 @@ public class Utils ""string"": ""1.0.2"", ""integer"": 2, ""boolean"": true, - ""float"": {""in"": ""valid""}, + ""float"": {""in"": ""valid""} } - }, + } } }"; From c1a1ce3e84b6f0998007b23f37d63f0b13b09f03 Mon Sep 17 00:00:00 2001 From: Todd Baert Date: Tue, 22 Apr 2025 14:23:14 -0400 Subject: [PATCH 54/56] chore: update release permissions Signed-off-by: Todd Baert --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd00e230..3d484428 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: environment: publish runs-on: windows-latest permissions: - contents: read + contents: write pull-requests: write steps: From 9f4760807f6d5ddf416a8ec7bd931f698f4f30b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:38:03 -0400 Subject: [PATCH 55/56] chore(deps): update dependency semver to v3 (#351) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index 19b06d34..f2163064 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -24,7 +24,7 @@ - + From ffc0b6467df49fa9acfb37dfb985b350cba43686 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:35:11 -0400 Subject: [PATCH 56/56] chore(main): release OpenFeature.Contrib.Providers.Flagd 0.3.1 (#275) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .../CHANGELOG.md | 30 +++++++++++++++++++ ...OpenFeature.Contrib.Providers.Flagd.csproj | 2 +- .../version.txt | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1b3f81a7..d5fde3c1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { "src/OpenFeature.Contrib.Hooks.Otel": "0.2.0", - "src/OpenFeature.Contrib.Providers.Flagd": "0.3.0", + "src/OpenFeature.Contrib.Providers.Flagd": "0.3.1", "src/OpenFeature.Contrib.Providers.GOFeatureFlag": "0.2.1", "src/OpenFeature.Contrib.Providers.Flagsmith": "0.2.0", "src/OpenFeature.Contrib.Providers.ConfigCat": "0.1.1", diff --git a/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md b/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md index 693fc5a8..2b0b8c82 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md +++ b/src/OpenFeature.Contrib.Providers.Flagd/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [0.3.1](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flagd-v0.3.0...OpenFeature.Contrib.Providers.Flagd-v0.3.1) (2025-04-22) + + +### 🐛 Bug Fixes + +* migrate to System.Text.Json and JsonLogic ([#347](https://github.com/open-feature/dotnet-sdk-contrib/issues/347)) ([ef98686](https://github.com/open-feature/dotnet-sdk-contrib/commit/ef9868688f0804e26a1b69b6ea25be5f105c26b5)) + + +### ✨ New Features + +* Update in-process resolver to support flag metadata [#305](https://github.com/open-feature/dotnet-sdk-contrib/issues/305) ([#309](https://github.com/open-feature/dotnet-sdk-contrib/issues/309)) ([e603c08](https://github.com/open-feature/dotnet-sdk-contrib/commit/e603c08df7c19f360b2d8896caef3e3a5bcdcefd)) + + +### 🧹 Chore + +* **deps:** update dependency google.protobuf to 3.28.2 ([#272](https://github.com/open-feature/dotnet-sdk-contrib/issues/272)) ([1c45c1a](https://github.com/open-feature/dotnet-sdk-contrib/commit/1c45c1a3578ddc814483ac83549c2be5579d403c)) +* **deps:** update dependency google.protobuf to 3.30.2 ([#335](https://github.com/open-feature/dotnet-sdk-contrib/issues/335)) ([3f63d35](https://github.com/open-feature/dotnet-sdk-contrib/commit/3f63d35540979dfb42e1f9d80ba5d2bba0b4a509)) +* **deps:** update dependency grpc.net.client to 2.66.0 ([#282](https://github.com/open-feature/dotnet-sdk-contrib/issues/282)) ([04803d7](https://github.com/open-feature/dotnet-sdk-contrib/commit/04803d7cfcf739ea17c11dc576444ae75ba85192)) +* **deps:** update dependency grpc.net.client to 2.70.0 ([#336](https://github.com/open-feature/dotnet-sdk-contrib/issues/336)) ([cd4cd4f](https://github.com/open-feature/dotnet-sdk-contrib/commit/cd4cd4f29bedebcca0a11085307bed72e6e7b794)) +* **deps:** update dependency grpc.tools to 2.66.0 ([#271](https://github.com/open-feature/dotnet-sdk-contrib/issues/271)) ([161fb63](https://github.com/open-feature/dotnet-sdk-contrib/commit/161fb638f22eecae2d4caa84c6c595878c8c48c9)) +* **deps:** update dependency grpc.tools to 2.71.0 ([#286](https://github.com/open-feature/dotnet-sdk-contrib/issues/286)) ([84acae2](https://github.com/open-feature/dotnet-sdk-contrib/commit/84acae2663677cf60c7e9691fb22fd250af6fd64)) +* **deps:** update dependency semver to v3 ([#351](https://github.com/open-feature/dotnet-sdk-contrib/issues/351)) ([9f47608](https://github.com/open-feature/dotnet-sdk-contrib/commit/9f4760807f6d5ddf416a8ec7bd931f698f4f30b2)) +* **deps:** update ghcr.io/open-feature/flagd-testbed docker tag to v0.5.21 ([#291](https://github.com/open-feature/dotnet-sdk-contrib/issues/291)) ([29553b2](https://github.com/open-feature/dotnet-sdk-contrib/commit/29553b252344057dc4eba7379b95acb085e9caa1)) +* **deps:** update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v0.5.21 ([#323](https://github.com/open-feature/dotnet-sdk-contrib/issues/323)) ([faa44cc](https://github.com/open-feature/dotnet-sdk-contrib/commit/faa44cc6db5b014069f3dd72b1bf34e3e5ada1df)) +* **deps:** update ghcr.io/open-feature/flagd-testbed-unstable docker tag to v1 ([#339](https://github.com/open-feature/dotnet-sdk-contrib/issues/339)) ([308cc42](https://github.com/open-feature/dotnet-sdk-contrib/commit/308cc42afce6196ff4c4ffc89350454a44f1d1e0)) +* **deps:** update ghcr.io/open-feature/sync-testbed-unstable docker tag to v0.5.13 ([#333](https://github.com/open-feature/dotnet-sdk-contrib/issues/333)) ([6cbf656](https://github.com/open-feature/dotnet-sdk-contrib/commit/6cbf6563e7f5b2f4c4b8e0b557f978cfe12f79c9)) +* **deps:** update src/openfeature.contrib.providers.flagd/schemas digest to 9b0ee43 ([#332](https://github.com/open-feature/dotnet-sdk-contrib/issues/332)) ([1f7214d](https://github.com/open-feature/dotnet-sdk-contrib/commit/1f7214d28f04e504fdf8f3dac7fa14ff613fa677)) +* **deps:** update src/openfeature.contrib.providers.flagd/schemas digest to c707f56 ([#343](https://github.com/open-feature/dotnet-sdk-contrib/issues/343)) ([5d142fd](https://github.com/open-feature/dotnet-sdk-contrib/commit/5d142fd798da9b668d8b45f5f6310a03b1424c36)) +* Use TestContainers instead of github services / docker for e2e tests ([#345](https://github.com/open-feature/dotnet-sdk-contrib/issues/345)) ([1173f4f](https://github.com/open-feature/dotnet-sdk-contrib/commit/1173f4f1c0a06f191d4aa6b0353ac54f81889ec6)) + ## [0.3.0](https://github.com/open-feature/dotnet-sdk-contrib/compare/OpenFeature.Contrib.Providers.Flagd-v0.2.3...OpenFeature.Contrib.Providers.Flagd-v0.3.0) (2024-08-22) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj index f2163064..22d1ee73 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj +++ b/src/OpenFeature.Contrib.Providers.Flagd/OpenFeature.Contrib.Providers.Flagd.csproj @@ -2,7 +2,7 @@ OpenFeature.Contrib.Providers.Flagd - 0.3.0 + 0.3.1 $(VersionNumber) $(VersionNumber) $(VersionNumber) diff --git a/src/OpenFeature.Contrib.Providers.Flagd/version.txt b/src/OpenFeature.Contrib.Providers.Flagd/version.txt index 0d91a54c..9e11b32f 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/version.txt +++ b/src/OpenFeature.Contrib.Providers.Flagd/version.txt @@ -1 +1 @@ -0.3.0 +0.3.1