Skip to content

Commit

Permalink
feat: implement in-memory provider (open-feature#232)
Browse files Browse the repository at this point in the history
Implements in-memory provider as per spec, updates gherkin to use spec
version, removes flagd deps.

Signed-off-by: Todd Baert <[email protected]>
Co-authored-by: Joris Goovaerts <[email protected]>
Co-authored-by: André Silva <[email protected]>
Signed-off-by: Artyom Tonoyan <[email protected]>
  • Loading branch information
3 people authored and arttonoyan committed Nov 17, 2024
1 parent d2c895b commit 0449d1c
Show file tree
Hide file tree
Showing 12 changed files with 554 additions and 29 deletions.
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
File renamed without changes.
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;

#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;

#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
{

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()))
{
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

0 comments on commit 0449d1c

Please sign in to comment.