Last active
October 6, 2021 20:34
-
-
Save forgetaboutit/183dcec83f6635bad09b0919e84faa69 to your computer and use it in GitHub Desktop.
json-filter-to-expression
This file contains hidden or 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
public enum Operation | |
{ | |
Gt, | |
Gte, | |
Lt, | |
Lte, | |
Eq, | |
Neq | |
} | |
public static class Functions | |
{ | |
public static TOut? Chain<TIn, TOut>( | |
this TIn? input, | |
Func<TIn, TOut?> fn) | |
{ | |
if (input is null) | |
{ | |
return default; | |
} | |
return fn(input); | |
} | |
public static Func<Expression, Expression, Expression> | |
OperationToCombinatorExpression( | |
Operation operation) | |
=> operation switch | |
{ | |
Operation.Gt => Expression.GreaterThan, | |
Operation.Gte => Expression.GreaterThanOrEqual, | |
Operation.Lt => Expression.LessThan, | |
Operation.Lte => Expression.LessThanOrEqual, | |
Operation.Eq => Expression.Equal, | |
Operation.Neq => Expression.NotEqual, | |
_ => throw new ArgumentOutOfRangeException(nameof(operation)) | |
}; | |
public static TOut? AggregateNonEmptyOrDefault<TIn, TOut>( | |
IReadOnlyCollection<TIn> source, | |
TOut defaultValue, | |
Func<TIn?, TOut?> selector, | |
Func<TOut, TOut, TOut?> aggregator) | |
{ | |
if (!source.Any()) | |
{ | |
return defaultValue; | |
} | |
return selector(source.First()). | |
Chain( | |
firstResult => source. | |
Skip(1). | |
Aggregate<TIn?, TOut?>( | |
firstResult, | |
(acc, cur) => acc. | |
Chain( | |
accumulated => selector(cur). | |
Chain(nextResult => aggregator(accumulated, nextResult))))); | |
} | |
public static Expression? AggregateGenExprs( | |
IReadOnlyList<IOperator> ops, | |
GenExprContext context, | |
Func<Expression, Expression, Expression> combinator) | |
=> AggregateNonEmptyOrDefault( | |
ops, | |
Expression.Constant(true), | |
op => op?.GenExpr(context), | |
combinator); | |
public static Expression? GenStringComparisonExpr( | |
GenExprContext context, | |
string member, | |
string value, | |
Func<Expression, Expression, Expression> mkExpr) | |
=> context. | |
GenStringMemberAccess(member). | |
Chain( | |
t => mkExpr( | |
t, | |
Expression.Constant(value))); | |
public static Expression? GenNumericComparisonExpr( | |
GenExprContext context, | |
string member, | |
double value, | |
Func<Expression, Expression, Expression> mkExpr) | |
=> context. | |
GenNumericMemberAccess(member). | |
Chain( | |
t => mkExpr( | |
t.Item2, | |
Expression.Constant( | |
Convert.ChangeType( | |
value, | |
t.Item1)))); | |
public static Expression<Func<T, bool>>? CompileFilter<T>( | |
IOperator op) | |
{ | |
var parameter = Expression.Parameter( | |
typeof(T), | |
"t"); | |
var ctx = new GenExprContext( | |
parameter, | |
typeof(T)); | |
return op. | |
GenExpr(ctx). | |
Chain(expr => | |
Expression.Lambda( | |
expr, | |
false, | |
parameter) as Expression<Func<T, bool>>); | |
} | |
} | |
public sealed record GenExprContext( | |
ParameterExpression InputParameter, | |
Type Type) | |
{ | |
private static readonly Type[] NumericTypes = new[] | |
{ | |
typeof(double), | |
typeof(float), | |
typeof(long), | |
typeof(int), | |
typeof(short), | |
typeof(sbyte), | |
typeof(ulong), | |
typeof(uint), | |
typeof(ushort), | |
typeof(byte), | |
}; | |
MemberInfo? FindMember( | |
string memberName, | |
Type memberType) | |
=> Type. | |
GetMember(memberName). | |
FirstOrDefault( | |
member => (member is PropertyInfo pi | |
&& pi.PropertyType == memberType | |
&& pi.CanRead) | |
|| (member is FieldInfo fi | |
&& fi.FieldType == memberType)); | |
public IReadOnlyCollection<MemberInfo> FindStringMembers() | |
=> Type. | |
GetMembers(). | |
Where(member => (member is PropertyInfo pi | |
&& pi.PropertyType == typeof(string) | |
&& pi.CanRead) | |
|| (member is FieldInfo fi | |
&& fi.FieldType == typeof(string))). | |
ToList(); | |
Tuple<Type, MemberInfo>? FindNumericMember( | |
string memberName) | |
=> Type. | |
GetMember(memberName). | |
Select( | |
member => (member is PropertyInfo pi | |
&& NumericTypes.Contains(pi.PropertyType) | |
&& pi.CanRead) | |
? Tuple.Create(pi.PropertyType, (MemberInfo)pi) | |
: ((member is FieldInfo fi | |
&& NumericTypes.Contains(fi.FieldType)) | |
? Tuple.Create(fi.FieldType, (MemberInfo)fi) | |
: null)). | |
FirstOrDefault(t => t is not null); | |
public Expression? GenStringMemberAccess( | |
string memberName) | |
=> FindMember( | |
memberName, | |
typeof(string)). | |
Chain( | |
member => Expression.MakeMemberAccess( | |
InputParameter, | |
member)); | |
public Tuple<Type, Expression>? GenNumericMemberAccess( | |
string memberName) | |
=> FindNumericMember( | |
memberName). | |
Chain( | |
t => Tuple.Create( | |
t.Item1, | |
(Expression)Expression.MakeMemberAccess( | |
InputParameter, | |
t.Item2))); | |
} | |
[JsonConverter(typeof(OperatorConverter))] | |
public interface IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context); | |
} | |
public sealed record OrOperator( | |
IReadOnlyList<IOperator> SubOps) | |
: IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> Functions.AggregateGenExprs( | |
SubOps, | |
context, | |
Expression.OrElse); | |
} | |
public sealed record AndOperator( | |
IReadOnlyList<IOperator> SubOps) | |
: IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> Functions.AggregateGenExprs( | |
SubOps, | |
context, | |
Expression.AndAlso); | |
} | |
public sealed record StringOperator( | |
string Member, | |
string Value, | |
Operation Operation) | |
: IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> Functions.GenStringComparisonExpr( | |
context, | |
Member, | |
Value, | |
Functions.OperationToCombinatorExpression(Operation)); | |
} | |
public sealed record NumOperator( | |
string Member, | |
double Value, | |
Operation Operation) | |
: IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> Functions.GenNumericComparisonExpr( | |
context, | |
Member, | |
Value, | |
Functions.OperationToCombinatorExpression(Operation)); | |
} | |
public sealed record LikeOperator( | |
string Member, | |
string Needle) | |
: IOperator | |
{ | |
public static readonly MethodInfo ContainsMethod = | |
typeof(string).GetMethod( | |
"Contains", | |
new[] { typeof(string) })!; | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> context. | |
GenStringMemberAccess(Member). | |
Chain( | |
memberAccess => Expression.Call( | |
memberAccess, | |
ContainsMethod, | |
Expression.Constant(Needle))); | |
} | |
public sealed record GoogleOperator( | |
string Needle) | |
: IOperator | |
{ | |
public Expression? GenExpr( | |
GenExprContext context) | |
=> Functions.AggregateNonEmptyOrDefault( | |
context.FindStringMembers(), | |
Expression.Constant(true), | |
mi => (Expression) Expression.Call( | |
Expression.MakeMemberAccess( | |
context.InputParameter, | |
mi), | |
LikeOperator.ContainsMethod, | |
Expression.Constant(Needle)), | |
Expression.OrElse); | |
} | |
public sealed class OperatorConverter | |
: JsonConverter | |
{ | |
public override bool CanConvert( | |
Type objectType) | |
{ | |
throw new NotImplementedException(); | |
} | |
public override object? ReadJson( | |
JsonReader reader, | |
Type objectType, | |
object? existingValue, | |
JsonSerializer serializer) | |
{ | |
var @object = JObject.Load(reader); | |
return ParseOperator(@object); | |
} | |
private static IOperator ParseOperator( | |
JObject @object) | |
{ | |
if (ParseAny( | |
@object, | |
o => ParseCombinator( | |
o, | |
"and", | |
ops => new AndOperator(ops)), | |
o => ParseCombinator( | |
o, | |
"or", | |
ops => new OrOperator(ops)), | |
ParseLike, | |
ParseGoogle, | |
o => ParseComparator( | |
o, | |
"eq", | |
Operation.Eq), | |
o => ParseComparator( | |
o, | |
"neq", | |
Operation.Neq), | |
o => ParseComparator( | |
o, | |
"gt", | |
Operation.Gt), | |
o => ParseComparator( | |
o, | |
"gte", | |
Operation.Gte), | |
o => ParseComparator( | |
o, | |
"lt", | |
Operation.Lt), | |
o => ParseComparator( | |
o, | |
"lte", | |
Operation.Lte)) is IOperator op) | |
{ | |
return op; | |
} | |
throw new NotImplementedException(); | |
} | |
private static string? ParseString( | |
JToken? token) | |
=> token is JValue { Type: JTokenType.String, Value: string s } | |
? s | |
: null; | |
private static double? ParseDouble( | |
JToken? token) | |
=> token is JValue { Type: JTokenType.Float, Value: double d } | |
? d | |
: null; | |
private static long? ParseLong( | |
JToken? token) | |
=> token is JValue { Type: JTokenType.Integer, Value: long l } | |
? l | |
: null; | |
private static IOperator? ParseAny( | |
JObject @object, | |
params Func< | |
JObject, | |
IOperator?>[] parsers) | |
=> parsers. | |
Select(p => p(@object)). | |
FirstOrDefault(result => result is not null); | |
private static IOperator? ParseGoogle( | |
JObject @object) | |
{ | |
if (@object["google"] is JToken google | |
&& ParseString(google) is string needle) | |
{ | |
return new GoogleOperator(needle); | |
} | |
return null; | |
} | |
private static IOperator? ParseCombinator( | |
JObject @object, | |
string combinatorName, | |
Func<IReadOnlyList<IOperator>, IOperator?> mkCombinator) | |
{ | |
if (@object[combinatorName] is JArray array | |
&& array. | |
Children<JObject>(). | |
Select(ParseOperator). | |
ToList() is { Count: > 0 } ops) | |
{ | |
return mkCombinator(ops); | |
} | |
return null; | |
} | |
private static IOperator? ParseLike( | |
JObject @object) | |
{ | |
if (@object["like"] is JObject likeObject | |
&& ParseString(likeObject["path"]) is string path | |
&& ParseString(likeObject["value"]) is string needle) | |
{ | |
return new LikeOperator(path, needle); | |
} | |
return null; | |
} | |
private static IOperator? ParseComparator( | |
JObject @object, | |
string comparatorName, | |
Operation operation) | |
{ | |
if (@object[comparatorName] is JObject { } eq | |
&& ParseString(eq["path"]) is string path) | |
{ | |
var value = eq["value"]; | |
if (ParseDouble(value) is double d) | |
{ | |
return new NumOperator( | |
path, | |
(double) d, | |
operation); | |
} | |
else if (ParseLong(value) is long l) | |
{ | |
return new NumOperator( | |
path, | |
(double) l, | |
operation); | |
} | |
else if (ParseString(value) is string s) | |
{ | |
return new StringOperator( | |
path, | |
s, | |
operation); | |
} | |
} | |
return null; | |
} | |
public override void WriteJson( | |
JsonWriter writer, | |
object? value, | |
JsonSerializer serializer) | |
=> throw new NotImplementedException(); | |
} | |
public sealed class Model | |
{ | |
public Model() { } | |
public string? S { get; set; } | |
public double D { get; set; } | |
public DateTime? Dt { get; set; } | |
public int I { get; set; } | |
public string? S1 { get; set; } | |
public string? S2 { get; set; } | |
public string? S3 { get; set; } | |
} | |
public void Main() | |
{ | |
var json = @" | |
{ | |
""or"": [ | |
{ ""like"": { ""path"": ""S"", ""value"": ""needle"" }}, | |
{ ""eq"": { ""path"": ""S"", ""value"": ""5"" }}, | |
{ ""eq"": { ""path"": ""I"", ""value"": 5.12 }}, | |
{ ""eq"": { ""path"": ""D"", ""value"": 5.12 }} | |
] | |
}"; | |
var googleJson = @" | |
{ | |
""google"": ""foobar"" | |
}"; | |
// { ""eq"": { ""path"": ""Dt"", ""value"": ""2021 - 01 - 05"" } }, | |
var filter = JsonConvert. | |
DeserializeObject<IOperator>(googleJson). | |
Chain(op => Functions.CompileFilter<Model>(op)); | |
filter.Dump(); | |
var filter2 = new GoogleOperator("banana"). | |
Chain(op => Functions.CompileFilter<Model>(op)); | |
filter2.Dump(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment