Skip to content

Commit

Permalink
Feat: Add support for provider hooks
Browse files Browse the repository at this point in the history
- [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 <[email protected]>
  • Loading branch information
benjiro committed Aug 9, 2022
1 parent aae53c6 commit 8cb5f6b
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using OpenFeature.SDK.Model;

Expand All @@ -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.
/// </summary>
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/providers.md">Provider specification</seealso>
public interface IFeatureProvider
public abstract class FeatureProvider
{
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
public virtual IReadOnlyList<Hook> GetProviderHooks() => Array.Empty<Hook>();

/// <summary>
/// Metadata describing the provider.
/// </summary>
/// <returns><see cref="Metadata"/></returns>
Metadata GetMetadata();
public abstract Metadata GetMetadata();

/// <summary>
/// Resolves a boolean feature flag
Expand All @@ -24,7 +39,7 @@ public interface IFeatureProvider
/// <param name="context"><see cref="EvaluationContext"/></param>
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
/// <returns><see cref="ResolutionDetails{T}"/></returns>
Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
public abstract Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);

/// <summary>
Expand All @@ -35,7 +50,7 @@ Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultVa
/// <param name="context"><see cref="EvaluationContext"/></param>
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
/// <returns><see cref="ResolutionDetails{T}"/></returns>
Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
public abstract Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);

/// <summary>
Expand All @@ -46,7 +61,7 @@ Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaul
/// <param name="context"><see cref="EvaluationContext"/></param>
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
/// <returns><see cref="ResolutionDetails{T}"/></returns>
Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
public abstract Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);

/// <summary>
Expand All @@ -57,7 +72,7 @@ Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValu
/// <param name="context"><see cref="EvaluationContext"/></param>
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
/// <returns><see cref="ResolutionDetails{T}"/></returns>
Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
public abstract Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);

/// <summary>
Expand All @@ -69,7 +84,7 @@ Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaul
/// <param name="config"><see cref="FlagEvaluationOptions"/></param>
/// <typeparam name="T">Type of object</typeparam>
/// <returns><see cref="ResolutionDetails{T}"/></returns>
Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue,
public abstract Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue,
EvaluationContext context = null, FlagEvaluationOptions config = null);
}
}
2 changes: 1 addition & 1 deletion src/OpenFeature.SDK/Model/ResolutionDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace OpenFeature.SDK.Model
{
/// <summary>
/// Defines the contract that the <see cref="IFeatureProvider"/> is required to return
/// Defines the contract that the <see cref="FeatureProvider"/> is required to return
/// Describes the details of the feature flag being evaluated
/// </summary>
/// <typeparam name="T">Flag value type</typeparam>
Expand Down
14 changes: 7 additions & 7 deletions src/OpenFeature.SDK/NoOpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}

public Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}

public Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}

public Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue, EvaluationContext context = null,
FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}

public Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
public override Task<ResolutionDetails<T>> ResolveStructureValue<T>(string flagKey, T defaultValue, EvaluationContext context = null, FlagEvaluationOptions config = null)
{
return Task.FromResult(NoOpResponse(flagKey, defaultValue));
}
Expand Down
12 changes: 6 additions & 6 deletions src/OpenFeature.SDK/OpenFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hook> _hooks = new List<Hook>();

/// <summary>
Expand All @@ -29,14 +29,14 @@ private OpenFeature() { }
/// <summary>
/// Sets the feature provider
/// </summary>
/// <param name="featureProvider">Implementation of <see cref="IFeatureProvider"/></param>
public void SetProvider(IFeatureProvider featureProvider) => this._featureProvider = featureProvider;
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
public void SetProvider(FeatureProvider featureProvider) => this._featureProvider = featureProvider;

/// <summary>
/// Gets the feature provider
/// </summary>
/// <returns><see cref="IFeatureProvider"/></returns>
public IFeatureProvider GetProvider() => this._featureProvider;
/// <returns><see cref="FeatureProvider"/></returns>
public FeatureProvider GetProvider() => this._featureProvider;

/// <summary>
/// Gets providers metadata
Expand Down Expand Up @@ -81,7 +81,7 @@ public FeatureClient GetClient(string name = null, string version = null, ILogge
/// Sets the global <see cref="EvaluationContext"/>
/// </summary>
/// <param name="context"></param>
public void SetContext(EvaluationContext context) => this._evaluationContext = context;
public void SetContext(EvaluationContext context) => this._evaluationContext = context ?? new EvaluationContext();

/// <summary>
/// Gets the global <see cref="EvaluationContext"/>
Expand Down
7 changes: 4 additions & 3 deletions src/OpenFeature.SDK/OpenFeatureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hook> _hooks = new List<Hook>();
private readonly ILogger _logger;

/// <summary>
/// Initializes a new instance of the <see cref="FeatureClient"/> class.
/// </summary>
/// <param name="featureProvider">Feature provider used by client <see cref="IFeatureProvider"/></param>
/// <param name="featureProvider">Feature provider used by client <see cref="FeatureProvider"/></param>
/// <param name="name">Name of client <see cref="ClientMetadata"/></param>
/// <param name="version">Version of client <see cref="ClientMetadata"/></param>
/// <param name="logger">Logger used by client</param>
/// <exception cref="ArgumentNullException">Throws if any of the required parameters are null</exception>
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);
Expand Down Expand Up @@ -209,6 +209,7 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlag<T>(
.Concat(OpenFeature.Instance.GetHooks())
.Concat(this._hooks)
.Concat(options?.Hooks ?? Enumerable.Empty<Hook>())
.Concat(this._featureProvider.GetProviderHooks())
.ToList()
.AsReadOnly();

Expand Down
13 changes: 13 additions & 0 deletions test/OpenFeature.SDK.Tests/ClearOpenFeatureInstanceFixture.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
4 changes: 2 additions & 2 deletions test/OpenFeature.SDK.Tests/FeatureProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.")]
Expand Down Expand Up @@ -67,7 +67,7 @@ public async Task Provider_Must_ErrorType()
var defaultIntegerValue = fixture.Create<int>();
var defaultDoubleValue = fixture.Create<double>();
var defaultStructureValue = fixture.Create<TestStructure>();
var providerMock = new Mock<IFeatureProvider>();
var providerMock = new Mock<FeatureProvider>(MockBehavior.Strict);

providerMock.Setup(x => x.ResolveBooleanValue(flagName, defaultBoolValue, It.IsAny<EvaluationContext>(), It.IsAny<FlagEvaluationOptions>()))
.ReturnsAsync(new ResolutionDetails<bool>(flagName, defaultBoolValue, ErrorType.General, NoOpProvider.ReasonNoOp, NoOpProvider.Variant));
Expand Down
Loading

0 comments on commit 8cb5f6b

Please sign in to comment.