From 8cb5f6b9b683f004c34dea384453324947ee8460 Mon Sep 17 00:00:00 2001
From: Benjamin Evenson <2031163+benjiro@users.noreply.github.com>
Date: Sat, 6 Aug 2022 12:36:49 +1000
Subject: [PATCH] Feat: Add support for provider hooks
- [Breaking] Remove IFeatureProvider interface infavor of FeatureProvider abstract class
so default implementations can exist
- Make sure Provider hooks are called in the correct order
- Use strick mocking mode so sequence is validated correctly
- Update test cases for provider hooks support
Signed-off-by: Benjamin Evenson <2031163+benjiro@users.noreply.github.com>
---
...IFeatureProvider.cs => FeatureProvider.cs} | 29 ++-
.../Model/ResolutionDetails.cs | 2 +-
src/OpenFeature.SDK/NoOpProvider.cs | 14 +-
src/OpenFeature.SDK/OpenFeature.cs | 12 +-
src/OpenFeature.SDK/OpenFeatureClient.cs | 7 +-
.../ClearOpenFeatureInstanceFixture.cs | 13 ++
.../FeatureProviderTests.cs | 4 +-
.../OpenFeatureClientTests.cs | 62 +++++-
.../OpenFeatureHookTests.cs | 207 ++++++++++++------
.../OpenFeature.SDK.Tests/OpenFeatureTests.cs | 14 +-
.../TestImplementations.cs | 30 ++-
11 files changed, 268 insertions(+), 126 deletions(-)
rename src/OpenFeature.SDK/{IFeatureProvider.cs => FeatureProvider.cs} (70%)
create mode 100644 test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs
diff --git a/src/OpenFeature.SDK/IFeatureProvider.cs b/src/OpenFeature.SDK/FeatureProvider.cs
similarity index 70%
rename from src/OpenFeature.SDK/IFeatureProvider.cs
rename to src/OpenFeature.SDK/FeatureProvider.cs
index 5a463eb5..ef767b4a 100644
--- a/src/OpenFeature.SDK/IFeatureProvider.cs
+++ b/src/OpenFeature.SDK/FeatureProvider.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
using OpenFeature.SDK.Model;
@@ -8,13 +10,26 @@ namespace OpenFeature.SDK
/// A provider acts as the translates layer between the generic feature flag structure to a target feature flag system.
///
/// Provider specification
- public interface IFeatureProvider
+ public abstract class FeatureProvider
{
+ ///
+ /// Gets a immutable list of hooks that belong to the provider.
+ /// By default return a empty list
+ ///
+ /// Executed in the order of hooks
+ /// before: API, Client, Invocation, Provider
+ /// after: Provider, Invocation, Client, API
+ /// error (if applicable): Provider, Invocation, Client, API
+ /// finally: Provider, Invocation, Client, API
+ ///
+ ///
+ public virtual IReadOnlyList GetProviderHooks() => Array.Empty();
+
///
/// Metadata describing the provider.
///
///
- Metadata GetMetadata();
+ public abstract Metadata GetMetadata();
///
/// Resolves a boolean feature flag
@@ -24,7 +39,7 @@ public interface IFeatureProvider
///
///
///
- Task> ResolveBooleanValue(string flagKey, bool defaultValue,
+ public abstract Task> ResolveBooleanValue(string flagKey, bool defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
///
@@ -35,7 +50,7 @@ Task> ResolveBooleanValue(string flagKey, bool defaultVa
///
///
///
- Task> ResolveStringValue(string flagKey, string defaultValue,
+ public abstract Task> ResolveStringValue(string flagKey, string defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
///
@@ -46,7 +61,7 @@ Task> ResolveStringValue(string flagKey, string defaul
///
///
///
- Task> ResolveIntegerValue(string flagKey, int defaultValue,
+ public abstract Task> ResolveIntegerValue(string flagKey, int defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
///
@@ -57,7 +72,7 @@ Task> ResolveIntegerValue(string flagKey, int defaultValu
///
///
///
- Task> ResolveDoubleValue(string flagKey, double defaultValue,
+ public abstract Task> ResolveDoubleValue(string flagKey, double defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
///
@@ -69,7 +84,7 @@ Task> ResolveDoubleValue(string flagKey, double defaul
///
/// Type of object
///
- Task> ResolveStructureValue(string flagKey, T defaultValue,
+ public abstract Task> ResolveStructureValue(string flagKey, T defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
}
}
diff --git a/src/OpenFeature.SDK/Model/ResolutionDetails.cs b/src/OpenFeature.SDK/Model/ResolutionDetails.cs
index 98313f9e..4672327f 100644
--- a/src/OpenFeature.SDK/Model/ResolutionDetails.cs
+++ b/src/OpenFeature.SDK/Model/ResolutionDetails.cs
@@ -3,7 +3,7 @@
namespace OpenFeature.SDK.Model
{
///
- /// Defines the contract that the is required to return
+ /// Defines the contract that the is required to return
/// Describes the details of the feature flag being evaluated
///
/// Flag value type
diff --git a/src/OpenFeature.SDK/NoOpProvider.cs b/src/OpenFeature.SDK/NoOpProvider.cs
index 9ad298c2..0b5d621b 100644
--- a/src/OpenFeature.SDK/NoOpProvider.cs
+++ b/src/OpenFeature.SDK/NoOpProvider.cs
@@ -4,37 +4,37 @@
namespace OpenFeature.SDK
{
- internal class NoOpFeatureProvider : IFeatureProvider
+ internal class NoOpFeatureProvider : FeatureProvider
{
private readonly Metadata _metadata = new Metadata(NoOpProvider.NoOpProviderName);
- public Metadata GetMetadata()
+ public override Metadata GetMetadata()
{
return this._metadata;
}
- public Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
+ public override Task> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
- public Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
+ public override Task> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
- public Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
+ public override Task> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
- public Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
+ public override Task> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
- public Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
+ public override Task> ResolveStructureValue(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
diff --git a/src/OpenFeature.SDK/OpenFeature.cs b/src/OpenFeature.SDK/OpenFeature.cs
index 66f04612..8099299b 100644
--- a/src/OpenFeature.SDK/OpenFeature.cs
+++ b/src/OpenFeature.SDK/OpenFeature.cs
@@ -12,7 +12,7 @@ namespace OpenFeature.SDK
public sealed class OpenFeature
{
private EvaluationContext _evaluationContext = new EvaluationContext();
- private IFeatureProvider _featureProvider = new NoOpFeatureProvider();
+ private FeatureProvider _featureProvider = new NoOpFeatureProvider();
private readonly List _hooks = new List();
///
@@ -29,14 +29,14 @@ private OpenFeature() { }
///
/// Sets the feature provider
///
- /// Implementation of
- public void SetProvider(IFeatureProvider featureProvider) => this._featureProvider = featureProvider;
+ /// Implementation of
+ public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider;
///
/// Gets the feature provider
///
- ///
- public IFeatureProvider GetProvider() => this._featureProvider;
+ ///
+ public FeatureProvider GetProvider() => this._featureProvider;
///
/// Gets providers metadata
@@ -81,7 +81,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge
/// Sets the global
///
///
- public void SetContext(EvaluationContext context) => this._evaluationContext = context;
+ public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? new EvaluationContext();
///
/// Gets the global
diff --git a/src/OpenFeature.SDK/OpenFeatureClient.cs b/src/OpenFeature.SDK/OpenFeatureClient.cs
index 0a54df3d..35904823 100644
--- a/src/OpenFeature.SDK/OpenFeatureClient.cs
+++ b/src/OpenFeature.SDK/OpenFeatureClient.cs
@@ -17,19 +17,19 @@ namespace OpenFeature.SDK
public sealed class FeatureClient : IFeatureClient
{
private readonly ClientMetadata _metadata;
- private readonly IFeatureProvider _featureProvider;
+ private readonly FeatureProvider _featureProvider;
private readonly List _hooks = new List();
private readonly ILogger _logger;
///
/// Initializes a new instance of the class.
///
- /// Feature provider used by client
+ /// Feature provider used by client
/// Name of client
/// Version of client
/// Logger used by client
/// Throws if any of the required parameters are null
- public FeatureClient(IFeatureProvider featureProvider, string name, string version, ILogger logger = null)
+ public FeatureClient(FeatureProvider featureProvider, string name, string version, ILogger logger = null)
{
this._featureProvider = featureProvider ?? throw new ArgumentNullException(nameof(featureProvider));
this._metadata = new ClientMetadata(name, version);
@@ -209,6 +209,7 @@ private async Task> EvaluateFlag(
.Concat(OpenFeature.Instance.GetHooks())
.Concat(this._hooks)
.Concat(options?.Hooks ?? Enumerable.Empty())
+ .Concat(this._featureProvider.GetProviderHooks())
.ToList()
.AsReadOnly();
diff --git a/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs b/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs
new file mode 100644
index 00000000..3cf4bb5d
--- /dev/null
+++ b/test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs
@@ -0,0 +1,13 @@
+namespace OpenFeature.SDK.Tests
+{
+ public class ClearOpenFeatureInstanceFixture
+ {
+ // Make sure the singleton is cleared between tests
+ public ClearOpenFeatureInstanceFixture()
+ {
+ OpenFeature.Instance.SetContext(null);
+ OpenFeature.Instance.ClearHooks();
+ OpenFeature.Instance.SetProvider(new NoOpFeatureProvider());
+ }
+ }
+}
diff --git a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs
index 0a48205d..b92ccb6a 100644
--- a/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs
+++ b/test/OpenFeature.SDK.Tests/FeatureProviderTests.cs
@@ -9,7 +9,7 @@
namespace OpenFeature.SDK.Tests
{
- public class FeatureProviderTests
+ public class FeatureProviderTests : ClearOpenFeatureInstanceFixture
{
[Fact]
[Specification("2.1", "The provider interface MUST define a `metadata` member or accessor, containing a `name` field or accessor of type string, which identifies the provider implementation.")]
@@ -67,7 +67,7 @@ public async Task Provider_Must_ErrorType()
var defaultIntegerValue = fixture.Create();
var defaultDoubleValue = fixture.Create();
var defaultStructureValue = fixture.Create();
- var providerMock = new Mock();
+ var providerMock = new Mock(MockBehavior.Strict);
providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny(), It.IsAny()))
.ReturnsAsync(new ResolutionDetails(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant));
diff --git a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs
index 6319aa10..f10e2d2c 100644
--- a/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs
+++ b/test/OpenFeature.SDK.Tests/OpenFeatureClientTests.cs
@@ -14,7 +14,7 @@
namespace OpenFeature.SDK.Tests
{
- public class OpenFeatureClientTests
+ public class OpenFeatureClientTests : ClearOpenFeatureInstanceFixture
{
[Fact]
[Specification("1.2.1", "The client MUST provide a method to add `hooks` which accepts one or more API-conformant `hooks`, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")]
@@ -22,9 +22,9 @@ public void OpenFeatureClient_Should_Allow_Hooks()
{
var fixture = new Fixture();
var clientName = fixture.Create();
- var hook1 = new Mock().Object;
- var hook2 = new Mock().Object;
- var hook3 = new Mock().Object;
+ var hook1 = new Mock(MockBehavior.Strict).Object;
+ var hook2 = new Mock(MockBehavior.Strict).Object;
+ var hook3 = new Mock(MockBehavior.Strict).Object;
var client = OpenFeature.Instance.GetClient(clientName);
@@ -160,8 +160,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc
var clientVersion = fixture.Create();
var flagName = fixture.Create();
var defaultValue = fixture.Create();
- var mockedFeatureProvider = new Mock();
- var mockedLogger = new Mock>();
+ var mockedFeatureProvider = new Mock(MockBehavior.Strict);
+ var mockedLogger = new Mock>(MockBehavior.Default);
// This will fail to case a String to TestStructure
mockedFeatureProvider
@@ -169,6 +169,8 @@ public async Task OpenFeatureClient_Should_Return_DefaultValue_When_Type_Mismatc
.ReturnsAsync(new ResolutionDetails