Skip to content
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
5 changes: 5 additions & 0 deletions samples/AspNetCore/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
"welcome-message", new Flag<bool>(
new Dictionary<string, bool> { { "show", true }, { "hide", false } }, "show")
},
{
"disabled-flag", new Flag<string>(
new Dictionary<string, string> { { "on", "This flag is on" }, { "off", "This flag is off" } }, "off",
disabled: true)
},
{
"test-config", new Flag<Value>(new Dictionary<string, Value>()
{
Expand Down
29 changes: 26 additions & 3 deletions src/OpenFeature/Providers/Memory/Flag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ namespace OpenFeature.Providers.Memory;
/// <summary>
/// Flag representation for the in-memory provider.
/// </summary>
public interface Flag;
public interface Flag
{
/// <summary>
/// Indicates if the flag is disabled. When disabled, the flag will resolve to the default value.
/// </summary>
bool Disabled { get; }
}

/// <summary>
/// Flag representation for the in-memory provider.
Expand All @@ -26,16 +32,33 @@ public sealed class Flag<T> : Flag
/// <param name="defaultVariant">default variant (should match 1 key in variants dictionary)</param>
/// <param name="contextEvaluator">optional context-sensitive evaluation function</param>
/// <param name="flagMetadata">optional metadata for the flag</param>
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null, ImmutableMetadata? flagMetadata = null)
/// <param name="disabled">indicates if the flag is disabled</param>
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null, ImmutableMetadata? flagMetadata = null, bool disabled = false)
{
this._variants = variants;
this._defaultVariant = defaultVariant;
this._contextEvaluator = contextEvaluator;
this._flagMetadata = flagMetadata;
this.Disabled = disabled;
}

internal ResolutionDetails<T> Evaluate(string flagKey, T _, EvaluationContext? evaluationContext)
/// <summary>
/// Indicates if the flag is disabled. When disabled, the flag will resolve to the default value.
/// </summary>
public bool Disabled { get; }

internal ResolutionDetails<T> Evaluate(string flagKey, T defaultValue, EvaluationContext? evaluationContext)
{
if (this.Disabled)
{
return new ResolutionDetails<T>(
flagKey,
defaultValue,
reason: Reason.Disabled,
flagMetadata: this._flagMetadata
);
}

if (this._contextEvaluator == null)
{
return this.EvaluateDefaultVariant(flagKey);
Expand Down
1 change: 0 additions & 1 deletion test/OpenFeature.E2ETests/Steps/ExcludedTagsStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ namespace OpenFeature.E2ETests.Steps;
[Scope(Tag = "immutability")]
[Scope(Tag = "async")]
[Scope(Tag = "reason-codes-cached")]
[Scope(Tag = "reason-codes-disabled")]
[Scope(Tag = "deprecated")]
public class ExcludedTagsStep
{
Expand Down
19 changes: 10 additions & 9 deletions test/OpenFeature.E2ETests/Utils/FlagDictionaryJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private static Flag ReadFlag(string flagKey, JsonElement flagElement)
if (inferredKind == null)
throw new JsonException($"Flag '{flagKey}' has no variants");

var defaultVariant = InferDefaultVariant(flagElement, variantsElement);
var defaultVariant = InferDefaultVariant(flagElement);

var contextEvaluator = flagElement.TryGetProperty("contextEvaluator", out var ctxElem) && ctxElem.ValueKind == JsonValueKind.String
? ContextEvaluatorUtility.BuildContextEvaluator(ctxElem.GetString()!)
Expand All @@ -60,15 +60,15 @@ private static Flag ReadFlag(string flagKey, JsonElement flagElement)
? BuildMetadata(metaElem)
: null;

// NOTE: The current Flag<T> type does not model 'disabled'
var disabled = flagElement.TryGetProperty("disabled", out var disabledElem) && disabledElem.ValueKind == JsonValueKind.True;

return inferredKind switch
{
VariantKind.Boolean => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetBoolean()),
VariantKind.Integer => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetInt32()),
VariantKind.Double => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetDouble()),
VariantKind.String => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetString()!),
VariantKind.Object => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, ExtractObjectVariant),
VariantKind.Boolean => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetBoolean()),
VariantKind.Integer => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetInt32()),
VariantKind.Double => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetDouble()),
VariantKind.String => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetString()!),
VariantKind.Object => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, ExtractObjectVariant),
_ => throw new JsonException($"Unsupported variant kind for flag '{flagKey}'")
};
}
Expand All @@ -81,17 +81,18 @@ private static Flag<T> BuildFlag<T>(
string? defaultVariant,
Func<EvaluationContext, string>? contextEvaluator,
ImmutableMetadata? metadata,
bool? disabled,
Func<JsonElement, T> projector)
{
var dict = new Dictionary<string, T>(StringComparer.Ordinal);
foreach (var v in variantsElement.EnumerateObject())
{
dict[v.Name] = projector(v.Value);
}
return new Flag<T>(dict, defaultVariant!, contextEvaluator, metadata);
return new Flag<T>(dict, defaultVariant!, contextEvaluator, metadata, disabled ?? false);
}

private static string? InferDefaultVariant(JsonElement flagElement, JsonElement variantsElement)
private static string? InferDefaultVariant(JsonElement flagElement)
{
if (flagElement.TryGetProperty("defaultVariant", out var dv))
{
Expand Down
122 changes: 122 additions & 0 deletions test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ public InMemoryProviderTests()
defaultVariant: "on"
)
},
{
"boolean-disabled-flag", new Flag<bool>(
disabled: true,
variants: new Dictionary<string, bool>(){
{ "on", true },
{ "off", false }
},
defaultVariant: "on"
)
},
{
"string-flag", new Flag<string>(
variants: new Dictionary<string, string>(){
Expand All @@ -30,6 +40,16 @@ public InMemoryProviderTests()
defaultVariant: "greeting"
)
},
{
"string-disabled-flag", new Flag<string>(
disabled: true,
variants: new Dictionary<string, string>(){
{ "greeting", "hi" },
{ "parting", "bye" }
},
defaultVariant: "greeting"
)
},
{
"integer-flag", new Flag<int>(
variants: new Dictionary<string, int>(){
Expand All @@ -39,6 +59,16 @@ public InMemoryProviderTests()
defaultVariant: "ten"
)
},
{
"integer-disabled-flag", new Flag<int>(
disabled: true,
variants: new Dictionary<string, int>(){
{ "one", 1 },
{ "ten", 10 }
},
defaultVariant: "ten"
)
},
{
"float-flag", new Flag<double>(
variants: new Dictionary<string, double>(){
Expand All @@ -48,6 +78,16 @@ public InMemoryProviderTests()
defaultVariant: "half"
)
},
{
"float-disabled-flag", new Flag<double>(
disabled: true,
variants: new Dictionary<string, double>(){
{ "tenth", 0.1 },
{ "half", 0.5 }
},
defaultVariant: "half"
)
},
{
"context-aware", new Flag<string>(
variants: new Dictionary<string, string>(){
Expand Down Expand Up @@ -78,6 +118,21 @@ public InMemoryProviderTests()
defaultVariant: "template"
)
},
{
"object-disabled-flag", new Flag<Value>(
disabled: true,
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"
)
},
{
"invalid-flag", new Flag<bool>(
variants: new Dictionary<string, bool>(){
Expand Down Expand Up @@ -137,6 +192,18 @@ public async Task GetBoolean_WithNoEvaluationContext_ShouldEvaluateWithReasonAnd
Assert.Equal("on", details.Variant);
}

[Fact]
public async Task GetBoolean_WhenDisabled_ShouldEvaluateWithDefaultReason()
{
// Act
ResolutionDetails<bool> details = await this.commonProvider.ResolveBooleanValueAsync("boolean-disabled-flag", false, EvaluationContext.Empty);

// Assert
Assert.False(details.Value);
Assert.Equal(Reason.Disabled, details.Reason);
Assert.Null(details.Variant);
}

[Fact]
public async Task GetString_ShouldEvaluateWithReasonAndVariant()
{
Expand All @@ -158,6 +225,18 @@ public async Task GetString_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
Assert.Equal("greeting", details.Variant);
}

[Fact]
public async Task GetString_WhenDisabled_ShouldEvaluateWithDefaultReason()
{
// Act
ResolutionDetails<string> details = await this.commonProvider.ResolveStringValueAsync("string-disabled-flag", "nope");

// Assert
Assert.Equal("nope", details.Value);
Assert.Equal(Reason.Disabled, details.Reason);
Assert.Null(details.Variant);
}

[Fact]
public async Task GetInt_ShouldEvaluateWithReasonAndVariant()
{
Expand All @@ -179,6 +258,18 @@ public async Task GetInt_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVari
Assert.Equal("ten", details.Variant);
}

[Fact]
public async Task GetInt_WhenDisabled_ShouldEvaluateWithDefaultReason()
{
// Act
ResolutionDetails<int> details = await this.commonProvider.ResolveIntegerValueAsync("integer-disabled-flag", 13);

// Assert
Assert.Equal(13, details.Value);
Assert.Equal(Reason.Disabled, details.Reason);
Assert.Null(details.Variant);
}

[Fact]
public async Task GetDouble_ShouldEvaluateWithReasonAndVariant()
{
Expand All @@ -200,6 +291,18 @@ public async Task GetDouble_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
Assert.Equal("half", details.Variant);
}

[Fact]
public async Task GetDouble_WhenDisabled_ShouldEvaluateWithDefaultReason()
{
// Act
ResolutionDetails<double> details = await this.commonProvider.ResolveDoubleValueAsync("float-disabled-flag", 1.3);

// Assert
Assert.Equal(1.3, details.Value);
Assert.Equal(Reason.Disabled, details.Reason);
Assert.Null(details.Variant);
}

[Fact]
public async Task GetStruct_ShouldEvaluateWithReasonAndVariant()
{
Expand All @@ -225,6 +328,25 @@ public async Task GetStruct_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
Assert.Equal("template", details.Variant);
}

[Fact]
public async Task GetStruct_WhenDisabled_ShouldEvaluateWithDefaultReason()
{
// Arrange
var defaultValue = new Value(
Structure.Builder()
.Set("default", true)
.Build()
);

// Act
ResolutionDetails<Value> details = await this.commonProvider.ResolveStructureValueAsync("object-disabled-flag", defaultValue);

// Assert
Assert.Equal(true, details.Value.AsStructure?["default"].AsBoolean);
Assert.Equal(Reason.Disabled, details.Reason);
Assert.Null(details.Variant);
}

[Fact]
public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant()
{
Expand Down
Loading