Skip to content

Commit

Permalink
Merge branch 'main' into askpt/192-feature-implement-flag-metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
toddbaert committed Feb 10, 2024
2 parents 2b2a64f + 1082094 commit adf885a
Show file tree
Hide file tree
Showing 17 changed files with 596 additions and 70 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
13 changes: 6 additions & 7 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,19 @@
</ItemGroup>

<ItemGroup Label="test">
<PackageVersion Include="AutoFixture" Version="4.17.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.1" />
<PackageVersion Include="coverlet.collector" Version="3.1.2" />
<PackageVersion Include="coverlet.msbuild" Version="3.1.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="GitHubActionsTestLogger" Version="2.3.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<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" />
<PackageVersion Include="xunit" Version="[2.4.1, 3.0)" />
<PackageVersion Include="xunit.runner.visualstudio" Version="[2.4.3, 3.0)" />
<PackageVersion Include="xunit" Version="2.6.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
</ItemGroup>

<ItemGroup Condition="'$(OS)' == 'Unix'">
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ bool flagValue = await client.GetBooleanValue("some-flag", false, reqCtx);
### Hooks

[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle.
Here is [a complete list of available hooks](https://openfeature.dev/docs/reference/technologies/server/dotnet/).
Look [here](https://openfeature.dev/ecosystem/?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Bcategory%5D%5B0%5D=Server-side&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=.NET) for a complete list of available hooks.
If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.

Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
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 adf885a

Please sign in to comment.