Skip to content

Commit df1765c

Browse files
feat: Add disabled flag support to InMemoryProvider (#632)
* Add support for disabling InMemoryProvider flags Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Enable disabled reason e2e tests Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Add example disabled in memory flag Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> * Address gemini code review comments Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com> --------- Signed-off-by: Kyle Julian <38759683+kylejuliandev@users.noreply.github.com>
1 parent 8b472d8 commit df1765c

File tree

5 files changed

+163
-13
lines changed

5 files changed

+163
-13
lines changed

samples/AspNetCore/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
"welcome-message", new Flag<bool>(
5353
new Dictionary<string, bool> { { "show", true }, { "hide", false } }, "show")
5454
},
55+
{
56+
"disabled-flag", new Flag<string>(
57+
new Dictionary<string, string> { { "on", "This flag is on" }, { "off", "This flag is off" } }, "off",
58+
disabled: true)
59+
},
5560
{
5661
"test-config", new Flag<Value>(new Dictionary<string, Value>()
5762
{

src/OpenFeature/Providers/Memory/Flag.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ namespace OpenFeature.Providers.Memory;
77
/// <summary>
88
/// Flag representation for the in-memory provider.
99
/// </summary>
10-
public interface Flag;
10+
public interface Flag
11+
{
12+
/// <summary>
13+
/// Indicates if the flag is disabled. When disabled, the flag will resolve to the default value.
14+
/// </summary>
15+
bool Disabled { get; }
16+
}
1117

1218
/// <summary>
1319
/// Flag representation for the in-memory provider.
@@ -26,16 +32,33 @@ public sealed class Flag<T> : Flag
2632
/// <param name="defaultVariant">default variant (should match 1 key in variants dictionary)</param>
2733
/// <param name="contextEvaluator">optional context-sensitive evaluation function</param>
2834
/// <param name="flagMetadata">optional metadata for the flag</param>
29-
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null, ImmutableMetadata? flagMetadata = null)
35+
/// <param name="disabled">indicates if the flag is disabled</param>
36+
public Flag(Dictionary<string, T> variants, string defaultVariant, Func<EvaluationContext, string>? contextEvaluator = null, ImmutableMetadata? flagMetadata = null, bool disabled = false)
3037
{
3138
this._variants = variants;
3239
this._defaultVariant = defaultVariant;
3340
this._contextEvaluator = contextEvaluator;
3441
this._flagMetadata = flagMetadata;
42+
this.Disabled = disabled;
3543
}
3644

37-
internal ResolutionDetails<T> Evaluate(string flagKey, T _, EvaluationContext? evaluationContext)
45+
/// <summary>
46+
/// Indicates if the flag is disabled. When disabled, the flag will resolve to the default value.
47+
/// </summary>
48+
public bool Disabled { get; }
49+
50+
internal ResolutionDetails<T> Evaluate(string flagKey, T defaultValue, EvaluationContext? evaluationContext)
3851
{
52+
if (this.Disabled)
53+
{
54+
return new ResolutionDetails<T>(
55+
flagKey,
56+
defaultValue,
57+
reason: Reason.Disabled,
58+
flagMetadata: this._flagMetadata
59+
);
60+
}
61+
3962
if (this._contextEvaluator == null)
4063
{
4164
return this.EvaluateDefaultVariant(flagKey);

test/OpenFeature.E2ETests/Steps/ExcludedTagsStep.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ namespace OpenFeature.E2ETests.Steps;
55
[Scope(Tag = "immutability")]
66
[Scope(Tag = "async")]
77
[Scope(Tag = "reason-codes-cached")]
8-
[Scope(Tag = "reason-codes-disabled")]
98
[Scope(Tag = "deprecated")]
109
public class ExcludedTagsStep
1110
{

test/OpenFeature.E2ETests/Utils/FlagDictionaryJsonConverter.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private static Flag ReadFlag(string flagKey, JsonElement flagElement)
5050
if (inferredKind == null)
5151
throw new JsonException($"Flag '{flagKey}' has no variants");
5252

53-
var defaultVariant = InferDefaultVariant(flagElement, variantsElement);
53+
var defaultVariant = InferDefaultVariant(flagElement);
5454

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

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

6565
return inferredKind switch
6666
{
67-
VariantKind.Boolean => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetBoolean()),
68-
VariantKind.Integer => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetInt32()),
69-
VariantKind.Double => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetDouble()),
70-
VariantKind.String => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, static e => e.GetString()!),
71-
VariantKind.Object => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, ExtractObjectVariant),
67+
VariantKind.Boolean => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetBoolean()),
68+
VariantKind.Integer => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetInt32()),
69+
VariantKind.Double => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetDouble()),
70+
VariantKind.String => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, static e => e.GetString()!),
71+
VariantKind.Object => BuildFlag(variantsElement, defaultVariant, contextEvaluator, metadata, disabled, ExtractObjectVariant),
7272
_ => throw new JsonException($"Unsupported variant kind for flag '{flagKey}'")
7373
};
7474
}
@@ -81,17 +81,18 @@ private static Flag<T> BuildFlag<T>(
8181
string? defaultVariant,
8282
Func<EvaluationContext, string>? contextEvaluator,
8383
ImmutableMetadata? metadata,
84+
bool? disabled,
8485
Func<JsonElement, T> projector)
8586
{
8687
var dict = new Dictionary<string, T>(StringComparer.Ordinal);
8788
foreach (var v in variantsElement.EnumerateObject())
8889
{
8990
dict[v.Name] = projector(v.Value);
9091
}
91-
return new Flag<T>(dict, defaultVariant!, contextEvaluator, metadata);
92+
return new Flag<T>(dict, defaultVariant!, contextEvaluator, metadata, disabled ?? false);
9293
}
9394

94-
private static string? InferDefaultVariant(JsonElement flagElement, JsonElement variantsElement)
95+
private static string? InferDefaultVariant(JsonElement flagElement)
9596
{
9697
if (flagElement.TryGetProperty("defaultVariant", out var dv))
9798
{

test/OpenFeature.Tests/Providers/Memory/InMemoryProviderTests.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ public InMemoryProviderTests()
2121
defaultVariant: "on"
2222
)
2323
},
24+
{
25+
"boolean-disabled-flag", new Flag<bool>(
26+
disabled: true,
27+
variants: new Dictionary<string, bool>(){
28+
{ "on", true },
29+
{ "off", false }
30+
},
31+
defaultVariant: "on"
32+
)
33+
},
2434
{
2535
"string-flag", new Flag<string>(
2636
variants: new Dictionary<string, string>(){
@@ -30,6 +40,16 @@ public InMemoryProviderTests()
3040
defaultVariant: "greeting"
3141
)
3242
},
43+
{
44+
"string-disabled-flag", new Flag<string>(
45+
disabled: true,
46+
variants: new Dictionary<string, string>(){
47+
{ "greeting", "hi" },
48+
{ "parting", "bye" }
49+
},
50+
defaultVariant: "greeting"
51+
)
52+
},
3353
{
3454
"integer-flag", new Flag<int>(
3555
variants: new Dictionary<string, int>(){
@@ -39,6 +59,16 @@ public InMemoryProviderTests()
3959
defaultVariant: "ten"
4060
)
4161
},
62+
{
63+
"integer-disabled-flag", new Flag<int>(
64+
disabled: true,
65+
variants: new Dictionary<string, int>(){
66+
{ "one", 1 },
67+
{ "ten", 10 }
68+
},
69+
defaultVariant: "ten"
70+
)
71+
},
4272
{
4373
"float-flag", new Flag<double>(
4474
variants: new Dictionary<string, double>(){
@@ -48,6 +78,16 @@ public InMemoryProviderTests()
4878
defaultVariant: "half"
4979
)
5080
},
81+
{
82+
"float-disabled-flag", new Flag<double>(
83+
disabled: true,
84+
variants: new Dictionary<string, double>(){
85+
{ "tenth", 0.1 },
86+
{ "half", 0.5 }
87+
},
88+
defaultVariant: "half"
89+
)
90+
},
5191
{
5292
"context-aware", new Flag<string>(
5393
variants: new Dictionary<string, string>(){
@@ -78,6 +118,21 @@ public InMemoryProviderTests()
78118
defaultVariant: "template"
79119
)
80120
},
121+
{
122+
"object-disabled-flag", new Flag<Value>(
123+
disabled: true,
124+
variants: new Dictionary<string, Value>(){
125+
{ "empty", new Value() },
126+
{ "template", new Value(Structure.Builder()
127+
.Set("showImages", true)
128+
.Set("title", "Check out these pics!")
129+
.Set("imagesPerPage", 100).Build()
130+
)
131+
}
132+
},
133+
defaultVariant: "template"
134+
)
135+
},
81136
{
82137
"invalid-flag", new Flag<bool>(
83138
variants: new Dictionary<string, bool>(){
@@ -137,6 +192,18 @@ public async Task GetBoolean_WithNoEvaluationContext_ShouldEvaluateWithReasonAnd
137192
Assert.Equal("on", details.Variant);
138193
}
139194

195+
[Fact]
196+
public async Task GetBoolean_WhenDisabled_ShouldEvaluateWithDefaultReason()
197+
{
198+
// Act
199+
ResolutionDetails<bool> details = await this.commonProvider.ResolveBooleanValueAsync("boolean-disabled-flag", false, EvaluationContext.Empty);
200+
201+
// Assert
202+
Assert.False(details.Value);
203+
Assert.Equal(Reason.Disabled, details.Reason);
204+
Assert.Null(details.Variant);
205+
}
206+
140207
[Fact]
141208
public async Task GetString_ShouldEvaluateWithReasonAndVariant()
142209
{
@@ -158,6 +225,18 @@ public async Task GetString_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
158225
Assert.Equal("greeting", details.Variant);
159226
}
160227

228+
[Fact]
229+
public async Task GetString_WhenDisabled_ShouldEvaluateWithDefaultReason()
230+
{
231+
// Act
232+
ResolutionDetails<string> details = await this.commonProvider.ResolveStringValueAsync("string-disabled-flag", "nope");
233+
234+
// Assert
235+
Assert.Equal("nope", details.Value);
236+
Assert.Equal(Reason.Disabled, details.Reason);
237+
Assert.Null(details.Variant);
238+
}
239+
161240
[Fact]
162241
public async Task GetInt_ShouldEvaluateWithReasonAndVariant()
163242
{
@@ -179,6 +258,18 @@ public async Task GetInt_WithNoEvaluationContext_ShouldEvaluateWithReasonAndVari
179258
Assert.Equal("ten", details.Variant);
180259
}
181260

261+
[Fact]
262+
public async Task GetInt_WhenDisabled_ShouldEvaluateWithDefaultReason()
263+
{
264+
// Act
265+
ResolutionDetails<int> details = await this.commonProvider.ResolveIntegerValueAsync("integer-disabled-flag", 13);
266+
267+
// Assert
268+
Assert.Equal(13, details.Value);
269+
Assert.Equal(Reason.Disabled, details.Reason);
270+
Assert.Null(details.Variant);
271+
}
272+
182273
[Fact]
183274
public async Task GetDouble_ShouldEvaluateWithReasonAndVariant()
184275
{
@@ -200,6 +291,18 @@ public async Task GetDouble_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
200291
Assert.Equal("half", details.Variant);
201292
}
202293

294+
[Fact]
295+
public async Task GetDouble_WhenDisabled_ShouldEvaluateWithDefaultReason()
296+
{
297+
// Act
298+
ResolutionDetails<double> details = await this.commonProvider.ResolveDoubleValueAsync("float-disabled-flag", 1.3);
299+
300+
// Assert
301+
Assert.Equal(1.3, details.Value);
302+
Assert.Equal(Reason.Disabled, details.Reason);
303+
Assert.Null(details.Variant);
304+
}
305+
203306
[Fact]
204307
public async Task GetStruct_ShouldEvaluateWithReasonAndVariant()
205308
{
@@ -225,6 +328,25 @@ public async Task GetStruct_WithNoEvaluationContext_ShouldEvaluateWithReasonAndV
225328
Assert.Equal("template", details.Variant);
226329
}
227330

331+
[Fact]
332+
public async Task GetStruct_WhenDisabled_ShouldEvaluateWithDefaultReason()
333+
{
334+
// Arrange
335+
var defaultValue = new Value(
336+
Structure.Builder()
337+
.Set("default", true)
338+
.Build()
339+
);
340+
341+
// Act
342+
ResolutionDetails<Value> details = await this.commonProvider.ResolveStructureValueAsync("object-disabled-flag", defaultValue);
343+
344+
// Assert
345+
Assert.Equal(true, details.Value.AsStructure?["default"].AsBoolean);
346+
Assert.Equal(Reason.Disabled, details.Reason);
347+
Assert.Null(details.Variant);
348+
}
349+
228350
[Fact]
229351
public async Task GetString_ContextSensitive_ShouldEvaluateWithReasonAndVariant()
230352
{

0 commit comments

Comments
 (0)