-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add custom JsonLogic evaluators (#159)
Signed-off-by: Florian Bacher <[email protected]> Co-authored-by: Todd Baert <[email protected]>
- Loading branch information
Showing
11 changed files
with
898 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
...penFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FlagdProperties.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators | ||
{ | ||
internal class FlagdProperties | ||
{ | ||
|
||
internal const string FlagdPropertiesKey = "$flagd"; | ||
internal const string FlagKeyKey = "flagKey"; | ||
internal const string TimestampKey = "timestamp"; | ||
internal const string TargetingKeyKey = "targetingKey"; | ||
|
||
internal string FlagKey { get; set; } | ||
internal long Timestamp { get; set; } | ||
internal string TargetingKey { get; set; } | ||
|
||
internal FlagdProperties(object from) | ||
{ | ||
//object value; | ||
if (from is Dictionary<string, object> dict) | ||
{ | ||
if (dict.TryGetValue(TargetingKeyKey, out object targetingKeyValue) | ||
&& targetingKeyValue is string targetingKeyString) | ||
{ | ||
TargetingKey = targetingKeyString; | ||
} | ||
if (dict.TryGetValue(FlagdPropertiesKey, out object flagdPropertiesObj) | ||
&& flagdPropertiesObj is Dictionary<string, object> flagdProperties) | ||
{ | ||
if (flagdProperties.TryGetValue(FlagKeyKey, out object flagKeyObj) | ||
&& flagKeyObj is string flagKey) | ||
{ | ||
FlagKey = flagKey; | ||
} | ||
if (flagdProperties.TryGetValue(TimestampKey, out object timestampObj) | ||
&& timestampObj is long timestamp) | ||
{ | ||
Timestamp = timestamp; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
...eature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/FractionalEvaluator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using JsonLogic.Net; | ||
using Microsoft.Extensions.Logging; | ||
using Murmur; | ||
using Newtonsoft.Json.Linq; | ||
using Semver; | ||
|
||
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators | ||
{ | ||
/// <inheritdoc/> | ||
public class FractionalEvaluator | ||
{ | ||
|
||
internal ILogger Logger { get; set; } | ||
|
||
internal FractionalEvaluator() | ||
{ | ||
var loggerFactory = LoggerFactory.Create( | ||
builder => builder | ||
// add console as logging target | ||
.AddConsole() | ||
// add debug output as logging target | ||
.AddDebug() | ||
// set minimum level to log | ||
.SetMinimumLevel(LogLevel.Debug) | ||
); | ||
Logger = loggerFactory.CreateLogger<FractionalEvaluator>(); | ||
} | ||
|
||
class FractionalEvaluationDistribution | ||
{ | ||
public string variant; | ||
public int percentage; | ||
} | ||
|
||
internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) | ||
{ | ||
// check if we have at least two arguments: | ||
// 1. the property value | ||
// 2. the array containing the buckets | ||
|
||
if (args.Length == 0) | ||
{ | ||
return null; | ||
} | ||
|
||
var flagdProperties = new FlagdProperties(data); | ||
|
||
// check if the first argument is a string (i.e. the property to base the distribution on | ||
var propertyValue = flagdProperties.TargetingKey; | ||
var bucketStartIndex = 0; | ||
|
||
var arg0 = p.Apply(args[0], data); | ||
|
||
if (arg0 is string stringValue) | ||
{ | ||
propertyValue = stringValue; | ||
bucketStartIndex = 1; | ||
} | ||
|
||
var distributions = new List<FractionalEvaluationDistribution>(); | ||
var distributionSum = 0; | ||
|
||
for (var i = bucketStartIndex; i < args.Length; i++) | ||
{ | ||
var bucket = p.Apply(args[i], data); | ||
|
||
if (!bucket.IsEnumerable()) | ||
{ | ||
continue; | ||
} | ||
|
||
var bucketArr = bucket.MakeEnumerable().ToArray(); | ||
|
||
if (bucketArr.Count() < 2) | ||
{ | ||
continue; | ||
} | ||
|
||
if (!bucketArr.ElementAt(1).IsNumeric()) | ||
{ | ||
continue; | ||
} | ||
|
||
|
||
var percentage = Convert.ToInt32(bucketArr.ElementAt(1)); | ||
distributions.Add(new FractionalEvaluationDistribution | ||
{ | ||
variant = bucketArr.ElementAt(0).ToString(), | ||
percentage = percentage | ||
}); | ||
|
||
distributionSum += percentage; | ||
} | ||
|
||
if (distributionSum != 100) | ||
{ | ||
Logger.LogDebug("Sum of distribution values is not eqyal to 100"); | ||
return null; | ||
} | ||
|
||
var valueToDistribute = flagdProperties.FlagKey + propertyValue; | ||
var murmur32 = MurmurHash.Create32(); | ||
var bytes = Encoding.ASCII.GetBytes(valueToDistribute); | ||
var hashBytes = murmur32.ComputeHash(bytes); | ||
var hash = BitConverter.ToInt32(hashBytes, 0); | ||
|
||
var bucketValue = (int)(Math.Abs((float)hash) / Int32.MaxValue * 100); | ||
|
||
var rangeEnd = 0; | ||
|
||
foreach (var dist in distributions) | ||
{ | ||
rangeEnd += dist.percentage; | ||
if (bucketValue < rangeEnd) | ||
{ | ||
return dist.variant; | ||
} | ||
} | ||
|
||
Logger.LogDebug("No matching bucket found"); | ||
return ""; | ||
} | ||
} | ||
} |
90 changes: 90 additions & 0 deletions
90
...penFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/SemVerEvaluator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
using System; | ||
using JsonLogic.Net; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Newtonsoft.Json.Linq; | ||
using Semver; | ||
|
||
namespace OpenFeature.Contrib.Providers.Flagd.Resolver.InProcess.CustomEvaluators | ||
{ | ||
/// <inheritdoc/> | ||
public class SemVerEvaluator | ||
{ | ||
internal ILogger Logger { get; set; } | ||
|
||
internal SemVerEvaluator() | ||
{ | ||
var loggerFactory = LoggerFactory.Create( | ||
builder => builder | ||
// add console as logging target | ||
.AddConsole() | ||
// add debug output as logging target | ||
.AddDebug() | ||
// set minimum level to log | ||
.SetMinimumLevel(LogLevel.Debug) | ||
); | ||
Logger = loggerFactory.CreateLogger<SemVerEvaluator>(); | ||
} | ||
|
||
|
||
const string OperatorEqual = "="; | ||
const string OperatorNotEqual = "!="; | ||
const string OperatorLess = "<"; | ||
const string OperatorLessOrEqual = "<="; | ||
const string OperatorGreater = ">"; | ||
const string OperatorGreaterOrEqual = ">="; | ||
const string OperatorMatchMajor = "^"; | ||
const string OperatorMatchMinor = "~"; | ||
|
||
internal object Evaluate(IProcessJsonLogic p, JToken[] args, object data) | ||
{ | ||
// check if we have at least 3 arguments | ||
if (args.Length < 3) | ||
{ | ||
return false; | ||
} | ||
// get the value from the provided evaluation context | ||
var versionString = p.Apply(args[0], data).ToString(); | ||
|
||
// get the operator | ||
var semVerOperator = p.Apply(args[1], data).ToString(); | ||
|
||
// get the target version | ||
var targetVersionString = p.Apply(args[2], data).ToString(); | ||
|
||
//convert to semantic versions | ||
try | ||
{ | ||
var version = SemVersion.Parse(versionString, SemVersionStyles.Strict); | ||
var targetVersion = SemVersion.Parse(targetVersionString, SemVersionStyles.Strict); | ||
|
||
switch (semVerOperator) | ||
{ | ||
case OperatorEqual: | ||
return version.CompareSortOrderTo(targetVersion) == 0; | ||
case OperatorNotEqual: | ||
return version.CompareSortOrderTo(targetVersion) != 0; | ||
case OperatorLess: | ||
return version.CompareSortOrderTo(targetVersion) < 0; | ||
case OperatorLessOrEqual: | ||
return version.CompareSortOrderTo(targetVersion) <= 0; | ||
case OperatorGreater: | ||
return version.CompareSortOrderTo(targetVersion) > 0; | ||
case OperatorGreaterOrEqual: | ||
return version.CompareSortOrderTo(targetVersion) >= 0; | ||
case OperatorMatchMajor: | ||
return version.Major == targetVersion.Major; | ||
case OperatorMatchMinor: | ||
return version.Major == targetVersion.Major && version.Minor == targetVersion.Minor; | ||
default: | ||
return false; | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
Logger?.LogDebug("Exception during SemVer evaluation: " + e.Message); | ||
return false; | ||
} | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...penFeature.Contrib.Providers.Flagd/Resolver/InProcess/CustomEvaluators/StringEvaluator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.