diff --git a/src/GoatQuery/src/Ast/Literals.cs b/src/GoatQuery/src/Ast/Literals.cs index 455b32d..21f228f 100644 --- a/src/GoatQuery/src/Ast/Literals.cs +++ b/src/GoatQuery/src/Ast/Literals.cs @@ -95,4 +95,14 @@ public BooleanLiteral(Token token, bool value) : base(token) { Value = value; } -} \ No newline at end of file +} + +public sealed class EnumSymbolLiteral : QueryExpression +{ + public string Value { get; set; } + + public EnumSymbolLiteral(Token token, string value) : base(token) + { + Value = value; + } +} diff --git a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs index ee38258..07463ef 100644 --- a/src/GoatQuery/src/Evaluator/FilterEvaluator.cs +++ b/src/GoatQuery/src/Evaluator/FilterEvaluator.cs @@ -52,21 +52,27 @@ private static Result EvaluatePropertyPathExpression( (Expression)context.CurrentLambda.Parameter : context.RootParameter; - var propertyPathResult = BuildPropertyPath(propertyPath, baseExpression, context.PropertyMappingTree); - if (propertyPathResult.IsFailed) return Result.Fail(propertyPathResult.Errors); + var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments, baseExpression, context.PropertyMappingTree); + if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors); - var finalProperty = propertyPathResult.Value; + var (finalProperty, guard, container) = safePathResult.Value; if (exp.Right is NullLiteral) { - var nullComparison = CreateNullComparison(exp, finalProperty); - return nullComparison; + return ComposeNestedNullComparison(finalProperty, guard, exp.Operator); } var comparisonResult = EvaluateValueComparison(exp, finalProperty); if (comparisonResult.IsFailed) return comparisonResult; - return comparisonResult.Value; + var comparison = comparisonResult.Value; + + var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right); + var combinedGuard = requireFinalNotNull + ? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type))) + : guard; + + return Expression.AndAlso(combinedGuard, comparison); } private static Result BuildPropertyPath( @@ -84,7 +90,6 @@ private static Result BuildPropertyPath( current = Expression.Property(current, propertyNode.ActualPropertyName); - // Navigate to nested mapping for next segment if (!isLast) { if (!propertyNode.HasNestedMapping) @@ -97,6 +102,55 @@ private static Result BuildPropertyPath( return Result.Ok((MemberExpression)current); } + private static Result<(MemberExpression Final, Expression Guard, Expression Container)> BuildPropertyPathWithGuard( + IList segments, + Expression startExpression, + PropertyMappingTree propertyMappingTree) + { + if (segments == null || segments.Count == 0) + return Result.Fail("Property path segments cannot be empty"); + + var current = startExpression; + var currentMappingTree = propertyMappingTree; + + Expression guard = Expression.Constant(true); + Expression container = startExpression; + + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + var isLast = i == segments.Count - 1; + + if (!currentMappingTree.TryGetProperty(segment, out var propertyNode)) + return Result.Fail($"Invalid property '{segment}' in path"); + + var next = Expression.Property(current, propertyNode.ActualPropertyName); + + if (!isLast) + { + if (!propertyNode.HasNestedMapping) + return Result.Fail($"Property '{segment}' does not support nested navigation"); + + if (!next.Type.IsValueType || Nullable.GetUnderlyingType(next.Type) != null) + { + var notNull = Expression.NotEqual(next, Expression.Constant(null, next.Type)); + guard = Expression.AndAlso(guard, notNull); + } + + current = next; + container = current; + currentMappingTree = propertyNode.NestedMapping; + } + else + { + var final = Expression.Property(current, propertyNode.ActualPropertyName); + return Result.Ok(((MemberExpression)final, guard, container)); + } + } + + return Result.Fail("Invalid property path"); + } + private static Result ResolvePropertyPathForCollection( PropertyPath propertyPath, Expression baseExpression, @@ -113,8 +167,6 @@ private static Result ResolvePropertyPathForCollection( return Result.Fail($"Invalid property '{segment}' in lambda expression property path"); current = Expression.Property(current, propertyNode.ActualPropertyName); - - // Navigate to nested mapping for next segment if (i < propertyPath.Segments.Count - 1) { if (!propertyNode.HasNestedMapping) @@ -224,6 +276,22 @@ private static Result CreateComparisonExpression(string operatorKeyw private static Result CreateConstantExpression(QueryExpression literal, Expression expression) { + if (IsEnumOrNullableEnum(expression.Type)) + { + if (literal is StringLiteral enumString) + { + return CreateEnumConstantFromString(enumString.Value, expression.Type); + } + if (literal is IntegerLiteral enumInt) + { + return CreateEnumConstantFromInteger(enumInt.Value, expression.Type); + } + if (literal is EnumSymbolLiteral enumSymbol) + { + return CreateEnumConstantFromString(enumSymbol.Value, expression.Type); + } + } + return literal switch { IntegerLiteral intLit => CreateIntegerConstant(intLit.Value, expression), @@ -236,6 +304,7 @@ private static Result CreateConstantExpression(QueryExpressi DateTimeLiteral dtLit => Result.Ok(Expression.Constant(dtLit.Value, expression.Type)), BooleanLiteral boolLit => Result.Ok(Expression.Constant(boolLit.Value, expression.Type)), NullLiteral _ => Result.Ok(Expression.Constant(null, expression.Type)), + EnumSymbolLiteral enumSym => Result.Fail("Unquoted identifiers are only allowed for enum values"), _ => Result.Fail($"Unsupported literal type: {literal.GetType().Name}") }; } @@ -326,7 +395,6 @@ private static Result EvaluateLambdaExpression(QueryLambdaExpression var (collectionProperty, elementType, lambdaParameter) = setupResult.Value; - // Enter lambda scope context.EnterLambdaScope(lambdaExp.Parameter, lambdaParameter, elementType); try @@ -382,7 +450,7 @@ private static Result ResolveCollectionProperty(QueryExpressio { return Result.Fail($"Invalid property '{identifier.TokenLiteral()}' in lambda expression"); } - return Expression.Property(baseExpression, propertyNode.ActualPropertyName); + return Expression.Property(baseExpression, propertyNode.ActualPropertyName) as MemberExpression; case PropertyPath propertyPath: return ResolvePropertyPathForCollection(propertyPath, baseExpression, propertyMappingTree); @@ -392,6 +460,42 @@ private static Result ResolveCollectionProperty(QueryExpressio } } + private static bool RequiresFinalNotNull(string operatorKeyword, MemberExpression finalProperty, QueryExpression right) + { + if (operatorKeyword.Equals(Keywords.Contains, StringComparison.OrdinalIgnoreCase)) + return true; + + if (right is NullLiteral) + return false; + + var type = finalProperty.Type; + if (!type.IsValueType) + return true; + + return Nullable.GetUnderlyingType(type) != null; + } + + private static Expression ComposeNestedNullComparison(MemberExpression finalProperty, Expression guard, string operatorKeyword) + { + var isEq = operatorKeyword.Equals(Keywords.Eq, StringComparison.OrdinalIgnoreCase); + var isNe = operatorKeyword.Equals(Keywords.Ne, StringComparison.OrdinalIgnoreCase); + var nullConst = Expression.Constant(null, finalProperty.Type); + var finalEqNull = Expression.Equal(finalProperty, nullConst); + var finalNeNull = Expression.NotEqual(finalProperty, nullConst); + var notGuard = Expression.Not(guard); + + if (isEq) + { + return Expression.OrElse(notGuard, finalEqNull); + } + else if (isNe) + { + return Expression.AndAlso(guard, finalNeNull); + } + + return Expression.AndAlso(guard, finalEqNull); + } + private static Result EvaluateLambdaBody(QueryExpression expression, FilterEvaluationContext context) { return expression switch @@ -431,7 +535,6 @@ private static Result EvaluateLambdaBodyIdentifier(InfixExpression e if (identifierName.Equals(context.CurrentLambda.ParameterName, StringComparison.OrdinalIgnoreCase)) { - // For primitive types (string, int, etc.), allow direct comparisons with the lambda parameter if (IsPrimitiveType(context.CurrentLambda.ElementType)) { return EvaluateValueComparison(exp, context.CurrentLambda.Parameter); @@ -467,28 +570,29 @@ private static Result EvaluateLambdaBodyLogicalOperator(InfixExpress private static Result EvaluateLambdaPropertyPath(InfixExpression exp, PropertyPath propertyPath, ParameterExpression lambdaParameter) { - // Skip the first segment (lambda parameter name) and build property path from lambda parameter - var current = (Expression)lambdaParameter; var elementType = lambdaParameter.Type; + var mapping = PropertyMappingTreeBuilder.BuildMappingTree(elementType, GetDefaultMaxDepth()); - // Build property path from lambda parameter - var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType); - if (pathResult.IsFailed) return pathResult; + var safePathResult = BuildPropertyPathWithGuard(propertyPath.Segments.Skip(1).ToList(), lambdaParameter, mapping); + if (safePathResult.IsFailed) return Result.Fail(safePathResult.Errors); - current = pathResult.Value; + var (finalProperty, guard, container) = safePathResult.Value; - var finalProperty = (MemberExpression)current; - - // Handle null comparisons if (exp.Right is NullLiteral) { - return exp.Operator == Keywords.Eq - ? Expression.Equal(finalProperty, Expression.Constant(null, finalProperty.Type)) - : Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type)); + return ComposeNestedNullComparison(finalProperty, guard, exp.Operator); } - // Handle value comparisons - return EvaluateValueComparison(exp, finalProperty); + var comparisonResult = EvaluateValueComparison(exp, finalProperty); + if (comparisonResult.IsFailed) return comparisonResult; + + var comparison = comparisonResult.Value; + var requireFinalNotNull = RequiresFinalNotNull(exp.Operator, finalProperty, exp.Right); + var combinedGuard = requireFinalNotNull + ? Expression.AndAlso(guard, Expression.NotEqual(finalProperty, Expression.Constant(null, finalProperty.Type))) + : guard; + + return Expression.AndAlso(combinedGuard, comparison); } private static Expression CreateAnyExpression(MemberExpression collection, LambdaExpression lambda, Type elementType) @@ -522,7 +626,6 @@ private static Result BuildLambdaPropertyPath(Expression startExpres current = Expression.Property(current, propertyNode.ActualPropertyName); - // Update mapping tree for nested navigation if (propertyNode.HasNestedMapping) { currentMappingTree = propertyNode.NestedMapping; @@ -539,7 +642,6 @@ private static int GetDefaultMaxDepth() private static Type GetCollectionElementType(Type collectionType) { - // Handle IEnumerable if (collectionType.IsGenericType) { var genericArgs = collectionType.GetGenericArguments(); @@ -550,7 +652,6 @@ private static Type GetCollectionElementType(Type collectionType) } } - // Handle arrays if (collectionType.IsArray) { return collectionType.GetElementType(); @@ -563,7 +664,11 @@ private static Result GetIntegerExpressionConstant(int value { try { - // Fetch the underlying type if it's nullable. + if (IsEnumOrNullableEnum(targetType)) + { + return CreateEnumConstantFromInteger(value, targetType); + } + var underlyingType = Nullable.GetUnderlyingType(targetType); var type = underlyingType ?? targetType; @@ -591,4 +696,58 @@ private static Result GetIntegerExpressionConstant(int value return Result.Fail($"Error converting {value} to {targetType.Name}: {ex.Message}"); } } -} \ No newline at end of file + + private static bool IsEnumOrNullableEnum(Type type) + { + var underlying = Nullable.GetUnderlyingType(type) ?? type; + return underlying.IsEnum; + } + + private static Result CreateEnumConstantFromString(string value, Type targetType) + { + var isNullable = Nullable.GetUnderlyingType(targetType) != null; + var enumType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + try + { + var enumValue = Enum.Parse(enumType, value, ignoreCase: true); + + if (isNullable) + { + var nullableType = typeof(Nullable<>).MakeGenericType(enumType); + var boxedNullable = Activator.CreateInstance(nullableType, enumValue); + return Expression.Constant(boxedNullable, targetType); + } + + return Expression.Constant(enumValue, targetType); + } + catch (ArgumentException) + { + return Result.Fail($"'{value}' is not a valid value for enum type {enumType.Name}"); + } + } + + private static Result CreateEnumConstantFromInteger(int intValue, Type targetType) + { + var isNullable = Nullable.GetUnderlyingType(targetType) != null; + var enumType = Nullable.GetUnderlyingType(targetType) ?? targetType; + + try + { + var enumValue = Enum.ToObject(enumType, intValue); + + if (isNullable) + { + var nullableType = typeof(Nullable<>).MakeGenericType(enumType); + var boxedNullable = Activator.CreateInstance(nullableType, enumValue); + return Expression.Constant(boxedNullable, targetType); + } + + return Expression.Constant(enumValue, targetType); + } + catch (Exception ex) + { + return Result.Fail($"Error converting integer {intValue} to enum type {enumType.Name}: {ex.Message}"); + } + } +} diff --git a/src/GoatQuery/src/Parser/Parser.cs b/src/GoatQuery/src/Parser/Parser.cs index 984665b..02289b1 100644 --- a/src/GoatQuery/src/Parser/Parser.cs +++ b/src/GoatQuery/src/Parser/Parser.cs @@ -190,7 +190,7 @@ private Result ParseFilterStatement() var statement = new InfixExpression(_currentToken, leftExpression, _currentToken.Literal); - if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE, TokenType.DATE, TokenType.NULL, TokenType.BOOLEAN)) + if (!PeekTokenIn(TokenType.STRING, TokenType.INT, TokenType.GUID, TokenType.DATETIME, TokenType.DECIMAL, TokenType.FLOAT, TokenType.DOUBLE, TokenType.DATE, TokenType.NULL, TokenType.BOOLEAN, TokenType.IDENT)) { return Result.Fail("Invalid value type within filter"); } @@ -295,6 +295,7 @@ private QueryExpression ParseLiteral(Token token) TokenType.BOOLEAN => bool.TryParse(token.Literal, out var boolValue) ? new BooleanLiteral(token, boolValue) : null, + TokenType.IDENT => new EnumSymbolLiteral(token, token.Literal), _ => null }; } diff --git a/src/GoatQuery/src/Utilities/PropertyMappingTree.cs b/src/GoatQuery/src/Utilities/PropertyMappingTree.cs index cec70a4..3e0f56d 100644 --- a/src/GoatQuery/src/Utilities/PropertyMappingTree.cs +++ b/src/GoatQuery/src/Utilities/PropertyMappingTree.cs @@ -23,7 +23,30 @@ public bool TryGetProperty(string jsonPropertyName, out PropertyMappingNode node return false; } - return ((Dictionary)Properties).TryGetValue(jsonPropertyName, out node); + if (((Dictionary)Properties).TryGetValue(jsonPropertyName, out node)) + { + return true; + } + + if (jsonPropertyName.EndsWith("Id", StringComparison.OrdinalIgnoreCase) && SourceType != null) + { + var typeName = SourceType.Name; + var expectedAlias = typeName + "Id"; + if (jsonPropertyName.Equals(expectedAlias, StringComparison.OrdinalIgnoreCase)) + { + foreach (var kvp in (Dictionary)Properties) + { + if (string.Equals(kvp.Value.ActualPropertyName, "Id", StringComparison.OrdinalIgnoreCase)) + { + node = kvp.Value; + return true; + } + } + } + } + + node = null; + return false; } internal void AddProperty(string jsonPropertyName, PropertyMappingNode node) @@ -172,11 +195,19 @@ private static bool ShouldCreateNestedMapping(Type type) private static bool IsPrimitiveType(Type type) { + if (type.IsEnum) + return true; + if (type.IsPrimitive || PrimitiveTypes.Contains(type)) return true; - // Handle nullable types var underlyingType = Nullable.GetUnderlyingType(type); - return underlyingType != null && (underlyingType.IsPrimitive || PrimitiveTypes.Contains(underlyingType)); + if (underlyingType == null) + return false; + + if (underlyingType.IsEnum) + return true; + + return underlyingType.IsPrimitive || PrimitiveTypes.Contains(underlyingType); } } \ No newline at end of file diff --git a/src/GoatQuery/tests/Filter/FilterParserTest.cs b/src/GoatQuery/tests/Filter/FilterParserTest.cs index b60a764..1eeabd6 100644 --- a/src/GoatQuery/tests/Filter/FilterParserTest.cs +++ b/src/GoatQuery/tests/Filter/FilterParserTest.cs @@ -23,6 +23,7 @@ public sealed class FilterParserTest [InlineData("dateOfBirth gte 2000-01-01", "dateOfBirth", "gte", "2000-01-01")] [InlineData("dateOfBirth eq 2023-01-30T09:29:55.1750906Z", "dateOfBirth", "eq", "2023-01-30T09:29:55.1750906Z")] [InlineData("balance eq null", "balance", "eq", "null")] + [InlineData("status eq Active", "status", "eq", "Active")] [InlineData("balance ne null", "balance", "ne", "null")] [InlineData("name eq NULL", "name", "eq", "NULL")] public void Test_ParsingFilterStatement(string input, string expectedLeft, string expectedOperator, string expectedRight) @@ -168,6 +169,7 @@ public void Test_ParsingFilterStatementWithAndAndOr() [Theory] [InlineData("manager/firstName eq 'John'", new string[] { "manager", "firstName" }, "eq", "John")] [InlineData("manager/manager/firstName eq 'John'", new string[] { "manager", "manager", "firstName" }, "eq", "John")] + [InlineData("manager/status eq Active", new string[] { "manager", "status" }, "eq", "Active")] public void Test_ParsingFilterStatementWithNestedProperty(string input, string[] expectedLeft, string expectedOperator, string expectedRight) { var lexer = new QueryLexer(input); diff --git a/src/GoatQuery/tests/Filter/FilterTest.cs b/src/GoatQuery/tests/Filter/FilterTest.cs index 35f6354..d8ab6bc 100644 --- a/src/GoatQuery/tests/Filter/FilterTest.cs +++ b/src/GoatQuery/tests/Filter/FilterTest.cs @@ -102,6 +102,11 @@ public static IEnumerable Parameters() new[] { TestData.Users["Harry"] } }; + yield return new object[] { + "userid eq e4c7772b-8947-4e46-98ed-644b417d2a08 or id eq 01998fda-e310-793c-bd8d-f6a92f87b31b", + new[] { TestData.Users["Jane"], TestData.Users["Harry"] } + }; + yield return new object[] { "age lt 3", new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Harry"], TestData.Users["Doe"] } @@ -497,6 +502,119 @@ public static IEnumerable Parameters() "tags/all(x: x eq 'premium')", new[] { TestData.Users["Egg"] } }; + + // Status enum tests + yield return new object[] { + "status eq 'Active'", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Doe"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "status eq 'Inactive'", + new[] { TestData.Users["Jane"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status eq 'Suspended'", + new[] { TestData.Users["Harry"] } + }; + + yield return new object[] { + "status ne 'Active'", + new[] { TestData.Users["Jane"], TestData.Users["Harry"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status ne 'Inactive'", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Harry"], TestData.Users["Doe"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "status ne 'Suspended'", + new[] { TestData.Users["John"], TestData.Users["Jane"], TestData.Users["Apple"], TestData.Users["Doe"], TestData.Users["Egg"], TestData.Users["NullUser"] } + }; + + // Status combined with other properties + yield return new object[] { + "status eq 'Active' and age eq 1", + new[] { TestData.Users["Apple"], TestData.Users["Doe"] } + }; + + yield return new object[] { + "status eq 'Inactive' or age eq 33", + new[] { TestData.Users["Jane"], TestData.Users["Egg"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status eq 'Active' and isEmailVerified eq true", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Doe"] } + }; + + yield return new object[] { + "status ne 'Active' and age lt 10", + new[] { TestData.Users["Jane"], TestData.Users["Harry"], TestData.Users["NullUser"] } + }; + + // Manager status tests + yield return new object[] { + "manager/status eq 'Active'", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "manager ne null and manager/status eq 'Active'", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "status eq Active", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Doe"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "status eq Inactive", + new[] { TestData.Users["Jane"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status eq Suspended", + new[] { TestData.Users["Harry"] } + }; + + yield return new object[] { + "status ne Active", + new[] { TestData.Users["Jane"], TestData.Users["Harry"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status ne Inactive", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Harry"], TestData.Users["Doe"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "status ne Suspended", + new[] { TestData.Users["John"], TestData.Users["Jane"], TestData.Users["Apple"], TestData.Users["Doe"], TestData.Users["Egg"], TestData.Users["NullUser"] } + }; + + yield return new object[] { + "status eq Active and age eq 1", + new[] { TestData.Users["Apple"], TestData.Users["Doe"] } + }; + + yield return new object[] { + "status eq Active and isEmailVerified eq true", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Doe"] } + }; + + yield return new object[] { + "manager/status eq Active", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Egg"] } + }; + + yield return new object[] { + "manager ne null and manager/status eq Active", + new[] { TestData.Users["John"], TestData.Users["Apple"], TestData.Users["Egg"] } + }; } [Theory] @@ -524,6 +642,7 @@ public void Test_Filter(string filter, IEnumerable expected) [InlineData("addresses/any(addr: addr/nonExistentProperty eq 'test')")] [InlineData("addresses/invalid(addr: addr/city/name eq 'test')")] [InlineData("nonExistentCollection/any(item: item eq 'test')")] + [InlineData("firstname eq John")] // Unquoted RHS on non-enum should error public void Test_InvalidFilterReturnsError(string filter) { var query = new Query diff --git a/src/GoatQuery/tests/TestData.cs b/src/GoatQuery/tests/TestData.cs index e04c1f0..b6ac06e 100644 --- a/src/GoatQuery/tests/TestData.cs +++ b/src/GoatQuery/tests/TestData.cs @@ -1,5 +1,16 @@ public static class TestData { + private static readonly User Manager01 = new User + { + Id = Guid.Parse("671e6bac-b6de-4cc7-b3e9-1a6ac4546b43"), + Age = 16, + Firstname = "Manager 01", + DateOfBirth = DateTime.Parse("2000-01-01 00:00:00").ToUniversalTime(), + BalanceDecimal = 2.00m, + IsEmailVerified = false, + Status = Status.Active + }; + public static readonly Dictionary Users = new Dictionary { ["John"] = new User @@ -9,6 +20,7 @@ public static class TestData DateOfBirth = DateTime.Parse("2004-01-31 23:59:59").ToUniversalTime(), BalanceDecimal = 1.50m, IsEmailVerified = true, + Status = Status.Active, Addresses = new[] { new Address @@ -22,22 +34,17 @@ public static class TestData City = new City { Name = "Boston", Country = "USA" } } }, - Manager = new User - { - Age = 16, - Firstname = "Manager 01", - DateOfBirth = DateTime.Parse("2000-01-01 00:00:00").ToUniversalTime(), - BalanceDecimal = 2.00m, - IsEmailVerified = false - } + Manager = Manager01 }, ["Jane"] = new User { + Id = Guid.Parse("01998fda-e310-793c-bd8d-f6a92f87b31b"), Age = 9, Firstname = "Jane", DateOfBirth = DateTime.Parse("2020-05-09 15:30:00").ToUniversalTime(), BalanceDecimal = 0, IsEmailVerified = false, + Status = Status.Inactive, Addresses = new[] { new Address @@ -59,6 +66,7 @@ public static class TestData DateOfBirth = DateTime.Parse("1980-12-31 00:00:01").ToUniversalTime(), BalanceFloat = 1204050.98f, IsEmailVerified = true, + Status = Status.Active, Addresses = new[] { new Address @@ -72,23 +80,18 @@ public static class TestData City = new City { Name = "New York", Country = "USA" } } }, - Manager = new User - { - Age = 16, - Firstname = "Manager 01", - DateOfBirth = DateTime.Parse("2000-01-01 00:00:00").ToUniversalTime(), - BalanceDecimal = 2.00m, - IsEmailVerified = true - }, + Manager = Manager01, Tags = ["vip", "premium"] }, ["Harry"] = new User { + Id = Guid.Parse("e4c7772b-8947-4e46-98ed-644b417d2a08"), Age = 1, Firstname = "Harry", DateOfBirth = DateTime.Parse("2002-08-01").ToUniversalTime(), BalanceDecimal = 0.5372958205929493m, IsEmailVerified = false, + Status = Status.Suspended, Addresses = Array.Empty
() }, ["Doe"] = new User @@ -98,6 +101,7 @@ public static class TestData DateOfBirth = DateTime.Parse("2023-07-26 12:00:30").ToUniversalTime(), BalanceDecimal = null, IsEmailVerified = true, + Status = Status.Active, Addresses = new[] { new Address @@ -114,6 +118,7 @@ public static class TestData DateOfBirth = DateTime.Parse("2000-01-01 00:00:00").ToUniversalTime(), BalanceDouble = 1334534453453433.33435443343231235652d, IsEmailVerified = false, + Status = Status.Active, Addresses = new[] { new Address @@ -134,6 +139,7 @@ public static class TestData DateOfBirth = DateTime.Parse("1999-04-21 00:00:00").ToUniversalTime(), BalanceDecimal = 19.00m, IsEmailVerified = true, + Status = Status.Active, Manager = new User { Age = 30, @@ -141,13 +147,15 @@ public static class TestData DateOfBirth = DateTime.Parse("1993-04-21 00:00:00").ToUniversalTime(), BalanceDecimal = 29.00m, IsEmailVerified = true, + Status = Status.Active, Manager = new User { Age = 40, Firstname = "Manager 04", DateOfBirth = DateTime.Parse("1983-04-21 00:00:00").ToUniversalTime(), BalanceDecimal = 39.00m, - IsEmailVerified = true + IsEmailVerified = true, + Status = Status.Active }, Company = new Company { @@ -167,7 +175,8 @@ public static class TestData BalanceDouble = null, BalanceFloat = null, IsEmailVerified = true, + Status = Status.Inactive, Addresses = Array.Empty
() }, }; -} \ No newline at end of file +} diff --git a/src/GoatQuery/tests/User.cs b/src/GoatQuery/tests/User.cs index 6021c6d..b98dd55 100644 --- a/src/GoatQuery/tests/User.cs +++ b/src/GoatQuery/tests/User.cs @@ -10,6 +10,7 @@ public record User public float? BalanceFloat { get; set; } public DateTime? DateOfBirth { get; set; } public bool IsEmailVerified { get; set; } + public Status Status { get; set; } public Company? Company { get; set; } public User? Manager { get; set; } public IEnumerable
Addresses { get; set; } = Array.Empty
(); @@ -41,4 +42,12 @@ public record Company public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Department { get; set; } = string.Empty; +} + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Status +{ + Active, + Inactive, + Suspended } \ No newline at end of file