using Greencore.platform.rules;
namespace Greencore.platform.validators;
/// Motor secuencial común para árboles locales y contextuales.
public static class RuleEvaluator
{
public static RuleEvaluation Evaluate(RuleNode tree, T value, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default) => Evaluate(tree, value, default(NoRuleContext), options, cancellationToken);
public static ValueTask EvaluateAsync(RuleNode tree, T value, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default) => EvaluateAsync(tree, value, default(NoRuleContext), options, cancellationToken);
public static RuleEvaluation Evaluate(RuleNode tree, T value, C context, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(tree);
if (tree.IsAsync) throw new InvalidOperationException("This tree requires asynchronous evaluation.");
// Solo las hojas síncronas pueden llegar aquí; no se bloquea ninguna tarea externa.
return EvaluateAsync(tree, value, context, options, cancellationToken).GetAwaiter().GetResult();
}
public static ValueTask EvaluateAsync(RuleNode tree, T value, C context, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(tree);
var settings = options ?? new();
if (!Enum.IsDefined(settings.Execution) || !Enum.IsDefined(settings.Detail)) throw new ArgumentOutOfRangeException(nameof(options));
return Visit(tree, value, context, settings, cancellationToken);
}
private static async ValueTask Visit(RuleNode node, T value, C context, RuleEvaluationOptions options, CancellationToken token)
{
token.ThrowIfCancellationRequested();
if (node.Definition is { } definition)
{
var result = await definition.AsyncCondition(value, context, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
ArgumentNullException.ThrowIfNull(result);
return Result(node, result.IsSatisfied ? RuleStatus.Satisfied : RuleStatus.Unsatisfied, [], result.Evidence);
}
var children = new List();
if (node.Kind is RuleNodeKind.When or RuleNodeKind.Prerequisite)
{
var gate = await Visit(node.Children[0], value, context, options, token).ConfigureAwait(false);
children.Add(gate);
if (gate.Status != RuleStatus.Satisfied)
{
children.Add(Skip(node.Children[1], node.Kind == RuleNodeKind.When ? RuleSkipReason.NotApplicable : RuleSkipReason.Prerequisite));
var status = node.Kind == RuleNodeKind.When || gate.Status == RuleStatus.Skipped ? RuleStatus.Skipped : RuleStatus.Unsatisfied;
return Finish(node, status, children, options);
}
var dependent = await Visit(node.Children[1], value, context, options, token).ConfigureAwait(false);
children.Add(dependent);
return Finish(node, dependent.Status, children, options);
}
if (node.Kind == RuleNodeKind.Not)
{
var child = await Visit(node.Children[0], value, context, options, token).ConfigureAwait(false);
var status = child.Status switch { RuleStatus.Satisfied => RuleStatus.Unsatisfied, RuleStatus.Unsatisfied => RuleStatus.Satisfied, _ => RuleStatus.Skipped };
return Finish(node, status, [child], options);
}
var stopped = false;
foreach (var child in node.Children)
{
if (stopped) { children.Add(Skip(child, RuleSkipReason.ShortCircuit)); continue; }
var result = await Visit(child, value, context, options, token).ConfigureAwait(false);
children.Add(result);
stopped = node.Kind == RuleNodeKind.AnyOf && result.Status == RuleStatus.Satisfied
|| options.Execution == RuleExecution.ShortCircuit && node.Kind == RuleNodeKind.AllOf && result.Status == RuleStatus.Unsatisfied;
}
var applicable = children.Where(child => child.Status != RuleStatus.Skipped).ToArray();
var final = applicable.Length == 0 ? RuleStatus.Skipped
: node.Kind == RuleNodeKind.AllOf
? applicable.All(child => child.Status == RuleStatus.Satisfied) ? RuleStatus.Satisfied : RuleStatus.Unsatisfied
: applicable.Any(child => child.Status == RuleStatus.Satisfied) ? RuleStatus.Satisfied : RuleStatus.Unsatisfied;
return Finish(node, final, children, options);
}
private static RuleEvaluation Finish(RuleNode node, RuleStatus status, IEnumerable children, RuleEvaluationOptions options)
{
var retained = options.Detail == RuleDetail.Trace ? children
: status == RuleStatus.Unsatisfied ? children.Where(child => child.Status == RuleStatus.Unsatisfied || node.Kind == RuleNodeKind.Not)
: [];
return Result(node, status, retained);
}
private static RuleEvaluation Result(RuleNode node, RuleStatus status, IEnumerable children, object? evidence = null) => new(node.NodeId, node.Definition?.Id, node.Path, node.Kind, status, status == RuleStatus.Skipped ? RuleSkipReason.NotApplicable : RuleSkipReason.None, evidence, children);
private static RuleEvaluation Skip(RuleNode node, RuleSkipReason reason) => new(node.NodeId, node.Definition?.Id, node.Path, node.Kind, RuleStatus.Skipped, reason, null, []);
}