Created
February 18, 2026 02:59
-
-
Save reed-lawrence/a4b544c1581f8afd8740191b601701f4 to your computer and use it in GitHub Desktop.
Abstract HTTP request AST, translator, and evaluator
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
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using System.Linq.Expressions; | |
| using System.Text.RegularExpressions; | |
| public sealed class RequestAbstract | |
| { | |
| public string Path { get; init; } = ""; | |
| public string Method { get; init; } = ""; | |
| public IReadOnlyDictionary<string, string> Headers { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| public IReadOnlyDictionary<string, string> QueryParameters { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| public IReadOnlyDictionary<string, string> PathParameters { get; init; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); | |
| public JsonPathProxy JsonPath { get; } = new(); | |
| public PathTemplateProxy PathTemplate { get; } = new(); | |
| } | |
| public sealed class JsonPathProxy | |
| { | |
| public string this[string path] => throw new NotSupportedException(); | |
| public JsonPathIntProxy Int => new(); | |
| public JsonPathDecimalProxy Decimal => new(); | |
| public JsonPathBoolProxy Bool => new(); | |
| } | |
| public sealed class JsonPathIntProxy | |
| { | |
| public int this[string path] => throw new NotSupportedException(); | |
| } | |
| public sealed class JsonPathDecimalProxy | |
| { | |
| public decimal this[string path] => throw new NotSupportedException(); | |
| } | |
| public sealed class JsonPathBoolProxy | |
| { | |
| public bool this[string path] => throw new NotSupportedException(); | |
| } | |
| public sealed class PathTemplateProxy | |
| { | |
| public bool this[string template] => throw new NotSupportedException(); | |
| } | |
| // ------------------ AST ------------------ | |
| public abstract record FilterNode; | |
| public sealed record AndNode(FilterNode Left, FilterNode Right) : FilterNode; | |
| public sealed record OrNode(FilterNode Left, FilterNode Right) : FilterNode; | |
| public sealed record NotNode(FilterNode Inner) : FilterNode; | |
| public sealed record FieldRef(string Kind, string Name, string? SubKey = null); | |
| public enum CompareOp | |
| { | |
| Eq, Neq, Gt, Gte, Lt, Lte, | |
| Contains, StartsWith, EndsWith, | |
| In, Exists, Regex | |
| } | |
| public sealed record CompareNode( | |
| CompareOp Op, | |
| FieldRef Field, | |
| string? Value = null, | |
| string? ValueType = null, | |
| StringComparison? Comparison = null | |
| ) : FilterNode; | |
| public sealed record PathTemplateNode(string Template, bool CaseSensitive = false) : FilterNode; | |
| // ------------------ Translator ------------------ | |
| public static class V2ExpressionTranslator | |
| { | |
| public static FilterNode Translate(Expression<Func<RequestAbstract, bool>> expr) | |
| => TranslateExpr(expr.Body); | |
| private static FilterNode TranslateExpr(Expression expr) | |
| => expr switch | |
| { | |
| BinaryExpression b when b.NodeType == ExpressionType.AndAlso | |
| => new AndNode(TranslateExpr(b.Left), TranslateExpr(b.Right)), | |
| BinaryExpression b when b.NodeType == ExpressionType.OrElse | |
| => new OrNode(TranslateExpr(b.Left), TranslateExpr(b.Right)), | |
| BinaryExpression b when b.NodeType == ExpressionType.Equal | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Eq), | |
| BinaryExpression b when b.NodeType == ExpressionType.NotEqual | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Neq), | |
| BinaryExpression b when b.NodeType == ExpressionType.GreaterThan | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Gt), | |
| BinaryExpression b when b.NodeType == ExpressionType.GreaterThanOrEqual | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Gte), | |
| BinaryExpression b when b.NodeType == ExpressionType.LessThan | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Lt), | |
| BinaryExpression b when b.NodeType == ExpressionType.LessThanOrEqual | |
| => TranslateCompare(b.Left, b.Right, CompareOp.Lte), | |
| UnaryExpression u when u.NodeType == ExpressionType.Not | |
| => new NotNode(TranslateExpr(u.Operand)), | |
| MethodCallExpression m when IsStringMethod(m, out var op, out var comparison) | |
| => TranslateStringMethod(m, op, comparison), | |
| MethodCallExpression m when IsRegexIsMatch(m) | |
| => TranslateRegex(m), | |
| MethodCallExpression m when IsContainsKey(m, out var dictName, out var key) | |
| => new CompareNode(CompareOp.Exists, new FieldRef("dict", dictName, key)), | |
| MethodCallExpression m when IsDictIndexer(m, out var dictName2, out var key2) | |
| => new CompareNode(CompareOp.Exists, new FieldRef("dict", dictName2, key2)), | |
| _ => throw new NotSupportedException($"Unsupported expression: {expr}") | |
| }; | |
| private static FilterNode TranslateCompare(Expression left, Expression right, CompareOp op) | |
| { | |
| if (!TryGetField(left, out var field, out var valueType)) | |
| { | |
| if (TryGetField(right, out field, out valueType)) | |
| return TranslateCompare(right, left, op); | |
| throw new NotSupportedException("Comparison must include a supported field."); | |
| } | |
| var value = ExtractConstantAsString(right); | |
| return new CompareNode(op, field, value, valueType); | |
| } | |
| private static FilterNode TranslateStringMethod(MethodCallExpression m, CompareOp op, StringComparison comparison) | |
| { | |
| if (!TryGetField(m.Object!, out var field, out var valueType)) | |
| throw new NotSupportedException("String method must be called on a supported field."); | |
| var value = ExtractConstantAsString(m.Arguments[0]); | |
| return new CompareNode(op, field, value, valueType, comparison); | |
| } | |
| private static FilterNode TranslateRegex(MethodCallExpression m) | |
| { | |
| // Regex.IsMatch(input, pattern) | |
| var input = m.Arguments[0]; | |
| var pattern = m.Arguments[1]; | |
| if (!TryGetField(input, out var field, out var valueType)) | |
| throw new NotSupportedException("Regex.IsMatch input must be a supported field."); | |
| var value = ExtractConstantAsString(pattern); | |
| return new CompareNode(CompareOp.Regex, field, value, valueType); | |
| } | |
| private static bool TryGetField(Expression expr, out FieldRef field, out string? valueType) | |
| { | |
| // RequestAbstract direct fields | |
| if (expr is MemberExpression me && me.Expression?.Type == typeof(RequestAbstract)) | |
| { | |
| if (me.Member.Name == "Path") | |
| { | |
| field = new FieldRef("path", "path"); | |
| valueType = "string"; | |
| return true; | |
| } | |
| if (me.Member.Name == "Method") | |
| { | |
| field = new FieldRef("method", "method"); | |
| valueType = "string"; | |
| return true; | |
| } | |
| } | |
| // Dictionary indexer: req.Headers["k"] | |
| if (IsDictIndexer(expr, out var dictName, out var key)) | |
| { | |
| field = new FieldRef("dict", dictName, key); | |
| valueType = "string"; | |
| return true; | |
| } | |
| // JsonPath proxy indexer: req.JsonPath["$.status"] | |
| if (IsJsonPathIndexer(expr, out var path, out var type)) | |
| { | |
| field = new FieldRef("body", "jsonPath", path); | |
| valueType = type; | |
| return true; | |
| } | |
| // PathTemplate: req.PathTemplate["/orders/{id:int}"] is boolean | |
| if (IsPathTemplateIndexer(expr, out var template)) | |
| { | |
| field = new FieldRef("pathTemplate", "pathTemplate", template); | |
| valueType = "bool"; | |
| return true; | |
| } | |
| field = null!; | |
| valueType = null; | |
| return false; | |
| } | |
| private static bool IsStringMethod(MethodCallExpression m, out CompareOp op, out StringComparison comparison) | |
| { | |
| op = default; | |
| comparison = StringComparison.Ordinal; | |
| if (m.Method.DeclaringType != typeof(string)) | |
| return false; | |
| if (m.Method.Name == "Contains") { op = CompareOp.Contains; return true; } | |
| if (m.Method.Name == "StartsWith") { op = CompareOp.StartsWith; return true; } | |
| if (m.Method.Name == "EndsWith") { op = CompareOp.EndsWith; return true; } | |
| if (m.Method.Name == "Equals") | |
| { | |
| op = CompareOp.Eq; | |
| if (m.Arguments.Count == 2 && m.Arguments[1] is ConstantExpression ce && ce.Value is StringComparison sc) | |
| comparison = sc; | |
| return true; | |
| } | |
| return false; | |
| } | |
| private static bool IsRegexIsMatch(MethodCallExpression m) | |
| => m.Method.DeclaringType == typeof(Regex) && m.Method.Name == "IsMatch"; | |
| private static bool IsContainsKey(MethodCallExpression m, out string dictName, out string key) | |
| { | |
| if (m.Method.Name == "ContainsKey" && m.Object is MemberExpression dict | |
| && dict.Member.Name is "Headers" or "QueryParameters" or "PathParameters") | |
| { | |
| dictName = dict.Member.Name; | |
| key = ExtractConstantAsString(m.Arguments[0]); | |
| return true; | |
| } | |
| dictName = key = ""; | |
| return false; | |
| } | |
| private static bool IsDictIndexer(Expression expr, out string dictName, out string key) | |
| { | |
| if (expr is MethodCallExpression call && call.Method.Name == "get_Item" | |
| && call.Object is MemberExpression dict | |
| && dict.Member.Name is "Headers" or "QueryParameters" or "PathParameters") | |
| { | |
| dictName = dict.Member.Name; | |
| key = ExtractConstantAsString(call.Arguments[0]); | |
| return true; | |
| } | |
| dictName = key = ""; | |
| return false; | |
| } | |
| private static bool IsJsonPathIndexer(Expression expr, out string path, out string valueType) | |
| { | |
| // req.JsonPath["$.x"] or req.JsonPath.Int["$.x"] | |
| if (expr is MethodCallExpression call && call.Method.Name == "get_Item") | |
| { | |
| if (call.Object is MemberExpression me && me.Member.Name == "JsonPath") | |
| { | |
| path = ExtractConstantAsString(call.Arguments[0]); | |
| valueType = "string"; | |
| return true; | |
| } | |
| if (call.Object is MemberExpression me2 && me2.Member.Name is "Int" or "Decimal" or "Bool" | |
| && me2.Expression is MemberExpression parent && parent.Member.Name == "JsonPath") | |
| { | |
| path = ExtractConstantAsString(call.Arguments[0]); | |
| valueType = me2.Member.Name switch | |
| { | |
| "Int" => "int", | |
| "Decimal" => "decimal", | |
| "Bool" => "bool", | |
| _ => "string" | |
| }; | |
| return true; | |
| } | |
| } | |
| path = valueType = ""; | |
| return false; | |
| } | |
| private static bool IsPathTemplateIndexer(Expression expr, out string template) | |
| { | |
| if (expr is MethodCallExpression call && call.Method.Name == "get_Item" | |
| && call.Object is MemberExpression me && me.Member.Name == "PathTemplate") | |
| { | |
| template = ExtractConstantAsString(call.Arguments[0]); | |
| return true; | |
| } | |
| template = ""; | |
| return false; | |
| } | |
| private static string ExtractConstantAsString(Expression expr) | |
| { | |
| if (expr is ConstantExpression c && c.Value is string s) | |
| return s; | |
| var lambda = Expression.Lambda(expr); | |
| var value = lambda.Compile().DynamicInvoke(); | |
| return value?.ToString() ?? ""; | |
| } | |
| } | |
| // ------------------ Evaluator ------------------ | |
| public sealed class V2Evaluator | |
| { | |
| public Func<RequestAbstract, bool> Compile(FilterNode node) | |
| => req => Evaluate(node, req); | |
| private bool Evaluate(FilterNode node, RequestAbstract req) | |
| => node switch | |
| { | |
| AndNode a => Evaluate(a.Left, req) && Evaluate(a.Right, req), | |
| OrNode o => Evaluate(o.Left, req) || Evaluate(o.Right, req), | |
| NotNode n => !Evaluate(n.Inner, req), | |
| PathTemplateNode p => MatchPathTemplate(req.Path, p.Template, p.CaseSensitive), | |
| CompareNode c => MatchCompare(c, req), | |
| _ => false | |
| }; | |
| private bool MatchCompare(CompareNode c, RequestAbstract req) | |
| { | |
| var actual = ResolveField(c.Field, req); | |
| if (c.Field.Kind == "pathTemplate") | |
| return actual == "true"; | |
| return c.Op switch | |
| { | |
| CompareOp.Eq => string.Equals(actual, c.Value, c.Comparison ?? StringComparison.Ordinal), | |
| CompareOp.Neq => !string.Equals(actual, c.Value, c.Comparison ?? StringComparison.Ordinal), | |
| CompareOp.Contains => (actual ?? "").Contains(c.Value ?? "", c.Comparison ?? StringComparison.Ordinal), | |
| CompareOp.StartsWith => (actual ?? "").StartsWith(c.Value ?? "", c.Comparison ?? StringComparison.Ordinal), | |
| CompareOp.EndsWith => (actual ?? "").EndsWith(c.Value ?? "", c.Comparison ?? StringComparison.Ordinal), | |
| CompareOp.Regex => Regex.IsMatch(actual ?? "", c.Value ?? ""), | |
| CompareOp.Exists => !string.IsNullOrEmpty(actual), | |
| CompareOp.Gt or CompareOp.Gte or CompareOp.Lt or CompareOp.Lte => CompareTyped(actual, c), | |
| _ => false | |
| }; | |
| } | |
| private string? ResolveField(FieldRef field, RequestAbstract req) | |
| => field.Kind switch | |
| { | |
| "path" => req.Path, | |
| "method" => req.Method, | |
| "dict" => GetValue(req, field.Name, field.SubKey), | |
| "body" => JsonPathSelect(req, field.SubKey, field.Name), | |
| "pathTemplate" => MatchPathTemplate(req.Path, field.SubKey ?? "", false) ? "true" : "false", | |
| _ => null | |
| }; | |
| private static string? GetValue(RequestAbstract req, string dictName, string? key) | |
| { | |
| if (key == null) return null; | |
| var dict = dictName switch | |
| { | |
| "Headers" => req.Headers, | |
| "QueryParameters" => req.QueryParameters, | |
| "PathParameters" => req.PathParameters, | |
| _ => null | |
| }; | |
| if (dict == null) return null; | |
| return dict.TryGetValue(key, out var value) ? value : null; | |
| } | |
| private static string? JsonPathSelect(RequestAbstract req, string? path, string name) | |
| { | |
| // Replace with real JSONPath implementation and cached body parsing | |
| _ = name; | |
| _ = req; | |
| return path == null ? null : ""; | |
| } | |
| private static bool CompareTyped(string? actual, CompareNode c) | |
| { | |
| if (actual == null) return false; | |
| if (c.ValueType == "int" && int.TryParse(actual, out var a) && int.TryParse(c.Value, out var b)) | |
| return c.Op switch | |
| { | |
| CompareOp.Gt => a > b, | |
| CompareOp.Gte => a >= b, | |
| CompareOp.Lt => a < b, | |
| CompareOp.Lte => a <= b, | |
| _ => false | |
| }; | |
| if (c.ValueType == "decimal" && decimal.TryParse(actual, out var da) && decimal.TryParse(c.Value, out var db)) | |
| return c.Op switch | |
| { | |
| CompareOp.Gt => da > db, | |
| CompareOp.Gte => da >= db, | |
| CompareOp.Lt => da < db, | |
| CompareOp.Lte => da <= db, | |
| _ => false | |
| }; | |
| if (c.ValueType == "bool" && bool.TryParse(actual, out var ba) && bool.TryParse(c.Value, out var bb)) | |
| return c.Op switch | |
| { | |
| CompareOp.Eq => ba == bb, | |
| CompareOp.Neq => ba != bb, | |
| _ => false | |
| }; | |
| return false; | |
| } | |
| private static bool MatchPathTemplate(string path, string template, bool caseSensitive) | |
| { | |
| // Minimal stub; replace with a robust path template engine. | |
| var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; | |
| if (template.Contains("{") && template.Contains("}")) | |
| return path.Split('/').Length == template.Split('/').Length; | |
| return string.Equals(path, template, comparison); | |
| } | |
| } | |
| // ------------------ Example ------------------ | |
| public static class Example | |
| { | |
| public static void Run() | |
| { | |
| Expression<Func<RequestAbstract, bool>> expr = | |
| req => | |
| req.PathTemplate["/orders/{id:int}"] && | |
| req.Method == "POST" && | |
| req.Headers["Authorization"] == "Bearer ABC123" && | |
| req.JsonPath["$.status"] == "approved" && | |
| req.JsonPath.Int["$.amount"] > 100; | |
| var ast = V2ExpressionTranslator.Translate(expr); | |
| var predicate = new V2Evaluator().Compile(ast); | |
| var req = new RequestAbstract | |
| { | |
| Path = "/orders/123", | |
| Method = "POST", | |
| Headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) | |
| { | |
| ["Authorization"] = "Bearer ABC123" | |
| } | |
| }; | |
| var result = predicate(req); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment