Skip to content

Commit

Permalink
fixup! 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 <[email protected]>
  • Loading branch information
chrfwow committed Jan 17, 2025
1 parent 55609f5 commit 6b5210d
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 65 deletions.
10 changes: 10 additions & 0 deletions .fleet/run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"configurations": [
{
"type": "command",
"name": "debug",
"program": "$PROJECT_DIR$\\test\\OpenFeature.Contrib.Providers.Flagd.Test\\bin\\Debug\\net6.0\\testhost.exe",
"args": []
}
]
}
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[submodule "src/OpenFeature.Contrib.Providers.Flagd/schemas"]
path = src/OpenFeature.Contrib.Providers.Flagd/schemas
url = git@github.com:open-feature/schemas.git
url = https://github.com/open-feature/schemas.git
[submodule "spec"]
path = spec
url = https://github.com/open-feature/spec.git
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,21 @@

namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess
{

internal class FlagConfiguration
{
[JsonProperty("state")]
internal string State { get; set; }
[JsonProperty("defaultVariant")]
internal string DefaultVariant { get; set; }
[JsonProperty("variants")]
internal Dictionary<string, object> Variants { get; set; }
[JsonProperty("targeting")]
internal object Targeting { get; set; }
[JsonProperty("source")]
internal string Source { get; set; }
[JsonProperty("metadata")]
internal Dictionary<string, object> Metadata { get; set; }
[JsonProperty("state")] internal string State { get; set; }
[JsonProperty("defaultVariant")] internal string DefaultVariant { get; set; }
[JsonProperty("variants")] internal Dictionary<string, object> Variants { get; set; }
[JsonProperty("targeting")] internal object Targeting { get; set; }
[JsonProperty("source")] internal string Source { get; set; }
[JsonProperty("metadata")] internal Dictionary<string, object> Metadata { get; set; }
}

internal class FlagSyncData
{
[JsonProperty("flags")]
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; }
[JsonProperty("flags")] 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 Down Expand Up @@ -93,16 +83,61 @@ internal FlagSyncData Parse(string flagConfigurations)
});
}

Console.Error.WriteLine("flagConfigurations " + flagConfigurations);

FlagSyncData data = JsonConvert.DeserializeObject<FlagSyncData>(transformed);
Console.Error.WriteLine("metadata " + data.Metadata);
Console.Error.WriteLine("metadata count " + data.Metadata?.Count);
Console.WriteLine("metadata " + data.Metadata);//.WriteLine();
Console.WriteLine("metadata count " + data.Metadata?.Count);
var data = JsonConvert.DeserializeObject<FlagSyncData>(transformed);
if (data.Metadata == null)
{
data.Metadata = new Dictionary<string, object>();
}
else
{
foreach (var key in new List<string>(data.Metadata.Keys))
{
var value = data.Metadata[key];
if (value is long longValue)
{
data.Metadata[key] = (int)longValue;
continue;
}

VerifyMetadataValue(key, value);
}
}

foreach (var flagConfig in data.Flags)
{
if (flagConfig.Value.Metadata == null)
{
continue;
}

foreach (var key in new List<string>(flagConfig.Value.Metadata.Keys))
{
var value = flagConfig.Value.Metadata[key];
if (value is long longValue)
{
flagConfig.Value.Metadata[key] = (int)longValue;
continue;
}

VerifyMetadataValue(key, value);
}
}

return data;
}

private static void VerifyMetadataValue(string key, object value)
{
if (value is int || value is double || value is string || value is bool)
{
return;
}

throw new ParseErrorException("Metadata entry for key " + key + " and value " + value +
" is of unknown type");
}

internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurations)
{
var flagConfigsMap = Parse(flagConfigurations);
Expand All @@ -111,14 +146,7 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat
{
case FlagConfigurationUpdateType.ALL:
_flags = flagConfigsMap.Flags;
if (flagConfigsMap.Metadata == null)
{
_flagSetMetadata.Clear();
}
else
{
_flagSetMetadata = flagConfigsMap.Metadata;
}
_flagSetMetadata = flagConfigsMap.Metadata;

break;
case FlagConfigurationUpdateType.ADD:
Expand All @@ -127,61 +155,73 @@ internal void Sync(FlagConfigurationUpdateType updateType, string flagConfigurat
{
_flags[keyAndValue.Key] = keyAndValue.Value;
}

foreach (var metadata in flagConfigsMap.Metadata)
{
_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;
}
}

public ResolutionDetails<bool> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null)
public ResolutionDetails<bool> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}

public ResolutionDetails<string> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext context = null)
public ResolutionDetails<string> ResolveStringValueAsync(string flagKey, string defaultValue,
EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}

public ResolutionDetails<int> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext context = null)
public ResolutionDetails<int> ResolveIntegerValueAsync(string flagKey, int defaultValue,
EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}

public ResolutionDetails<double> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext context = null)
public ResolutionDetails<double> ResolveDoubleValueAsync(string flagKey, double defaultValue,
EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}

public ResolutionDetails<Value> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext context = null)
public ResolutionDetails<Value> ResolveStructureValueAsync(string flagKey, Value defaultValue,
EvaluationContext context = null)
{
return ResolveValue(flagKey, defaultValue, context);
}

private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, EvaluationContext context = null)
private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue,
EvaluationContext context = null)
{
// check if we find the flag key
var reason = Reason.Static;
if (_flags.TryGetValue(flagKey, out var flagConfiguration))
{
if ("DISABLED" == flagConfiguration.State)
{
throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' is disabled");
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)
if (flagConfiguration.Metadata != null)
{
foreach (var metadataEntry in flagConfiguration.Metadata)
{
Expand All @@ -191,12 +231,15 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva

var flagMetadata = new ImmutableMetadata(combinedMetadata);
var variant = flagConfiguration.DefaultVariant;
if (flagConfiguration.Targeting != null && !String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) && flagConfiguration.Targeting.ToString() != "{}")
if (flagConfiguration.Targeting != null &&
!String.IsNullOrEmpty(flagConfiguration.Targeting.ToString()) &&
flagConfiguration.Targeting.ToString() != "{}")
{
reason = Reason.TargetingMatch;
var flagdProperties = new Dictionary<string, Value>();
flagdProperties.Add(FlagdProperties.FlagKeyKey, new Value(flagKey));
flagdProperties.Add(FlagdProperties.TimestampKey, new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));
flagdProperties.Add(FlagdProperties.TimestampKey,
new Value(DateTimeOffset.UtcNow.ToUnixTimeSeconds()));

if (context == null)
{
Expand All @@ -206,7 +249,7 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
var targetingContext = context.AsDictionary().Add(
FlagdProperties.FlagdPropertiesKey,
new Value(new Structure(flagdProperties))
);
);

var targetingString = flagConfiguration.Targeting.ToString();
// Parse json into hierarchical structure
Expand Down Expand Up @@ -235,34 +278,39 @@ private ResolutionDetails<T> ResolveValue<T>(string flagKey, T defaultValue, Eva
{
// if variant is null, revert to default
reason = Reason.Default;
flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant, out var defaultVariantValue);
flagConfiguration.Variants.TryGetValue(flagConfiguration.DefaultVariant,
out var defaultVariantValue);
if (defaultVariantValue == null)
{
throw new FeatureProviderException(ErrorType.ParseError, "PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
throw new FeatureProviderException(ErrorType.ParseError,
"PARSE_ERROR: flag '" + flagKey + "' has missing or invalid defaultVariant.");
}

var value = ExtractFoundVariant<T>(defaultVariantValue, flagKey);
return new ResolutionDetails<T>(
flagKey: flagKey,
value,
reason: reason,
variant: variant,
flagMetadata: flagMetadata
);
flagKey: flagKey,
value,
reason: reason,
variant: variant,
flagMetadata: flagMetadata
);
}
else if (flagConfiguration.Variants.TryGetValue(variant, out var foundVariantValue))
{
// if variant can be found, return it - this could be TARGETING_MATCH or STATIC.
var value = ExtractFoundVariant<T>(foundVariantValue, flagKey);
return new ResolutionDetails<T>(
flagKey: flagKey,
value,
reason: reason,
variant: variant,
flagMetadata: flagMetadata
);
flagKey: flagKey,
value,
reason: reason,
variant: variant,
flagMetadata: flagMetadata
);
}
}
throw new FeatureProviderException(ErrorType.FlagNotFound, "FLAG_NOT_FOUND: flag '" + flagKey + "' not found");

throw new FeatureProviderException(ErrorType.FlagNotFound,
"FLAG_NOT_FOUND: flag '" + flagKey + "' not found");
}

static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
Expand All @@ -271,6 +319,7 @@ static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
{
foundVariantValue = Convert.ToInt32(foundVariantValue);
}

if (typeof(T) == typeof(double))
{
foundVariantValue = Convert.ToDouble(foundVariantValue);
Expand All @@ -279,11 +328,14 @@ static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
{
foundVariantValue = ConvertJObjectToOpenFeatureValue(value);
}

if (foundVariantValue is T castValue)
{
return castValue;
}
throw new FeatureProviderException(ErrorType.TypeMismatch, "TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type");

throw new FeatureProviderException(ErrorType.TypeMismatch,
"TYPE_MISMATCH: flag '" + flagKey + "' does not match the expected type");
}

static dynamic ConvertToDynamicObject(IImmutableDictionary<string, Value> dictionary)
Expand All @@ -294,7 +346,9 @@ static dynamic ConvertToDynamicObject(IImmutableDictionary<string, Value> dictio
foreach (var kvp in dictionary)
{
expandoDict.Add(kvp.Key,
kvp.Value.IsStructure ? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary()) : kvp.Value.AsObject);
kvp.Value.IsStructure
? ConvertToDynamicObject(kvp.Value.AsStructure.AsDictionary())
: kvp.Value.AsObject);
}

return expandoObject;
Expand Down Expand Up @@ -337,4 +391,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue)
return new Value(new Structure(result));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,9 @@ public void TestJsonEvaluatorFlagMetadataOverwritesFlagSetMetadata()
jsonEvaluator.Sync(FlagConfigurationUpdateType.ALL, Utils.metadataFlags);

var result = jsonEvaluator.ResolveBooleanValueAsync("metadata-flag", false);

var a = result.FlagMetadata.GetInt("integer");

Assert.NotNull(result.FlagMetadata);
Assert.Equal("1.0.2", result.FlagMetadata.GetString("string"));
Assert.Equal(2, result.FlagMetadata.GetInt("integer"));
Expand All @@ -389,7 +392,7 @@ public void TestJsonEvaluatorThrowsOnInvalidFlagSetMetadata()

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

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

[Fact]
Expand All @@ -399,7 +402,7 @@ public void TestJsonEvaluatorThrowsOnInvalidFlagMetadata()

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

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

0 comments on commit 6b5210d

Please sign in to comment.