From 4ea851ad57cda46bf9749bbfe4fba308087f5c0d Mon Sep 17 00:00:00 2001 From: Ben Papillon Date: Mon, 10 Feb 2025 16:52:31 -0500 Subject: [PATCH] feat: Adding OpenFeature provider for Schematic Signed-off-by: Ben Papillon --- DotnetSdkContrib.sln | 14 +++ release-please-config.json | 10 ++ ...Feature.Contrib.Providers.Schematic.csproj | 29 +++++ .../README.md | 110 +++++++++++++++++ .../SchematicProvider.cs | 106 ++++++++++++++++ .../version.txt | 1 + ...re.Contrib.Providers.Schematic.Test.csproj | 9 ++ .../SchematicProviderTest.cs | 116 ++++++++++++++++++ 8 files changed, 395 insertions(+) create mode 100644 src/OpenFeature.Contrib.Providers.Schematic/OpenFeature.Contrib.Providers.Schematic.csproj create mode 100644 src/OpenFeature.Contrib.Providers.Schematic/README.md create mode 100644 src/OpenFeature.Contrib.Providers.Schematic/SchematicProvider.cs create mode 100644 src/OpenFeature.Contrib.Providers.Schematic/version.txt create mode 100644 test/OpenFeature.Contrib.Providers.Schematic.Test/OpenFeature.Contrib.Providers.Schematic.Test.csproj create mode 100644 test/OpenFeature.Contrib.Providers.Schematic.Test/SchematicProviderTest.cs diff --git a/DotnetSdkContrib.sln b/DotnetSdkContrib.sln index 57004386..e1ab21da 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.Schematic", "src\OpenFeature.Contrib.Providers.Schematic\OpenFeature.Contrib.Providers.Schematic.csproj", "{CF1AB517-1D51-455F-80C0-56B4856E6A6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenFeature.Contrib.Providers.Schematic.Test", "test\OpenFeature.Contrib.Providers.Schematic.Test\OpenFeature.Contrib.Providers.Schematic.Test.csproj", "{08BD26A8-0C14-40F1-BFAF-7D413B76EF6B}" +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 + {CF1AB517-1D51-455F-80C0-56B4856E6A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF1AB517-1D51-455F-80C0-56B4856E6A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF1AB517-1D51-455F-80C0-56B4856E6A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF1AB517-1D51-455F-80C0-56B4856E6A6B}.Release|Any CPU.Build.0 = Release|Any CPU + {08BD26A8-0C14-40F1-BFAF-7D413B76EF6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08BD26A8-0C14-40F1-BFAF-7D413B76EF6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08BD26A8-0C14-40F1-BFAF-7D413B76EF6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08BD26A8-0C14-40F1-BFAF-7D413B76EF6B}.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} + {CF1AB517-1D51-455F-80C0-56B4856E6A6B} = {0E563821-BD08-4B7F-BF9D-395CAD80F026} + {08BD26A8-0C14-40F1-BFAF-7D413B76EF6B} = {B6D3230B-5E4D-4FF1-868E-2F4E325C84FE} EndGlobalSection EndGlobal diff --git a/release-please-config.json b/release-please-config.json index cabbd73f..87580707 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.Schematic": { + "package-name": "OpenFeature.Contrib.Providers.Schematic", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [ + "OpenFeature.Contrib.Providers.Schematic.csproj" + ] } }, "changelog-sections": [ diff --git a/src/OpenFeature.Contrib.Providers.Schematic/OpenFeature.Contrib.Providers.Schematic.csproj b/src/OpenFeature.Contrib.Providers.Schematic/OpenFeature.Contrib.Providers.Schematic.csproj new file mode 100644 index 00000000..23274ae1 --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Schematic/OpenFeature.Contrib.Providers.Schematic.csproj @@ -0,0 +1,29 @@ + + + + OpenFeature.Contrib.Providers.Schematic + 0.1.0 + $(VersionNumber) + $(VersionNumber) + $(VersionNumber) + Schematic provider for .NET + README.md + Benjamin Papillon + + + + + <_Parameter1>$(MSBuildProjectName).Test + + + + + + + + + + + + + diff --git a/src/OpenFeature.Contrib.Providers.Schematic/README.md b/src/OpenFeature.Contrib.Providers.Schematic/README.md new file mode 100644 index 00000000..176e561f --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Schematic/README.md @@ -0,0 +1,110 @@ +# Schematic .NET Provider + +The Schematic provider allows you to connect to your Schematic 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 **Schematic OpenFeature provider**. + +### .NET Cli +```shell +dotnet add package OpenFeature.Contrib.Providers.Schematic +``` +### Package Manager + +```shell +NuGet\Install-Package OpenFeature.Contrib.Providers.Schematic +``` +### Package Reference + +```xml + +``` +### Paket cli + +```shell +paket add OpenFeature.Contrib.Providers.Schematic +``` + +### Cake + +```shell +// Install OpenFeature.Contrib.Providers.Schematic as a Cake Addin +#addin nuget:?package=OpenFeature.Contrib.Providers.Schematic + +// Install OpenFeature.Contrib.Providers.Schematic as a Cake Tool +#tool nuget:?package=OpenFeature.Contrib.Providers.Schematic +``` + +## Using the Schematic Provider with the OpenFeature SDK + +To use Schematic as an OpenFeature provider, define your provider and Schematic settings. + +```csharp +using OpenFeature; +using OpenFeature.Contrib.Providers.Schematic; +using System; + +var schematicProvider = new SchematicFeatureProvider("your-api-key"); + +// Set the schematicProvider as the provider for the OpenFeature SDK +await OpenFeature.Api.Instance.SetProviderAsync(flagsmithProvider); + +// Get an OpenFeature client +var client = OpenFeature.Api.Instance.GetClient("my-app"); + +// Set company and/or user context +var context = EvaluationContext.Builder() + .Set("company", new Dictionary { { "id", "your-company-id" } }) + .Set("user", new Dictionary { { "id", "your-user-id" } }) + .Build(); + +// Evaluate a flag +var val = await client.GetBooleanValueAsync("your-flag-key", false, context); + +// Print the value of the 'your-flag-key' feature flag +Console.WriteLine(val); +``` + +You can also provide additional configuration options to the provider to manage caching behavior, offline mode, and other capabilities: + +```csharp +using OpenFeature; +using OpenFeature.Contrib.Providers.Schematic; +using System; + +var options = new ClientOptions +{ + Offline = true, + FlagDefaults = new Dictionary + { + { "some-flag-key", true } + } +}; + +var schematicProvider = new SchematicFeatureProvider("your-api-key", options); + +// Set the schematicProvider as the provider for the OpenFeature SDK +await OpenFeature.Api.Instance.SetProviderAsync(flagsmithProvider); + +// Get an OpenFeature client +var client = OpenFeature.Api.Instance.GetClient("my-app"); + +// Set company and/or user context +var context = EvaluationContext.Builder() + .Set("company", new Dictionary { { "id", "your-company-id" } }) + .Set("user", new Dictionary { { "id", "your-user-id" } }) + .Build(); + +// Evaluate a flag +var val = await client.GetBooleanValueAsync("your-flag-key", false, context); + +// Print the value of the 'your-flag-key' feature flag +Console.WriteLine(val); +``` diff --git a/src/OpenFeature.Contrib.Providers.Schematic/SchematicProvider.cs b/src/OpenFeature.Contrib.Providers.Schematic/SchematicProvider.cs new file mode 100644 index 00000000..afbaafdc --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Schematic/SchematicProvider.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenFeature; +using OpenFeature.Model; +using SchematicHQ.Client; + +namespace OpenFeature.Contrib.Providers.Schematic +{ + public class SchematicFeatureProvider : FeatureProvider + { + private readonly Schematic _schematic; + private readonly ISchematicLogger _logger; + + public SchematicFeatureProvider(string apiKey, ClientOptions? options = null) + { + options ??= new ClientOptions(); + _logger = options.Logger ?? new ConsoleLogger(); + _schematic = new Schematic(apiKey, options); + } + + public override Metadata GetMetadata() => new Metadata("schematic-provider"); + + public override async Task> ResolveBooleanValueAsync( + string flagKey, + bool defaultValue, + EvaluationContext? context = null, + CancellationToken cancellationToken = default) + { + _logger.Debug("evaluating boolean flag: {0}", flagKey); + var company = context?.GetValue("company") as Dictionary; + var user = context?.GetValue("user") as Dictionary; + + try + { + bool value = await _schematic.CheckFlag(flagKey, company, user); + _logger.Debug("evaluated flag: {0} => {1}", flagKey, value); + return new ResolutionDetails(value, value ? "on" : "off", "schematic evaluation"); + } + catch (Exception ex) + { + _logger.Error("error evaluating flag {0}: {1}. using default {2}", flagKey, ex.Message, defaultValue); + return new ResolutionDetails(defaultValue, defaultValue ? "on" : "off", "error", "provider_error"); + } + } + + public override Task> ResolveStringValueAsync( + string flagKey, + string defaultValue, + EvaluationContext? context = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(defaultValue, defaultValue, "unsupported type")); + } + + public override Task> ResolveIntegerValueAsync( + string flagKey, + int defaultValue, + EvaluationContext? context = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(defaultValue, defaultValue.ToString(), "unsupported type")); + } + + public override Task> ResolveDoubleValueAsync( + string flagKey, + double defaultValue, + EvaluationContext? context = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(defaultValue, defaultValue.ToString(), "unsupported type")); + } + + public override Task> ResolveStructureValueAsync( + string flagKey, + object defaultValue, + EvaluationContext? context = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new ResolutionDetails(defaultValue, defaultValue?.ToString() ?? string.Empty, "unsupported type")); + } + + public override Task InitializeAsync(ProviderConfiguration? configuration, CancellationToken cancellationToken = default) + { + _logger.Debug("initializing schematic provider"); + return Task.CompletedTask; + } + + public async Task ShutdownAsync(CancellationToken cancellationToken = default) + { + _logger.Debug("shutting down schematic provider"); + await _schematic.Shutdown(); + } + + public Task TrackEventAsync(string eventName, EvaluationContext? context = null, CancellationToken cancellationToken = default) + { + _logger.Debug("tracking event: {0}", eventName); + var company = context?.GetValue("company") as Dictionary; + var user = context?.GetValue("user") as Dictionary; + var traits = context?.GetValue("traits") as Dictionary; + _schematic.Track(eventName, company, user, traits); + return Task.CompletedTask; + } + } +} diff --git a/src/OpenFeature.Contrib.Providers.Schematic/version.txt b/src/OpenFeature.Contrib.Providers.Schematic/version.txt new file mode 100644 index 00000000..6e8bf73a --- /dev/null +++ b/src/OpenFeature.Contrib.Providers.Schematic/version.txt @@ -0,0 +1 @@ +0.1.0 diff --git a/test/OpenFeature.Contrib.Providers.Schematic.Test/OpenFeature.Contrib.Providers.Schematic.Test.csproj b/test/OpenFeature.Contrib.Providers.Schematic.Test/OpenFeature.Contrib.Providers.Schematic.Test.csproj new file mode 100644 index 00000000..34c226c8 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Schematic.Test/OpenFeature.Contrib.Providers.Schematic.Test.csproj @@ -0,0 +1,9 @@ + + + latest + + + + + + diff --git a/test/OpenFeature.Contrib.Providers.Schematic.Test/SchematicProviderTest.cs b/test/OpenFeature.Contrib.Providers.Schematic.Test/SchematicProviderTest.cs new file mode 100644 index 00000000..585355f5 --- /dev/null +++ b/test/OpenFeature.Contrib.Providers.Schematic.Test/SchematicProviderTest.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; +using OpenFeature; +using OpenFeature.Model; +using OpenFeature.Contrib.Providers.Schematic; + +namespace OpenFeature.Contrib.Providers.Schematic.Tests +{ + public class SchematicProviderTests + { + private SchematicFeatureProvider CreateProvider(ClientOptions options = null) + { + options ??= new ClientOptions { Offline = true }; + return new SchematicFeatureProvider("dummy-api-key", options); + } + + [Fact] + public void get_metadata_returns_correct_name() + { + var provider = CreateProvider(); + var metadata = provider.GetMetadata(); + Assert.NotNull(metadata); + Assert.Equal("schematic-provider", metadata.Name); + } + + [Fact] + public async Task resolve_boolean_returns_correct_value_from_flag_defaults() + { + var options = new ClientOptions + { + Offline = true, + FlagDefaults = new Dictionary + { + { "test_flag", true } + } + }; + + var provider = CreateProvider(options); + var result = await provider.ResolveBooleanValueAsync("test_flag", false); + Assert.True(result.Value); + Assert.Equal("on", result.Variant); + Assert.Equal("schematic evaluation", result.Reason); + } + + [Fact] + public async Task resolve_boolean_returns_default_when_flag_not_set() + { + // when no flag default is set, schematic returns false. + var options = new ClientOptions { Offline = true }; + var provider = CreateProvider(options); + var result = await provider.ResolveBooleanValueAsync("nonexistent_flag", true); + Assert.False(result.Value); + Assert.Equal("off", result.Variant); + Assert.Equal("schematic evaluation", result.Reason); + } + + [Fact] + public async Task resolve_string_returns_unsupported_type() + { + var provider = CreateProvider(); + var result = await provider.ResolveStringValueAsync("string_flag", "default"); + Assert.Equal("default", result.Value); + Assert.Equal("unsupported type", result.Reason); + } + + [Fact] + public async Task resolve_integer_returns_unsupported_type() + { + var provider = CreateProvider(); + var result = await provider.ResolveIntegerValueAsync("int_flag", 42); + Assert.Equal(42, result.Value); + Assert.Equal("unsupported type", result.Reason); + } + + [Fact] + public async Task resolve_double_returns_unsupported_type() + { + var provider = CreateProvider(); + var result = await provider.ResolveDoubleValueAsync("double_flag", 3.14); + Assert.Equal(3.14, result.Value); + Assert.Equal("unsupported type", result.Reason); + } + + [Fact] + public async Task resolve_structure_returns_unsupported_type() + { + var defaultObj = new Dictionary { { "key", "value" } }; + var provider = CreateProvider(); + var result = await provider.ResolveStructureValueAsync("object_flag", defaultObj); + Assert.Equal(defaultObj, result.Value); + Assert.Equal("unsupported type", result.Reason); + } + + [Fact] + public async Task track_event_completes_without_error() + { + var provider = CreateProvider(); + // build an evaluation context with sample data + var context = EvaluationContext.Builder() + .Set("company", new Dictionary { { "id", "your-company-id" } }) + .Set("user", new Dictionary { { "id", "your-user-id" } }) + .Build(); + + // tracking should complete without throwing + await provider.TrackEventAsync("test_event", context); + } + + [Fact] + public async Task shutdown_completes_without_error() + { + var provider = CreateProvider(); + await provider.ShutdownAsync(); + } + } +}