Skip to content

Commit

Permalink
Add helper methods for converting numeric/datetime/string array value…
Browse files Browse the repository at this point in the history
…s to User Object attribute values
  • Loading branch information
adams85 committed Sep 19, 2023
1 parent 6d387f4 commit c4e2063
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 48 deletions.
39 changes: 38 additions & 1 deletion src/ConfigCat.Client.Tests/UserTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Globalization;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ConfigCat.Client.Tests;
Expand Down Expand Up @@ -124,4 +125,40 @@ public void CreateUser_ShouldSetIdentifier(string identifier, string expectedVal
Assert.AreEqual(expectedValue, user.Identifier);
Assert.AreEqual(expectedValue, user.GetAllAttributes()[nameof(User.Identifier)]);
}


[DataTestMethod]
[DataRow(typeof(DateTimeOffset), "2023-09-19T11:01:35.0000000+00:00", "1695121295")]
[DataRow(typeof(DateTimeOffset), "2023-09-19T13:01:35.0000000+02:00", "1695121295")]
[DataRow(typeof(DateTimeOffset), "2023-09-19T11:01:35.0510886+00:00", "1695121295.051")]
[DataRow(typeof(DateTimeOffset), "2023-09-19T13:01:35.0510886+02:00", "1695121295.051")]
[DataRow(typeof(double), "3", "3")]
[DataRow(typeof(double), "3.14", "3.14")]
[DataRow(typeof(double), "-1.23e-100", "-1.23e-100")]
[DataRow(typeof(string[]), "a,,b,c", "[\"a\",\"\",\"b\",\"c\"]")]
public void HelperMethodsShouldWork(Type type, string value, string expectedAttributeValue)
{
string actualAttributeValue;
if (type == typeof(DateTimeOffset))
{
var dateTimeOffset = DateTimeOffset.ParseExact(value, "o", CultureInfo.InvariantCulture);
actualAttributeValue = User.AttributeValueFrom(dateTimeOffset);
}
else if (type == typeof(double))
{
var number = double.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
actualAttributeValue = User.AttributeValueFrom(number);
}
else if (type == typeof(string[]))
{
var items = value.Split(',');
actualAttributeValue = User.AttributeValueFrom(items);
}
else
{
throw new InvalidOperationException();
}

Assert.AreEqual(expectedAttributeValue, actualAttributeValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"user": {
"Identifier": "12345",
"Email": "[email protected]",
"Country": "USA",
"Country": "[\"USA\"]",
"Version": "1.0.0",
"Number": "1.0",
"Date": "1693497500"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"[email protected]","Country":"USA","Version":"1.0.0","Number":"1.0","Date":"1693497500"}'
INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"[email protected]","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}'
Evaluating targeting rules and applying the first match if any:
- IF User.Email EQUALS '<hashed value>' => true
AND User.Email NOT EQUALS '<hashed value>' => false, skipping the remaining AND conditions
Expand Down
36 changes: 21 additions & 15 deletions src/ConfigCat.Client.Tests/data/testmatrix_comparators_v6.csv
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute
##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken
;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken
[email protected];[email protected];##null##;##null##;False;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken
[email protected];[email protected];Hungary;0;False;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Falcon
[email protected];[email protected];United Kingdom;1680307199.9;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon
[email protected];[email protected];Hungary;1681118000.56;True;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Falcon
[email protected];[email protected];##null##;1682899200.1;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Horse;Chicken;Chicken
[email protected];[email protected];Austria;1682999200;False;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Dog;Falcon;Chicken;Falcon
[email protected];[email protected];Bahamas;read,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon
[email protected];[email protected];Belgium;write, execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Horse;NotFound;Horse
[email protected];[email protected];Canada;execute, Read;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse
[email protected];[email protected];China;Write;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Falcon;NotFound;Horse
[email protected];[email protected];France;read, write,execute;False;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse
[email protected];[email protected];Greece;,execute;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse
[email protected];[email protected];Monaco;,null, ,,nil, None;False;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Falcon;NotFound;Horse
##null##;;;;FALSE;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken
;;;;FALSE;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken
[email protected];[email protected];##null##;##null##;FALSE;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken
[email protected];[email protected];Hungary;0;FALSE;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon
[email protected];[email protected];United Kingdom;1680307199.9;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon
[email protected];[email protected];Hungary;1681118000.56;TRUE;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon
[email protected];[email protected];##null##;1682899200.1;FALSE;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken
[email protected];[email protected];Austria;1682999200;FALSE;Cat;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon
[email protected];[email protected];Bahamas;read,execute;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon
[email protected];[email protected];Belgium;write, execute;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse
[email protected];[email protected];Canada;execute, Read;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse
[email protected];[email protected];China;Write;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse
[email protected];[email protected];France;read, write,execute;FALSE;Cat;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse
[email protected];[email protected];Greece;,execute;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse
[email protected];[email protected];Bahamas;["read","execute"];FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon
[email protected];[email protected];Belgium;["write", "execute"];FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse
[email protected];[email protected];Canada;["execute", "Read"];FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse
[email protected];[email protected];China;["Write"];FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse
[email protected];[email protected];France;["read", "write","execute"];FALSE;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse
[email protected];[email protected];Greece;["","execute"];FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse
[email protected];[email protected];Monaco;,null, ,,nil, None;FALSE;Cat;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse
50 changes: 21 additions & 29 deletions src/ConfigCatClient/Evaluation/RolloutEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
case UserComparator.SemVerNotOneOf:
if (!SemVersion.TryParse(userAttributeValue!.Trim(), out var version, strict: true))
{
error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue);
error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version");
return false;
}
return EvaluateSemVerOneOf(version, condition.StringListValue, negate: comparator == UserComparator.SemVerNotOneOf);
Expand All @@ -366,7 +366,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
case UserComparator.SemVerGreaterThanEqual:
if (!SemVersion.TryParse(userAttributeValue!.Trim(), out version, strict: true))
{
error = HandleInvalidSemVerUserAttribute(condition, context.Key, userAttributeName, userAttributeValue);
error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid semantic version");
return false;
}
return EvaluateSemVerRelation(version, comparator, condition.StringValue);
Expand All @@ -379,7 +379,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
case UserComparator.NumberGreaterThanEqual:
if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out var number))
{
error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue);
error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid decimal number");
return false;
}
return EvaluateNumberRelation(number, condition.Comparator, condition.DoubleValue);
Expand All @@ -388,14 +388,24 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
case UserComparator.DateTimeAfter:
if (!double.TryParse(userAttributeValue!.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out number))
{
error = HandleInvalidNumberUserAttribute(condition, context.Key, userAttributeName, userAttributeValue, isDateTime: true);
error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)");
return false;
}
return EvaluateDateTimeRelation(number, condition.DoubleValue, before: comparator == UserComparator.DateTimeBefore);

case UserComparator.SensitiveArrayContains:
case UserComparator.SensitiveArrayNotContains:
return EvaluateSensitiveArrayContains(userAttributeValue!, condition.StringListValue,
string[]? array;
try { array = userAttributeValue!.Deserialize<string[]>(); }
catch { array = null; }

if (array is null)
{
error = HandleInvalidUserAttribute(condition, context.Key, userAttributeName, $"'{userAttributeValue}' is not a valid JSON string array");
return false;
}

return EvaluateSensitiveArrayContains(array, condition.StringListValue,
EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContains);

default:
Expand Down Expand Up @@ -560,25 +570,17 @@ private static bool EvaluateDateTimeRelation(double number, double? comparisonVa
return before ? number < number2 : number > number2;
}

private static bool EvaluateSensitiveArrayContains(string csvText, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate)
private static bool EvaluateSensitiveArrayContains(string[] array, string[]? comparisonValues, string configJsonSalt, string contextSalt, bool negate)
{
EnsureComparisonValue(comparisonValues);

int index;
for (var startIndex = 0; startIndex < csvText.Length; startIndex = index + 1)
for (var i = 0; i < array.Length; i++)
{
index = csvText.IndexOf(',', startIndex);
if (index < 0)
{
index = csvText.Length;
}
var hash = HashComparisonValue(array[i].AsSpan(), configJsonSalt, contextSalt);

var slice = csvText.AsSpan(startIndex, index - startIndex).Trim();
var hash = HashComparisonValue(slice, configJsonSalt, contextSalt);

for (var i = 0; i < comparisonValues.Length; i++)
for (var j = 0; j < comparisonValues.Length; j++)
{
if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[i]).AsSpan()))
if (hash.Equals(hexString: EnsureComparisonValue(comparisonValues[j]).AsSpan()))
{
return !negate;
}
Expand Down Expand Up @@ -719,18 +721,8 @@ private static T EnsureComparisonValue<T>([NotNull] T? value)
return value ?? throw new InvalidOperationException("Comparison value is missing or invalid.");
}

private string HandleInvalidSemVerUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue)
{
var reason = $"'{userAttributeValue}' is not a valid semantic version";
this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName);
return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason);
}

private string HandleInvalidNumberUserAttribute(UserCondition condition, string key, string userAttributeName, string userAttributeValue, bool isDateTime = false)
private string HandleInvalidUserAttribute(UserCondition condition, string key, string userAttributeName, string reason)
{
var reason = isDateTime
? $"'{userAttributeValue}' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)"
: $"'{userAttributeValue}' is not a valid decimal number";
this.logger.UserObjectAttributeIsInvalid(condition.ToString(), key, reason, userAttributeName);
return string.Format(CultureInfo.InvariantCulture, InvalidUserAttributeError, userAttributeName, reason);
}
Expand Down
8 changes: 7 additions & 1 deletion src/ConfigCatClient/Extensions/SerializationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using Newtonsoft.Json;
#else
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
#endif
Expand All @@ -12,6 +13,11 @@ internal static class SerializationExtensions
{
#if USE_NEWTONSOFT_JSON
private static readonly JsonSerializer Serializer = JsonSerializer.Create();
#else
private static readonly JsonSerializerOptions SerializerOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
#endif

public static T? Deserialize<T>(this string json) => json.AsSpan().Deserialize<T>();
Expand Down Expand Up @@ -46,7 +52,7 @@ public static string Serialize<T>(this T objectToSerialize)
#if USE_NEWTONSOFT_JSON
return JsonConvert.SerializeObject(objectToSerialize);
#else
return JsonSerializer.Serialize(objectToSerialize);
return JsonSerializer.Serialize(objectToSerialize, SerializerOptions);
#endif
}
}
45 changes: 45 additions & 0 deletions src/ConfigCatClient/User.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using ConfigCat.Client.Utils;
using System.Linq;

#if USE_NEWTONSOFT_JSON
using Newtonsoft.Json;
Expand All @@ -13,6 +17,47 @@ namespace ConfigCat.Client;
/// </summary>
public class User
{
/// <summary>
/// Converts the specified <see cref="DateTimeOffset"/> value to the format expected by datetime comparison operators (BEFORE/AFTER).
/// </summary>
/// <param name="dateTime">The <see cref="DateTimeOffset"/> value to convert.</param>
/// <returns>The User Object attribute value in the expected format.</returns>
public static string AttributeValueFrom(DateTimeOffset dateTime)
{
var unixTimeSeconds = DateTimeUtils.ToUnixTimeMilliseconds(dateTime.UtcDateTime) / 1000.0;
return unixTimeSeconds.ToString("0.###", CultureInfo.InvariantCulture);
}

/// <summary>
/// Converts the specified <see cref="double"/> value to the format expected by number comparison operators.
/// </summary>
/// <param name="number">The <see cref="double"/> value to convert.</param>
/// <returns>The User Object attribute value in the expected format.</returns>
public static string AttributeValueFrom(double number)
{
return number.ToString("g", CultureInfo.InvariantCulture); // format "g" allows scientific notation as well
}

/// <summary>
/// Converts the specified <see cref="string"/> items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF).
/// </summary>
/// <param name="items">The <see cref="string"/> items to convert.</param>
/// <returns>The User Object attribute value in the expected format.</returns>
public static string AttributeValueFrom(params string[] items)
{
return AttributeValueFrom(items.AsEnumerable());
}

/// <summary>
/// Converts the specified <see cref="string"/> items to the format expected by array comparison operators (ARRAY CONTAINS ANY OF/ARRAY NOT CONTAINS ANY OF).
/// </summary>
/// <param name="items">The <see cref="string"/> items to convert.</param>
/// <returns>The User Object attribute value in the expected format.</returns>
public static string AttributeValueFrom(IEnumerable<string> items)
{
return (items ?? throw new ArgumentNullException("items")).Serialize();
}

internal const string DefaultIdentifierValue = "";

/// <summary>
Expand Down

0 comments on commit c4e2063

Please sign in to comment.