| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | using ArturRios.Output; |
| | | 3 | | using FluentValidation; |
| | | 4 | | |
| | | 5 | | namespace ArturRios.Validation; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Base validator that turns a FluentValidation result into the shapes an application consumes: a plain |
| | | 9 | | /// array of messages, or an <see cref="ArturRios.Output"/> envelope. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <typeparam name="T">The model type being validated.</typeparam> |
| | | 12 | | /// <remarks> |
| | | 13 | | /// Subclass it and declare the rules in the constructor exactly as with |
| | | 14 | | /// <see cref="AbstractValidator{T}"/>; the helpers below come for free. Validation never throws for a model |
| | | 15 | | /// that simply fails its rules — the failures arrive on the result, which is the family's convention. |
| | | 16 | | /// </remarks> |
| | | 17 | | public partial class FluentValidator<T> : AbstractValidator<T>, IFluentValidator<T> |
| | | 18 | | { |
| | | 19 | | /// <summary> |
| | | 20 | | /// Upper bound, in milliseconds, on stripping the special characters from one message, so a pathological |
| | | 21 | | /// message can never spin. |
| | | 22 | | /// </summary> |
| | | 23 | | private const int MatchTimeoutMilliseconds = 100; |
| | | 24 | | |
| | | 25 | | /// <inheritdoc /> |
| | | 26 | | public string[] ValidateAndReturnErrors(T model, bool removeSpecialChars = false) => |
| | 69 | 27 | | Messages(Validate(model), removeSpecialChars); |
| | | 28 | | |
| | | 29 | | /// <inheritdoc /> |
| | | 30 | | public ProcessOutput ValidateAndReturnProcessOutput(T model, bool removeSpecialChars = false) => |
| | 55 | 31 | | ProcessOutput.New.WithErrors(ValidateAndReturnErrors(model, removeSpecialChars)); |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Validates <paramref name="model"/> and returns the failures as a <see cref="DataOutput{T}"/> envelope |
| | | 35 | | /// that also carries the model back. |
| | | 36 | | /// </summary> |
| | | 37 | | /// <param name="model">The model to validate.</param> |
| | | 38 | | /// <param name="removeSpecialChars">Strips the apostrophes and full stops from the messages when <see langword="tru |
| | | 39 | | /// <returns> |
| | | 40 | | /// An envelope carrying <paramref name="model"/> whether or not it is valid, so a caller can report the |
| | | 41 | | /// failures alongside what produced them. |
| | | 42 | | /// </returns> |
| | | 43 | | /// <remarks> |
| | | 44 | | /// This one is absent from <see cref="IFluentValidator{T}"/>: the interface is contravariant in |
| | | 45 | | /// <typeparamref name="T"/>, and a <see cref="DataOutput{T}"/> return puts the type parameter in an |
| | | 46 | | /// output position. |
| | | 47 | | /// </remarks> |
| | | 48 | | public DataOutput<T> ValidateAndReturnDataOutput(T model, bool removeSpecialChars = false) => |
| | 4 | 49 | | DataOutput<T>.New |
| | 4 | 50 | | .WithData(model) |
| | 4 | 51 | | .WithErrors(ValidateAndReturnErrors(model, removeSpecialChars)); |
| | | 52 | | |
| | | 53 | | /// <inheritdoc /> |
| | | 54 | | public async Task<string[]> ValidateAndReturnErrorsAsync( |
| | | 55 | | T model, |
| | | 56 | | bool removeSpecialChars = false, |
| | | 57 | | CancellationToken cancellationToken = default) => |
| | 9 | 58 | | Messages(await ValidateAsync(model, cancellationToken).ConfigureAwait(false), removeSpecialChars); |
| | | 59 | | |
| | | 60 | | /// <inheritdoc /> |
| | | 61 | | public async Task<ProcessOutput> ValidateAndReturnProcessOutputAsync( |
| | | 62 | | T model, |
| | | 63 | | bool removeSpecialChars = false, |
| | | 64 | | CancellationToken cancellationToken = default) => |
| | 3 | 65 | | ProcessOutput.New.WithErrors( |
| | 3 | 66 | | await ValidateAndReturnErrorsAsync(model, removeSpecialChars, cancellationToken).ConfigureAwait(false)); |
| | | 67 | | |
| | | 68 | | /// <summary> |
| | | 69 | | /// Asynchronously validates <paramref name="model"/> and returns the failures as a |
| | | 70 | | /// <see cref="DataOutput{T}"/> envelope that also carries the model back. |
| | | 71 | | /// </summary> |
| | | 72 | | /// <param name="model">The model to validate.</param> |
| | | 73 | | /// <param name="removeSpecialChars">Strips the apostrophes and full stops from the messages when <see langword="tru |
| | | 74 | | /// <param name="cancellationToken">Cancels the validation.</param> |
| | | 75 | | /// <returns>An envelope carrying <paramref name="model"/> whether or not it is valid.</returns> |
| | | 76 | | public async Task<DataOutput<T>> ValidateAndReturnDataOutputAsync( |
| | | 77 | | T model, |
| | | 78 | | bool removeSpecialChars = false, |
| | | 79 | | CancellationToken cancellationToken = default) => |
| | 1 | 80 | | DataOutput<T>.New |
| | 1 | 81 | | .WithData(model) |
| | 1 | 82 | | .WithErrors(await ValidateAndReturnErrorsAsync(model, removeSpecialChars, cancellationToken) |
| | 1 | 83 | | .ConfigureAwait(false)); |
| | | 84 | | |
| | | 85 | | /// <summary> |
| | | 86 | | /// Projects a validation result onto its messages, optionally stripped of the default special characters. |
| | | 87 | | /// </summary> |
| | | 88 | | private static string[] Messages(FluentValidation.Results.ValidationResult result, bool removeSpecialChars) |
| | | 89 | | { |
| | 156 | 90 | | var messages = result.Errors.Select(error => error.ErrorMessage); |
| | | 91 | | |
| | 76 | 92 | | if (removeSpecialChars) |
| | | 93 | | { |
| | 17 | 94 | | messages = messages.Select(message => SpecialChars().Replace(message, string.Empty)); |
| | | 95 | | } |
| | | 96 | | |
| | 76 | 97 | | return [.. messages]; |
| | | 98 | | } |
| | | 99 | | |
| | | 100 | | /// <summary> |
| | | 101 | | /// Matches the apostrophes and full stops FluentValidation puts in its default messages. |
| | | 102 | | /// </summary> |
| | | 103 | | [GeneratedRegex(@"['.]", RegexOptions.None, MatchTimeoutMilliseconds)] |
| | | 104 | | private static partial Regex SpecialChars(); |
| | | 105 | | } |