using System.Linq.Expressions; namespace Greencore.platform.validators; public static class Options { public static OptionsBuilder For() => new(); } public sealed class OptionsBuilder { public FieldOptionsBuilder Field(Expression> property) => new(ValidationPath.Get(property)); } public sealed class FieldOptionsBuilder { private readonly string _path; internal FieldOptionsBuilder(string path) => _path = path; public FieldOptions Load(Func>> load) => new(_path, load, []); } /// Fuente de opciones independiente de las reglas, con dependencias de campos explĂ­citas. public sealed class FieldOptions { private readonly IReadOnlyList<(string Path, Func Select)> _dependencies; internal Func>> Loader { get; } public string Path { get; } public IReadOnlyList Dependencies => Array.AsReadOnly(_dependencies.Select(item => item.Path).ToArray()); internal FieldOptions(string path, Func>> load, IEnumerable<(string Path, Func Select)> dependencies) { ArgumentNullException.ThrowIfNull(load); Path = path; Loader = load; _dependencies = Array.AsReadOnly(dependencies.ToArray()); } public FieldOptions DependsOn(Expression> property) { var path = ValidationPath.Get(property); if (_dependencies.Any(item => item.Path == path)) throw new ArgumentException("The dependency is already registered.", nameof(property)); var select = property.Compile(); return new(Path, Loader, [.. _dependencies, (path, candidate => select(candidate))]); } public OptionsSession CreateSession(Action? dispatch = null) => new(this, dispatch); internal object?[] ReadDependencies(T candidate) => _dependencies.Select(item => item.Select(candidate)).ToArray(); } /// Recarga al cambiar dependencias o contexto y descarta respuestas de selecciones anteriores. public sealed class OptionsSession : IDisposable { private readonly FieldOptions _definition; private readonly AsyncDataSession> _session; private readonly object _gate = new(); private object?[]? _previous; private C? _context; private Task>>? _pending; private bool _disposed; private volatile bool _retry; public DataSnapshot> Snapshot => _session.Snapshot; public string Path => _definition.Path; public IReadOnlyList Dependencies => _definition.Dependencies; public event Action>>? Changed; internal OptionsSession(FieldOptions definition, Action? dispatch) { _definition = definition; _session = new(dispatch); _session.Changed += snapshot => { if (snapshot.State is DataState.Failed or DataState.Canceled) _retry = true; Changed?.Invoke(snapshot); }; } public async ValueTask RefreshAsync(T candidate, C context, CancellationToken cancellationToken = default) => _ = await LoadAsync(candidate, context, false, cancellationToken).ConfigureAwait(false); public ValueTask>> EnsureLoadedAsync(T candidate, C context, CancellationToken cancellationToken = default) => LoadAsync(candidate, context, false, cancellationToken, true); public ValueTask>> ReloadAsync(T candidate, C context, CancellationToken cancellationToken = default) => LoadAsync(candidate, context, true, cancellationToken); private ValueTask>> LoadAsync(T candidate, C context, bool force, CancellationToken cancellationToken, bool cacheWithoutDependencies = false) { cancellationToken.ThrowIfCancellationRequested(); var dependencies = _definition.ReadDependencies(candidate); AsyncDataSession>.Request request; TaskCompletionSource>> completion; lock (_gate) { ObjectDisposedException.ThrowIf(_disposed, this); if (!force && !_retry && (cacheWithoutDependencies || dependencies.Length > 0) && _previous is not null && dependencies.SequenceEqual(_previous) && EqualityComparer.Default.Equals(context, _context)) return new(_pending!.WaitAsync(cancellationToken)); _previous = dependencies; _context = context; _retry = false; request = _session.Begin(cancellationToken); completion = new(TaskCreationOptions.RunContinuationsAsynchronously); _pending = completion.Task; } _ = ExecuteAsync(request, candidate, context, dependencies, completion); return new(completion.Task); } private async Task ExecuteAsync(AsyncDataSession>.Request request, T candidate, C context, object?[] dependencies, TaskCompletionSource>> completion) { try { var operation = await _session.RunRequestAsync(request, async token => { try { var result = await _definition.Loader(candidate, context, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); ArgumentNullException.ThrowIfNull(result); return Array.AsReadOnly(result.ToArray()); } catch { lock (_gate) { if (ReferenceEquals(_previous, dependencies)) _retry = true; } throw; } }).ConfigureAwait(false); completion.TrySetResult(operation); } catch (Exception exception) { completion.TrySetException(exception); } } public void Dispose() { lock (_gate) { if (_disposed) return; _disposed = true; } _session.Dispose(); Changed = null; } }