Skip to content

Commit 1082094

Browse files
toddbaertCommCodyaskpt
authored
feat: implement in-memory provider (#232)
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]>
1 parent 1d523cf commit 1082094

File tree

12 files changed

+554
-29
lines changed

12 files changed

+554
-29
lines changed

.github/workflows/e2e.yml

+1-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ on:
1313
jobs:
1414
e2e-tests:
1515
runs-on: ubuntu-latest
16-
services:
17-
flagd:
18-
image: ghcr.io/open-feature/flagd-testbed:latest
19-
ports:
20-
- 8013:8013
2116
steps:
2217
- uses: actions/checkout@v4
2318
with:
@@ -36,7 +31,7 @@ jobs:
3631
- name: Initialize Tests
3732
run: |
3833
git submodule update --init --recursive
39-
cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
34+
cp spec/specification/assets/gherkin/evaluation.feature test/OpenFeature.E2ETests/Features/
4035
4136
- name: Run Tests
4237
run: dotnet test test/OpenFeature.E2ETests/ --configuration Release --logger GitHubActions

.gitmodules

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
[submodule "test-harness"]
2-
path = test-harness
3-
url = https://github.com/open-feature/test-harness.git
1+
[submodule "spec"]
2+
path = spec
3+
url = https://github.com/open-feature/spec.git

CONTRIBUTING.md

-6
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,6 @@ To be able to run the e2e tests, first we need to initialize the submodule and c
6767
git submodule update --init --recursive && cp test-harness/features/evaluation.feature test/OpenFeature.E2ETests/Features/
6868
```
6969

70-
Afterwards, you need to start flagd locally:
71-
72-
```bash
73-
docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest
74-
```
75-
7670
Now you can run the tests using:
7771

7872
```bash

Directory.Packages.props

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" />
2121
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
2222
<PackageVersion Include="NSubstitute" Version="5.1.0" />
23-
<PackageVersion Include="OpenFeature.Contrib.Providers.Flagd" Version="0.1.8" />
2423
<PackageVersion Include="SpecFlow" Version="3.9.74" />
2524
<PackageVersion Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.74" />
2625
<PackageVersion Include="SpecFlow.xUnit" Version="3.9.74" />

spec

Submodule spec added at b58c3b4
File renamed without changes.
+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using OpenFeature.Constant;
4+
using OpenFeature.Error;
5+
using OpenFeature.Model;
6+
7+
#nullable enable
8+
namespace OpenFeature.Providers.Memory
9+
{
10+
/// <summary>
11+
/// Flag representation for the in-memory provider.
12+
/// </summary>
13+
public interface Flag
14+
{
15+
16+
}
17+
18+
/// <summary>
19+
/// Flag representation for the in-memory provider.
20+
/// </summary>
21+
public sealed class Flag<T> : Flag
22+
{
23+
private Dictionary<string, T> Variants;
24+
private string DefaultVariant;
25+
private Func<EvaluationContext, string>? ContextEvaluator;
26+
27+
/// <summary>
28+
/// Flag representation for the in-memory provider.
29+
/// </summary>
30+
/// <param name="variants">dictionary of variants and their corresponding values</param>
31+
/// <param name="defaultVariant">default variant (should match 1 key in variants dictionary)</param>
32+
/// <param name="contextEvaluator">optional context-sensitive evaluation function</param>
33+
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null)
34+
{
35+
this.Variants = variants;
36+
this.DefaultVariant = defaultVariant;
37+
this.ContextEvaluator = contextEvaluator;
38+
}
39+
40+
internal ResolutionDetails<T> Evaluate(string flagKey, T _, EvaluationContext? evaluationContext)
41+
{
42+
T? value = default;
43+
if (this.ContextEvaluator == null)
44+
{
45+
if (this.Variants.TryGetValue(this.DefaultVariant, out value))
46+
{
47+
return new ResolutionDetails<T>(
48+
flagKey,
49+
value,
50+
variant: this.DefaultVariant,
51+
reason: Reason.Static
52+
);
53+
}
54+
else
55+
{
56+
throw new GeneralException($"variant {this.DefaultVariant} not found");
57+
}
58+
}
59+
else
60+
{
61+
var variant = this.ContextEvaluator.Invoke(evaluationContext ?? EvaluationContext.Empty);
62+
if (!this.Variants.TryGetValue(variant, out value))
63+
{
64+
throw new GeneralException($"variant {variant} not found");
65+
}
66+
else
67+
{
68+
return new ResolutionDetails<T>(
69+
flagKey,
70+
value,
71+
variant: variant,
72+
reason: Reason.TargetingMatch
73+
);
74+
}
75+
}
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using OpenFeature.Constant;
7+
using OpenFeature.Error;
8+
using OpenFeature.Model;
9+
10+
#nullable enable
11+
namespace OpenFeature.Providers.Memory
12+
{
13+
/// <summary>
14+
/// The in memory provider.
15+
/// Useful for testing and demonstration purposes.
16+
/// </summary>
17+
/// <seealso href="https://openfeature.dev/specification/appendix-a#in-memory-provider">In Memory Provider specification</seealso>
18+
public class InMemoryProvider : FeatureProvider
19+
{
20+
21+
private readonly Metadata _metadata = new Metadata("InMemory");
22+
23+
private Dictionary<string, Flag> _flags;
24+
25+
/// <inheritdoc/>
26+
public override Metadata GetMetadata()
27+
{
28+
return this._metadata;
29+
}
30+
31+
/// <summary>
32+
/// Construct a new InMemoryProvider.
33+
/// </summary>
34+
/// <param name="flags">dictionary of Flags</param>
35+
public InMemoryProvider(IDictionary<string, Flag>? flags = null)
36+
{
37+
if (flags == null)
38+
{
39+
this._flags = new Dictionary<string, Flag>();
40+
}
41+
else
42+
{
43+
this._flags = new Dictionary<string, Flag>(flags); // shallow copy
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Updating provider flags configuration, replacing all flags.
49+
/// </summary>
50+
/// <param name="flags">the flags to use instead of the previous flags.</param>
51+
public async ValueTask UpdateFlags(IDictionary<string, Flag>? flags = null)
52+
{
53+
var changed = this._flags.Keys.ToList();
54+
if (flags == null)
55+
{
56+
this._flags = new Dictionary<string, Flag>();
57+
}
58+
else
59+
{
60+
this._flags = new Dictionary<string, Flag>(flags); // shallow copy
61+
}
62+
changed.AddRange(this._flags.Keys.ToList());
63+
var @event = new ProviderEventPayload
64+
{
65+
Type = ProviderEventTypes.ProviderConfigurationChanged,
66+
ProviderName = _metadata.Name,
67+
FlagsChanged = changed, // emit all
68+
Message = "flags changed",
69+
};
70+
await this.EventChannel.Writer.WriteAsync(@event).ConfigureAwait(false);
71+
}
72+
73+
/// <inheritdoc/>
74+
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(
75+
string flagKey,
76+
bool defaultValue,
77+
EvaluationContext? context = null)
78+
{
79+
return Task.FromResult(Resolve(flagKey, defaultValue, context));
80+
}
81+
82+
/// <inheritdoc/>
83+
public override Task<ResolutionDetails<string>> ResolveStringValue(
84+
string flagKey,
85+
string defaultValue,
86+
EvaluationContext? context = null)
87+
{
88+
return Task.FromResult(Resolve(flagKey, defaultValue, context));
89+
}
90+
91+
/// <inheritdoc/>
92+
public override Task<ResolutionDetails<int>> ResolveIntegerValue(
93+
string flagKey,
94+
int defaultValue,
95+
EvaluationContext? context = null)
96+
{
97+
return Task.FromResult(Resolve(flagKey, defaultValue, context));
98+
}
99+
100+
/// <inheritdoc/>
101+
public override Task<ResolutionDetails<double>> ResolveDoubleValue(
102+
string flagKey,
103+
double defaultValue,
104+
EvaluationContext? context = null)
105+
{
106+
return Task.FromResult(Resolve(flagKey, defaultValue, context));
107+
}
108+
109+
/// <inheritdoc/>
110+
public override Task<ResolutionDetails<Value>> ResolveStructureValue(
111+
string flagKey,
112+
Value defaultValue,
113+
EvaluationContext? context = null)
114+
{
115+
return Task.FromResult(Resolve(flagKey, defaultValue, context));
116+
}
117+
118+
private ResolutionDetails<T> Resolve<T>(string flagKey, T defaultValue, EvaluationContext? context)
119+
{
120+
if (!this._flags.TryGetValue(flagKey, out var flag))
121+
{
122+
throw new FlagNotFoundException($"flag {flagKey} not found");
123+
}
124+
else
125+
{
126+
// This check returns False if a floating point flag is evaluated as an integer flag, and vice-versa.
127+
// In a production provider, such behavior is probably not desirable; consider supporting conversion.
128+
if (typeof(Flag<T>).Equals(flag.GetType()))
129+
{
130+
return ((Flag<T>)flag).Evaluate(flagKey, defaultValue, context);
131+
}
132+
else
133+
{
134+
throw new TypeMismatchException($"flag {flagKey} is not of type ${typeof(T)}");
135+
}
136+
}
137+
}
138+
}
139+
}

test-harness

-1
This file was deleted.

test/OpenFeature.E2ETests/OpenFeature.E2ETests.csproj

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
<PrivateAssets>all</PrivateAssets>
1717
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1818
</PackageReference>
19-
<PackageReference Include="OpenFeature.Contrib.Providers.Flagd" />
2019
<PackageReference Include="SpecFlow" />
2120
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" />
2221
<PackageReference Include="SpecFlow.xUnit" />

0 commit comments

Comments
 (0)