using System.Reflection;
using Flee.PublicTypes;
namespace MyIA.AI.ComponentModel.Rules;
///
/// Compiles a business rule written as a string (Flee expression syntax) into a reusable
/// predicate over an entity type. This is the "universal predicate" of the socle
/// (EPIC #7265, nugget B4 "le liant universel"): a non-developper writes a rule as text,
/// the builder compiles it once, and it filters any collection of entities discovered by
/// reflection (cf. ). The rule changes
/// without recompiling the host — the low-code bridge between data and code.
///
///
/// Expression syntax. Flee uses a VB-like operator set: and / or
/// / not (words, not &&/||) and = for equality
/// (not ==). Comparisons (>, <, >=, <>)
/// and arithmetic are as expected. Example: "Montant > 1000 and Pays = \"FR\"".
/// How it binds. Public instance properties of become
/// the rule's variables, by name. The expression is compiled once against the property
/// types (a non-null sentinel of each type is set before compile so Flee can infer them),
/// then evaluated per entity by reflecting its current property values. Compile cost is
/// paid once; evaluation is a variable-mutation + native delegate call.
///
public static class FleePredicateBuilder
{
///
/// Compiles into a predicate over .
///
/// Entity type whose public properties are the rule variables.
/// Flee expression evaluating to a boolean, e.g.
/// "Montant > 1000 and Pays = \"FR\"".
/// A predicate that, given a instance, reflects its
/// properties into the compiled expression and returns the boolean verdict.
/// is null/empty.
/// The rule is syntactically
/// invalid or references a property that does not exist on .
public static Func Create(string rule)
{
if (string.IsNullOrWhiteSpace(rule))
{
throw new ArgumentNullException(nameof(rule));
}
// Reflect the public instance properties once: they become the rule's variables.
var props = typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
// Indexers are not scalar variables — exclude them (Flee binds names to values).
.Where(p => p.GetIndexParameters().Length == 0)
.ToArray();
var context = new ExpressionContext();
context.Options.CaseSensitive = true;
// A non-null sentinel of each property type lets Flee infer variable types at
// compile time. Flee rejects null values, so string => "" and reference types
// without a parameterless ctor fall back to "" (they bind by name, not by value).
foreach (var prop in props)
{
context.Variables[prop.Name] = NonNullDefault(prop.PropertyType);
}
var compiled = context.CompileGeneric(rule);
return entity =>
{
foreach (var prop in props)
{
var value = prop.GetValue(entity);
context.Variables[prop.Name] = value ?? NonNullDefault(prop.PropertyType);
}
return compiled.Evaluate();
};
}
private static object NonNullDefault(Type type)
{
if (type == typeof(string))
{
return string.Empty;
}
if (type.IsValueType)
{
return Activator.CreateInstance(type)!;
}
// Reference type: prefer a parameterless ctor, else an empty string sentinel
// (the variable binds by name; the placeholder is replaced per-entity at eval time).
return Activator.CreateInstance(type) ?? (object)string.Empty;
}
}