Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config v6 #75

Merged
merged 50 commits into from
Nov 21, 2023
Merged
Changes from 1 commit
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
b1041b9
Update config JSON model to v6
adams85 Jun 24, 2023
4d9e1af
Refactor evaluator and evaluation logging to prepare it for the new f…
adams85 Jun 29, 2023
8a5367c
Implement segment condition evaluation
adams85 Jun 29, 2023
008fec7
Implement prerequisite flag condition evaluation
adams85 Jun 29, 2023
a591ba8
Implement new comparison operators
adams85 Jun 30, 2023
9d44952
Implement SDK key format validation + fix broken tests
adams85 Jun 30, 2023
3bbf636
Add matrix tests
adams85 Jun 30, 2023
754e3bc
Add more tests (sdk key format, circular dependency detection)
adams85 Jul 3, 2023
2895001
Finalize evaluation error handling + implement ToString() in model cl…
adams85 Jul 27, 2023
065ff10
Evaluation log test data
adams85 Jul 31, 2023
d794cc2
Add tests for evaluation logging
adams85 Jul 31, 2023
cd4b022
Add benchmarks for flag evaluation
adams85 Jul 1, 2023
a9063d0
Requested changes
adams85 Sep 1, 2023
c89b279
Add evaluation log tests for invalid values
adams85 Sep 1, 2023
5a7a6ac
Fixes for previous commit
adams85 Sep 1, 2023
4ac5094
Improve comparator display texts
adams85 Sep 1, 2023
73be9cf
Add more evaluation log tests
adams85 Sep 1, 2023
81777f4
Minor improvements
adams85 Sep 1, 2023
2648fc7
ARRAY (NOT) CONTAINS -> ARRAY (NOT) CONTAINS ANY OF
adams85 Sep 6, 2023
f530ff0
Refactor matrix and model tests to load configs directly from CDN ins…
adams85 Sep 6, 2023
121290c
Update matrix tests from Python SDK
adams85 Sep 7, 2023
4171dbb
Improve model and evaluation of conditions
adams85 Sep 7, 2023
2848429
Rename Comparator/ComparisonCondition to UserComparator/UserCondition…
adams85 Sep 7, 2023
67b8c11
Add tests for advanced flag override use cases
adams85 Sep 7, 2023
6d387f4
Remove operator name from warning 3004
adams85 Sep 8, 2023
432719b
Add helper methods for converting numeric/datetime/string array value…
adams85 Sep 15, 2023
4ed6d34
Run matrix tests on migrated configs + update SDK keys in new tests
adams85 Oct 4, 2023
f043974
XML documentation review
adams85 Oct 5, 2023
fc231c4
Fix sonar issues
adams85 Oct 5, 2023
c4aef60
Add more evaluation log tests
adams85 Oct 6, 2023
9b5925a
Fix list truncation format
adams85 Oct 18, 2023
22e6b69
Add more matrix tests for ANY OF comparators
adams85 Oct 18, 2023
30cecc0
Add clear text comparators
adams85 Oct 20, 2023
14f84f8
Clean up setting type mismatch handling & logging logic + eliminate d…
adams85 Oct 24, 2023
7306e00
Adjust model and tests to config v6 schema changes
adams85 Oct 25, 2023
9d708fc
Adjust evaluation logic to config v6 schema changes (length prefixed …
adams85 Oct 26, 2023
553b7e4
Add matrix tests for clear text comparators and non-ASCII comparison …
adams85 Oct 26, 2023
0862919
Requested changes
adams85 Oct 27, 2023
834bc4d
Fix failure case in segment evaluation + add more tests for segment e…
adams85 Oct 27, 2023
1fb9f2d
Fix typos & minor improvements
adams85 Oct 25, 2023
24ac423
Prevent user attributes dictionary from being created multiple times …
adams85 Nov 7, 2023
9414874
Adds test for utils + minor improvements
adams85 Nov 7, 2023
c2c2dee
Remove unnecessary ItemGroup from ConfigCat.Client.Tests.csproj
adams85 Nov 10, 2023
b551d23
Seal IndentedTextBuilder
adams85 Nov 13, 2023
ce15a57
Add tests for EvaluationDetails.MatchedTargetingRule/MatchedPercentag…
adams85 Nov 13, 2023
c148867
Improve error handling consistency in prerequisite flag evaluation
adams85 Nov 15, 2023
4066377
Don't force users to pass user object attributes as strings
adams85 Nov 15, 2023
d836236
Allow passing NaN values to number/datetime comparisons
adams85 Nov 20, 2023
846b310
Merge branch 'master' into config-v6
adams85 Nov 20, 2023
dcfe26e
Fix merged test
adams85 Nov 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement prerequisite flag condition evaluation
  • Loading branch information
adams85 committed Aug 4, 2023
commit 008fec78ed1e2790ae22b777313456ee3f0548fe
13 changes: 12 additions & 1 deletion src/ConfigCatClient/Evaluation/EvaluateContext.cs
Original file line number Diff line number Diff line change
@@ -5,22 +5,33 @@ namespace ConfigCat.Client.Evaluation;

internal struct EvaluateContext
{
public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user)
public EvaluateContext(string key, Setting setting, SettingValue defaultValue, User? user, IReadOnlyDictionary<string, Setting> settings)
{
this.Key = key;
this.Setting = setting;
this.DefaultValue = defaultValue;
this.User = user;
this.Settings = settings;

this.userAttributes = null;
this.visitedFlags = null;
this.IsMissingUserObjectLogged = this.IsMissingUserObjectAttributeLogged = false;
this.LogBuilder = null; // initialized by RolloutEvaluator.Evaluate
}

public EvaluateContext(string key, Setting setting, ref EvaluateContext dependentFlagContext)
: this(key, setting, dependentFlagContext.DefaultValue, dependentFlagContext.User, dependentFlagContext.Settings)
{
this.userAttributes = dependentFlagContext.userAttributes;
this.visitedFlags = dependentFlagContext.VisitedFlags; // crucial to use the property here to make sure the list is created!
this.LogBuilder = dependentFlagContext.LogBuilder;
}

public readonly string Key;
adams85 marked this conversation as resolved.
Show resolved Hide resolved
public readonly Setting Setting;
public readonly SettingValue DefaultValue;
public readonly User? User;
public readonly IReadOnlyDictionary<string, Setting> Settings;

private IReadOnlyDictionary<string, string>? userAttributes;
public IReadOnlyDictionary<string, string>? UserAttributes => this.userAttributes ??= this.User?.GetAllAttributes();
15 changes: 15 additions & 0 deletions src/ConfigCatClient/Evaluation/EvaluateLogHelper.cs
Original file line number Diff line number Diff line change
@@ -58,6 +58,11 @@ public static IndentedTextBuilder AppendComparisonCondition(this IndentedTextBui
: builder.AppendComparisonCondition(comparisonAttribute, comparator, (object?)null);
}

public static IndentedTextBuilder AppendPrerequisiteFlagCondition(this IndentedTextBuilder builder, string? prerequisiteFlagKey, PrerequisiteFlagComparator comparator, object? comparisonValue)
{
return builder.Append($"Flag '{prerequisiteFlagKey}' {comparator.ToDisplayText()} '{comparisonValue ?? InvalidValuePlaceholder}'");
}

public static IndentedTextBuilder AppendSegmentCondition(this IndentedTextBuilder builder, SegmentComparator comparator, Segment? segment)
{
var segmentName = segment?.Name ??
@@ -132,6 +137,16 @@ public static string ToDisplayText(this Comparator comparator)
};
}

public static string ToDisplayText(this PrerequisiteFlagComparator comparator)
{
return comparator switch
{
PrerequisiteFlagComparator.Equals => "EQUALS",
PrerequisiteFlagComparator.NotEquals => "NOT EQUALS",
_ => InvalidOperatorPlaceholder
};
}

public static string ToDisplayText(this SegmentComparator comparator)
{
return comparator switch
7 changes: 5 additions & 2 deletions src/ConfigCatClient/Evaluation/EvaluateResult.cs
Original file line number Diff line number Diff line change
@@ -4,12 +4,15 @@ internal readonly struct EvaluateResult
{
public EvaluateResult(SettingValueContainer selectedValue, TargetingRule? matchedTargetingRule = null, PercentageOption? matchedPercentageOption = null)
{
this.SelectedValue = selectedValue;
this.selectedValue = selectedValue;
this.MatchedTargetingRule = matchedTargetingRule;
this.MatchedPercentageOption = matchedPercentageOption;
}

public readonly SettingValueContainer SelectedValue;
private readonly SettingValueContainer selectedValue;
public SettingValue Value => this.selectedValue.Value;
public string? VariationId => this.selectedValue.VariationId;

public readonly TargetingRule? MatchedTargetingRule;
public readonly PercentageOption? MatchedPercentageOption;
}
6 changes: 3 additions & 3 deletions src/ConfigCatClient/Evaluation/EvaluationDetails.cs
Original file line number Diff line number Diff line change
@@ -27,15 +27,15 @@ internal static EvaluationDetails<TValue> FromEvaluateResult<TValue>(string key,
+ $"Please use a default value which corresponds to the setting type {settingType}.");
}

instance = new EvaluationDetails<TValue>(key, evaluateResult.SelectedValue.Value.GetValue<TValue>(settingType));
instance = new EvaluationDetails<TValue>(key, evaluateResult.Value.GetValue<TValue>(settingType));
}
else
{
EvaluationDetails evaluationDetails = new EvaluationDetails<object>(key, evaluateResult.SelectedValue.Value.GetValue(settingType)!);
EvaluationDetails evaluationDetails = new EvaluationDetails<object>(key, evaluateResult.Value.GetValue(settingType)!);
instance = (EvaluationDetails<TValue>)evaluationDetails;
}

instance.VariationId = evaluateResult.SelectedValue.VariationId;
instance.VariationId = evaluateResult.VariationId;
if (fetchTime is not null)
{
instance.FetchTime = fetchTime.Value;
97 changes: 94 additions & 3 deletions src/ConfigCatClient/Evaluation/RolloutEvaluator.cs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ namespace ConfigCat.Client.Evaluation;
internal sealed class RolloutEvaluator : IRolloutEvaluator
{
private const string MissingUserObjectError = "cannot evaluate, User Object is missing";
private const string CircularDependencyError = "cannot evaluate, circular dependency detected";

private readonly LoggerWrapper logger;

@@ -41,7 +42,7 @@ public EvaluateResult Evaluate(ref EvaluateContext context)
try
{
var result = EvaluateSetting(ref context);
returnValue = result.SelectedValue.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder;
returnValue = result.Value.GetValue(context.Setting.SettingType, throwIfInvalid: false) ?? EvaluateLogHelper.InvalidValuePlaceholder;
return result;
}
catch
@@ -264,8 +265,10 @@ private bool TryEvaluateConditions<TCondition>(TCondition[] conditions, Func<TCo
newLineBeforeThen = conditions.Length > 1;
break;

case PrerequisiteFlagCondition:
throw new NotImplementedException(); // TODO
case PrerequisiteFlagCondition prerequisiteFlagCondition:
conditionResult = EvaluatePrerequisiteFlagCondition(prerequisiteFlagCondition, ref context, out error);
newLineBeforeThen = error is null || conditions.Length > 1;
break;

case SegmentCondition segmentCondition:
conditionResult = EvaluateSegmentCondition(segmentCondition, ref context, out error);
@@ -521,6 +524,94 @@ private static bool EvaluateNumberRelation(double number, Comparator comparator,
};
}

private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition condition, ref EvaluateContext context, out string? error)
{
error = null;

var logBuilder = context.LogBuilder;

var prerequisiteFlagKey = condition.PrerequisiteFlagKey;
var comparator = condition.Comparator;

Setting? prerequisiteFlag = null;
object? comparisonValue = null;

if (prerequisiteFlagKey is not { Length: > 0 })
{
// TODO: error handling - prerequisite flag is not specified or invalid
}
else if (!context.Settings.TryGetValue(prerequisiteFlagKey, out prerequisiteFlag))
{
// TODO: error handling - prerequisite flag reference is invalid
}
else if ((comparisonValue = condition.ComparisonValue.GetValue(throwIfInvalid: false)) is null)
{
// TODO: error handling - comparison value is invalid (not available/multiple values specified)
}
else if (comparisonValue.GetType().ToSettingType() != prerequisiteFlag.SettingType)
{
// TODO: error handling - comparison value and prereq flag types mismatch
comparisonValue = null;
}

logBuilder?.AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue);

if (comparisonValue is null)
{
return false;
}

context.VisitedFlags.Add(context.Key);
if (context.VisitedFlags.Contains(prerequisiteFlagKey!))
{
context.VisitedFlags.Add(prerequisiteFlagKey!);
var dependencyCycle = new StringListFormatter(context.VisitedFlags).ToString("a", CultureInfo.InvariantCulture);
this.logger.CircularDependencyDetected(context.Key, dependencyCycle);

context.VisitedFlags.RemoveRange(context.VisitedFlags.Count - 2, 2);
error = CircularDependencyError;
return false;
}

var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, ref context);

logBuilder?
.NewLine("(")
.IncreaseIndent()
.NewLine().Append($"Evaluating prerequisite flag '{prerequisiteFlagKey}':");

// TODO: how to handle prereq flags w.r.t. flag overrides (when flag override setting depends on downloaded config setting or vice versa)?

var prerequisiteFlagEvaluateResult = EvaluateSetting(ref prerequisiteFlagContext);

context.VisitedFlags.RemoveAt(context.VisitedFlags.Count - 1);

var prerequisiteFlagValue = prerequisiteFlagEvaluateResult.Value.GetValue(prerequisiteFlag!.SettingType, throwIfInvalid: false);

var result = comparator switch
{
PrerequisiteFlagComparator.Equals => prerequisiteFlagValue is not null
? prerequisiteFlagValue.Equals(comparisonValue)
: false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value?

PrerequisiteFlagComparator.NotEquals => prerequisiteFlagValue is not null
? !prerequisiteFlagValue.Equals(comparisonValue)
: false, // TODO: error handling - how to handle when prereq flag evaluates to an invalid value?

_ => throw new InvalidOperationException(), // TODO: error handling - comparator was not set
};

logBuilder?
.NewLine().Append($"Prerequisite flag evaluation result: '{prerequisiteFlagValue ?? EvaluateLogHelper.InvalidValuePlaceholder}'.")
.NewLine("Condition (")
.AppendPrerequisiteFlagCondition(prerequisiteFlagKey, comparator, comparisonValue)
.Append(") evaluates to ").AppendEvaluationResult(result).Append(".")
.DecreaseIndent()
.NewLine(")");

return result;
}

private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateContext context, out string? error)
{
error = null;
23 changes: 7 additions & 16 deletions src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs
Original file line number Diff line number Diff line change
@@ -7,14 +7,6 @@ namespace ConfigCat.Client.Evaluation;

internal static class RolloutEvaluatorExtensions
{
public static EvaluationDetails<T> Evaluate<T>(this IRolloutEvaluator evaluator, Setting setting, string key, T defaultValue, User? user,
ProjectConfig? remoteConfig)
{
var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user);
var evaluateResult = evaluator.Evaluate(ref evaluateContext);
return EvaluationDetails.FromEvaluateResult<T>(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user);
}

public static EvaluationDetails<T> Evaluate<T>(this IRolloutEvaluator evaluator, Dictionary<string, Setting>? settings, string key, T defaultValue, User? user,
ProjectConfig? remoteConfig, LoggerWrapper logger)
{
@@ -33,14 +25,11 @@ public static EvaluationDetails<T> Evaluate<T>(this IRolloutEvaluator evaluator,
return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage);
}

return evaluator.Evaluate(setting, key, defaultValue, user, remoteConfig);
}
// TODO: error handling - what to do when setting is null?

public static EvaluationDetails Evaluate(this IRolloutEvaluator evaluator, Setting setting, string key, User? user, ProjectConfig? remoteConfig)
{
var evaluateContext = new EvaluateContext(key, setting, default, user);
var evaluateContext = new EvaluateContext(key, setting, defaultValue.ToSettingValue(out _), user, settings);
var evaluateResult = evaluator.Evaluate(ref evaluateContext);
return EvaluationDetails.FromEvaluateResult<object>(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user);
return EvaluationDetails.FromEvaluateResult<T>(key, evaluateResult, setting.SettingType, fetchTime: remoteConfig?.TimeStamp, user);
}

public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator, Dictionary<string, Setting>? settings, User? user,
@@ -56,12 +45,14 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator,
List<Exception>? exceptionList = null;

var index = 0;
foreach (var kvp in settings)
foreach (var kvp in settings) // TODO: error handling - what to do when setting is null?
{
EvaluationDetails evaluationDetails;
try
{
evaluationDetails = evaluator.Evaluate(kvp.Value, kvp.Key, user, remoteConfig);
var evaluateContext = new EvaluateContext(kvp.Key, kvp.Value, defaultValue: default, user, settings);
var evaluateResult = evaluator.Evaluate(ref evaluateContext);
evaluationDetails = EvaluationDetails.FromEvaluateResult<object>(kvp.Key, evaluateResult, kvp.Value.SettingType, fetchTime: remoteConfig?.TimeStamp, user);
}
catch (Exception ex)
{
5 changes: 5 additions & 0 deletions src/ConfigCatClient/Logging/LogMessages.cs
Original file line number Diff line number Diff line change
@@ -37,6 +37,11 @@ public static FormattableLogMessage ForceRefreshError(this LoggerWrapper logger,
$"Error occurred in the `{methodName}` method.",
"METHOD_NAME");

public static FormattableLogMessage CircularDependencyDetected(this LoggerWrapper logger, string key, string dependencyCycle) => logger.LogInterpolated(
LogLevel.Error, 2003, // TODO: this should be a 1xxx error (or should this be an error instead of a warning in the first place?)
$"Cannot evaluate targeting rules for '{key}' (circular dependency detected between the following depending flags: {dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.",
"KEY", "DEPENDENCY_CYCLE");

public static FormattableLogMessage FetchFailedDueToInvalidSdkKey(this LoggerWrapper logger) => logger.Log(
LogLevel.Error, 1100,
"Your SDK Key seems to be wrong. You can find the valid SDK Key at https://app.configcat.com/sdkkey");
1 change: 1 addition & 0 deletions src/ConfigCatClient/Utils/IndentedTextBuilder.cs
Original file line number Diff line number Diff line change
@@ -82,6 +82,7 @@ public AppendInterpolatedStringHandler(int literalLength, int formattedCount, In
public void AppendFormatted(object? value, int alignment = 0, string? format = null) => this.handler.AppendFormatted(value, alignment, format);

public void AppendFormatted(StringListFormatter value) => value.AppendWith(this.handler);
public void AppendFormatted(StringListFormatter value, string? format) => value.AppendWith(this.handler, format);
}
#endif

9 changes: 6 additions & 3 deletions src/ConfigCatClient/Utils/StringListFormatter.cs
Original file line number Diff line number Diff line change
@@ -18,21 +18,24 @@ public StringListFormatter(ICollection<string> collection, int maxLength = 0, Fu
this.getOmittedItemsText = getOmittedItemsText;
}

private static string GetSeparator(string? format) => format == "a" ? "' -> '" : "', '";

#if NET6_0_OR_GREATER
public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler)
public void AppendWith(StringBuilder.AppendInterpolatedStringHandler handler, string? format = null)
{
if (this.collection is { Count: > 0 })
{
var i = 0;
var n = this.maxLength > 0 && this.collection.Count > this.maxLength ? this.maxLength : this.collection.Count;
var separator = GetSeparator(format);
var currentSeparator = string.Empty;

handler.AppendLiteral("'");
foreach (var item in this.collection)
{
handler.AppendLiteral(currentSeparator);
handler.AppendLiteral(item);
currentSeparator = "', '";
currentSeparator = separator;

if (++i >= n)
{
@@ -66,7 +69,7 @@ public string ToString(string? format, IFormatProvider? formatProvider)
appendix = string.Empty;
}

return "'" + string.Join("', '", items) + "'" + appendix;
return "'" + string.Join(GetSeparator(format), items) + "'" + appendix;
}

return string.Empty;