using System.ComponentModel; using Greencore.platform.rules; namespace Greencore.platform.validators; /// Selecciona resúmenes por campo, detalles terminales o el árbol completo. public enum DiagnosticView { Summary, Details, All } public sealed record ValidationReport(RuleEvaluation Evaluation, IReadOnlyList Diagnostics) { public bool IsValid => Evaluation.Status != RuleStatus.Unsatisfied; /// Recorre el árbol sin perder la distinción entre grupos y detalles. public IEnumerable EnumerateDiagnostics(bool includeGroups = true) { var pending = new Stack(Diagnostics.Reverse()); while (pending.TryPop(out var diagnostic)) { if (includeGroups || diagnostic.Children.Count == 0) yield return diagnostic; foreach (var child in diagnostic.Children.Reverse()) pending.Push(child); } } public IReadOnlyList ForField(string path, bool includeGroups = false) { ArgumentException.ThrowIfNullOrWhiteSpace(path); return EnumerateDiagnostics(includeGroups).Where(item => item.Path == path).ToArray(); } public IReadOnlyList ForField(System.Linq.Expressions.Expression> property, bool includeGroups = false) => ForField(ValidationPath.Get(property), includeGroups); /// Devuelve mensajes del campo sin repetir resúmenes y detalles de la misma rama. public IReadOnlyList ForField(string path, DiagnosticView view) { ArgumentException.ThrowIfNullOrWhiteSpace(path); if (!Enum.IsDefined(view)) throw new ArgumentOutOfRangeException(nameof(view)); if (view != DiagnosticView.Summary) return ForField(path, view == DiagnosticView.All); var result = new List(); var pending = new Stack(Diagnostics.Reverse()); while (pending.TryPop(out var item)) { if (item.Path == path) result.Add(item); else foreach (var child in item.Children.Reverse()) pending.Push(child); } return result.AsReadOnly(); } public IReadOnlyList ForField(System.Linq.Expressions.Expression> property, DiagnosticView view) => ForField(ValidationPath.Get(property), view); } /// Plan listo para evaluar, con árbol y diagnósticos comprobados al construir. public sealed class ValidationPlan { public RuleNode Tree { get; } public RuleDiagnostics Diagnostics { get; } public FormValidationSession CreateSession(Action? dispatch = null) => new(this, dispatch); public FormValidationSession CreateSession(TimeSpan debounce, Action? dispatch = null) => new(this, debounce, dispatch); public ValidationPlan(RuleNode tree, RuleDiagnostics diagnostics) { ArgumentNullException.ThrowIfNull(tree); ArgumentNullException.ThrowIfNull(diagnostics); diagnostics.Validate(tree); Tree = tree; Diagnostics = diagnostics; } public ValidationReport Validate(T value, C context, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default) { var result = RuleEvaluator.Evaluate(Tree, value, context, options, cancellationToken); return new(result, Diagnostics.Project(result)); } public async ValueTask ValidateAsync(T value, C context, CancellationToken cancellationToken = default, RuleEvaluationOptions? options = null) { var result = await RuleEvaluator.EvaluateAsync(Tree, value, context, options, cancellationToken).ConfigureAwait(false); return new(result, Diagnostics.Project(result)); } } public static class LocalValidationExtensions { [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask> UpdateAsync(this FormValidationSession session, T candidate, CancellationToken cancellationToken = default) => session.UpdateAsync(candidate, default, cancellationToken); public static ValueTask UpdateWithResultAsync(this FormValidationSession session, T candidate, CancellationToken cancellationToken = default) => session.UpdateWithResultAsync(candidate, default, cancellationToken); public static ValueTask EvaluateAsync(this FormValidationSession session, T candidate, TimeSpan delay = default, CancellationToken cancellationToken = default) => session.EvaluateAsync(candidate, default, delay, cancellationToken); [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask> ValidateWithResultAsync(this FormValidationSession session, T candidate, TimeSpan delay = default, CancellationToken cancellationToken = default) => session.ValidateWithResultAsync(candidate, default, delay, cancellationToken); public static ValidationBuilder Include(this ValidationBuilder builder, ValidationPlan plan) { ArgumentNullException.ThrowIfNull(plan); RuleNode Lift(RuleNode node) => new(node.NodeId, node.Kind, node.Children.Select(Lift), node.Definition?.WithContext(), node.Path); return builder.Include(new ValidationPlan(Lift(plan.Tree), plan.Diagnostics)); } [EditorBrowsable(EditorBrowsableState.Never)] public static ValueTask ValidateAsync(this FormValidationSession session, T candidate, TimeSpan delay = default, CancellationToken cancellationToken = default) => session.ValidateAsync(candidate, default, delay, cancellationToken); public static ValidationReport Validate(this ValidationPlan plan, T value, RuleEvaluationOptions? options = null, CancellationToken cancellationToken = default) => plan.Validate(value, default, options, cancellationToken); public static ValueTask ValidateAsync(this ValidationPlan plan, T value, CancellationToken cancellationToken = default) => plan.ValidateAsync(value, default, cancellationToken); public static ValueTask ValidateAsync(this ValidationPlan plan, T value, RuleEvaluationOptions options, CancellationToken cancellationToken = default) => plan.ValidateAsync(value, default, cancellationToken, options); }