Skip to content

Commit 3853460

Browse files
feat: Added support for enums (#105)
1 parent 03ad3b3 commit 3853460

File tree

10 files changed

+403
-25
lines changed

10 files changed

+403
-25
lines changed

example/Dto/UserDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public record UserDto
88
public string Firstname { get; set; } = string.Empty;
99
public string Lastname { get; set; } = string.Empty;
1010
public int Age { get; set; }
11+
public Gender Gender { get; set; }
1112
public bool IsEmailVerified { get; set; }
1213
public double Test { get; set; }
1314
public int? NullableInt { get; set; }

example/Entities/User.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.ComponentModel.DataAnnotations.Schema;
2+
using System.Text.Json.Serialization;
23

34
public record User
45
{
56
public Guid Id { get; set; }
67
public string Firstname { get; set; } = string.Empty;
78
public string Lastname { get; set; } = string.Empty;
89
public int Age { get; set; }
10+
public Gender Gender { get; set; }
911
public bool IsDeleted { get; set; }
1012
public bool IsEmailVerified { get; set; }
1113
public double Test { get; set; }
@@ -41,4 +43,13 @@ public record Company
4143
public Guid Id { get; set; }
4244
public string Name { get; set; } = string.Empty;
4345
public string Department { get; set; } = string.Empty;
46+
}
47+
48+
[JsonConverter(typeof(JsonStringEnumConverter))]
49+
public enum Gender
50+
{
51+
Male,
52+
Female,
53+
[JsonStringEnumMemberName("Alternative")]
54+
Other
4455
}

example/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
.RuleFor(x => x.Firstname, f => f.Person.FirstName)
5252
.RuleFor(x => x.Lastname, f => f.Person.LastName)
5353
.RuleFor(x => x.Age, f => f.Random.Int(0, 100))
54+
.RuleFor(x => x.Gender, f => f.PickRandom<Gender>())
5455
.RuleFor(x => x.IsDeleted, f => f.Random.Bool())
5556
.RuleFor(x => x.Test, f => f.Random.Double())
5657
.RuleFor(x => x.NullableInt, f => f.Random.Bool() ? f.Random.Int(1, 100) : null)

src/GoatQuery/src/Evaluator/FilterEvaluator.cs

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Linq.Expressions;
55
using System.Reflection;
6+
using System.Text.Json.Serialization;
67
using FluentResults;
78

89
public static class FilterEvaluator
@@ -127,11 +128,6 @@ private static Result<MemberExpression> ResolvePropertyPathForCollection(
127128
return (MemberExpression)current;
128129
}
129130

130-
private static bool IsNullableReferenceType(Type type)
131-
{
132-
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
133-
}
134-
135131
private static bool IsPrimitiveType(Type type)
136132
{
137133
return type.IsPrimitive || type == typeof(string) || type == typeof(decimal) ||
@@ -226,13 +222,13 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
226222
{
227223
return literal switch
228224
{
229-
IntegerLiteral intLit => CreateIntegerConstant(intLit.Value, expression),
225+
IntegerLiteral intLit => CreateIntegerOrEnumConstant(intLit.Value, expression.Type),
230226
DateLiteral dateLit => Result.Ok(CreateDateConstant(dateLit, expression)),
231227
GuidLiteral guidLit => Result.Ok(Expression.Constant(guidLit.Value, expression.Type)),
232228
DecimalLiteral decLit => Result.Ok(Expression.Constant(decLit.Value, expression.Type)),
233229
FloatLiteral floatLit => Result.Ok(Expression.Constant(floatLit.Value, expression.Type)),
234230
DoubleLiteral dblLit => Result.Ok(Expression.Constant(dblLit.Value, expression.Type)),
235-
StringLiteral strLit => Result.Ok(Expression.Constant(strLit.Value, expression.Type)),
231+
StringLiteral strLit => CreateStringOrEnumConstant(strLit.Value, expression.Type),
236232
DateTimeLiteral dtLit => Result.Ok(Expression.Constant(dtLit.Value, expression.Type)),
237233
BooleanLiteral boolLit => Result.Ok(Expression.Constant(boolLit.Value, expression.Type)),
238234
NullLiteral _ => Result.Ok(Expression.Constant(null, expression.Type)),
@@ -248,11 +244,6 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
248244
return Result.Ok((constantResult.Value, property));
249245
}
250246

251-
private static Result<ConstantExpression> CreateIntegerConstant(int value, Expression expression)
252-
{
253-
return GetIntegerExpressionConstant(value, expression.Type);
254-
}
255-
256247
private static ConstantExpression CreateDateConstant(DateLiteral dateLiteral, Expression expression)
257248
{
258249
if (expression.Type == typeof(DateTime?))
@@ -563,9 +554,7 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
563554
{
564555
try
565556
{
566-
// Fetch the underlying type if it's nullable.
567-
var underlyingType = Nullable.GetUnderlyingType(targetType);
568-
var type = underlyingType ?? targetType;
557+
var type = GetNonNullableType(targetType);
569558

570559
object convertedValue = type switch
571560
{
@@ -586,9 +575,76 @@ private static Result<ConstantExpression> GetIntegerExpressionConstant(int value
586575
{
587576
return Result.Fail($"Value {value} is too large for type {targetType.Name}");
588577
}
589-
catch (Exception ex)
578+
catch (Exception)
579+
{
580+
return Result.Fail($"Error converting {value} to {targetType.Name}");
581+
}
582+
}
583+
584+
private static Result<ConstantExpression> CreateIntegerOrEnumConstant(int value, Type targetType)
585+
{
586+
var actualType = GetNonNullableType(targetType);
587+
588+
if (actualType.IsEnum)
589+
{
590+
return ConvertIntegerToEnum(value, actualType, targetType);
591+
}
592+
593+
return GetIntegerExpressionConstant(value, targetType);
594+
}
595+
596+
private static Result<ConstantExpression> ConvertIntegerToEnum(int value, Type actualType, Type targetType)
597+
{
598+
try
599+
{
600+
var enumValue = Enum.ToObject(actualType, value);
601+
602+
return Result.Ok(Expression.Constant(enumValue, targetType));
603+
}
604+
catch (Exception)
590605
{
591-
return Result.Fail($"Error converting {value} to {targetType.Name}: {ex.Message}");
606+
return Result.Fail($"Error converting {value} to enum type {targetType.Name}");
592607
}
593608
}
594-
}
609+
610+
private static Result<ConstantExpression> CreateStringOrEnumConstant(string value, Type targetType)
611+
{
612+
var actualType = GetNonNullableType(targetType);
613+
614+
if (actualType.IsEnum)
615+
{
616+
return ConvertStringToEnum(value, actualType, targetType);
617+
}
618+
619+
return Result.Ok(Expression.Constant(value, targetType));
620+
}
621+
622+
private static Result<ConstantExpression> ConvertStringToEnum(string value, Type actualType, Type targetType)
623+
{
624+
try
625+
{
626+
var enumValue = Enum.Parse(actualType, value, true);
627+
628+
return Result.Ok(Expression.Constant(enumValue, targetType));
629+
}
630+
catch (Exception)
631+
{
632+
foreach (var field in actualType.GetFields(BindingFlags.Public | BindingFlags.Static))
633+
{
634+
var memberNameAttribute = field.GetCustomAttribute<JsonStringEnumMemberNameAttribute>();
635+
636+
if (memberNameAttribute != null && memberNameAttribute.Name.Equals(value, StringComparison.Ordinal))
637+
{
638+
return Result.Ok(Expression.Constant(field.GetValue(null), targetType));
639+
}
640+
}
641+
642+
return Result.Fail($"Value '{value}' is not a valid member of enum {actualType.Name}");
643+
}
644+
}
645+
646+
private static Type GetNonNullableType(Type type)
647+
{
648+
return Nullable.GetUnderlyingType(type) ?? type;
649+
}
650+
}

src/GoatQuery/src/Utilities/PropertyMappingTree.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ private static bool IsPrimitiveType(Type type)
175175
if (type.IsPrimitive || PrimitiveTypes.Contains(type))
176176
return true;
177177

178-
// Handle nullable types
179178
var underlyingType = Nullable.GetUnderlyingType(type);
180179
return underlyingType != null && (underlyingType.IsPrimitive || PrimitiveTypes.Contains(underlyingType));
181180
}

src/GoatQuery/tests/Filter/FilterLexerTest.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,109 @@ public static IEnumerable<object[]> Parameters()
523523
new (TokenType.RPAREN, ")"),
524524
}
525525
};
526+
527+
yield return new object[]
528+
{
529+
"status eq 0",
530+
new KeyValuePair<TokenType, string>[]
531+
{
532+
new (TokenType.IDENT, "status"),
533+
new (TokenType.IDENT, "eq"),
534+
new (TokenType.INT, "0"),
535+
}
536+
};
537+
538+
yield return new object[]
539+
{
540+
"status eq 1",
541+
new KeyValuePair<TokenType, string>[]
542+
{
543+
new (TokenType.IDENT, "status"),
544+
new (TokenType.IDENT, "eq"),
545+
new (TokenType.INT, "1"),
546+
}
547+
};
548+
549+
yield return new object[]
550+
{
551+
"status ne 0",
552+
new KeyValuePair<TokenType, string>[]
553+
{
554+
new (TokenType.IDENT, "status"),
555+
new (TokenType.IDENT, "ne"),
556+
new (TokenType.INT, "0"),
557+
}
558+
};
559+
560+
yield return new object[]
561+
{
562+
"gender eq 'Male'",
563+
new KeyValuePair<TokenType, string>[]
564+
{
565+
new (TokenType.IDENT, "gender"),
566+
new (TokenType.IDENT, "eq"),
567+
new (TokenType.STRING, "Male"),
568+
}
569+
};
570+
571+
yield return new object[]
572+
{
573+
"gender eq 'Female'",
574+
new KeyValuePair<TokenType, string>[]
575+
{
576+
new (TokenType.IDENT, "gender"),
577+
new (TokenType.IDENT, "eq"),
578+
new (TokenType.STRING, "Female"),
579+
}
580+
};
581+
582+
yield return new object[]
583+
{
584+
"gender eq 'Alternative'",
585+
new KeyValuePair<TokenType, string>[]
586+
{
587+
new (TokenType.IDENT, "gender"),
588+
new (TokenType.IDENT, "eq"),
589+
new (TokenType.STRING, "Alternative"),
590+
}
591+
};
592+
593+
yield return new object[]
594+
{
595+
"gender ne 'Male'",
596+
new KeyValuePair<TokenType, string>[]
597+
{
598+
new (TokenType.IDENT, "gender"),
599+
new (TokenType.IDENT, "ne"),
600+
new (TokenType.STRING, "Male"),
601+
}
602+
};
603+
604+
yield return new object[]
605+
{
606+
"gender eq null",
607+
new KeyValuePair<TokenType, string>[]
608+
{
609+
new (TokenType.IDENT, "gender"),
610+
new (TokenType.IDENT, "eq"),
611+
new (TokenType.NULL, "null"),
612+
}
613+
};
614+
615+
yield return new object[]
616+
{
617+
"gender eq 'Male' and status eq 0",
618+
new KeyValuePair<TokenType, string>[]
619+
{
620+
new (TokenType.IDENT, "gender"),
621+
new (TokenType.IDENT, "eq"),
622+
new (TokenType.STRING, "Male"),
623+
new (TokenType.IDENT, "and"),
624+
new (TokenType.IDENT, "status"),
625+
new (TokenType.IDENT, "eq"),
626+
new (TokenType.INT, "0"),
627+
}
628+
};
526629
}
527630

528631
[Theory]

0 commit comments

Comments
 (0)