Skip to content

Commit

Permalink
Update in-process resolver to support flag metadata #305
Browse files Browse the repository at this point in the history
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
  • Loading branch information
chrfwow committed Jan 15, 2025
1 parent 2f4907e commit 79062cb
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using JsonLogic.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenFeature.Constant;
using OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators;
using OpenFeature.Error;
using OpenFeature.Model;
using System.Text.RegularExpressions;

namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess
{
Expand All @@ -26,6 +26,8 @@ internal class FlagConfiguration
internal object Targeting { get; set; }
[JsonProperty("source")]
internal string Source { get; set; }
[JsonProperty("metadata")]
internal Dictionary<string, object> Metadata { get; set; }
}

internal class FlagSyncData
Expand All @@ -34,6 +36,8 @@ internal class FlagSyncData
internal Dictionary<string, FlagConfiguration> Flags { get; set; }
[JsonProperty("$evaluators")]
internal Dictionary<string, object> Evaluators { get; set; }
[JsonProperty("metadata")]
internal Dictionary<string, object> Metadata { get; set; }
}

internal class FlagConfigurationSync
Expand All @@ -53,6 +57,7 @@ internal enum FlagConfigurationUpdateType
internal class JsonEvaluator
{
private Dictionary<string, FlagConfiguration> _flags = new Dictionary<string, FlagConfiguration>();
private Dictionary<string, object> _flagSetMetadata = new Dictionary<string, object>();

private string _selector;

Expand Down Expand Up @@ -99,26 +104,29 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat
{
case FlagConfigurationUpdateType.ALL:
_flags = flagConfigsMap.Flags;
_flagSetMetadata = flagConfigsMap.Metadata;
break;
case FlagConfigurationUpdateType.ADD:
case FlagConfigurationUpdateType.UPDATE:
foreach (var keyAndValue in flagConfigsMap.Flags)
{
_flags[keyAndValue.Key] = keyAndValue.Value;
}
break;
case FlagConfigurationUpdateType.UPDATE:
foreach (var keyAndValue in flagConfigsMap.Flags)
foreach (var metadata in flagConfigsMap.Metadata)
{
_flags[keyAndValue.Key] = keyAndValue.Value;
_flagSetMetadata[metadata.Key] = metadata.Value;
}
break;
case FlagConfigurationUpdateType.DELETE:
foreach (var keyAndValue in flagConfigsMap.Flags)
{
_flags.Remove(keyAndValue.Key);
}
foreach (var keyValuePair in flagConfigsMap.Metadata)
{
_flagSetMetadata.Remove(keyValuePair.Key);
}
break;

}
}

Expand Down Expand Up @@ -157,6 +165,16 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
{
throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
}
Dictionary<string, object> combinedMetadata = new Dictionary<string, object>(_flagSetMetadata);
if(flagConfiguration.Metadata != null)
{
foreach (var (key,value) in flagConfiguration.Metadata)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Cannot infer the type of implicitly-typed deconstruction variable 'key'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Cannot infer the type of implicitly-typed deconstruction variable 'value'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Cannot infer the type of implicitly-typed deconstruction variable 'key'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Cannot infer the type of implicitly-typed deconstruction variable 'value'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

Cannot infer the type of implicitly-typed deconstruction variable 'key'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

Cannot infer the type of implicitly-typed deconstruction variable 'value'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

Cannot infer the type of implicitly-typed deconstruction variable 'key'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

Cannot infer the type of implicitly-typed deconstruction variable 'value'.

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

'KeyValuePair<string, object>' does not contain a definition for 'Deconstruct' and no accessible extension method 'Deconstruct' accepting a first argument of type 'KeyValuePair<string, object>' could be found (are you missing a using directive or an assembly reference?)

Check failure on line 171 in src/OpenFeature.Contrib.Providers.Flagd/Resolver/InProcess/JsonEvaluator.cs

View workflow job for this annotation

GitHub Actions / e2e

No suitable 'Deconstruct' instance or extension method was found for type 'KeyValuePair<string, object>', with 2 out parameters and a void return type.
{
combinedMetadata[key] = value;
}
}

var flagMetadata = new ImmutableMetadata(combinedMetadata);
var variant = flagConfiguration.DefaultVariant;
if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}")
{
Expand Down Expand Up @@ -212,7 +230,8 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
flagKey: flagKey,
value,
reason: reason,
variant: variant
variant: variant,
flagMetadata: flagMetadata
);
}
else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue))
Expand All @@ -223,7 +242,8 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
flagKey: flagKey,
value,
reason: reason,
variant: variant
variant: variant,
flagMetadata: flagMetadata
);
}
}
Expand Down
115 changes: 98 additions & 17 deletions test/OpenFeature.Contrib.Providers.Flagd.Test/JsonEvaluatorTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Immutable;
using AutoFixture;
using OpenFeature.Constant;
Expand All @@ -10,7 +11,6 @@ namespace OpenFeature.Contrib.Providers.Flagd.Test
{
public class UnitTestJsonEvaluator
{

[Fact]
public void TestJsonEvaluatorAddFlagConfig()
{
Expand All @@ -23,7 +23,6 @@ public void TestJsonEvaluatorAddFlagConfig()
var result = jsonEvaluator.ResolveBooleanValueAsync("validFlag", false);

Assert.True(result.Value);

}

[Fact]
Expand All @@ -40,7 +39,6 @@ public void TestJsonEvaluatorAddStaticStringEvaluation()
Assert.Equal("#CC0000", result.Value);
Assert.Equal("red", result.Variant);
Assert.Equal(Reason.Static, result.Reason);

}

[Fact]
Expand All @@ -57,7 +55,7 @@ public void TestJsonEvaluatorDynamicBoolEvaluation()

var builder = EvaluationContext.Builder();
builder
.Set("color", "yellow");
.Set("color", "yellow");

var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlag", false, builder.Build());

Expand All @@ -80,7 +78,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyFlagKey()

var builder = EvaluationContext.Builder();

var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build());
var result =
jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdProperty", false, builder.Build());

Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
Expand All @@ -101,7 +100,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationUsingFlagdPropertyTimestamp()

var builder = EvaluationContext.Builder();

var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false, builder.Build());
var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingFlagdPropertyTimestamp", false,
builder.Build());

Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
Expand All @@ -119,7 +119,8 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluator()

var builder = EvaluationContext.Builder().Set("email", "test@faas.com");

var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build());
var result =
jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluator", false, builder.Build());

Assert.True(result.Value);
Assert.Equal("bool1", result.Variant);
Expand All @@ -137,7 +138,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationSharedEvaluatorReturningBoolTy

var builder = EvaluationContext.Builder().Set("email", "test@faas.com");

var result = jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false, builder.Build());
var result =
jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagUsingSharedEvaluatorReturningBoolType", false,
builder.Build());

Assert.True(result.Value);
Assert.Equal("true", result.Variant);
Expand All @@ -155,7 +158,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithMissingDefaultVariant()

var builder = EvaluationContext.Builder();

Assert.Throws<FeatureProviderException>(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false, builder.Build()));
Assert.Throws<FeatureProviderException>(() =>
jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithMissingDefaultVariant", false,
builder.Build()));
}

[Fact]
Expand All @@ -169,7 +174,9 @@ public void TestJsonEvaluatorDynamicBoolEvaluationWithUnexpectedVariantType()

var builder = EvaluationContext.Builder();

Assert.Throws<FeatureProviderException>(() => jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false, builder.Build()));
Assert.Throws<FeatureProviderException>(() =>
jsonEvaluator.ResolveBooleanValueAsync("targetingBoolFlagWithUnexpectedVariantType", false,
builder.Build()));
}

[Fact]
Expand All @@ -186,7 +193,7 @@ public void TestJsonEvaluatorDynamicStringEvaluation()

var builder = EvaluationContext.Builder();
builder
.Set("color", "yellow");
.Set("color", "yellow");

var result = jsonEvaluator.ResolveStringValueAsync("targetingStringFlag", "", builder.Build());

Expand All @@ -209,7 +216,7 @@ public void TestJsonEvaluatorDynamicFloatEvaluation()

var builder = EvaluationContext.Builder();
builder
.Set("color", "yellow");
.Set("color", "yellow");

var result = jsonEvaluator.ResolveDoubleValueAsync("targetingFloatFlag", 0, builder.Build());

Expand All @@ -232,7 +239,7 @@ public void TestJsonEvaluatorDynamicIntEvaluation()

var builder = EvaluationContext.Builder();
builder
.Set("color", "yellow");
.Set("color", "yellow");

var result = jsonEvaluator.ResolveIntegerValueAsync("targetingNumberFlag", 0, builder.Build());

Expand All @@ -255,7 +262,7 @@ public void TestJsonEvaluatorDynamicObjectEvaluation()

var builder = EvaluationContext.Builder();
builder
.Set("color", "yellow");
.Set("color", "yellow");

var result = jsonEvaluator.ResolveStructureValueAsync("targetingObjectFlag", null, builder.Build());

Expand All @@ -280,7 +287,8 @@ public void TestJsonEvaluatorDisabledBoolEvaluation()
builder
.Set("color", "yellow");

Assert.Throws<FeatureProviderException>(() => jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build()));
Assert.Throws<FeatureProviderException>(() =>
jsonEvaluator.ResolveBooleanValueAsync("disabledFlag", false, builder.Build()));
}

[Fact]
Expand All @@ -299,7 +307,8 @@ public void TestJsonEvaluatorFlagNotFoundEvaluation()
builder
.Set("color", "yellow");

Assert.Throws<FeatureProviderException>(() => jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build()));
Assert.Throws<FeatureProviderException>(() =>
jsonEvaluator.ResolveBooleanValueAsync("missingFlag", false, builder.Build()));
}

[Fact]
Expand All @@ -318,7 +327,79 @@ public void TestJsonEvaluatorWrongTypeEvaluation()
builder
.Set("color", "yellow");

Assert.Throws<FeatureProviderException>(() => jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build()));
Assert.Throws<FeatureProviderException>(() =>
jsonEvaluator.ResolveBooleanValueAsync("staticStringFlag", false, builder.Build()));
}

[Fact]
public void TestJsonEvaluatorReturnsFlagMetadata()
{
var fixture = new Fixture();

var jsonEvaluator = new JsonEvaluator(fixture.Create<string>());

jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.flags);

var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false);
Assert.NotNull(result.FlagMetadata);
Assert.Equal("1.0.2", result.FlagMetadata.GetString("string"));
Assert.Equal(2, result.FlagMetadata.GetInt("integer"));
Assert.Equal(true, result.FlagMetadata.GetBool("boolean"));
Assert.Equal(.1, result.FlagMetadata.GetDouble("float"));
}

[Fact]
public void TestJsonEvaluatorAddsFlagSetMetadataToFlagWithoutMetdata()
{
var fixture = new Fixture();

var jsonEvaluator = new JsonEvaluator(fixture.Create<string>());

jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags);

var result = jsonEvaluator.ResolveBooleanValueAsync("without-metadata-flag", false);
Assert.NotNull(result.FlagMetadata);
Assert.Equal("1.0.3", result.FlagMetadata.GetString("string"));
Assert.Equal(3, result.FlagMetadata.GetInt("integer"));
Assert.Equal(false, result.FlagMetadata.GetBool("boolean"));
Assert.Equal(.2, result.FlagMetadata.GetDouble("float"));
}

[Fact]
public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata()
{
var fixture = new Fixture();

var jsonEvaluator = new JsonEvaluator(fixture.Create<string>());

jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags);

var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false);
Assert.NotNull(result.FlagMetadata);
Assert.Equal("1.0.2", result.FlagMetadata.GetString("string"));
Assert.Equal(2, result.FlagMetadata.GetInt("integer"));
Assert.Equal(true, result.FlagMetadata.GetBool("boolean"));
Assert.Equal(.1, result.FlagMetadata.GetDouble("float"));
}

[Fact]
public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata()
{
var fixture = new Fixture();

var jsonEvaluator = new JsonEvaluator(fixture.Create<string>());

Assert.Throws<NullReferenceException>(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagSetMetadata));
}

[Fact]
public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata()
{
var fixture = new Fixture();

var jsonEvaluator = new JsonEvaluator(fixture.Create<string>());

Assert.Throws<NullReferenceException>(() => jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.invalidFlagMetadata));
}
}
}
Loading

0 comments on commit 79062cb

Please sign in to comment.