Skip to content

Commit

Permalink
feat: Update in-process resolver to support flag metadata #305 (#309)
Browse files Browse the repository at this point in the history
Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com>
  • Loading branch information
chrfwow authored Jan 27, 2025
1 parent 2f4907e commit e603c08
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 68 deletions.
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 @@ -2,38 +2,32 @@
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
{

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("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("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 All @@ -53,6 +47,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 @@ -88,7 +83,57 @@ internal FlagSyncData Parse(string flagConfigurations)
});
}

return JsonConvert.DeserializeObject<FlagSyncData>(transformed);

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)
{
value = data.Metadata[key] = (int)longValue;
}

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)
{
value = flagConfig.Value.Metadata[key] = (int)longValue;
}

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)
Expand All @@ -99,71 +144,100 @@ 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);
}
break;

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)
{
foreach (var metadataEntry in flagConfiguration.Metadata)
{
combinedMetadata[metadataEntry.Key] = metadataEntry.Value;
}
}

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 @@ -173,7 +247,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 @@ -202,32 +276,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
);
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
);
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 @@ -236,6 +317,7 @@ static T ExtractFoundVariant<T>(object foundVariantValue, string flagKey)
{
foundVariantValue = Convert.ToInt32(foundVariantValue);
}

if (typeof(T) == typeof(double))
{
foundVariantValue = Convert.ToDouble(foundVariantValue);
Expand All @@ -244,11 +326,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 @@ -259,7 +344,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 @@ -302,4 +389,4 @@ static Value ConvertJObjectToOpenFeatureValue(JObject jsonValue)
return new Value(new Structure(result));
}
}
}
}
Loading

0 comments on commit e603c08

Please sign in to comment.