Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement in-memory provider #232

Merged
merged 16 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ on:
jobs:
e2e-tests:
runs-on: ubuntu-latest
services:
flagd:
image: ghcr.io/open-feature/flagd-testbed:latest
ports:
- 8013:8013
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -36,7 +31,7 @@ jobs:
- name: Initialize Tests
run: |
git submodule update --init --recursive
cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/

- name: Run Tests
run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "test-harness"]
path = test-harness
url = https://github.com/open-feature/test-harness.git
[submodule "spec"]
path = spec
url = https://github.com/open-feature/spec.git
6 changes: 0 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,6 @@ To be able to run the e2e tests, first we need to initialize the submodule and c
git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
```

Afterwards, you need to start flagd locally:

```bash
docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest
```

Now you can run the tests using:

```bash
Expand Down
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="OpenFeature.Contrib.Providers.Flagd" Version="0.1.8" />
<PackageVersion Include="SpecFlow" Version="3.9.74" />
<PackageVersion Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.74" />
<PackageVersion Include="SpecFlow.xUnit" Version="3.9.74" />
Expand Down
1 change: 1 addition & 0 deletions spec
Submodule spec added at b58c3b
78 changes: 78 additions & 0 deletions src/OpenFeature/Providers/Memory/Flag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;

toddbaert marked this conversation as resolved.
Show resolved Hide resolved
#nullable enable
namespace OpenFeature.Providers.Memory
{
/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
public interface Flag
{

}

/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
public sealed class Flag<T> : Flag
{
private Dictionary<string, T> Variants;
private string DefaultVariant;
private Func<EvaluationContext, string>? ContextEvaluator;

/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
/// <param name="variants">dictionary of variants and their corresponding values</param>
/// <param name="defaultVariant">default variant (should match 1 key in variants dictionary)</param>
/// <param name="contextEvaluator">optional context-sensitive evaluation function</param>
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null)
{
this.Variants = variants;
this.DefaultVariant = defaultVariant;
this.ContextEvaluator = contextEvaluator;
}

internal ResolutionDetails<T> Evaluate(string flagKey, T _, EvaluationContext? evaluationContext)
{
T? value = default;
if (this.ContextEvaluator == null)
{
if (this.Variants.TryGetValue(this.DefaultVariant, out value))
{
return new ResolutionDetails<T>(
flagKey,
value,
variant: this.DefaultVariant,
reason: Reason.Static
);
}
else
{
throw new GeneralException($"variant {this.DefaultVariant} not found");
}
}
else
{
var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty);
if (!this.Variants.TryGetValue(variant, out value))
{
throw new GeneralException($"variant {variant} not found");
}
else
{
return new ResolutionDetails<T>(
flagKey,
value,
variant: variant,
reason: Reason.TargetingMatch
);
}
}
}
}
}
139 changes: 139 additions & 0 deletions src/OpenFeature/Providers/Memory/InMemoryProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;

toddbaert marked this conversation as resolved.
Show resolved Hide resolved
#nullable enable
namespace OpenFeature.Providers.Memory
{
/// <summary>
/// The in memory provider.
/// Useful for testing and demonstration purposes.
/// </summary>
/// <seealso href="https://openfeature.dev/specification/appendix-a#in-memory-provider">In Memory Provider specification</seealso>
public class InMemoryProvider : FeatureProvider
askpt marked this conversation as resolved.
Show resolved Hide resolved
{

private readonly Metadata _metadata = new Metadata("InMemory");

private Dictionary<string, Flag> _flags;

/// <inheritdoc/>
public override Metadata GetMetadata()
{
return this._metadata;
}

/// <summary>
/// Construct a new InMemoryProvider.
/// </summary>
/// <param name="flags">dictionary of Flags</param>
public InMemoryProvider(IDictionary<string, Flag>? flags = null)
{
if (flags == null)
{
this._flags = new Dictionary<string, Flag>();
}
else
{
this._flags = new Dictionary<string, Flag>(flags); // shallow copy
}
}

/// <summary>
/// Updating provider flags configuration, replacing all flags.
/// </summary>
/// <param name="flags">the flags to use instead of the previous flags.</param>
public async ValueTask UpdateFlags(IDictionary<string, Flag>? flags = null)
{
var changed = this._flags.Keys.ToList();
if (flags == null)
{
this._flags = new Dictionary<string, Flag>();
}
else
{
this._flags = new Dictionary<string, Flag>(flags); // shallow copy
}
changed.AddRange(this._flags.Keys.ToList());
var @event = new ProviderEventPayload
{
Type = ProviderEventTypes.ProviderConfigurationChanged,
ProviderName = _metadata.Name,
FlagsChanged = changed, // emit all
Message = "flags changed",
};
await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false);
}

/// <inheritdoc/>
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(
string flagKey,
bool defaultValue,
EvaluationContext? context = null)
{
return Task.FromResult(Resolve(flagKey, defaultValue, context));
}

/// <inheritdoc/>
public override Task<ResolutionDetails<string>> ResolveStringValue(
string flagKey,
string defaultValue,
EvaluationContext? context = null)
{
return Task.FromResult(Resolve(flagKey, defaultValue, context));
}

/// <inheritdoc/>
public override Task<ResolutionDetails<int>> ResolveIntegerValue(
string flagKey,
int defaultValue,
EvaluationContext? context = null)
{
return Task.FromResult(Resolve(flagKey, defaultValue, context));
}

/// <inheritdoc/>
public override Task<ResolutionDetails<double>> ResolveDoubleValue(
string flagKey,
double defaultValue,
EvaluationContext? context = null)
{
return Task.FromResult(Resolve(flagKey, defaultValue, context));
}

/// <inheritdoc/>
public override Task<ResolutionDetails<Value>> ResolveStructureValue(
string flagKey,
Value defaultValue,
EvaluationContext? context = null)
{
return Task.FromResult(Resolve(flagKey, defaultValue, context));
}

private ResolutionDetails<T> Resolve<T>(string flagKey, T defaultValue, EvaluationContext? context)
{
if (!this._flags.TryGetValue(flagKey, out var flag))
{
throw new FlagNotFoundException($"flag {flagKey} not found");
}
else
{
// This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa.
// In a production provider, such behavior is probably not desirable; consider supporting conversion.
if (typeof(Flag<T>).Equals(flag.GetType()))
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
{
return ((Flag<T>)flag).Evaluate(flagKey, defaultValue, context);
}
else
{
throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}");
}
}
}
}
}
1 change: 0 additions & 1 deletion test-harness
Submodule test-harness deleted from 01c4a4
1 change: 0 additions & 1 deletion test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OpenFeature.Contrib.Providers.Flagd" />
<PackageReference Include="SpecFlow" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" />
<PackageReference Include="SpecFlow.xUnit" />
Expand Down
Loading