< Summary

Line coverage
100%
Covered lines: 22
Uncovered lines: 0
Coverable lines: 22
Total lines: 372
Line coverage: 100%
Branch coverage
70%
Covered branches: 7
Total branches: 10
Branch coverage: 70%
Method coverage
100%
Covered methods: 10
Fully covered methods: 10
Total methods: 10
Method coverage: 100%
Full method coverage: 100%

Metrics

File(s)

/home/ci/agent/workspace/dotnet-validation-docs_main/app/src/FluentValidator.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using ArturRios.Output;
 3using FluentValidation;
 4using FluentValidation.Results;
 5
 6namespace ArturRios.Validation;
 7
 8/// <summary>
 9/// Base validator that turns a FluentValidation result into the shapes an application consumes: a plain
 10/// array of messages, or an <see cref="ArturRios.Output"/> envelope.
 11/// </summary>
 12/// <typeparam name="T">The model type being validated.</typeparam>
 13/// <remarks>
 14/// Subclass it and declare the rules in the constructor exactly as with
 15/// <see cref="AbstractValidator{T}"/>; the helpers below come for free. Validation never throws for a model
 16/// that simply fails its rules — the failures arrive on the result, which is the family's convention.
 17/// </remarks>
 18public partial class FluentValidator<T> : AbstractValidator<T>, IFluentValidator<T>
 19{
 20    /// <summary>
 21    /// Upper bound, in milliseconds, on stripping the special characters from one message, so a pathological
 22    /// message can never spin.
 23    /// </summary>
 24    private const int MatchTimeoutMilliseconds = 100;
 25
 26    /// <inheritdoc />
 27    public string[] ValidateAndReturnErrors(T model, bool removeSpecialChars = false) =>
 7528        Messages(Validate(model), removeSpecialChars);
 29
 30    /// <inheritdoc />
 31    public ProcessOutput ValidateAndReturnProcessOutput(T model, bool removeSpecialChars = false) =>
 5632        ProcessOutput.New.WithErrors(ValidateAndReturnErrors(model, removeSpecialChars));
 33
 34    /// <summary>
 35    /// Validates <paramref name="model"/> and returns the failures as a <see cref="DataOutput{T}"/> envelope
 36    /// that also carries the model back.
 37    /// </summary>
 38    /// <param name="model">The model to validate.</param>
 39    /// <param name="removeSpecialChars">Strips the quoting apostrophes and sentence-ending full stops from the messages
 40    /// <returns>
 41    /// An envelope carrying <paramref name="model"/> whether or not it is valid, so a caller can report the
 42    /// failures alongside what produced them.
 43    /// </returns>
 44    /// <remarks>
 45    /// This one is absent from <see cref="IFluentValidator{T}"/>: the interface is contravariant in
 46    /// <typeparamref name="T"/>, and a <see cref="DataOutput{T}"/> return puts the type parameter in an
 47    /// output position.
 48    /// </remarks>
 49    public DataOutput<T> ValidateAndReturnDataOutput(T model, bool removeSpecialChars = false) =>
 550        DataOutput<T>.New
 551            .WithData(model)
 552            .WithErrors(ValidateAndReturnErrors(model, removeSpecialChars));
 53
 54    /// <inheritdoc />
 55    public async Task<string[]> ValidateAndReturnErrorsAsync(
 56        T model,
 57        bool removeSpecialChars = false,
 58        CancellationToken cancellationToken = default) =>
 1059        Messages(await ValidateAsync(model, cancellationToken).ConfigureAwait(false), removeSpecialChars);
 60
 61    /// <inheritdoc />
 62    public async Task<ProcessOutput> ValidateAndReturnProcessOutputAsync(
 63        T model,
 64        bool removeSpecialChars = false,
 65        CancellationToken cancellationToken = default) =>
 366        ProcessOutput.New.WithErrors(
 367            await ValidateAndReturnErrorsAsync(model, removeSpecialChars, cancellationToken).ConfigureAwait(false));
 68
 69    /// <summary>
 70    /// Asynchronously validates <paramref name="model"/> and returns the failures as a
 71    /// <see cref="DataOutput{T}"/> envelope that also carries the model back.
 72    /// </summary>
 73    /// <param name="model">The model to validate.</param>
 74    /// <param name="removeSpecialChars">Strips the quoting apostrophes and sentence-ending full stops from the messages
 75    /// <param name="cancellationToken">Cancels the validation.</param>
 76    /// <returns>An envelope carrying <paramref name="model"/> whether or not it is valid.</returns>
 77    public async Task<DataOutput<T>> ValidateAndReturnDataOutputAsync(
 78        T model,
 79        bool removeSpecialChars = false,
 80        CancellationToken cancellationToken = default) =>
 181        DataOutput<T>.New
 182            .WithData(model)
 183            .WithErrors(await ValidateAndReturnErrorsAsync(model, removeSpecialChars, cancellationToken)
 184                .ConfigureAwait(false));
 85
 86    /// <summary>
 87    /// Projects a validation result onto its messages, one per failure, optionally stripped of the default
 88    /// special characters.
 89    /// </summary>
 90    private static string[] Messages(FluentValidation.Results.ValidationResult result, bool removeSpecialChars) =>
 17891        [.. result.Errors.Select(failure => Message(failure, removeSpecialChars))];
 92
 93    /// <summary>
 94    /// The message reported for one failure — never blank.
 95    /// </summary>
 96    /// <remarks>
 97    /// <see cref="ProcessOutput.AddErrors"/> drops blank entries, so a failure whose message is blank — a
 98    /// <c>WithMessage</c> callback that returned nothing, or a message made only of the characters
 99    /// <paramref name="removeSpecialChars"/> strips — would vanish from the envelope and leave it reporting
 100    /// <see cref="ProcessOutput.Success"/> for an invalid model. Such a failure is reported with
 101    /// FluentValidation's own wording for a broken condition instead.
 102    /// </remarks>
 103    private static string Message(ValidationFailure failure, bool removeSpecialChars)
 104    {
 95105        var message = Clean(failure.ErrorMessage, removeSpecialChars);
 106
 95107        if (!string.IsNullOrWhiteSpace(message))
 108        {
 92109            return message;
 110        }
 111
 3112        var fallback = string.IsNullOrWhiteSpace(failure.PropertyName)
 3113            ? "The specified condition was not met."
 3114            : $"The specified condition was not met for '{failure.PropertyName}'.";
 115
 3116        return Clean(fallback, removeSpecialChars);
 117    }
 118
 119    private static string Clean(string? message, bool removeSpecialChars) =>
 98120        removeSpecialChars ? SpecialChars().Replace(message ?? string.Empty, string.Empty) : message ?? string.Empty;
 121
 122    /// <summary>
 123    /// Matches the quoting apostrophes and the sentence-ending full stops FluentValidation puts in its
 124    /// default messages — <c>'Name' must not be empty.</c> — and nothing that belongs to the text itself.
 125    /// </summary>
 126    /// <remarks>
 127    /// An apostrophe between two letters is part of a word (<c>can't</c>, <c>owner's</c>) and a full stop
 128    /// followed by anything but whitespace is part of a value (<c>0.5</c>, <c>example.org</c>); stripping
 129    /// those changed what the message said — <c>greater than '0.5'</c> read <c>greater than 05</c>.
 130    /// </remarks>
 131    [GeneratedRegex(@"(?<!\p{L})'|'(?!\p{L})|\.(?!\S)", RegexOptions.None, MatchTimeoutMilliseconds)]
 132    private static partial Regex SpecialChars();
 133}

/home/ci/agent/workspace/dotnet-validation-docs_main/app/src/obj/Release/net10.0/generated/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

#LineLine coverage
 1// <auto-generated/>
 2#nullable enable
 3#pragma warning disable
 4
 5namespace ArturRios.Validation
 6{
 7    partial class FluentValidator<T>
 8    {
 9        /// <remarks>
 10        /// Pattern:<br/>
 11        /// <code>(?&lt;!\\p{L})'|'(?!\\p{L})|\\.(?!\\S)</code><br/>
 12        /// Explanation:<br/>
 13        /// <code>
 14        /// ○ Match with 3 alternative expressions, atomically.<br/>
 15        ///     ○ Match a sequence of expressions.<br/>
 16        ///         ○ Zero-width negative lookbehind.<br/>
 17        ///             ○ Match a Unicode letter right-to-left.<br/>
 18        ///         ○ Match '\''.<br/>
 19        ///     ○ Match a sequence of expressions.<br/>
 20        ///         ○ Match '\''.<br/>
 21        ///         ○ Zero-width negative lookahead.<br/>
 22        ///             ○ Match a Unicode letter.<br/>
 23        ///     ○ Match a sequence of expressions.<br/>
 24        ///         ○ Match '.'.<br/>
 25        ///         ○ Zero-width negative lookahead.<br/>
 26        ///             ○ Match any character other than a whitespace character.<br/>
 27        /// </code>
 28        /// </remarks>
 29        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.RegularExpressions.Generator", "10.0.15.504
 2430        private static partial global::System.Text.RegularExpressions.Regex SpecialChars() => global::System.Text.Regula
 31    }
 32}
 33
 34namespace System.Text.RegularExpressions.Generated
 35{
 36    using System;
 37    using System.Buffers;
 38    using System.CodeDom.Compiler;
 39    using System.Collections;
 40    using System.ComponentModel;
 41    using System.Globalization;
 42    using System.Runtime.CompilerServices;
 43    using System.Text.RegularExpressions;
 44    using System.Threading;
 45
 46    /// <summary>Custom <see cref="Regex"/>-derived type for the SpecialChars method.</summary>
 47    [GeneratedCodeAttribute("System.Text.RegularExpressions.Generator", "10.0.15.504")]
 48    file sealed class SpecialChars_0 : Regex
 49    {
 50        /// <summary>Cached, thread-safe singleton instance.</summary>
 51        internal static readonly SpecialChars_0 Instance = new();
 52
 53        /// <summary>Initializes the instance.</summary>
 54        private SpecialChars_0()
 55        {
 56            base.pattern = "(?<!\\p{L})'|'(?!\\p{L})|\\.(?!\\S)";
 57            base.roptions = RegexOptions.None;
 58            base.internalMatchTimeout = TimeSpan.FromMilliseconds(100);
 59            base.factory = new RunnerFactory();
 60            base.capsize = 1;
 61        }
 62
 63        /// <summary>Provides a factory for creating <see cref="RegexRunner"/> instances to be used by methods on <see c
 64        private sealed class RunnerFactory : RegexRunnerFactory
 65        {
 66            /// <summary>Creates an instance of a <see cref="RegexRunner"/> used by methods on <see cref="Regex"/>.</sum
 67            protected override RegexRunner CreateInstance() => new Runner();
 68
 69            /// <summary>Provides the runner that contains the custom logic implementing the specified regular expressio
 70            private sealed class Runner : RegexRunner
 71            {
 72                /// <summary>Scan the <paramref name="inputSpan"/> starting from base.runtextstart for the next match.</
 73                /// <param name="inputSpan">The text being scanned by the regular expression.</param>
 74                protected override void Scan(ReadOnlySpan<char> inputSpan)
 75                {
 76                    // Search until we can't find a valid starting position, we find a match, or we reach the end of the
 77                    while (TryFindNextPossibleStartingPosition(inputSpan) &&
 78                           !TryMatchAtCurrentPosition(inputSpan) &&
 79                           base.runtextpos != inputSpan.Length)
 80                    {
 81                        base.runtextpos++;
 82                        base.CheckTimeout();
 83                    }
 84                }
 85
 86                /// <summary>Search <paramref name="inputSpan"/> starting from base.runtextpos for the next location a m
 87                /// <param name="inputSpan">The text being scanned by the regular expression.</param>
 88                /// <returns>true if a possible match was found; false if no more matches are possible.</returns>
 89                private bool TryFindNextPossibleStartingPosition(ReadOnlySpan<char> inputSpan)
 90                {
 91                    int pos = base.runtextpos;
 92
 93                    // Empty matches aren't possible.
 94                    if ((uint)pos < (uint)inputSpan.Length)
 95                    {
 96                        // The pattern begins with a character in the set ['.].
 97                        // Find the next occurrence. If it can't be found, there's no match.
 98                        int i = inputSpan.Slice(pos).IndexOfAny('\'', '.');
 99                        if (i >= 0)
 100                        {
 101                            base.runtextpos = pos + i;
 102                            return true;
 103                        }
 104                    }
 105
 106                    // No match found.
 107                    base.runtextpos = inputSpan.Length;
 108                    return false;
 109                }
 110
 111                /// <summary>Determine whether <paramref name="inputSpan"/> at base.runtextpos is a match for the regula
 112                /// <param name="inputSpan">The text being scanned by the regular expression.</param>
 113                /// <returns>true if the regular expression matches at the current position; otherwise, false.</returns>
 114                private bool TryMatchAtCurrentPosition(ReadOnlySpan<char> inputSpan)
 115                {
 116                    int pos = base.runtextpos;
 117                    int matchStart = pos;
 118                    ReadOnlySpan<char> slice = inputSpan.Slice(pos);
 119
 120                    // Match with 3 alternative expressions, atomically.
 121                    {
 122                        int alternation_starting_pos = pos;
 123
 124                        // Branch 0
 125                        {
 126                            // Zero-width negative lookbehind.
 127                            {
 128                                slice = inputSpan.Slice(pos);
 129                                int negativelookbehind__starting_pos = pos;
 130                                base.CheckTimeout();
 131
 132                                // Match a Unicode letter right-to-left.
 133                                if ((uint)(pos - 1) >= inputSpan.Length || !char.IsLetter(inputSpan[pos - 1]))
 134                                {
 135                                    goto NegativeLookaroundMatch;
 136                                }
 137                                pos--;
 138
 139                                goto AlternationBranch;
 140
 141                                NegativeLookaroundMatch:
 142                                pos = negativelookbehind__starting_pos;
 143                                slice = inputSpan.Slice(pos);
 144                            }
 145
 146                            // Match '\''.
 147                            if (slice.IsEmpty || slice[0] != '\'')
 148                            {
 149                                goto AlternationBranch;
 150                            }
 151
 152                            pos++;
 153                            slice = inputSpan.Slice(pos);
 154                            goto AlternationMatch;
 155
 156                            AlternationBranch:
 157                            pos = alternation_starting_pos;
 158                            slice = inputSpan.Slice(pos);
 159                        }
 160
 161                        // Branch 1
 162                        {
 163                            // Match '\''.
 164                            if (slice.IsEmpty || slice[0] != '\'')
 165                            {
 166                                goto AlternationBranch1;
 167                            }
 168
 169                            // Zero-width negative lookahead.
 170                            {
 171                                pos++;
 172                                slice = inputSpan.Slice(pos);
 173                                int negativelookahead__starting_pos = pos;
 174                                base.CheckTimeout();
 175
 176                                // Match a Unicode letter.
 177                                if (slice.IsEmpty || !char.IsLetter(slice[0]))
 178                                {
 179                                    goto NegativeLookaroundMatch1;
 180                                }
 181
 182                                goto AlternationBranch1;
 183
 184                                NegativeLookaroundMatch1:
 185                                pos = negativelookahead__starting_pos;
 186                                slice = inputSpan.Slice(pos);
 187                            }
 188
 189                            goto AlternationMatch;
 190
 191                            AlternationBranch1:
 192                            pos = alternation_starting_pos;
 193                            slice = inputSpan.Slice(pos);
 194                        }
 195
 196                        // Branch 2
 197                        {
 198                            // Match '.'.
 199                            if (slice.IsEmpty || slice[0] != '.')
 200                            {
 201                                return false; // The input didn't match.
 202                            }
 203
 204                            // Zero-width negative lookahead.
 205                            {
 206                                pos++;
 207                                slice = inputSpan.Slice(pos);
 208                                int negativelookahead__starting_pos1 = pos;
 209                                base.CheckTimeout();
 210
 211                                // Match any character other than a whitespace character.
 212                                if (slice.IsEmpty || char.IsWhiteSpace(slice[0]))
 213                                {
 214                                    goto NegativeLookaroundMatch2;
 215                                }
 216
 217                                return false; // The input didn't match.
 218
 219                                NegativeLookaroundMatch2:
 220                                pos = negativelookahead__starting_pos1;
 221                                slice = inputSpan.Slice(pos);
 222                            }
 223
 224                        }
 225
 226                        AlternationMatch:;
 227                    }
 228
 229                    // The input matched.
 230                    base.runtextpos = pos;
 231                    base.Capture(0, matchStart, pos);
 232                    return true;
 233                }
 234            }
 235        }
 236
 237    }
 238
 239}