Skip to content

Commit

Permalink
feat: implement in-memory provider
Browse files Browse the repository at this point in the history
Signed-off-by: Todd Baert <[email protected]>
Co-authored-by: Joris Goovaerts <[email protected]>
  • Loading branch information
toddbaert and CommCody committed Feb 8, 2024
1 parent 2c237df commit 28d22aa
Show file tree
Hide file tree
Showing 12 changed files with 356 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 = git@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.8.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;

namespace OpenFeature.Providers.Memory
{
/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
public class Flag
{

}

/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
public 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 defaultValue, EvaluationContext evaluationContext)
{
T value;
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");

Check warning on line 55 in src/OpenFeature/Providers/Memory/Flag.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/Flag.cs#L54-L55

Added lines #L54 - L55 were not covered by tests
}
}
else
{
string variant = this.ContextEvaluator.Invoke(evaluationContext);
this.Variants.TryGetValue(variant, out value);

Check warning on line 61 in src/OpenFeature/Providers/Memory/Flag.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/Flag.cs#L59-L61

Added lines #L59 - L61 were not covered by tests
if (value == null)
{
throw new GeneralException($"variant {variant} not found");

Check warning on line 64 in src/OpenFeature/Providers/Memory/Flag.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/Flag.cs#L63-L64

Added lines #L63 - L64 were not covered by tests
}
else
{
return new ResolutionDetails<T>(
flagKey,
value,
variant: variant,
reason: Reason.TargetingMatch
);

Check warning on line 73 in src/OpenFeature/Providers/Memory/Flag.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/Flag.cs#L67-L73

Added lines #L67 - L73 were not covered by tests
}
}
}
}
}
126 changes: 126 additions & 0 deletions src/OpenFeature/Providers/Memory/InMemoryProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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;

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>();
}

Check warning on line 39 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L37-L39

Added lines #L37 - L39 were not covered by tests
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)
{
if (flags is null)
throw new ArgumentNullException(nameof(flags));

Check warning on line 53 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L53

Added line #L53 was not covered by tests
this._flags = new Dictionary<string, Flag>(flags); // shallow copy
var @event = new ProviderEventPayload
{
Type = ProviderEventTypes.ProviderConfigurationChanged,
ProviderName = _metadata.Name,
FlagsChanged = flags.Keys.ToList(), // 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));
}

Check warning on line 90 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L88-L90

Added lines #L88 - L90 were not covered by tests

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

Check warning on line 99 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L97-L99

Added lines #L97 - L99 were not covered by tests

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

Check warning on line 108 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L106-L108

Added lines #L106 - L108 were not covered by tests

private ResolutionDetails<T> Resolve<T>(string flagKey, T defaultValue, EvaluationContext context)
{
if (!this._flags.TryGetValue(flagKey, out var flag))
{
throw new FlagNotFoundException($"flag {flag} not found");
}
else
{
if (typeof(Flag<T>).Equals(flag.GetType())) {
return (flag as Flag<T>).Evaluate(flagKey, defaultValue, context);
} else {
throw new TypeMismatchException($"flag {flag} not found");

Check warning on line 121 in src/OpenFeature/Providers/Memory/InMemoryProvider.cs

View check run for this annotation

Codecov / codecov/patch

src/OpenFeature/Providers/Memory/InMemoryProvider.cs#L120-L121

Added lines #L120 - L121 were not covered by tests
}
}
}
}
}
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
102 changes: 91 additions & 11 deletions test/OpenFeature.E2ETests/Steps/EvaluationStepDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using OpenFeature.Constant;
using OpenFeature.Contrib.Providers.Flagd;
using OpenFeature.Extension;
using OpenFeature.Model;
using OpenFeature.Providers.Memory;
using TechTalk.SpecFlow;
using Xunit;

Expand Down Expand Up @@ -41,15 +42,14 @@ public class EvaluationStepDefinitions
public EvaluationStepDefinitions(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
var flagdProvider = new FlagdProvider();
Api.Instance.SetProviderAsync(flagdProvider).Wait();
client = Api.Instance.GetClient();
}

[Given(@"a provider is registered with cache disabled")]
public void Givenaproviderisregisteredwithcachedisabled()
[Given(@"a provider is registered")]
public void GivenAProviderIsRegistered()
{

var memProvider = new InMemoryProvider(e2eFlagConfig);
Api.Instance.SetProviderAsync(memProvider).Wait();
client = Api.Instance.GetClient();
}

[When(@"a boolean flag with key ""(.*)"" is evaluated with default value ""(.*)""")]
Expand Down Expand Up @@ -247,7 +247,7 @@ public void Thenthedefaultstringvalueshouldbereturned()
public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateamissingflagwith(string errorCode)
{
Assert.Equal(Reason.Error.ToString(), notFoundDetails.Reason);
Assert.Contains(errorCode, notFoundDetails.ErrorMessage);
Assert.Equal(errorCode, notFoundDetails.ErrorType.GetDescription());
}

[When(@"a string flag with key ""(.*)"" is evaluated as an integer, with details and a default value (.*)")]
Expand All @@ -268,8 +268,88 @@ public void Thenthedefaultintegervalueshouldbereturned()
public void Giventhereasonshouldindicateanerrorandtheerrorcodeshouldindicateatypemismatchwith(string errorCode)
{
Assert.Equal(Reason.Error.ToString(), typeErrorDetails.Reason);
Assert.Contains(errorCode, this.typeErrorDetails.ErrorMessage);
}

Assert.Equal(errorCode, typeErrorDetails.ErrorType.GetDescription());
}

private IDictionary<string, Flag> e2eFlagConfig = new Dictionary<string, Flag>(){
{
"boolean-flag", new Flag<bool>(
variants: new Dictionary<string, bool>(){
{ "on", true },
{ "off", false }
},
defaultVariant: "on"
)
},
{
"string-flag", new Flag<string>(
variants: new Dictionary<string, string>(){
{ "greeting", "hi" },
{ "parting", "bye" }
},
defaultVariant: "greeting"
)
},
{
"integer-flag", new Flag<int>(
variants: new Dictionary<string, int>(){
{ "one", 1 },
{ "ten", 10 }
},
defaultVariant: "ten"
)
},
{
"float-flag", new Flag<double>(
variants: new Dictionary<string, double>(){
{ "tenth", 0.1 },
{ "half", 0.5 }
},
defaultVariant: "half"
)
},
{
"object-flag", new Flag<Value>(
variants: new Dictionary<string, Value>(){
{ "empty", new Value() },
{ "template", new Value(Structure.Builder()
.Set("showImages", true)
.Set("title", "Check out these pics!")
.Set("imagesPerPage", 100).Build()
)
}
},
defaultVariant: "template"
)
},
{
"context-aware", new Flag<string>(
variants: new Dictionary<string, string>(){
{ "internal", "INTERNAL" },
{ "external", "EXTERNAL" }
},
defaultVariant: "external",
(context) => {
if (context.GetValue("fn").AsString == "Sulisław"
&& context.GetValue("ln").AsString == "Świętopełk"
&& context.GetValue("age").AsInteger == 29
&& context.GetValue("customer").AsBoolean == false)
{
return "internal";
}
else return "external";
}
)
},
{
"wrong-flag", new Flag<string>(
variants: new Dictionary<string, string>(){
{ "one", "uno" },
{ "two", "dos" }
},
defaultVariant: "one"
)
}
};
}
}
Loading

0 comments on commit 28d22aa

Please sign in to comment.