From e85251f51732f9a7a4000a54c8bf1ddfad6c5848 Mon Sep 17 00:00:00 2001 From: William Gross Date: Tue, 18 Mar 2025 17:16:33 -0400 Subject: [PATCH 01/28] Renamed ValidationResult to ValidationError In preparation for a new, public ValidationResult class that will be returned by all the Validator methods. --- Tewl/InputValidation/ValidationError.cs | 25 +++++++ .../InputValidation/ValidationErrorHandler.cs | 17 ++--- Tewl/InputValidation/ValidationResult.cs | 23 ------ Tewl/InputValidation/Validator.cs | 70 +++++++++---------- Tewl/InputValidation/ZipCode.cs | 10 ++- 5 files changed, 69 insertions(+), 76 deletions(-) create mode 100644 Tewl/InputValidation/ValidationError.cs delete mode 100644 Tewl/InputValidation/ValidationResult.cs diff --git a/Tewl/InputValidation/ValidationError.cs b/Tewl/InputValidation/ValidationError.cs new file mode 100644 index 0000000..ac5f4b9 --- /dev/null +++ b/Tewl/InputValidation/ValidationError.cs @@ -0,0 +1,25 @@ +namespace Tewl.InputValidation; + +internal class ValidationError { + public static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => + new() { ErrorCondition = errorCondition, errorMessage = errorMessage }; + + public static ValidationError NoError() => new(); + + public static ValidationError Invalid() => new() { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; + + public static ValidationError Empty() => new() { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; + + public static ValidationError TooSmall( object min, object max ) => + new() { ErrorCondition = ErrorCondition.TooLong, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; + + public static ValidationError TooLarge( object min, object max ) => + new() { ErrorCondition = ErrorCondition.TooLarge, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; + + public ErrorCondition ErrorCondition { get; private init; } = ErrorCondition.NoError; + private string errorMessage = ""; + + private ValidationError() {} + + public string GetMessage( string subject ) => string.Format( errorMessage, subject ); +} \ No newline at end of file diff --git a/Tewl/InputValidation/ValidationErrorHandler.cs b/Tewl/InputValidation/ValidationErrorHandler.cs index 943c781..2f254e5 100644 --- a/Tewl/InputValidation/ValidationErrorHandler.cs +++ b/Tewl/InputValidation/ValidationErrorHandler.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Tewl.Tools; - namespace Tewl.InputValidation { /// /// This class allows you to control what happens when a validation method generates an error. Every validation method @@ -19,7 +14,7 @@ public class ValidationErrorHandler { private readonly CustomHandler customHandler; private readonly Dictionary customMessages = new Dictionary(); - private ValidationResult validationResult = ValidationResult.NoError(); + private ValidationError validationResult = ValidationError.NoError(); /// /// Creates an error handler that adds standard error messages, based on the specified subject, to the validator. If the @@ -42,14 +37,12 @@ public class ValidationErrorHandler { /// will be used for all errors. This method has no effect if a custom handler has been specified. /// public void AddCustomErrorMessage( string message, params ErrorCondition[] errorConditions ) { - if( errorConditions.Length > 0 ) { + if( errorConditions.Length > 0 ) foreach( var e in errorConditions ) customMessages.Add( e, message ); - } - else { + else foreach( var e in EnumTools.GetValues() ) customMessages.Add( e, message ); - } } /// @@ -59,7 +52,7 @@ public void AddCustomErrorMessage( string message, params ErrorCondition[] error private bool used; - internal void SetValidationResult( ValidationResult validationResult ) { + internal void SetValidationResult( ValidationError validationResult ) { if( used ) throw new ApplicationException( "Validation error handlers cannot be re-used." ); used = true; @@ -89,7 +82,7 @@ internal void HandleResult( Validator validator, bool errorWouldResultInUnusable // build the error message if( !customMessages.TryGetValue( validationResult.ErrorCondition, out var message ) ) // NOTE: Do we really need custom message, or can the custom handler manage that? - message = validationResult.GetErrorMessage( Subject ); + message = validationResult.GetMessage( Subject ); validator.AddError( new Error( message, errorWouldResultInUnusableReturnValue ) ); } diff --git a/Tewl/InputValidation/ValidationResult.cs b/Tewl/InputValidation/ValidationResult.cs deleted file mode 100644 index 5a5a9bc..0000000 --- a/Tewl/InputValidation/ValidationResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Tewl.InputValidation { - internal class ValidationResult { - private string errorMessage = ""; - - private ValidationResult() { } - - public string GetErrorMessage( string subject ) => string.Format( errorMessage, subject ); - - public ErrorCondition ErrorCondition { get; private set; } = ErrorCondition.NoError; - - public static ValidationResult Custom( ErrorCondition errorCondition, string errorMessage ) => new ValidationResult { ErrorCondition = errorCondition, errorMessage = errorMessage }; - - public static ValidationResult NoError() => new ValidationResult(); - - public static ValidationResult Invalid() => new ValidationResult { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; - - public static ValidationResult Empty() => new ValidationResult { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; - - public static ValidationResult TooSmall( object min, object max ) => new ValidationResult { ErrorCondition = ErrorCondition.TooLong, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; - - public static ValidationResult TooLarge( object min, object max ) => new ValidationResult { ErrorCondition = ErrorCondition.TooLarge, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; - } -} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index bcaeda6..5ae735a 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -136,9 +136,9 @@ public bool GetBoolean( ValidationErrorHandler errorHandler, string booleanAsStr private static bool validateBoolean( string booleanAsString, ValidationErrorHandler errorHandler ) { if( booleanAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); else if( booleanAsString != "1" && booleanAsString != "0" && booleanAsString != true.ToString() && booleanAsString != false.ToString() ) - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return booleanAsString == "1" || booleanAsString == true.ToString(); } @@ -263,21 +263,21 @@ private static T validateGenericIntegerType( ValidationErrorHandler errorHand long intResult = 0; if( valueAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); else try { intResult = Convert.ToInt64( valueAsString ); if( intResult > maxValue ) - errorHandler.SetValidationResult( ValidationResult.TooLarge( minValue, maxValue ) ); + errorHandler.SetValidationResult( ValidationError.TooLarge( minValue, maxValue ) ); else if( intResult < minValue ) - errorHandler.SetValidationResult( ValidationResult.TooSmall( minValue, maxValue ) ); + errorHandler.SetValidationResult( ValidationError.TooSmall( minValue, maxValue ) ); } catch( FormatException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } catch( OverflowException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } if( errorHandler.LastResult != ErrorCondition.NoError ) @@ -310,22 +310,22 @@ public float GetFloat( ValidationErrorHandler errorHandler, string floatAsString private static float validateFloat( string floatAsString, ValidationErrorHandler errorHandler, float min, float max ) { float floatValue = 0; if( floatAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); else { try { floatValue = float.Parse( floatAsString ); } catch( FormatException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } catch( OverflowException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } if( floatValue < min ) - errorHandler.SetValidationResult( ValidationResult.TooSmall( min, max ) ); + errorHandler.SetValidationResult( ValidationError.TooSmall( min, max ) ); else if( floatValue > max ) - errorHandler.SetValidationResult( ValidationResult.TooLarge( min, max ) ); + errorHandler.SetValidationResult( ValidationError.TooLarge( min, max ) ); } return floatValue; @@ -369,7 +369,7 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAs private static decimal validateDecimal( string decimalAsString, ValidationErrorHandler errorHandler, decimal min, decimal max ) { if( decimalAsString.IsNullOrWhiteSpace() ) { - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); return 0; } @@ -377,12 +377,12 @@ private static decimal validateDecimal( string decimalAsString, ValidationErrorH try { decimalVal = decimal.Parse( decimalAsString ); if( decimalVal < min ) - errorHandler.SetValidationResult( ValidationResult.TooSmall( min, max ) ); + errorHandler.SetValidationResult( ValidationError.TooSmall( min, max ) ); else if( decimalVal > max ) - errorHandler.SetValidationResult( ValidationResult.TooLarge( min, max ) ); + errorHandler.SetValidationResult( ValidationError.TooLarge( min, max ) ); } catch { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } return decimalVal; @@ -416,9 +416,9 @@ public string GetString( ValidationErrorHandler errorHandler, string text, bool () => { var errorMessage = "The length of the " + errorHandler.Subject + " must be between " + minLength + " and " + maxLength + " characters."; if( text.Length > maxLength ) - errorHandler.SetValidationResult( ValidationResult.Custom( ErrorCondition.TooLong, errorMessage ) ); + errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooLong, errorMessage ) ); else if( text.Length < minLength ) - errorHandler.SetValidationResult( ValidationResult.Custom( ErrorCondition.TooShort, errorMessage ) ); + errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooShort, errorMessage ) ); return text.Trim(); } ); @@ -451,7 +451,7 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string email emailAddress, "^" + localPart + "@" + domain + "$", RegexOptions.IgnoreCase ) ) - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); // Max length is already checked by the string validation // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. } @@ -478,7 +478,7 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo () => { /* If the string is just a number, reject it right out. */ if( int.TryParse( url, out _ ) || double.TryParse( url, out _ ) ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return url; } @@ -486,7 +486,7 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo var testingValidator = new Validator(); testingValidator.GetEmailAddress( new ValidationErrorHandler( "" ), url, allowEmpty ); if( !testingValidator.ErrorsOccurred ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return url; } @@ -510,7 +510,7 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo throw new UriFormatException(); } catch( UriFormatException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } return url; @@ -586,7 +586,7 @@ internal PhoneNumber GetPhoneNumberAsObject( phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + input, "" ); } else - errorHandler.SetValidationResult( ValidationResult.Custom( ErrorCondition.Invalid, "The five digit phone number you entered isn't recognized." ) ); + errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.Invalid, "The five digit phone number you entered isn't recognized." ) ); } // International phone numbers // We require a country code and then at least 7 digits (but if country code is more than one digit, we require fewer subsequent digits). @@ -609,12 +609,12 @@ internal PhoneNumber GetPhoneNumberAsObject( phoneNumber = PhoneNumber.CreateFromParts( areaCode, number, extension ); if( !allowExtension && phoneNumber.Extension.Length > 0 ) errorHandler.SetValidationResult( - ValidationResult.Custom( + ValidationError.Custom( ErrorCondition.Invalid, invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ) ); } else - errorHandler.SetValidationResult( ValidationResult.Custom( ErrorCondition.Invalid, invalidMessage ) ); + errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.Invalid, invalidMessage ) ); } return phoneNumber; @@ -634,7 +634,7 @@ public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, stri () => { extension = extension.Trim(); if( !Regex.IsMatch( extension, @"^ *(?\d{1,5}) *$" ) ) - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return extension; } ); @@ -661,7 +661,7 @@ public string GetNumber( ValidationErrorHandler errorHandler, string text, int n text = text.Replace( garbageString, "" ); text = text.Trim(); if( !Regex.IsMatch( text, @"^\d{" + numberOfDigits + "}$" ) ) - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return text; } ); @@ -760,7 +760,7 @@ public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, s private static DateTime validateSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern ) { if( !DateTime.TryParseExact( dateAsString, pattern, Cultures.EnglishUnitedStates, DateTimeStyles.None, out var date ) ) - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); else validateNativeDateTime( errorHandler, date, false, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ); @@ -776,14 +776,14 @@ private static DateTime validateDateTime( ValidationErrorHandler errorHandler, s validateNativeDateTime( errorHandler, date, false, min, max ); } catch( FormatException ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } catch( ArgumentOutOfRangeException ) { // Undocumented exception that there are reports of being thrown - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); } catch( ArgumentNullException ) { - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); } return date; @@ -791,14 +791,14 @@ private static DateTime validateDateTime( ValidationErrorHandler errorHandler, s private static void validateNativeDateTime( ValidationErrorHandler errorHandler, DateTime? date, bool allowEmpty, DateTime minDate, DateTime maxDate ) { if( date == null && !allowEmpty ) - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); else if( date.HasValue ) { var minMaxMessage = " It must be between " + minDate.ToDayMonthYearString( false ) + " and " + maxDate.ToDayMonthYearString( false ) + "."; if( date < minDate ) errorHandler.SetValidationResult( - ValidationResult.Custom( ErrorCondition.TooEarly, "The " + errorHandler.Subject + " is too early." + minMaxMessage ) ); + ValidationError.Custom( ErrorCondition.TooEarly, "The " + errorHandler.Subject + " is too early." + minMaxMessage ) ); else if( date >= maxDate ) - errorHandler.SetValidationResult( ValidationResult.Custom( ErrorCondition.TooLate, "The " + errorHandler.Subject + " is too late." + minMaxMessage ) ); + errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooLate, "The " + errorHandler.Subject + " is too late." + minMaxMessage ) ); } } @@ -885,7 +885,7 @@ private static bool isEmpty( ValidationErrorHandler errorHandler, object valueAs var isEmpty = valueAsObject.ObjectToString( true ).Trim().Length == 0; if( !allowEmpty && isEmpty ) - errorHandler.SetValidationResult( ValidationResult.Empty() ); + errorHandler.SetValidationResult( ValidationError.Empty() ); return isEmpty; } diff --git a/Tewl/InputValidation/ZipCode.cs b/Tewl/InputValidation/ZipCode.cs index 16dd49c..70c32be 100644 --- a/Tewl/InputValidation/ZipCode.cs +++ b/Tewl/InputValidation/ZipCode.cs @@ -1,7 +1,4 @@ -using System; using System.Text.RegularExpressions; -using JetBrains.Annotations; -using Tewl.Tools; namespace Tewl.InputValidation { /// @@ -27,7 +24,7 @@ public class ZipCode { /// public string FullZipCode => StringTools.ConcatenateWithDelimiter( "-", Zip, Plus4 ); - internal ZipCode() { } + internal ZipCode() {} internal static ZipCode CreateUsZipCode( ValidationErrorHandler errorHandler, string entireZipCode ) { var match = Regex.Match( entireZipCode, usPattern ); @@ -48,10 +45,11 @@ internal static ZipCode CreateUsOrCanadianZipCode( ValidationErrorHandler errorH } private static ZipCode getZipCodeForFailure( ValidationErrorHandler errorHandler ) { - errorHandler.SetValidationResult( ValidationResult.Invalid() ); + errorHandler.SetValidationResult( ValidationError.Invalid() ); return new ZipCode(); } - private static ZipCode getZipCodeFromValidUsMatch( Match match ) => new ZipCode { Zip = match.Groups[ "zip" ].Value, Plus4 = match.Groups[ "plus4" ].Value }; + private static ZipCode getZipCodeFromValidUsMatch( Match match ) => + new ZipCode { Zip = match.Groups[ "zip" ].Value, Plus4 = match.Groups[ "plus4" ].Value }; } } \ No newline at end of file From d7bbafa772fbcd57c695a2df1cbb9f44c0aa7de4 Mon Sep 17 00:00:00 2001 From: William Gross Date: Wed, 19 Mar 2025 12:07:40 -0400 Subject: [PATCH 02/28] Restructured underlying validation methods Restructured and simplified underlying validation methods in Validator. They now return the error, set the value via a setter parameter, and no longer trim or check for empty. --- Tewl/InputValidation/Validator.cs | 446 +++++++++++++++--------------- Tewl/InputValidation/ZipCode.cs | 42 +-- 2 files changed, 250 insertions(+), 238 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 5ae735a..6e81f38 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -12,9 +12,6 @@ namespace Tewl.InputValidation; /// [ PublicAPI ] public class Validator { - // NOTE: There is already something called this. Also, put it in its own file. - private delegate T ValidationMethod(); - internal static readonly DateTime SqlSmallDateTimeMinValue = new DateTime( 1900, 1, 1 ); internal static readonly DateTime SqlSmallDateTimeMaxValue = new DateTime( 2079, 6, 6 ); @@ -116,11 +113,7 @@ internal void AddError( Error error ) { /// Passing an empty string or null will result in ErrorCondition.Empty. /// public bool GetBoolean( ValidationErrorHandler errorHandler, string booleanAsString ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - errorHandler, - booleanAsString, - false, - delegate { return validateBoolean( booleanAsString, errorHandler ); } ); + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, booleanAsString, false, validateBoolean ); /// /// Accepts either true/false (case-sensitive) or 1/0. @@ -128,19 +121,18 @@ public bool GetBoolean( ValidationErrorHandler errorHandler, string booleanAsStr /// If allowEmpty is true and the given string is empty, null will be returned. /// public bool? GetNullableBoolean( ValidationErrorHandler errorHandler, string booleanAsString, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, booleanAsString, allowEmpty, - () => validateBoolean( booleanAsString, errorHandler ) ); + ( valueSetter, trimmedInput ) => validateBoolean( value => valueSetter( value ), trimmedInput ) ); - private static bool validateBoolean( string booleanAsString, ValidationErrorHandler errorHandler ) { - if( booleanAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationError.Empty() ); - else if( booleanAsString != "1" && booleanAsString != "0" && booleanAsString != true.ToString() && booleanAsString != false.ToString() ) - errorHandler.SetValidationResult( ValidationError.Invalid() ); + private static ValidationError? validateBoolean( Action valueSetter, string trimmedInput ) { + if( trimmedInput != "1" && trimmedInput != "0" && trimmedInput != true.ToString() && trimmedInput != false.ToString() ) + return ValidationError.Invalid(); - return booleanAsString == "1" || booleanAsString == true.ToString(); + valueSetter( trimmedInput == "1" || trimmedInput == true.ToString() ); + return null; } /// @@ -154,22 +146,22 @@ private static bool validateBoolean( string booleanAsString, ValidationErrorHand /// Passing an empty string or null will result in ErrorCondition.Empty. /// public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString, byte min, byte max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, byteAsString, false, - delegate { return validateGenericIntegerType( errorHandler, byteAsString, min, max ); } ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); /// /// Returns the validated byte type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// public byte? GetNullableByte( ValidationErrorHandler errorHandler, string byteAsString, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, byteAsString, allowEmpty, - () => validateGenericIntegerType( errorHandler, byteAsString, byte.MinValue, byte.MaxValue ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, byte.MinValue, byte.MaxValue ) ); /// /// Returns the validated short type from the given string and validation package. @@ -182,11 +174,11 @@ public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString, b /// Passing an empty string or null will result in ErrorCondition.Empty. /// public short GetShort( ValidationErrorHandler errorHandler, string shortAsString, short min, short max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, shortAsString, false, - () => validateGenericIntegerType( errorHandler, shortAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); /// /// Returns the validated short type from the given string and validation package. @@ -200,11 +192,11 @@ public short GetShort( ValidationErrorHandler errorHandler, string shortAsString /// If allowEmpty is true and the given string is empty, null will be returned. /// public short? GetNullableShort( ValidationErrorHandler errorHandler, string shortAsString, bool allowEmpty, short min, short max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, shortAsString, allowEmpty, - () => validateGenericIntegerType( errorHandler, shortAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); /// /// Returns the validated int type from the given string and validation package. @@ -218,22 +210,22 @@ public short GetShort( ValidationErrorHandler errorHandler, string shortAsString /// and are inclusive. /// public int GetInt( ValidationErrorHandler errorHandler, string intAsString, int min, int max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, intAsString, false, - () => validateGenericIntegerType( errorHandler, intAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); /// /// Returns the validated int type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// public int? GetNullableInt( ValidationErrorHandler errorHandler, string intAsString, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, intAsString, allowEmpty, - () => validateGenericIntegerType( errorHandler, intAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); /// /// Returns the validated long type from the given string and validation package. @@ -241,11 +233,11 @@ public int GetInt( ValidationErrorHandler errorHandler, string intAsString, int /// and are inclusive. /// public long GetLong( ValidationErrorHandler errorHandler, string longAsString, long min = long.MinValue, long max = long.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, longAsString, false, - () => validateGenericIntegerType( errorHandler, longAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); /// /// Returns the validated long type from the given string and validation package. @@ -253,36 +245,32 @@ public long GetLong( ValidationErrorHandler errorHandler, string longAsString, l /// public long? GetNullableLong( ValidationErrorHandler errorHandler, string longAsString, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, longAsString, allowEmpty, - () => validateGenericIntegerType( errorHandler, longAsString, min, max ) ); + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); - private static T validateGenericIntegerType( ValidationErrorHandler errorHandler, string valueAsString, long minValue, long maxValue ) { - long intResult = 0; + private static ValidationError? validateGenericIntegerType( Action valueSetter, string trimmedInput, long minValue, long maxValue ) { + long intResult; - if( valueAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationError.Empty() ); - else - try { - intResult = Convert.ToInt64( valueAsString ); - - if( intResult > maxValue ) - errorHandler.SetValidationResult( ValidationError.TooLarge( minValue, maxValue ) ); - else if( intResult < minValue ) - errorHandler.SetValidationResult( ValidationError.TooSmall( minValue, maxValue ) ); - } - catch( FormatException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - } - catch( OverflowException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - } - - if( errorHandler.LastResult != ErrorCondition.NoError ) - return default; - return (T)Convert.ChangeType( intResult, typeof( T ) ); + try { + intResult = Convert.ToInt64( trimmedInput ); + + if( intResult > maxValue ) + return ValidationError.TooLarge( minValue, maxValue ); + if( intResult < minValue ) + return ValidationError.TooSmall( minValue, maxValue ); + } + catch( FormatException ) { + return ValidationError.Invalid(); + } + catch( OverflowException ) { + return ValidationError.Invalid(); + } + + valueSetter( (T)Convert.ChangeType( intResult, typeof( T ) ) ); + return null; } /// @@ -290,45 +278,42 @@ private static T validateGenericIntegerType( ValidationErrorHandler errorHand /// Passing an empty string or null will result in ErrorCondition.Empty. /// public float GetFloat( ValidationErrorHandler errorHandler, string floatAsString, float min, float max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, floatAsString, false, - () => validateFloat( floatAsString, errorHandler, min, max ) ); + ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// public float? GetNullableFloat( ValidationErrorHandler errorHandler, string floatAsString, bool allowEmpty, float min, float max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, floatAsString, allowEmpty, - () => validateFloat( floatAsString, errorHandler, min, max ) ); + ( valueSetter, trimmedInput ) => validateFloat( value => valueSetter( value ), trimmedInput, min, max ) ); - private static float validateFloat( string floatAsString, ValidationErrorHandler errorHandler, float min, float max ) { - float floatValue = 0; - if( floatAsString.IsNullOrWhiteSpace() ) - errorHandler.SetValidationResult( ValidationError.Empty() ); - else { - try { - floatValue = float.Parse( floatAsString ); - } - catch( FormatException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - } - catch( OverflowException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - } - - if( floatValue < min ) - errorHandler.SetValidationResult( ValidationError.TooSmall( min, max ) ); - else if( floatValue > max ) - errorHandler.SetValidationResult( ValidationError.TooLarge( min, max ) ); + private static ValidationError? validateFloat( Action valueSetter, string trimmedInput, float min, float max ) { + float floatValue; + try { + floatValue = float.Parse( trimmedInput ); + } + catch( FormatException ) { + return ValidationError.Invalid(); + } + catch( OverflowException ) { + return ValidationError.Invalid(); } - return floatValue; + if( floatValue < min ) + return ValidationError.TooSmall( min, max ); + if( floatValue > max ) + return ValidationError.TooLarge( min, max ); + + valueSetter( floatValue ); + return null; } /// @@ -343,11 +328,11 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAs /// Passing an empty string or null will result in ErrorCondition.Empty. /// public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAsString, decimal min, decimal max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, decimalAsString, false, - () => validateDecimal( decimalAsString, errorHandler, min, max ) ); + ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated decimal type from the given string and validation package. @@ -361,31 +346,27 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAs /// If allowEmpty is true and the given string is empty, null will be returned. /// public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string decimalAsString, bool allowEmpty, decimal min, decimal max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, decimalAsString, allowEmpty, - () => validateDecimal( decimalAsString, errorHandler, min, max ) ); - - private static decimal validateDecimal( string decimalAsString, ValidationErrorHandler errorHandler, decimal min, decimal max ) { - if( decimalAsString.IsNullOrWhiteSpace() ) { - errorHandler.SetValidationResult( ValidationError.Empty() ); - return 0; - } + ( valueSetter, trimmedInput ) => validateDecimal( value => valueSetter( value ), trimmedInput, min, max ) ); - decimal decimalVal = 0; + private static ValidationError? validateDecimal( Action valueSetter, string trimmedInput, decimal min, decimal max ) { + decimal decimalVal; try { - decimalVal = decimal.Parse( decimalAsString ); + decimalVal = decimal.Parse( trimmedInput ); if( decimalVal < min ) - errorHandler.SetValidationResult( ValidationError.TooSmall( min, max ) ); - else if( decimalVal > max ) - errorHandler.SetValidationResult( ValidationError.TooLarge( min, max ) ); + return ValidationError.TooSmall( min, max ); + if( decimalVal > max ) + return ValidationError.TooLarge( min, max ); } catch { - errorHandler.SetValidationResult( ValidationError.Invalid() ); + return ValidationError.Invalid(); } - return decimalVal; + valueSetter( decimalVal ); + return null; } /// @@ -413,14 +394,15 @@ public string GetString( ValidationErrorHandler errorHandler, string text, bool errorHandler, text, allowEmpty, - () => { + ( valueSetter, trimmedInput ) => { var errorMessage = "The length of the " + errorHandler.Subject + " must be between " + minLength + " and " + maxLength + " characters."; - if( text.Length > maxLength ) - errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooLong, errorMessage ) ); - else if( text.Length < minLength ) - errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooShort, errorMessage ) ); + if( trimmedInput.Length > maxLength ) + return ValidationError.Custom( ErrorCondition.TooLong, errorMessage ); + if( trimmedInput.Length < minLength ) + return ValidationError.Custom( ErrorCondition.TooShort, errorMessage ); - return text.Trim(); + valueSetter( trimmedInput ); + return null; } ); /// @@ -435,9 +417,9 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string email errorHandler, emailAddress, allowEmpty, - () => { + ( valueSetter, trimmedInput ) => { // Validate as a string with same restrictions - if it fails on that, return - emailAddress = GetString( errorHandler, emailAddress, allowEmpty, maxLength ); + trimmedInput = GetString( errorHandler, trimmedInput, allowEmpty, maxLength ); if( errorHandler.LastResult == ErrorCondition.NoError ) { // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. //([^@ \n\.]+\.)+ forces any positive number of (anything.)s to exist in a row. Doesn't allow "..". @@ -447,16 +429,17 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string email const string domainUnconditionallyPermittedCharacters = "[a-z0-9-]"; const string domain = "(" + domainUnconditionallyPermittedCharacters + @"+\.)+" + domainUnconditionallyPermittedCharacters + "+"; // The first two conditions are for performance only. - if( !emailAddress.Contains( "@" ) || !emailAddress.Contains( "." ) || !Regex.IsMatch( - emailAddress, + if( !trimmedInput.Contains( "@" ) || !trimmedInput.Contains( "." ) || !Regex.IsMatch( + trimmedInput, "^" + localPart + "@" + domain + "$", RegexOptions.IgnoreCase ) ) - errorHandler.SetValidationResult( ValidationError.Invalid() ); + return ValidationError.Invalid(); // Max length is already checked by the string validation // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. } - return emailAddress; + valueSetter( trimmedInput ); + return null; } ); /// @@ -475,32 +458,32 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo errorHandler, url, allowEmpty, - () => { + ( valueSetter, trimmedInput ) => { /* If the string is just a number, reject it right out. */ - if( int.TryParse( url, out _ ) || double.TryParse( url, out _ ) ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - return url; - } + if( int.TryParse( trimmedInput, out _ ) || double.TryParse( trimmedInput, out _ ) ) + return ValidationError.Invalid(); /* If it's an email, it's not an URL. */ var testingValidator = new Validator(); - testingValidator.GetEmailAddress( new ValidationErrorHandler( "" ), url, allowEmpty ); - if( !testingValidator.ErrorsOccurred ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - return url; - } + testingValidator.GetEmailAddress( new ValidationErrorHandler( "" ), trimmedInput, allowEmpty ); + if( !testingValidator.ErrorsOccurred ) + return ValidationError.Invalid(); /* If it doesn't start with one of our whitelisted schemes, add in the best guess. */ - url = GetString( errorHandler, validSchemes.Any( s => url.StartsWithIgnoreCase( s ) ) ? url : "http://" + url, true, maxUrlLength ); + trimmedInput = GetString( + errorHandler, + validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, + true, + maxUrlLength ); /* If the getstring didn't fail, keep on keepin keepin on. */ if( errorHandler.LastResult == ErrorCondition.NoError ) try { - if( !Uri.IsWellFormedUriString( url, UriKind.Absolute ) ) + if( !Uri.IsWellFormedUriString( trimmedInput, UriKind.Absolute ) ) throw new UriFormatException(); // Don't allow relative URLs - var uri = new Uri( url, UriKind.Absolute ); + var uri = new Uri( trimmedInput, UriKind.Absolute ); // Must be a valid DNS-style hostname or IP address // Must contain at least one '.', to prevent just host names @@ -510,10 +493,11 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo throw new UriFormatException(); } catch( UriFormatException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); + return ValidationError.Invalid(); } - return url; + valueSetter( trimmedInput ); + return null; } ); /// @@ -558,20 +542,20 @@ public string GetPhoneNumber( /// public string GetPhoneWithLastFiveMapping( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - Dictionary firstFives ) => + Dictionary? firstFives ) => GetPhoneNumberAsObject( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ).StandardPhoneString; internal PhoneNumber GetPhoneNumberAsObject( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - Dictionary firstFives ) { + Dictionary? firstFives ) { return executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, allowEmpty, - () => { - var invalidPrefix = "The " + errorHandler.Subject + " (" + input + ") is invalid."; + ( valueSetter, trimmedInput ) => { + var invalidPrefix = "The " + errorHandler.Subject + " (" + trimmedInput + ") is invalid."; // Remove all of the valid delimiter characters so we can just deal with numbers and whitespace - input = input.RemoveCharacters( '-', '(', ')', '.' ).Trim(); + trimmedInput = trimmedInput.RemoveCharacters( '-', '(', ')', '.' ).Trim(); var invalidMessage = invalidPrefix + " Phone numbers may be entered in any format, such as or xxx-xxx-xxxx, with an optional extension up to 5 digits long. International numbers should begin with a '+' sign."; @@ -580,27 +564,27 @@ internal PhoneNumber GetPhoneNumberAsObject( // NOTE: AllowSurroundingGarbage does not apply to first five or international numbers. // First-five shortcut (intra-org phone numbers) - if( firstFives != null && Regex.IsMatch( input, @"^\d{5}$" ) ) { - if( firstFives.ContainsKey( input.Substring( 0, 1 ) ) ) { - var firstFive = firstFives[ input.Substring( 0, 1 ) ]; - phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + input, "" ); + if( firstFives != null && Regex.IsMatch( trimmedInput, @"^\d{5}$" ) ) { + if( firstFives.ContainsKey( trimmedInput.Substring( 0, 1 ) ) ) { + var firstFive = firstFives[ trimmedInput.Substring( 0, 1 ) ]; + phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + trimmedInput, "" ); } else - errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.Invalid, "The five digit phone number you entered isn't recognized." ) ); + return ValidationError.Custom( ErrorCondition.Invalid, "The five digit phone number you entered isn't recognized." ); } // International phone numbers // We require a country code and then at least 7 digits (but if country code is more than one digit, we require fewer subsequent digits). // We feel this is a reasonable limit to ensure that they are entering an actual phone number, but there is no source for this limit. // We have no idea why we ever began accepting letters, but it's risky to stop accepting them and the consequences of accepting them are small. - else if( Regex.IsMatch( input, @"\+\s*[0|2-9]([a-zA-Z,#/ \.\(\)\*]*[0-9]){7}" ) ) - phoneNumber = PhoneNumber.CreateInternational( input ); + else if( Regex.IsMatch( trimmedInput, @"\+\s*[0|2-9]([a-zA-Z,#/ \.\(\)\*]*[0-9]){7}" ) ) + phoneNumber = PhoneNumber.CreateInternational( trimmedInput ); // Validated it as a North American Numbering Plan phone number else { var regex = @"(?\+?1)?\s*(?\d{3})\s*(?\d{3})\s*(?\d{4})\s*?(?:(?:x|\s|ext|ext\.|extension)\s*(?\d{1,5}))?\s*"; if( !allowSurroundingGarbage ) regex = "^" + regex + "$"; - var match = Regex.Match( input, regex ); + var match = Regex.Match( trimmedInput, regex ); if( match.Success ) { var areaCode = match.Groups[ "ac" ].Value; @@ -608,18 +592,18 @@ internal PhoneNumber GetPhoneNumberAsObject( var extension = match.Groups[ "ext" ].Value; phoneNumber = PhoneNumber.CreateFromParts( areaCode, number, extension ); if( !allowExtension && phoneNumber.Extension.Length > 0 ) - errorHandler.SetValidationResult( - ValidationError.Custom( - ErrorCondition.Invalid, - invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ) ); + return ValidationError.Custom( + ErrorCondition.Invalid, + invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ); } else - errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.Invalid, invalidMessage ) ); + return ValidationError.Custom( ErrorCondition.Invalid, invalidMessage ); } - return phoneNumber; + valueSetter( phoneNumber ); + return null; }, - PhoneNumber.CreateFromParts( "", "", "" ) ); + PhoneNumber.CreateFromParts( "", "", "" ) )!; } /// @@ -631,12 +615,12 @@ public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, stri errorHandler, extension, allowEmpty, - () => { - extension = extension.Trim(); - if( !Regex.IsMatch( extension, @"^ *(?\d{1,5}) *$" ) ) - errorHandler.SetValidationResult( ValidationError.Invalid() ); + ( valueSetter, trimmedInput ) => { + if( !Regex.IsMatch( trimmedInput, @"^ *(?\d{1,5}) *$" ) ) + return ValidationError.Invalid(); - return extension; + valueSetter( trimmedInput ); + return null; } ); /// @@ -656,36 +640,27 @@ public string GetNumber( ValidationErrorHandler errorHandler, string text, int n errorHandler, text, allowEmpty, - delegate { + ( valueSetter, trimmedInput ) => { foreach( var garbageString in acceptableGarbageStrings ) - text = text.Replace( garbageString, "" ); - text = text.Trim(); - if( !Regex.IsMatch( text, @"^\d{" + numberOfDigits + "}$" ) ) - errorHandler.SetValidationResult( ValidationError.Invalid() ); - return text; + trimmedInput = trimmedInput.Replace( garbageString, "" ); + trimmedInput = trimmedInput.Trim(); + if( !Regex.IsMatch( trimmedInput, @"^\d{" + numberOfDigits + "}$" ) ) + return ValidationError.Invalid(); + valueSetter( trimmedInput ); + return null; } ); /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// public ZipCode GetZipCode( ValidationErrorHandler errorHandler, string zipCode, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - errorHandler, - zipCode, - allowEmpty, - () => ZipCode.CreateUsZipCode( errorHandler, zipCode ), - new ZipCode() ); + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, zipCode, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; /// /// Gets a validated US or Canadian zip code. /// public ZipCode GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string zipCode, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - errorHandler, - zipCode, - allowEmpty, - () => ZipCode.CreateUsOrCanadianZipCode( errorHandler, zipCode ), - new ZipCode() ); + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, zipCode, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; /// /// Returns the validated DateTime type from the given string and validation package. @@ -693,11 +668,11 @@ public ZipCode GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, stri /// Passing an empty string or null will result in ErrorCondition.Empty. /// public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string dateAsString ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, dateAsString, false, - delegate { return validateDateTime( errorHandler, dateAsString, null, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ); } ); + ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, null, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) ); /// /// Returns the validated DateTime type from the given string and validation package. @@ -705,11 +680,16 @@ public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string /// If allowEmpty is true and the given string is empty, null will be returned. /// public DateTime? GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string dateAsString, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, dateAsString, allowEmpty, - () => validateDateTime( errorHandler, dateAsString, null, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) ); + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value ), + trimmedInput, + null, + SqlSmallDateTimeMinValue, + SqlSmallDateTimeMaxValue ) ); /// /// Returns the validated DateTime type from the given date part strings and validation package. @@ -741,65 +721,68 @@ private static string makeDateFromParts( string month, string day, string year ) /// Pattern specifies the date format, such as "MM/dd/yyyy". /// public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, dateAsString, false, - () => validateSqlSmallDateTimeExact( errorHandler, dateAsString, pattern ) ); + ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( valueSetter, trimmedInput, pattern ) ); /// /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// public DateTime? GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, dateAsString, allowEmpty, - delegate { return validateSqlSmallDateTimeExact( errorHandler, dateAsString, pattern ); } ); + ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( value => valueSetter( value ), trimmedInput, pattern ) ); - private static DateTime validateSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern ) { - if( !DateTime.TryParseExact( dateAsString, pattern, Cultures.EnglishUnitedStates, DateTimeStyles.None, out var date ) ) - errorHandler.SetValidationResult( ValidationError.Invalid() ); - else - validateNativeDateTime( errorHandler, date, false, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ); + private static ValidationError? validateSqlSmallDateTimeExact( Action valueSetter, string trimmedInput, string pattern ) { + if( !DateTime.TryParseExact( trimmedInput, pattern, Cultures.EnglishUnitedStates, DateTimeStyles.None, out var date ) ) + return ValidationError.Invalid(); + if( validateNativeDateTime( date, false, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) is {} error ) + return error; - return date; + valueSetter( date ); + return null; } - private static DateTime validateDateTime( ValidationErrorHandler errorHandler, string dateAsString, string[]? formats, DateTime min, DateTime max ) { - var date = DateTime.Now; + private static ValidationError? validateDateTime( Action valueSetter, string trimmedInput, string[]? formats, DateTime min, DateTime max ) { + DateTime date; try { date = formats is not null - ? DateTime.ParseExact( dateAsString, formats, null, DateTimeStyles.None ) - : DateTime.Parse( dateAsString, Cultures.EnglishUnitedStates ); - validateNativeDateTime( errorHandler, date, false, min, max ); + ? DateTime.ParseExact( trimmedInput, formats, null, DateTimeStyles.None ) + : DateTime.Parse( trimmedInput, Cultures.EnglishUnitedStates ); + if( validateNativeDateTime( date, false, min, max ) is {} error ) + return error; } catch( FormatException ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); + return ValidationError.Invalid(); } catch( ArgumentOutOfRangeException ) { // Undocumented exception that there are reports of being thrown - errorHandler.SetValidationResult( ValidationError.Invalid() ); + return ValidationError.Invalid(); } catch( ArgumentNullException ) { - errorHandler.SetValidationResult( ValidationError.Empty() ); + return ValidationError.Empty(); } - return date; + valueSetter( date ); + return null; } - private static void validateNativeDateTime( ValidationErrorHandler errorHandler, DateTime? date, bool allowEmpty, DateTime minDate, DateTime maxDate ) { - if( date == null && !allowEmpty ) - errorHandler.SetValidationResult( ValidationError.Empty() ); - else if( date.HasValue ) { + private static ValidationError? validateNativeDateTime( DateTime? date, bool allowEmpty, DateTime minDate, DateTime maxDate ) { + if( date is null && !allowEmpty ) + return ValidationError.Empty(); + if( date.HasValue ) { var minMaxMessage = " It must be between " + minDate.ToDayMonthYearString( false ) + " and " + maxDate.ToDayMonthYearString( false ) + "."; if( date < minDate ) - errorHandler.SetValidationResult( - ValidationError.Custom( ErrorCondition.TooEarly, "The " + errorHandler.Subject + " is too early." + minMaxMessage ) ); - else if( date >= maxDate ) - errorHandler.SetValidationResult( ValidationError.Custom( ErrorCondition.TooLate, "The " + errorHandler.Subject + " is too late." + minMaxMessage ) ); + return ValidationError.Custom( ErrorCondition.TooEarly, "The {0} is too early." + minMaxMessage ); + if( date >= maxDate ) + return ValidationError.Custom( ErrorCondition.TooLate, "The {0} is too late." + minMaxMessage ); } + return null; } /// @@ -807,59 +790,88 @@ private static void validateNativeDateTime( ValidationErrorHandler errorHandler, /// public DateTime? GetNullableDateTime( ValidationErrorHandler handler, string dateAsString, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, dateAsString, allowEmpty, - () => validateDateTime( handler, dateAsString, formats, minDate, maxDate ) ); + ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value ), trimmedInput, formats, minDate, maxDate ) ); /// /// Validates the date using given min and max constraints. /// public DateTime GetDateTime( ValidationErrorHandler handler, string dateAsString, string[]? formats, DateTime minDate, DateTime maxDate ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, dateAsString, false, - () => validateDateTime( handler, dateAsString, formats, minDate, maxDate ) ); + ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, formats, minDate, maxDate ) ); /// /// Validates the given time span. /// public TimeSpan? GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, timeSpan, allowEmpty, () => timeSpan ); + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + handler, + timeSpan, + allowEmpty, + ( valueSetter, trimmedInput ) => { + valueSetter( trimmedInput!.Value ); + return null; + } ); /// /// Validates the given time span. /// public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, timeSpan, false, () => timeSpan ?? default ); + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + handler, + timeSpan, + false, + ( valueSetter, trimmedInput ) => { + valueSetter( trimmedInput!.Value ); + return null; + } ); /// /// Validates the given time span. /// public TimeSpan? GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string timeSpanAsString, string[]? formats, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, timeSpanAsString, allowEmpty, - () => validateDateTime( handler, timeSpanAsString, formats, DateTime.MinValue, DateTime.MaxValue ).TimeOfDay ); + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value.TimeOfDay ), + trimmedInput, + formats, + DateTime.MinValue, + DateTime.MaxValue ) ); /// /// Validates the given time span. /// public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string timeSpanAsString, string[]? formats ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, timeSpanAsString, false, - () => validateDateTime( handler, timeSpanAsString, formats, DateTime.MinValue, DateTime.MaxValue ).TimeOfDay ); - - private T executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - ValidationErrorHandler handler, object valueAsObject, bool allowEmpty, ValidationMethod method, T customDefaultReturnValue = default ) { + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value.TimeOfDay ), + trimmedInput, + formats, + DateTime.MinValue, + DateTime.MaxValue ) ); + + private ValType? executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + ValidationErrorHandler handler, InputType input, bool allowEmpty, Func, InputType, ValidationError?> method, + ValType? customDefaultReturnValue = default ) { var result = customDefaultReturnValue; - if( !isEmpty( handler, valueAsObject, allowEmpty ) ) - result = method(); + if( !isEmpty( handler, input, allowEmpty ) ) { + // IMPORTANT: Pass trimmed input here. + var error = method( value => result = value, input ); + if( error is not null ) + handler.SetValidationResult( error ); + } // If there was an error of any kind, the result becomes the default value if( handler.LastResult != ErrorCondition.NoError ) @@ -869,9 +881,9 @@ private T executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( return result; } - private string handleEmptyAndReturnEmptyStringIfInvalid( - ValidationErrorHandler handler, object valueAsObject, bool allowEmpty, ValidationMethod method ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, valueAsObject, allowEmpty, method, "" ); + private string handleEmptyAndReturnEmptyStringIfInvalid( + ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, Func, InputType, ValidationError?> method ) => + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, valueAsObject, allowEmpty, method, "" )!; /// /// Determines if the given field is empty, and if it is empty, it @@ -881,8 +893,8 @@ private string handleEmptyAndReturnEmptyStringIfInvalid( /// if value is not empty and validation should continue. Validation methods should /// return immediately with no further action if this method returns true. /// - private static bool isEmpty( ValidationErrorHandler errorHandler, object valueAsObject, bool allowEmpty ) { - var isEmpty = valueAsObject.ObjectToString( true ).Trim().Length == 0; + private static bool isEmpty( ValidationErrorHandler errorHandler, InputType input, bool allowEmpty ) { + var isEmpty = input.ObjectToString( true )!.Trim().Length == 0; if( !allowEmpty && isEmpty ) errorHandler.SetValidationResult( ValidationError.Empty() ); diff --git a/Tewl/InputValidation/ZipCode.cs b/Tewl/InputValidation/ZipCode.cs index 70c32be..611f8fd 100644 --- a/Tewl/InputValidation/ZipCode.cs +++ b/Tewl/InputValidation/ZipCode.cs @@ -26,30 +26,30 @@ public class ZipCode { internal ZipCode() {} - internal static ZipCode CreateUsZipCode( ValidationErrorHandler errorHandler, string entireZipCode ) { - var match = Regex.Match( entireZipCode, usPattern ); - if( match.Success ) - return getZipCodeFromValidUsMatch( match ); - - return getZipCodeForFailure( errorHandler ); - } - - internal static ZipCode CreateUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string entireZipCode ) { - var match = Regex.Match( entireZipCode, usPattern ); - if( match.Success ) - return getZipCodeFromValidUsMatch( match ); - if( ( match = Regex.Match( entireZipCode, canadianPattern, RegexOptions.IgnoreCase ) ).Success ) - return new ZipCode { Zip = match.Groups[ "caZip" ].Value }; - - return getZipCodeForFailure( errorHandler ); + internal static ValidationError? CreateUsZipCode( Action valueSetter, string trimmedInput ) { + var match = Regex.Match( trimmedInput, usPattern ); + if( match.Success ) { + valueSetter( getZipCodeFromValidUsMatch( match ) ); + return null; + } + + return ValidationError.Invalid(); } - private static ZipCode getZipCodeForFailure( ValidationErrorHandler errorHandler ) { - errorHandler.SetValidationResult( ValidationError.Invalid() ); - return new ZipCode(); + internal static ValidationError? CreateUsOrCanadianZipCode( Action valueSetter, string trimmedInput ) { + var match = Regex.Match( trimmedInput, usPattern ); + if( match.Success ) { + valueSetter( getZipCodeFromValidUsMatch( match ) ); + return null; + } + if( ( match = Regex.Match( trimmedInput, canadianPattern, RegexOptions.IgnoreCase ) ).Success ) { + valueSetter( new ZipCode { Zip = match.Groups[ "caZip" ].Value } ); + return null; + } + + return ValidationError.Invalid(); } - private static ZipCode getZipCodeFromValidUsMatch( Match match ) => - new ZipCode { Zip = match.Groups[ "zip" ].Value, Plus4 = match.Groups[ "plus4" ].Value }; + private static ZipCode getZipCodeFromValidUsMatch( Match match ) => new() { Zip = match.Groups[ "zip" ].Value, Plus4 = match.Groups[ "plus4" ].Value }; } } \ No newline at end of file From a2cf592797de80c3d7983f6a02413c9f78f5ee96 Mon Sep 17 00:00:00 2001 From: William Gross Date: Wed, 19 Mar 2025 16:26:38 -0400 Subject: [PATCH 03/28] Renamed validation method input parameters --- Tewl/InputValidation/Validator.cs | 172 +++++++++++++++--------------- 1 file changed, 84 insertions(+), 88 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 6e81f38..9e35df1 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -112,18 +112,18 @@ internal void AddError( Error error ) { /// Returns the validated boolean type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public bool GetBoolean( ValidationErrorHandler errorHandler, string booleanAsString ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, booleanAsString, false, validateBoolean ); + public bool GetBoolean( ValidationErrorHandler errorHandler, string input ) => + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, false, validateBoolean ); /// /// Accepts either true/false (case-sensitive) or 1/0. /// Returns the validated boolean type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public bool? GetNullableBoolean( ValidationErrorHandler errorHandler, string booleanAsString, bool allowEmpty ) => + public bool? GetNullableBoolean( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - booleanAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateBoolean( value => valueSetter( value ), trimmedInput ) ); @@ -139,16 +139,16 @@ public bool GetBoolean( ValidationErrorHandler errorHandler, string booleanAsStr /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString ) => GetByte( errorHandler, byteAsString, byte.MinValue, byte.MaxValue ); + public byte GetByte( ValidationErrorHandler errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); /// /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString, byte min, byte max ) => + public byte GetByte( ValidationErrorHandler errorHandler, string input, byte min, byte max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - byteAsString, + input, false, ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); @@ -156,10 +156,10 @@ public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString, b /// Returns the validated byte type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public byte? GetNullableByte( ValidationErrorHandler errorHandler, string byteAsString, bool allowEmpty ) => + public byte? GetNullableByte( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - byteAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, byte.MinValue, byte.MaxValue ) ); @@ -167,16 +167,16 @@ public byte GetByte( ValidationErrorHandler errorHandler, string byteAsString, b /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public short GetShort( ValidationErrorHandler errorHandler, string shortAsString ) => GetShort( errorHandler, shortAsString, short.MinValue, short.MaxValue ); + public short GetShort( ValidationErrorHandler errorHandler, string input ) => GetShort( errorHandler, input, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public short GetShort( ValidationErrorHandler errorHandler, string shortAsString, short min, short max ) => + public short GetShort( ValidationErrorHandler errorHandler, string input, short min, short max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - shortAsString, + input, false, ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); @@ -184,17 +184,17 @@ public short GetShort( ValidationErrorHandler errorHandler, string shortAsString /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public short? GetNullableShort( ValidationErrorHandler errorHandler, string shortAsString, bool allowEmpty ) => - GetNullableShort( errorHandler, shortAsString, allowEmpty, short.MinValue, short.MaxValue ); + public short? GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + GetNullableShort( errorHandler, input, allowEmpty, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public short? GetNullableShort( ValidationErrorHandler errorHandler, string shortAsString, bool allowEmpty, short min, short max ) => + public short? GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty, short min, short max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - shortAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); @@ -202,17 +202,17 @@ public short GetShort( ValidationErrorHandler errorHandler, string shortAsString /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public int GetInt( ValidationErrorHandler errorHandler, string intAsString ) => GetInt( errorHandler, intAsString, int.MinValue, int.MaxValue ); + public int GetInt( ValidationErrorHandler errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); /// /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public int GetInt( ValidationErrorHandler errorHandler, string intAsString, int min, int max ) => + public int GetInt( ValidationErrorHandler errorHandler, string input, int min, int max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - intAsString, + input, false, ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); @@ -220,10 +220,10 @@ public int GetInt( ValidationErrorHandler errorHandler, string intAsString, int /// Returns the validated int type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public int? GetNullableInt( ValidationErrorHandler errorHandler, string intAsString, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => + public int? GetNullableInt( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - intAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); @@ -232,10 +232,10 @@ public int GetInt( ValidationErrorHandler errorHandler, string intAsString, int /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public long GetLong( ValidationErrorHandler errorHandler, string longAsString, long min = long.MinValue, long max = long.MaxValue ) => + public long GetLong( ValidationErrorHandler errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - longAsString, + input, false, ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); @@ -243,11 +243,10 @@ public long GetLong( ValidationErrorHandler errorHandler, string longAsString, l /// Returns the validated long type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public long? - GetNullableLong( ValidationErrorHandler errorHandler, string longAsString, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => + public long? GetNullableLong( ValidationErrorHandler errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - longAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); @@ -277,10 +276,10 @@ public long? /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public float GetFloat( ValidationErrorHandler errorHandler, string floatAsString, float min, float max ) => + public float GetFloat( ValidationErrorHandler errorHandler, string input, float min, float max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - floatAsString, + input, false, ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); @@ -288,10 +287,10 @@ public float GetFloat( ValidationErrorHandler errorHandler, string floatAsString /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public float? GetNullableFloat( ValidationErrorHandler errorHandler, string floatAsString, bool allowEmpty, float min, float max ) => + public float? GetNullableFloat( ValidationErrorHandler errorHandler, string input, bool allowEmpty, float min, float max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - floatAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateFloat( value => valueSetter( value ), trimmedInput, min, max ) ); @@ -320,17 +319,16 @@ public float GetFloat( ValidationErrorHandler errorHandler, string floatAsString /// Returns a validated decimal type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAsString ) => - GetDecimal( errorHandler, decimalAsString, decimal.MinValue, decimal.MaxValue ); + public decimal GetDecimal( ValidationErrorHandler errorHandler, string input ) => GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAsString, decimal min, decimal max ) => + public decimal GetDecimal( ValidationErrorHandler errorHandler, string input, decimal min, decimal max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - decimalAsString, + input, false, ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); @@ -338,17 +336,17 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAs /// Returns a validated decimal type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string decimalAsString, bool allowEmpty ) => - GetNullableDecimal( errorHandler, decimalAsString, allowEmpty, decimal.MinValue, decimal.MaxValue ); + public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + GetNullableDecimal( errorHandler, input, allowEmpty, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string decimalAsString, bool allowEmpty, decimal min, decimal max ) => + public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - decimalAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateDecimal( value => valueSetter( value ), trimmedInput, min, max ) ); @@ -374,25 +372,25 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string decimalAs /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string text, bool allowEmpty ) => GetString( errorHandler, text, allowEmpty, int.MaxValue ); + public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetString( errorHandler, input, allowEmpty, int.MaxValue ); /// /// Returns a validated string from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string text, bool allowEmpty, int maxLength ) => - GetString( errorHandler, text, allowEmpty, 0, maxLength ); + public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength ) => + GetString( errorHandler, input, allowEmpty, 0, maxLength ); /// /// Returns a validated string from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string text, bool allowEmpty, int minLength, int maxLength ) => + public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, - text, + input, allowEmpty, ( valueSetter, trimmedInput ) => { var errorMessage = "The length of the " + errorHandler.Subject + " must be between " + minLength + " and " + maxLength + " characters."; @@ -412,10 +410,10 @@ public string GetString( ValidationErrorHandler errorHandler, string text, bool /// The maxLength defaults to 254 per this source: http://en.wikipedia.org/wiki/E-mail_address#Syntax /// If you pass a different value for maxLength, you'd better have a good reason. /// - public string GetEmailAddress( ValidationErrorHandler errorHandler, string emailAddress, bool allowEmpty, int maxLength = 254 ) => + public string GetEmailAddress( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, - emailAddress, + input, allowEmpty, ( valueSetter, trimmedInput ) => { // Validate as a string with same restrictions - if it fails on that, return @@ -445,7 +443,7 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string email /// /// Returns a validated URL. /// - public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allowEmpty ) => GetUrl( errorHandler, url, allowEmpty, MaxUrlLength ); + public string GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetUrl( errorHandler, input, allowEmpty, MaxUrlLength ); private static readonly string[] validSchemes = { "http", "https", "ftp" }; @@ -453,10 +451,10 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string email /// Returns a validated URL. Note that you may run into problems with certain browsers if you pass a length longer than /// 2048. /// - public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allowEmpty, int maxUrlLength ) => + public string GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxUrlLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, - url, + input, allowEmpty, ( valueSetter, trimmedInput ) => { /* If the string is just a number, reject it right out. */ @@ -505,22 +503,21 @@ public string GetUrl( ValidationErrorHandler errorHandler, string url, bool allo /// This is useful when working with data that had the area code omitted because the number was local. /// public string GetPhoneNumberWithDefaultAreaCode( - ValidationErrorHandler errorHandler, string completePhoneNumber, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - string defaultAreaCode ) { + ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { var validator = new Validator(); // We need to use a separate one so that erroneous error messages don't get left in the collection var fakeHandler = new ValidationErrorHandler( "" ); - validator.GetPhoneNumber( fakeHandler, completePhoneNumber, allowExtension, allowEmpty, allowSurroundingGarbage ); + validator.GetPhoneNumber( fakeHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); if( fakeHandler.LastResult != ErrorCondition.NoError ) { fakeHandler = new ValidationErrorHandler( "" ); - validator.GetPhoneNumber( fakeHandler, defaultAreaCode + completePhoneNumber, allowExtension, allowEmpty, allowSurroundingGarbage ); + validator.GetPhoneNumber( fakeHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); // If the phone number was invalid without the area code, but is valid with the area code, we really validate using the default // area code and then return. In all other cases, we return what would have happened without tacking on the default area code. if( fakeHandler.LastResult == ErrorCondition.NoError ) - return GetPhoneNumber( errorHandler, defaultAreaCode + completePhoneNumber, allowExtension, allowEmpty, allowSurroundingGarbage ); + return GetPhoneNumber( errorHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); } - return GetPhoneNumber( errorHandler, completePhoneNumber, allowExtension, allowEmpty, allowSurroundingGarbage ); + return GetPhoneNumber( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); } /// @@ -530,9 +527,8 @@ public string GetPhoneNumberWithDefaultAreaCode( /// into 585-455-6476 /// and count as a valid phone number. /// - public string GetPhoneNumber( - ValidationErrorHandler errorHandler, string completePhoneNumber, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => - GetPhoneWithLastFiveMapping( errorHandler, completePhoneNumber, allowExtension, allowEmpty, allowSurroundingGarbage, null ); + public string GetPhoneNumber( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => + GetPhoneWithLastFiveMapping( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, null ); /// /// Returns a validated phone number as a standard phone number string given the complete phone number with optional @@ -610,10 +606,10 @@ internal PhoneNumber GetPhoneNumberAsObject( /// Returns a validated phone number extension as a string. /// If allow empty is true and the empty string or null is given, the empty string is returned. /// - public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, string extension, bool allowEmpty ) => + public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, - extension, + input, allowEmpty, ( valueSetter, trimmedInput ) => { if( !Regex.IsMatch( trimmedInput, @"^ *(?\d{1,5}) *$" ) ) @@ -627,18 +623,18 @@ public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, stri /// Returns a validated social security number from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// - public string GetSocialSecurityNumber( ValidationErrorHandler errorHandler, string ssn, bool allowEmpty ) => - GetNumber( errorHandler, ssn, 9, allowEmpty, "-" ); + public string GetSocialSecurityNumber( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + GetNumber( errorHandler, input, 9, allowEmpty, "-" ); /// /// Gets a string of the given length whose characters are only numeric values, after throwing out all acceptable garbage /// characters. /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). /// - public string GetNumber( ValidationErrorHandler errorHandler, string text, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => + public string GetNumber( ValidationErrorHandler errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, - text, + input, allowEmpty, ( valueSetter, trimmedInput ) => { foreach( var garbageString in acceptableGarbageStrings ) @@ -653,24 +649,24 @@ public string GetNumber( ValidationErrorHandler errorHandler, string text, int n /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// - public ZipCode GetZipCode( ValidationErrorHandler errorHandler, string zipCode, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, zipCode, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; + public ZipCode GetZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; /// /// Gets a validated US or Canadian zip code. /// - public ZipCode GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string zipCode, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, zipCode, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; + public ZipCode GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; /// /// Returns the validated DateTime type from the given string and validation package. /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string dateAsString ) => + public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string input ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - dateAsString, + input, false, ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, null, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) ); @@ -679,10 +675,10 @@ public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public DateTime? GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string dateAsString, bool allowEmpty ) => + public DateTime? GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - dateAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value ), @@ -720,10 +716,10 @@ private static string makeDateFromParts( string month, string day, string year ) /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern ) => + public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - dateAsString, + input, false, ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( valueSetter, trimmedInput, pattern ) ); @@ -731,10 +727,10 @@ public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, s /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public DateTime? GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string dateAsString, string pattern, bool allowEmpty ) => + public DateTime? GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, - dateAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( value => valueSetter( value ), trimmedInput, pattern ) ); @@ -788,31 +784,31 @@ public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, s /// /// Validates the date using given allowEmpty, min, and max constraints. /// - public DateTime? GetNullableDateTime( - ValidationErrorHandler handler, string dateAsString, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => + public DateTime? + GetNullableDateTime( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - dateAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value ), trimmedInput, formats, minDate, maxDate ) ); /// /// Validates the date using given min and max constraints. /// - public DateTime GetDateTime( ValidationErrorHandler handler, string dateAsString, string[]? formats, DateTime minDate, DateTime maxDate ) => + public DateTime GetDateTime( ValidationErrorHandler handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - dateAsString, + input, false, ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, formats, minDate, maxDate ) ); /// /// Validates the given time span. /// - public TimeSpan? GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan, bool allowEmpty ) => + public TimeSpan? GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? input, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - timeSpan, + input, allowEmpty, ( valueSetter, trimmedInput ) => { valueSetter( trimmedInput!.Value ); @@ -822,10 +818,10 @@ public DateTime GetDateTime( ValidationErrorHandler handler, string dateAsString /// /// Validates the given time span. /// - public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan ) => + public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - timeSpan, + input, false, ( valueSetter, trimmedInput ) => { valueSetter( trimmedInput!.Value ); @@ -835,10 +831,10 @@ public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan /// /// Validates the given time span. /// - public TimeSpan? GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string timeSpanAsString, string[]? formats, bool allowEmpty ) => + public TimeSpan? GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - timeSpanAsString, + input, allowEmpty, ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value.TimeOfDay ), @@ -850,10 +846,10 @@ public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? timeSpan /// /// Validates the given time span. /// - public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string timeSpanAsString, string[]? formats ) => + public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, - timeSpanAsString, + input, false, ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value.TimeOfDay ), From 73df1e37909d54da3d767767a260c90a67b3f3c9 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 11:37:55 -0400 Subject: [PATCH 04/28] Overhauled Validator.isEmpty to handle trimming --- Tewl/InputValidation/Validator.cs | 53 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 9e35df1..43d3ce0 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -27,6 +27,19 @@ public class Validator { /// public const decimal SqlDecimalDefaultMax = 9999999.99m; + private static bool isEmpty( InputType input, out InputType trimmedInput ) { + var type = typeof( InputType ); + + if( type == typeof( string ) ) { + var trimmedString = ( (string)(object)input! ).Trim(); + trimmedInput = (InputType)(object)trimmedString; + return trimmedString.Length == 0; + } + + trimmedInput = input; + return input is null; + } + private readonly List errors = new List(); /// @@ -860,41 +873,25 @@ public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string inp private ValType? executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( ValidationErrorHandler handler, InputType input, bool allowEmpty, Func, InputType, ValidationError?> method, - ValType? customDefaultReturnValue = default ) { - var result = customDefaultReturnValue; - if( !isEmpty( handler, input, allowEmpty ) ) { - // IMPORTANT: Pass trimmed input here. - var error = method( value => result = value, input ); - if( error is not null ) - handler.SetValidationResult( error ); + ValType? emptyValue = default ) { + if( isEmpty( input, out var trimmedInput ) ) { + if( !allowEmpty ) + handler.SetValidationResult( ValidationError.Empty() ); + handler.HandleResult( this, !allowEmpty ); + return emptyValue; } - // If there was an error of any kind, the result becomes the default value - if( handler.LastResult != ErrorCondition.NoError ) - result = customDefaultReturnValue; + var result = emptyValue; + if( method( value => result = value, trimmedInput ) is {} error ) { + handler.SetValidationResult( error ); + handler.HandleResult( this, !allowEmpty ); + return emptyValue; + } - handler.HandleResult( this, !allowEmpty ); return result; } private string handleEmptyAndReturnEmptyStringIfInvalid( ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, Func, InputType, ValidationError?> method ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, valueAsObject, allowEmpty, method, "" )!; - - /// - /// Determines if the given field is empty, and if it is empty, it - /// assigns the correct ErrorCondition to the validation package's ValidationResult - /// and adds an error message to the errors collection. - /// Returns true if val is empty and should not be validated further. Returns false - /// if value is not empty and validation should continue. Validation methods should - /// return immediately with no further action if this method returns true. - /// - private static bool isEmpty( ValidationErrorHandler errorHandler, InputType input, bool allowEmpty ) { - var isEmpty = input.ObjectToString( true )!.Trim().Length == 0; - - if( !allowEmpty && isEmpty ) - errorHandler.SetValidationResult( ValidationError.Empty() ); - - return isEmpty; - } } \ No newline at end of file From 6edc544bf1c12eedaf4cad0cf91f6935b3b79579 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 11:45:08 -0400 Subject: [PATCH 05/28] Re-introduced ValidationMethod delegate --- Tewl/InputValidation/Validator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 43d3ce0..7f8a6db 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -27,6 +27,8 @@ public class Validator { /// public const decimal SqlDecimalDefaultMax = 9999999.99m; + internal delegate ValidationError? ValidationMethod( Action valueSetter, InputType trimmedInput ); + private static bool isEmpty( InputType input, out InputType trimmedInput ) { var type = typeof( InputType ); @@ -872,8 +874,7 @@ public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string inp DateTime.MaxValue ) ); private ValType? executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - ValidationErrorHandler handler, InputType input, bool allowEmpty, Func, InputType, ValidationError?> method, - ValType? emptyValue = default ) { + ValidationErrorHandler handler, InputType input, bool allowEmpty, ValidationMethod method, ValType? emptyValue = default ) { if( isEmpty( input, out var trimmedInput ) ) { if( !allowEmpty ) handler.SetValidationResult( ValidationError.Empty() ); @@ -892,6 +893,6 @@ public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string inp } private string handleEmptyAndReturnEmptyStringIfInvalid( - ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, Func, InputType, ValidationError?> method ) => + ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, valueAsObject, allowEmpty, method, "" )!; } \ No newline at end of file From fa564647886c20b8bc6cbe7e59da63d6d15c31f5 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 13:58:23 -0400 Subject: [PATCH 06/28] Overhauled ExecuteValidation to return a result --- Tewl/InputValidation/ValidationError.cs | 27 ++++++++++++++------ Tewl/InputValidation/ValidationResult.cs | 32 ++++++++++++++++++++++++ Tewl/InputValidation/Validator.cs | 29 +++++++++++++-------- 3 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 Tewl/InputValidation/ValidationResult.cs diff --git a/Tewl/InputValidation/ValidationError.cs b/Tewl/InputValidation/ValidationError.cs index ac5f4b9..fec1811 100644 --- a/Tewl/InputValidation/ValidationError.cs +++ b/Tewl/InputValidation/ValidationError.cs @@ -1,25 +1,36 @@ namespace Tewl.InputValidation; -internal class ValidationError { - public static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => +/// +/// A validation error. +/// +public class ValidationError { + internal static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => new() { ErrorCondition = errorCondition, errorMessage = errorMessage }; - public static ValidationError NoError() => new(); + internal static ValidationError NoError() => new(); - public static ValidationError Invalid() => new() { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; + internal static ValidationError Invalid() => new() { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; - public static ValidationError Empty() => new() { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; + internal static ValidationError Empty() => new() { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; - public static ValidationError TooSmall( object min, object max ) => + internal static ValidationError TooSmall( object min, object max ) => new() { ErrorCondition = ErrorCondition.TooLong, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; - public static ValidationError TooLarge( object min, object max ) => + internal static ValidationError TooLarge( object min, object max ) => new() { ErrorCondition = ErrorCondition.TooLarge, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; + /// + /// Gets the error type. + /// public ErrorCondition ErrorCondition { get; private init; } = ErrorCondition.NoError; + private string errorMessage = ""; private ValidationError() {} - public string GetMessage( string subject ) => string.Format( errorMessage, subject ); + /// + /// Returns the standard message for this error. + /// + /// The name of the result value. If this is used to start a sentence, it will be automatically capitalized. + public string GetMessage( string valueName ) => string.Format( errorMessage, valueName ); } \ No newline at end of file diff --git a/Tewl/InputValidation/ValidationResult.cs b/Tewl/InputValidation/ValidationResult.cs new file mode 100644 index 0000000..c3753c5 --- /dev/null +++ b/Tewl/InputValidation/ValidationResult.cs @@ -0,0 +1,32 @@ +namespace Tewl.InputValidation; + +/// +/// The result of a validation. +/// +[ PublicAPI ] +public class ValidationResult { + /// + /// Gets the validated value. This is sometimes unusable if there was a validation error, and in that case the + /// will be true. + /// + public T Value { get; } + + private readonly ValidationError? error; + + /// + /// Validator.ExecuteValidation use only. + /// + internal ValidationResult( T value, ValidationError? error ) { + Value = value; + this.error = error; + } + + /// + /// Returns the validation error, or null if validation was successful. + /// + /// The validated value. + public ValidationError? Error( out T value ) { + value = Value; + return error; + } +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 7f8a6db..2d3eb5d 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -873,26 +873,35 @@ public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string inp DateTime.MinValue, DateTime.MaxValue ) ); - private ValType? executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - ValidationErrorHandler handler, InputType input, bool allowEmpty, ValidationMethod method, ValType? emptyValue = default ) { + /// + /// Executes a validation and returns the result. + /// + /// + /// + /// + /// + /// The result value that will be used if the input value is empty or if there is a validation error. + internal ValidationResult ExecuteValidation( + ValidationErrorHandler handler, InputType input, bool allowEmpty, ValidationMethod validationMethod, ValType? emptyValue = default ) { if( isEmpty( input, out var trimmedInput ) ) { - if( !allowEmpty ) + if( !allowEmpty ) { handler.SetValidationResult( ValidationError.Empty() ); - handler.HandleResult( this, !allowEmpty ); - return emptyValue; + handler.HandleResult( this, !allowEmpty ); + } + return new ValidationResult( emptyValue, allowEmpty ? null : ValidationError.Empty() ); } var result = emptyValue; - if( method( value => result = value, trimmedInput ) is {} error ) { + if( validationMethod( value => result = value, trimmedInput ) is {} error ) { handler.SetValidationResult( error ); handler.HandleResult( this, !allowEmpty ); - return emptyValue; + return new ValidationResult( emptyValue, error ); } - return result; + return new ValidationResult( result, null ); } - private string handleEmptyAndReturnEmptyStringIfInvalid( + private ValidationResult handleEmptyAndReturnEmptyStringIfInvalid( ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( handler, valueAsObject, allowEmpty, method, "" )!; + ExecuteValidation( handler, valueAsObject, allowEmpty, method, "" )!; } \ No newline at end of file From 2f148c24bc9695bc139577140bb09bd113cfc7cf Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 16:08:48 -0400 Subject: [PATCH 07/28] Updated public validation methods to return result --- Tewl/InputValidation/Validator.cs | 270 ++++++++++++++++-------------- 1 file changed, 141 insertions(+), 129 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 2d3eb5d..6aa7e4d 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -127,16 +127,16 @@ internal void AddError( Error error ) { /// Returns the validated boolean type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public bool GetBoolean( ValidationErrorHandler errorHandler, string input ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, false, validateBoolean ); + public ValidationResult GetBoolean( ValidationErrorHandler errorHandler, string input ) => + ExecuteValidation( errorHandler, input, false, validateBoolean ); /// /// Accepts either true/false (case-sensitive) or 1/0. /// Returns the validated boolean type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public bool? GetNullableBoolean( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableBoolean( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -154,14 +154,14 @@ public bool GetBoolean( ValidationErrorHandler errorHandler, string input ) => /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public byte GetByte( ValidationErrorHandler errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); + public ValidationResult GetByte( ValidationErrorHandler errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); /// /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public byte GetByte( ValidationErrorHandler errorHandler, string input, byte min, byte max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetByte( ValidationErrorHandler errorHandler, string input, byte min, byte max ) => + ExecuteValidation( errorHandler, input, false, @@ -171,8 +171,8 @@ public byte GetByte( ValidationErrorHandler errorHandler, string input, byte min /// Returns the validated byte type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public byte? GetNullableByte( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableByte( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -182,14 +182,15 @@ public byte GetByte( ValidationErrorHandler errorHandler, string input, byte min /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public short GetShort( ValidationErrorHandler errorHandler, string input ) => GetShort( errorHandler, input, short.MinValue, short.MaxValue ); + public ValidationResult GetShort( ValidationErrorHandler errorHandler, string input ) => + GetShort( errorHandler, input, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public short GetShort( ValidationErrorHandler errorHandler, string input, short min, short max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetShort( ValidationErrorHandler errorHandler, string input, short min, short max ) => + ExecuteValidation( errorHandler, input, false, @@ -199,15 +200,15 @@ public short GetShort( ValidationErrorHandler errorHandler, string input, short /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public short? GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetNullableShort( errorHandler, input, allowEmpty, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public short? GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty, short min, short max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty, short min, short max ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -217,15 +218,15 @@ public short GetShort( ValidationErrorHandler errorHandler, string input, short /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public int GetInt( ValidationErrorHandler errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); + public ValidationResult GetInt( ValidationErrorHandler errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); /// /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public int GetInt( ValidationErrorHandler errorHandler, string input, int min, int max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetInt( ValidationErrorHandler errorHandler, string input, int min, int max ) => + ExecuteValidation( errorHandler, input, false, @@ -235,8 +236,9 @@ public int GetInt( ValidationErrorHandler errorHandler, string input, int min, i /// Returns the validated int type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public int? GetNullableInt( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableInt( + ValidationErrorHandler errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -247,8 +249,8 @@ public int GetInt( ValidationErrorHandler errorHandler, string input, int min, i /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public long GetLong( ValidationErrorHandler errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetLong( ValidationErrorHandler errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => + ExecuteValidation( errorHandler, input, false, @@ -258,8 +260,9 @@ public long GetLong( ValidationErrorHandler errorHandler, string input, long min /// Returns the validated long type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public long? GetNullableLong( ValidationErrorHandler errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableLong( + ValidationErrorHandler errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -291,19 +294,15 @@ public long GetLong( ValidationErrorHandler errorHandler, string input, long min /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public float GetFloat( ValidationErrorHandler errorHandler, string input, float min, float max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); + public ValidationResult GetFloat( ValidationErrorHandler errorHandler, string input, float min, float max ) => + ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public float? GetNullableFloat( ValidationErrorHandler errorHandler, string input, bool allowEmpty, float min, float max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableFloat( ValidationErrorHandler errorHandler, string input, bool allowEmpty, float min, float max ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -334,32 +333,29 @@ public float GetFloat( ValidationErrorHandler errorHandler, string input, float /// Returns a validated decimal type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public decimal GetDecimal( ValidationErrorHandler errorHandler, string input ) => GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); + public ValidationResult GetDecimal( ValidationErrorHandler errorHandler, string input ) => + GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public decimal GetDecimal( ValidationErrorHandler errorHandler, string input, decimal min, decimal max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); + public ValidationResult GetDecimal( ValidationErrorHandler errorHandler, string input, decimal min, decimal max ) => + ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated decimal type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetNullableDecimal( errorHandler, input, allowEmpty, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public decimal? GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -387,14 +383,15 @@ public decimal GetDecimal( ValidationErrorHandler errorHandler, string input, de /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetString( errorHandler, input, allowEmpty, int.MaxValue ); + public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + GetString( errorHandler, input, allowEmpty, int.MaxValue ); /// /// Returns a validated string from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength ) => + public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength ) => GetString( errorHandler, input, allowEmpty, 0, maxLength ); /// @@ -402,7 +399,7 @@ public string GetString( ValidationErrorHandler errorHandler, string input, bool /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public string GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => + public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -425,31 +422,31 @@ public string GetString( ValidationErrorHandler errorHandler, string input, bool /// The maxLength defaults to 254 per this source: http://en.wikipedia.org/wiki/E-mail_address#Syntax /// If you pass a different value for maxLength, you'd better have a good reason. /// - public string GetEmailAddress( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => + public ValidationResult GetEmailAddress( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, allowEmpty, ( valueSetter, trimmedInput ) => { // Validate as a string with same restrictions - if it fails on that, return - trimmedInput = GetString( errorHandler, trimmedInput, allowEmpty, maxLength ); - if( errorHandler.LastResult == ErrorCondition.NoError ) { - // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. - //([^@ \n\.]+\.)+ forces any positive number of (anything.)s to exist in a row. Doesn't allow "..". - // Allows anything.anything123-anything@anything.anything123.anything - const string localPartUnconditionallyPermittedCharacters = @"[a-z0-9!#\$%&'\*\+\-/=\?\^_`\{\|}~]"; - const string localPart = "(" + localPartUnconditionallyPermittedCharacters + @"+\.?)*" + localPartUnconditionallyPermittedCharacters + "+"; - const string domainUnconditionallyPermittedCharacters = "[a-z0-9-]"; - const string domain = "(" + domainUnconditionallyPermittedCharacters + @"+\.)+" + domainUnconditionallyPermittedCharacters + "+"; - // The first two conditions are for performance only. - if( !trimmedInput.Contains( "@" ) || !trimmedInput.Contains( "." ) || !Regex.IsMatch( - trimmedInput, - "^" + localPart + "@" + domain + "$", - RegexOptions.IgnoreCase ) ) - return ValidationError.Invalid(); - // Max length is already checked by the string validation - // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. - } + if( GetString( errorHandler, trimmedInput, allowEmpty, maxLength ).Error( out _ ) is {} error ) + return error; + + // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. + //([^@ \n\.]+\.)+ forces any positive number of (anything.)s to exist in a row. Doesn't allow "..". + // Allows anything.anything123-anything@anything.anything123.anything + const string localPartUnconditionallyPermittedCharacters = @"[a-z0-9!#\$%&'\*\+\-/=\?\^_`\{\|}~]"; + const string localPart = "(" + localPartUnconditionallyPermittedCharacters + @"+\.?)*" + localPartUnconditionallyPermittedCharacters + "+"; + const string domainUnconditionallyPermittedCharacters = "[a-z0-9-]"; + const string domain = "(" + domainUnconditionallyPermittedCharacters + @"+\.)+" + domainUnconditionallyPermittedCharacters + "+"; + // The first two conditions are for performance only. + if( !trimmedInput.Contains( "@" ) || !trimmedInput.Contains( "." ) || !Regex.IsMatch( + trimmedInput, + "^" + localPart + "@" + domain + "$", + RegexOptions.IgnoreCase ) ) + return ValidationError.Invalid(); + // Max length is already checked by the string validation + // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. valueSetter( trimmedInput ); return null; @@ -458,7 +455,8 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string input /// /// Returns a validated URL. /// - public string GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetUrl( errorHandler, input, allowEmpty, MaxUrlLength ); + public ValidationResult GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + GetUrl( errorHandler, input, allowEmpty, MaxUrlLength ); private static readonly string[] validSchemes = { "http", "https", "ftp" }; @@ -466,7 +464,7 @@ public string GetEmailAddress( ValidationErrorHandler errorHandler, string input /// Returns a validated URL. Note that you may run into problems with certain browsers if you pass a length longer than /// 2048. /// - public string GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxUrlLength ) => + public ValidationResult GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxUrlLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -477,37 +475,36 @@ public string GetUrl( ValidationErrorHandler errorHandler, string input, bool al return ValidationError.Invalid(); /* If it's an email, it's not an URL. */ - var testingValidator = new Validator(); - testingValidator.GetEmailAddress( new ValidationErrorHandler( "" ), trimmedInput, allowEmpty ); - if( !testingValidator.ErrorsOccurred ) + if( new Validator().GetEmailAddress( new ValidationErrorHandler( "" ), trimmedInput, allowEmpty ).Error( out _ ) is null ) return ValidationError.Invalid(); /* If it doesn't start with one of our whitelisted schemes, add in the best guess. */ - trimmedInput = GetString( - errorHandler, - validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, - true, - maxUrlLength ); + if( GetString( + errorHandler, + validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, + true, + maxUrlLength ) + .Error( out trimmedInput ) is {} error ) + return error; /* If the getstring didn't fail, keep on keepin keepin on. */ - if( errorHandler.LastResult == ErrorCondition.NoError ) - try { - if( !Uri.IsWellFormedUriString( trimmedInput, UriKind.Absolute ) ) - throw new UriFormatException(); - - // Don't allow relative URLs - var uri = new Uri( trimmedInput, UriKind.Absolute ); - - // Must be a valid DNS-style hostname or IP address - // Must contain at least one '.', to prevent just host names - // Must be one of the common web browser-accessible schemes - if( uri.HostNameType != UriHostNameType.Dns && uri.HostNameType != UriHostNameType.IPv4 && uri.HostNameType != UriHostNameType.IPv6 || - uri.Host.All( c => c != '.' ) || validSchemes.All( s => s != uri.Scheme ) ) - throw new UriFormatException(); - } - catch( UriFormatException ) { - return ValidationError.Invalid(); - } + try { + if( !Uri.IsWellFormedUriString( trimmedInput, UriKind.Absolute ) ) + throw new UriFormatException(); + + // Don't allow relative URLs + var uri = new Uri( trimmedInput, UriKind.Absolute ); + + // Must be a valid DNS-style hostname or IP address + // Must contain at least one '.', to prevent just host names + // Must be one of the common web browser-accessible schemes + if( uri.HostNameType != UriHostNameType.Dns && uri.HostNameType != UriHostNameType.IPv4 && uri.HostNameType != UriHostNameType.IPv6 || + uri.Host.All( c => c != '.' ) || validSchemes.All( s => s != uri.Scheme ) ) + throw new UriFormatException(); + } + catch( UriFormatException ) { + return ValidationError.Invalid(); + } valueSetter( trimmedInput ); return null; @@ -517,7 +514,7 @@ public string GetUrl( ValidationErrorHandler errorHandler, string input, bool al /// The same as GetPhoneNumber, except the given default area code will be prepended on the phone number if necessary. /// This is useful when working with data that had the area code omitted because the number was local. /// - public string GetPhoneNumberWithDefaultAreaCode( + public ValidationResult GetPhoneNumberWithDefaultAreaCode( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { var validator = new Validator(); // We need to use a separate one so that erroneous error messages don't get left in the collection var fakeHandler = new ValidationErrorHandler( "" ); @@ -542,7 +539,8 @@ public string GetPhoneNumberWithDefaultAreaCode( /// into 585-455-6476 /// and count as a valid phone number. /// - public string GetPhoneNumber( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => + public ValidationResult GetPhoneNumber( + ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => GetPhoneWithLastFiveMapping( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, null ); /// @@ -551,15 +549,27 @@ public string GetPhoneNumber( ValidationErrorHandler errorHandler, string input, /// digits to five-digit groups that become the first five digits of the full number. If allow empty is true and an empty /// string or null is given, the empty string is returned. /// - public string GetPhoneWithLastFiveMapping( + public ValidationResult GetPhoneWithLastFiveMapping( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - Dictionary? firstFives ) => - GetPhoneNumberAsObject( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ).StandardPhoneString; + Dictionary? firstFives ) { + return handleEmptyAndReturnEmptyStringIfInvalid( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + if( GetPhoneNumberAsObject( errorHandler, trimmedInput, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ).Error( out var value ) is + {} error ) + return error; + + valueSetter( value.StandardPhoneString ); + return null; + } ); + } - internal PhoneNumber GetPhoneNumberAsObject( + internal ValidationResult GetPhoneNumberAsObject( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, Dictionary? firstFives ) { - return executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + return ExecuteValidation( errorHandler, input, allowEmpty, @@ -621,7 +631,7 @@ internal PhoneNumber GetPhoneNumberAsObject( /// Returns a validated phone number extension as a string. /// If allow empty is true and the empty string or null is given, the empty string is returned. /// - public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -638,7 +648,7 @@ public string GetPhoneNumberExtension( ValidationErrorHandler errorHandler, stri /// Returns a validated social security number from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// - public string GetSocialSecurityNumber( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetSocialSecurityNumber( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => GetNumber( errorHandler, input, 9, allowEmpty, "-" ); /// @@ -646,7 +656,8 @@ public string GetSocialSecurityNumber( ValidationErrorHandler errorHandler, stri /// characters. /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). /// - public string GetNumber( ValidationErrorHandler errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => + public ValidationResult GetNumber( + ValidationErrorHandler errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -664,22 +675,22 @@ public string GetNumber( ValidationErrorHandler errorHandler, string input, int /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// - public ZipCode GetZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; + public ValidationResult GetZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; /// /// Gets a validated US or Canadian zip code. /// - public ZipCode GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; + public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; /// /// Returns the validated DateTime type from the given string and validation package. /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string input ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string input ) => + ExecuteValidation( errorHandler, input, false, @@ -690,8 +701,8 @@ public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public DateTime? GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -708,7 +719,7 @@ public DateTime GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string /// Passing an empty string or null for each date part will result in ErrorCondition.Empty. /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. /// - public DateTime GetSqlSmallDateTimeFromParts( ValidationErrorHandler errorHandler, string month, string day, string year ) => + public ValidationResult GetSqlSmallDateTimeFromParts( ValidationErrorHandler errorHandler, string month, string day, string year ) => GetSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ) ); /// @@ -717,7 +728,8 @@ public DateTime GetSqlSmallDateTimeFromParts( ValidationErrorHandler errorHandle /// If allowEmpty is true and each date part string is empty, null will be returned. /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. /// - public DateTime? GetNullableSqlSmallDateTimeFromParts( ValidationErrorHandler errorHandler, string month, string day, string year, bool allowEmpty ) => + public ValidationResult GetNullableSqlSmallDateTimeFromParts( + ValidationErrorHandler errorHandler, string month, string day, string year, bool allowEmpty ) => GetNullableSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ), allowEmpty ); private static string makeDateFromParts( string month, string day, string year ) { @@ -731,8 +743,8 @@ private static string makeDateFromParts( string month, string day, string year ) /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern ) => + ExecuteValidation( errorHandler, input, false, @@ -742,8 +754,8 @@ public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, s /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public DateTime? GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern, bool allowEmpty ) => + ExecuteValidation( errorHandler, input, allowEmpty, @@ -799,9 +811,9 @@ public DateTime GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, s /// /// Validates the date using given allowEmpty, min, and max constraints. /// - public DateTime? - GetNullableDateTime( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableDateTime( + ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => + ExecuteValidation( handler, input, allowEmpty, @@ -810,8 +822,8 @@ public DateTime? /// /// Validates the date using given min and max constraints. /// - public DateTime GetDateTime( ValidationErrorHandler handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetDateTime( ValidationErrorHandler handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => + ExecuteValidation( handler, input, false, @@ -820,8 +832,8 @@ public DateTime GetDateTime( ValidationErrorHandler handler, string input, strin /// /// Validates the given time span. /// - public TimeSpan? GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? input, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? input, bool allowEmpty ) => + ExecuteValidation( handler, input, allowEmpty, @@ -833,8 +845,8 @@ public DateTime GetDateTime( ValidationErrorHandler handler, string input, strin /// /// Validates the given time span. /// - public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) => + ExecuteValidation( handler, input, false, @@ -846,8 +858,8 @@ public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) = /// /// Validates the given time span. /// - public TimeSpan? GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty ) => + ExecuteValidation( handler, input, allowEmpty, @@ -861,8 +873,8 @@ public TimeSpan GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) = /// /// Validates the given time span. /// - public TimeSpan GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats ) => - executeValidationMethodAndHandleEmptyAndReturnDefaultIfInvalid( + public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats ) => + ExecuteValidation( handler, input, false, From b3947f7864622b16817ba13aee11eb8fc3bfc969 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 16:23:06 -0400 Subject: [PATCH 08/28] Updated Validator usages elsewhere in TEWL --- Tewl/IO/ExcelWorksheet.cs | 25 ++++++++++--------------- Tewl/InputValidation/PhoneNumber.cs | 15 ++------------- TewlTester/Program.cs | 3 +-- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/Tewl/IO/ExcelWorksheet.cs b/Tewl/IO/ExcelWorksheet.cs index 06d92ef..e6516b4 100644 --- a/Tewl/IO/ExcelWorksheet.cs +++ b/Tewl/IO/ExcelWorksheet.cs @@ -83,31 +83,26 @@ private void addRowToWorksheet( bool bold, params string[] cellValues ) { } private void putRowValueInCell( IXLCell cell, string value ) { - var v = new Validator(); - var detectedDate = v.GetNullableDateTime( - new ValidationErrorHandler( "" ), - value, - DateTimeTools.DayMonthYearFormats.Concat( DateTimeTools.MonthDayYearFormats ).ToArray(), - false, - DateTime.MinValue, - DateTime.MaxValue ); - if( !v.ErrorsOccurred ) { + if( new Validator().GetNullableDateTime( + new ValidationErrorHandler( "" ), + value, + DateTimeTools.DayMonthYearFormats.Concat( DateTimeTools.MonthDayYearFormats ).ToArray(), + false, + DateTime.MinValue, + DateTime.MaxValue ) + .Error( out var detectedDate ) is null ) { setOrAddCellStyle( cell, date: true ); cell.Value = detectedDate; return; } - v = new Validator(); - v.GetEmailAddress( new ValidationErrorHandler( "" ), value, false ); - if( !v.ErrorsOccurred ) { + if( new Validator().GetEmailAddress( new ValidationErrorHandler( "" ), value, false ).Error( out _ ) is null ) { cell.Value = value; cell.SetHyperlink( new XLHyperlink( "mailto:" + value ) ); return; } - v = new Validator(); - var validatedUrl = v.GetUrl( new ValidationErrorHandler( "" ), value, false ); - if( !v.ErrorsOccurred ) { + if( new Validator().GetUrl( new ValidationErrorHandler( "" ), value, false ).Error( out var validatedUrl ) is null ) { cell.Value = value; cell.SetHyperlink( new XLHyperlink( validatedUrl ) ); return; diff --git a/Tewl/InputValidation/PhoneNumber.cs b/Tewl/InputValidation/PhoneNumber.cs index 23a9a7a..5c5d661 100644 --- a/Tewl/InputValidation/PhoneNumber.cs +++ b/Tewl/InputValidation/PhoneNumber.cs @@ -1,6 +1,3 @@ -using System; -using JetBrains.Annotations; - namespace Tewl.InputValidation { /// /// Represents a phone number consisting of area code, number, and optional extension. @@ -8,7 +5,7 @@ namespace Tewl.InputValidation { /// [ PublicAPI ] public class PhoneNumber { - private PhoneNumber() { } + private PhoneNumber() {} /// /// Creates a phone number object from the individual parts. Strings are trimmed in case they came from SQL Server char @@ -59,15 +56,7 @@ public static PhoneNumber CreateFromParts( string areaCode, string number, strin public static PhoneNumber CreateFromStandardPhoneString( string phoneString ) { var v = new Validator(); // NOTE: I don't like how this method is called, which calls a method in validator, which then calls one of the static constructors back here (but not this one! otherwise you are screwed) - var p = v.GetPhoneNumberAsObject( - new ValidationErrorHandler( "" ), - phoneString, - true, - true, - false, - null ); - // pass "" for the error message subject because we should never get errors - if( v.ErrorsOccurred ) + if( v.GetPhoneNumberAsObject( null, phoneString, true, true, false, null ).Error( out var p ) is not null ) throw new ApplicationException( "Unparsable standard phone number string encountered." ); return p; } diff --git a/TewlTester/Program.cs b/TewlTester/Program.cs index 7a02e05..b050be8 100644 --- a/TewlTester/Program.cs +++ b/TewlTester/Program.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using Tewl.InputValidation; using Tewl.IO; using Tewl.IO.TabularDataParsing; @@ -68,7 +67,7 @@ private static void testXls() { Console.WriteLine( $"Excel test: {xlsParser.RowsWithoutValidationErrors} rows imported without error." ); } - private static void importThing( Validator validator, ParsedLine line ) { + private static void importThing( Tewl.InputValidation.Validator validator, ParsedLine line ) { var value = line["dATe"]; var email = line[ "email" ]; var website = line[ "website" ]; From 8764a0221b944681adfef96da8f26004150a219c Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 16:25:16 -0400 Subject: [PATCH 09/28] Named usages of ExecuteValidation emptyValue param --- Tewl/InputValidation/Validator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 6aa7e4d..f4f311a 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -624,7 +624,7 @@ internal ValidationResult GetPhoneNumberAsObject( valueSetter( phoneNumber ); return null; }, - PhoneNumber.CreateFromParts( "", "", "" ) )!; + emptyValue: PhoneNumber.CreateFromParts( "", "", "" ) )!; } /// @@ -676,13 +676,13 @@ public ValidationResult GetNumber( /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// public ValidationResult GetZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, new ZipCode() )!; + ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, emptyValue: new ZipCode() )!; /// /// Gets a validated US or Canadian zip code. /// public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, new ZipCode() )!; + ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, emptyValue: new ZipCode() )!; /// /// Returns the validated DateTime type from the given string and validation package. @@ -915,5 +915,5 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler h private ValidationResult handleEmptyAndReturnEmptyStringIfInvalid( ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => - ExecuteValidation( handler, valueAsObject, allowEmpty, method, "" )!; + ExecuteValidation( handler, valueAsObject, allowEmpty, method, emptyValue: "" )!; } \ No newline at end of file From 144820456fdb970a1f918637565c6d93730d8d77 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:29:39 -0400 Subject: [PATCH 10/28] Removed result storage from ValidationErrorHandler --- .../InputValidation/ValidationErrorHandler.cs | 48 ++++----------- Tewl/InputValidation/Validator.cs | 59 ++++++++----------- 2 files changed, 39 insertions(+), 68 deletions(-) diff --git a/Tewl/InputValidation/ValidationErrorHandler.cs b/Tewl/InputValidation/ValidationErrorHandler.cs index 2f254e5..27d7b76 100644 --- a/Tewl/InputValidation/ValidationErrorHandler.cs +++ b/Tewl/InputValidation/ValidationErrorHandler.cs @@ -1,20 +1,17 @@ namespace Tewl.InputValidation { /// - /// This class allows you to control what happens when a validation method generates an error. Every validation method - /// takes a ValidationErrorHandler object - /// as the first parameter. Currently you can't re-use these objects for more than one validation call since most - /// validation methods don't reset LastResult. + /// This class allows you to control what happens when a validation method generates an error. Every validation method takes a ValidationErrorHandler object + /// as the first parameter. /// [ PublicAPI ] public class ValidationErrorHandler { /// /// Method that handles errors instead of the default handling mechanism. /// - public delegate void CustomHandler( Validator validator, ErrorCondition errorCondition ); + public delegate void CustomHandler( ErrorCondition errorCondition ); - private readonly CustomHandler customHandler; - private readonly Dictionary customMessages = new Dictionary(); - private ValidationError validationResult = ValidationError.NoError(); + private readonly CustomHandler? customHandler; + private readonly Dictionary customMessages = new(); /// /// Creates an error handler that adds standard error messages, based on the specified subject, to the validator. If the @@ -50,41 +47,22 @@ public void AddCustomErrorMessage( string message, params ErrorCondition[] error /// internal string Subject { get; } = "field"; - private bool used; - - internal void SetValidationResult( ValidationError validationResult ) { - if( used ) - throw new ApplicationException( "Validation error handlers cannot be re-used." ); - used = true; - this.validationResult = validationResult; - } - /// - /// Returns the ErrorCondition resulting from the validation of the data associated with this package. + /// Invokes the appropriate behavior according to how this error handler was created. /// - public ErrorCondition LastResult => validationResult.ErrorCondition; - - /// - /// If LastResult is not NoError, this method invokes the appropriate behavior according to how this error handler was - /// created. - /// - internal void HandleResult( Validator validator, bool errorWouldResultInUnusableReturnValue ) { - if( validationResult.ErrorCondition == ErrorCondition.NoError ) - return; - + internal string HandleError( ValidationError error ) { // if there is a custom handler, run it and do nothing else - if( customHandler != null ) { - validator.NoteError(); - customHandler( validator, validationResult.ErrorCondition ); - return; + if( customHandler is not null ) { + customHandler( error.ErrorCondition ); + return ""; } // build the error message - if( !customMessages.TryGetValue( validationResult.ErrorCondition, out var message ) ) + if( !customMessages.TryGetValue( error.ErrorCondition, out var message ) ) // NOTE: Do we really need custom message, or can the custom handler manage that? - message = validationResult.GetMessage( Subject ); + message = error.GetMessage( Subject ); - validator.AddError( new Error( message, errorWouldResultInUnusableReturnValue ) ); + return message; } } } \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index f4f311a..d73b907 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -105,7 +105,10 @@ public List ErrorMessages { /// message to the same collection /// that the error handlers use. /// - public void NoteErrorAndAddMessage( string message ) => AddError( new Error( message, false ) ); + public void NoteErrorAndAddMessage( string message ) { + NoteError(); + errors.Add( new Error( message, false ) ); + } /// /// Sets the ErrorsOccurred flag and add the given error messages to this validator. Use this if you want to add your own @@ -117,11 +120,6 @@ public void NoteErrorAndAddMessages( params string[] messages ) { NoteErrorAndAddMessage( message ); } - internal void AddError( Error error ) { - NoteError(); - errors.Add( error ); - } - /// /// Accepts either true/false (case-sensitive) or 1/0. /// Returns the validated boolean type from the given string and validation package. @@ -429,7 +427,7 @@ public ValidationResult GetEmailAddress( ValidationErrorHandler errorHan allowEmpty, ( valueSetter, trimmedInput ) => { // Validate as a string with same restrictions - if it fails on that, return - if( GetString( errorHandler, trimmedInput, allowEmpty, maxLength ).Error( out _ ) is {} error ) + if( new Validator().GetString( null, trimmedInput, allowEmpty, maxLength ).Error( out _ ) is {} error ) return error; // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. @@ -475,12 +473,12 @@ public ValidationResult GetUrl( ValidationErrorHandler errorHandler, str return ValidationError.Invalid(); /* If it's an email, it's not an URL. */ - if( new Validator().GetEmailAddress( new ValidationErrorHandler( "" ), trimmedInput, allowEmpty ).Error( out _ ) is null ) + if( new Validator().GetEmailAddress( null, trimmedInput, allowEmpty ).Error( out _ ) is null ) return ValidationError.Invalid(); /* If it doesn't start with one of our whitelisted schemes, add in the best guess. */ - if( GetString( - errorHandler, + if( new Validator().GetString( + null, validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, true, maxUrlLength ) @@ -516,18 +514,11 @@ public ValidationResult GetUrl( ValidationErrorHandler errorHandler, str /// public ValidationResult GetPhoneNumberWithDefaultAreaCode( ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { - var validator = new Validator(); // We need to use a separate one so that erroneous error messages don't get left in the collection - var fakeHandler = new ValidationErrorHandler( "" ); - - validator.GetPhoneNumber( fakeHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); - if( fakeHandler.LastResult != ErrorCondition.NoError ) { - fakeHandler = new ValidationErrorHandler( "" ); - validator.GetPhoneNumber( fakeHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); + if( new Validator().GetPhoneNumber( null, input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is not null ) // If the phone number was invalid without the area code, but is valid with the area code, we really validate using the default // area code and then return. In all other cases, we return what would have happened without tacking on the default area code. - if( fakeHandler.LastResult == ErrorCondition.NoError ) + if( new Validator().GetPhoneNumber( null, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is null ) return GetPhoneNumber( errorHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); - } return GetPhoneNumber( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); } @@ -557,8 +548,8 @@ public ValidationResult GetPhoneWithLastFiveMapping( input, allowEmpty, ( valueSetter, trimmedInput ) => { - if( GetPhoneNumberAsObject( errorHandler, trimmedInput, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ).Error( out var value ) is - {} error ) + if( new Validator().GetPhoneNumberAsObject( null, trimmedInput, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ) + .Error( out var value ) is {} error ) return error; valueSetter( value.StandardPhoneString ); @@ -895,22 +886,24 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler h /// The result value that will be used if the input value is empty or if there is a validation error. internal ValidationResult ExecuteValidation( ValidationErrorHandler handler, InputType input, bool allowEmpty, ValidationMethod validationMethod, ValType? emptyValue = default ) { - if( isEmpty( input, out var trimmedInput ) ) { - if( !allowEmpty ) { - handler.SetValidationResult( ValidationError.Empty() ); - handler.HandleResult( this, !allowEmpty ); - } - return new ValidationResult( emptyValue, allowEmpty ? null : ValidationError.Empty() ); - } + if( isEmpty( input, out var trimmedInput ) ) + return allowEmpty ? new ValidationResult( emptyValue, null ) : handleError( ValidationError.Empty() ); var result = emptyValue; - if( validationMethod( value => result = value, trimmedInput ) is {} error ) { - handler.SetValidationResult( error ); - handler.HandleResult( this, !allowEmpty ); - return new ValidationResult( emptyValue, error ); - } + if( validationMethod( value => result = value, trimmedInput ) is {} validationMethodError ) + return handleError( validationMethodError ); return new ValidationResult( result, null ); + + ValidationResult handleError( ValidationError error ) { + NoteError(); + + var message = handler.HandleError( error ); + if( message.Length > 0 ) + errors.Add( new Error( message, !allowEmpty ) ); + + return new ValidationResult( emptyValue, error ); + } } private ValidationResult handleEmptyAndReturnEmptyStringIfInvalid( From 68b95d357e47405e6e42af4d8afec7de62a7a61b Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:30:32 -0400 Subject: [PATCH 11/28] Removed ValidationPackage This hasn't been used in years, or even decades. --- Tewl/InputValidation/ValidationPackage.cs | 43 ----------------------- 1 file changed, 43 deletions(-) delete mode 100644 Tewl/InputValidation/ValidationPackage.cs diff --git a/Tewl/InputValidation/ValidationPackage.cs b/Tewl/InputValidation/ValidationPackage.cs deleted file mode 100644 index d901d9e..0000000 --- a/Tewl/InputValidation/ValidationPackage.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections; -using JetBrains.Annotations; - -namespace Tewl.InputValidation { - /// - /// Common data required to validate and build an error message for a piece of data. - /// - [ PublicAPI ] - public class ValidationPackage { - /// - /// The subject of the error message, if one needs to be generated. - /// - public string Subject { get; } - - /// - /// The map of ErrorConditions to error messages overrides. - /// - public IDictionary CustomMessages { get; } - - /// - /// Returns the ErrorCondition resulting from the validation of the data - /// associated with this package. - /// - public ErrorCondition ValidationResult { set; get; } = ErrorCondition.NoError; - - /// - /// Create a new package with which to validate a piece of data. - /// - /// The subject of the error message, if one needs to be generated. - /// The map of ErrorConditions to error messages overrides. Use null for no overrides. - public ValidationPackage( string subject, IDictionary customMessages ) { - Subject = subject; - CustomMessages = customMessages; - } - - /// - /// Create a new package with which to validate a piece of data without - /// specifying any custom messages overrides. - /// - /// The subject of the error message, if one needs to be generated. - public ValidationPackage( string subject ): this( subject, null ) { } - } -} \ No newline at end of file From aeb765a4f0a06068dcc8ccce5f22b29a1522a509 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:33:42 -0400 Subject: [PATCH 12/28] Removed ValidationError.NoError. --- Tewl/InputValidation/ValidationError.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tewl/InputValidation/ValidationError.cs b/Tewl/InputValidation/ValidationError.cs index fec1811..2b5352f 100644 --- a/Tewl/InputValidation/ValidationError.cs +++ b/Tewl/InputValidation/ValidationError.cs @@ -7,8 +7,6 @@ public class ValidationError { internal static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => new() { ErrorCondition = errorCondition, errorMessage = errorMessage }; - internal static ValidationError NoError() => new(); - internal static ValidationError Invalid() => new() { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; internal static ValidationError Empty() => new() { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; From afe056fa7961318effacceab3285de62f63f4206 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:38:33 -0400 Subject: [PATCH 13/28] Removed ErrorCondition.NoError. --- Tewl/InputValidation/ErrorCondition.cs | 99 ++++++++++++------------- Tewl/InputValidation/ValidationError.cs | 20 ++--- 2 files changed, 57 insertions(+), 62 deletions(-) diff --git a/Tewl/InputValidation/ErrorCondition.cs b/Tewl/InputValidation/ErrorCondition.cs index b6f18fb..25d8c3b 100644 --- a/Tewl/InputValidation/ErrorCondition.cs +++ b/Tewl/InputValidation/ErrorCondition.cs @@ -1,54 +1,47 @@ -using JetBrains.Annotations; - -namespace Tewl.InputValidation { - /// - /// The list of possible error types. - /// - [ PublicAPI ] - public enum ErrorCondition { - /// - /// NoError - /// - NoError, - - /// - /// Empty - /// - Empty, - - /// - /// Invalid - /// - Invalid, - - /// - /// TooLong - /// - TooLong, - - /// - /// TooShort - /// - TooShort, - - /// - /// TooSmall - /// - TooSmall, - - /// - /// TooLarge - /// - TooLarge, - - /// - /// TooEarly - /// - TooEarly, - - /// - /// TooLate - /// - TooLate - } +namespace Tewl.InputValidation; + +/// +/// The list of possible error types. +/// +[ PublicAPI ] +public enum ErrorCondition { + /// + /// Empty + /// + Empty, + + /// + /// Invalid + /// + Invalid, + + /// + /// TooLong + /// + TooLong, + + /// + /// TooShort + /// + TooShort, + + /// + /// TooSmall + /// + TooSmall, + + /// + /// TooLarge + /// + TooLarge, + + /// + /// TooEarly + /// + TooEarly, + + /// + /// TooLate + /// + TooLate } \ No newline at end of file diff --git a/Tewl/InputValidation/ValidationError.cs b/Tewl/InputValidation/ValidationError.cs index 2b5352f..5d66d04 100644 --- a/Tewl/InputValidation/ValidationError.cs +++ b/Tewl/InputValidation/ValidationError.cs @@ -4,27 +4,29 @@ /// A validation error. /// public class ValidationError { - internal static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => - new() { ErrorCondition = errorCondition, errorMessage = errorMessage }; + internal static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => new( errorCondition, errorMessage ); - internal static ValidationError Invalid() => new() { ErrorCondition = ErrorCondition.Invalid, errorMessage = "Please enter a valid {0}." }; + internal static ValidationError Invalid() => new( ErrorCondition.Invalid, "Please enter a valid {0}." ); - internal static ValidationError Empty() => new() { ErrorCondition = ErrorCondition.Empty, errorMessage = "Please enter the {0}." }; + internal static ValidationError Empty() => new( ErrorCondition.Empty, "Please enter the {0}." ); internal static ValidationError TooSmall( object min, object max ) => - new() { ErrorCondition = ErrorCondition.TooLong, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; + new( ErrorCondition.TooLong, "The {0} must be between " + min + " and " + max + " (inclusive)." ); internal static ValidationError TooLarge( object min, object max ) => - new() { ErrorCondition = ErrorCondition.TooLarge, errorMessage = "The {0} must be between " + min + " and " + max + " (inclusive)." }; + new( ErrorCondition.TooLarge, "The {0} must be between " + min + " and " + max + " (inclusive)." ); /// /// Gets the error type. /// - public ErrorCondition ErrorCondition { get; private init; } = ErrorCondition.NoError; + public ErrorCondition ErrorCondition { get; } - private string errorMessage = ""; + private readonly string errorMessage; - private ValidationError() {} + private ValidationError( ErrorCondition type, string message ) { + ErrorCondition = type; + errorMessage = message; + } /// /// Returns the standard message for this error. From d89ce218e5473044503c9750fd9b93d61c4b6428 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:51:40 -0400 Subject: [PATCH 14/28] Renamed ErrorCondition to ValidationErrorType --- Tewl/InputValidation/ValidationError.cs | 16 +++++++------- .../InputValidation/ValidationErrorHandler.cs | 21 +++++++++---------- ...rorCondition.cs => ValidationErrorType.cs} | 2 +- Tewl/InputValidation/Validator.cs | 14 ++++++------- 4 files changed, 26 insertions(+), 27 deletions(-) rename Tewl/InputValidation/{ErrorCondition.cs => ValidationErrorType.cs} (94%) diff --git a/Tewl/InputValidation/ValidationError.cs b/Tewl/InputValidation/ValidationError.cs index 5d66d04..b0aa751 100644 --- a/Tewl/InputValidation/ValidationError.cs +++ b/Tewl/InputValidation/ValidationError.cs @@ -4,27 +4,27 @@ /// A validation error. /// public class ValidationError { - internal static ValidationError Custom( ErrorCondition errorCondition, string errorMessage ) => new( errorCondition, errorMessage ); + internal static ValidationError Custom( ValidationErrorType errorType, string errorMessage ) => new( errorType, errorMessage ); - internal static ValidationError Invalid() => new( ErrorCondition.Invalid, "Please enter a valid {0}." ); + internal static ValidationError Invalid() => new( ValidationErrorType.Invalid, "Please enter a valid {0}." ); - internal static ValidationError Empty() => new( ErrorCondition.Empty, "Please enter the {0}." ); + internal static ValidationError Empty() => new( ValidationErrorType.Empty, "Please enter the {0}." ); internal static ValidationError TooSmall( object min, object max ) => - new( ErrorCondition.TooLong, "The {0} must be between " + min + " and " + max + " (inclusive)." ); + new( ValidationErrorType.TooLong, "The {0} must be between " + min + " and " + max + " (inclusive)." ); internal static ValidationError TooLarge( object min, object max ) => - new( ErrorCondition.TooLarge, "The {0} must be between " + min + " and " + max + " (inclusive)." ); + new( ValidationErrorType.TooLarge, "The {0} must be between " + min + " and " + max + " (inclusive)." ); /// /// Gets the error type. /// - public ErrorCondition ErrorCondition { get; } + public ValidationErrorType Type { get; } private readonly string errorMessage; - private ValidationError( ErrorCondition type, string message ) { - ErrorCondition = type; + private ValidationError( ValidationErrorType type, string message ) { + Type = type; errorMessage = message; } diff --git a/Tewl/InputValidation/ValidationErrorHandler.cs b/Tewl/InputValidation/ValidationErrorHandler.cs index 27d7b76..3ec67eb 100644 --- a/Tewl/InputValidation/ValidationErrorHandler.cs +++ b/Tewl/InputValidation/ValidationErrorHandler.cs @@ -8,10 +8,10 @@ public class ValidationErrorHandler { /// /// Method that handles errors instead of the default handling mechanism. /// - public delegate void CustomHandler( ErrorCondition errorCondition ); + public delegate void CustomHandler( ValidationErrorType errorType ); private readonly CustomHandler? customHandler; - private readonly Dictionary customMessages = new(); + private readonly Dictionary customMessages = new(); /// /// Creates an error handler that adds standard error messages, based on the specified subject, to the validator. If the @@ -29,16 +29,15 @@ public class ValidationErrorHandler { public ValidationErrorHandler( CustomHandler customHandler ) => this.customHandler = customHandler; /// - /// Modifies this error handler to use a custom message if any errors occur with the specified conditions. If no error - /// conditions are passed, the message - /// will be used for all errors. This method has no effect if a custom handler has been specified. + /// Modifies this error handler to use a custom message if any errors occur with the specified types. If no error types are passed, the message will be used + /// for all errors. This method has no effect if a custom handler has been specified. /// - public void AddCustomErrorMessage( string message, params ErrorCondition[] errorConditions ) { - if( errorConditions.Length > 0 ) - foreach( var e in errorConditions ) + public void AddCustomErrorMessage( string message, params ValidationErrorType[] errorTypes ) { + if( errorTypes.Length > 0 ) + foreach( var e in errorTypes ) customMessages.Add( e, message ); else - foreach( var e in EnumTools.GetValues() ) + foreach( var e in EnumTools.GetValues() ) customMessages.Add( e, message ); } @@ -53,12 +52,12 @@ public void AddCustomErrorMessage( string message, params ErrorCondition[] error internal string HandleError( ValidationError error ) { // if there is a custom handler, run it and do nothing else if( customHandler is not null ) { - customHandler( error.ErrorCondition ); + customHandler( error.Type ); return ""; } // build the error message - if( !customMessages.TryGetValue( error.ErrorCondition, out var message ) ) + if( !customMessages.TryGetValue( error.Type, out var message ) ) // NOTE: Do we really need custom message, or can the custom handler manage that? message = error.GetMessage( Subject ); diff --git a/Tewl/InputValidation/ErrorCondition.cs b/Tewl/InputValidation/ValidationErrorType.cs similarity index 94% rename from Tewl/InputValidation/ErrorCondition.cs rename to Tewl/InputValidation/ValidationErrorType.cs index 25d8c3b..2846601 100644 --- a/Tewl/InputValidation/ErrorCondition.cs +++ b/Tewl/InputValidation/ValidationErrorType.cs @@ -4,7 +4,7 @@ namespace Tewl.InputValidation; /// The list of possible error types. /// [ PublicAPI ] -public enum ErrorCondition { +public enum ValidationErrorType { /// /// Empty /// diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index d73b907..313d284 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -405,9 +405,9 @@ public ValidationResult GetString( ValidationErrorHandler errorHandler, ( valueSetter, trimmedInput ) => { var errorMessage = "The length of the " + errorHandler.Subject + " must be between " + minLength + " and " + maxLength + " characters."; if( trimmedInput.Length > maxLength ) - return ValidationError.Custom( ErrorCondition.TooLong, errorMessage ); + return ValidationError.Custom( ValidationErrorType.TooLong, errorMessage ); if( trimmedInput.Length < minLength ) - return ValidationError.Custom( ErrorCondition.TooShort, errorMessage ); + return ValidationError.Custom( ValidationErrorType.TooShort, errorMessage ); valueSetter( trimmedInput ); return null; @@ -582,7 +582,7 @@ internal ValidationResult GetPhoneNumberAsObject( phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + trimmedInput, "" ); } else - return ValidationError.Custom( ErrorCondition.Invalid, "The five digit phone number you entered isn't recognized." ); + return ValidationError.Custom( ValidationErrorType.Invalid, "The five digit phone number you entered isn't recognized." ); } // International phone numbers // We require a country code and then at least 7 digits (but if country code is more than one digit, we require fewer subsequent digits). @@ -605,11 +605,11 @@ internal ValidationResult GetPhoneNumberAsObject( phoneNumber = PhoneNumber.CreateFromParts( areaCode, number, extension ); if( !allowExtension && phoneNumber.Extension.Length > 0 ) return ValidationError.Custom( - ErrorCondition.Invalid, + ValidationErrorType.Invalid, invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ); } else - return ValidationError.Custom( ErrorCondition.Invalid, invalidMessage ); + return ValidationError.Custom( ValidationErrorType.Invalid, invalidMessage ); } valueSetter( phoneNumber ); @@ -792,9 +792,9 @@ public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandl if( date.HasValue ) { var minMaxMessage = " It must be between " + minDate.ToDayMonthYearString( false ) + " and " + maxDate.ToDayMonthYearString( false ) + "."; if( date < minDate ) - return ValidationError.Custom( ErrorCondition.TooEarly, "The {0} is too early." + minMaxMessage ); + return ValidationError.Custom( ValidationErrorType.TooEarly, "The {0} is too early." + minMaxMessage ); if( date >= maxDate ) - return ValidationError.Custom( ErrorCondition.TooLate, "The {0} is too late." + minMaxMessage ); + return ValidationError.Custom( ValidationErrorType.TooLate, "The {0} is too late." + minMaxMessage ); } return null; } From fa444f78500337e9c7bfb6e70500ca2c2d5a2f1b Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:53:31 -0400 Subject: [PATCH 15/28] Formatted ValidationErrorHandler --- .../InputValidation/ValidationErrorHandler.cs | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/Tewl/InputValidation/ValidationErrorHandler.cs b/Tewl/InputValidation/ValidationErrorHandler.cs index 3ec67eb..b487a16 100644 --- a/Tewl/InputValidation/ValidationErrorHandler.cs +++ b/Tewl/InputValidation/ValidationErrorHandler.cs @@ -1,67 +1,67 @@ -namespace Tewl.InputValidation { +namespace Tewl.InputValidation; + +/// +/// This class allows you to control what happens when a validation method generates an error. Every validation method takes a ValidationErrorHandler object +/// as the first parameter. +/// +[ PublicAPI ] +public class ValidationErrorHandler { /// - /// This class allows you to control what happens when a validation method generates an error. Every validation method takes a ValidationErrorHandler object - /// as the first parameter. + /// Method that handles errors instead of the default handling mechanism. /// - [ PublicAPI ] - public class ValidationErrorHandler { - /// - /// Method that handles errors instead of the default handling mechanism. - /// - public delegate void CustomHandler( ValidationErrorType errorType ); + public delegate void CustomHandler( ValidationErrorType errorType ); - private readonly CustomHandler? customHandler; - private readonly Dictionary customMessages = new(); + /// + /// The subject of the error message, if one needs to be generated. + /// + internal string Subject { get; } = "field"; - /// - /// Creates an error handler that adds standard error messages, based on the specified subject, to the validator. If the - /// subject is used to start a - /// sentence, it will be automatically capitalized. - /// - public ValidationErrorHandler( string subject ) => Subject = subject; + private readonly CustomHandler? customHandler; + private readonly Dictionary customMessages = new(); - /// - /// Creates an object that invokes code of your choice if an error occurs. The given custom handler will - /// only be invoked in the case of an error, and it will prevent the default error message from being - /// added to the validator's error collection. Even if the handler does not add an error to the validator, - /// validator.HasErrors will return true because an error has still occurred. - /// - public ValidationErrorHandler( CustomHandler customHandler ) => this.customHandler = customHandler; + /// + /// Creates an error handler that adds standard error messages, based on the specified subject, to the validator. If the + /// subject is used to start a + /// sentence, it will be automatically capitalized. + /// + public ValidationErrorHandler( string subject ) => Subject = subject; - /// - /// Modifies this error handler to use a custom message if any errors occur with the specified types. If no error types are passed, the message will be used - /// for all errors. This method has no effect if a custom handler has been specified. - /// - public void AddCustomErrorMessage( string message, params ValidationErrorType[] errorTypes ) { - if( errorTypes.Length > 0 ) - foreach( var e in errorTypes ) - customMessages.Add( e, message ); - else - foreach( var e in EnumTools.GetValues() ) - customMessages.Add( e, message ); - } + /// + /// Creates an object that invokes code of your choice if an error occurs. The given custom handler will + /// only be invoked in the case of an error, and it will prevent the default error message from being + /// added to the validator's error collection. Even if the handler does not add an error to the validator, + /// validator.HasErrors will return true because an error has still occurred. + /// + public ValidationErrorHandler( CustomHandler customHandler ) => this.customHandler = customHandler; - /// - /// The subject of the error message, if one needs to be generated. - /// - internal string Subject { get; } = "field"; + /// + /// Modifies this error handler to use a custom message if any errors occur with the specified types. If no error types are passed, the message will be used + /// for all errors. This method has no effect if a custom handler has been specified. + /// + public void AddCustomErrorMessage( string message, params ValidationErrorType[] errorTypes ) { + if( errorTypes.Length > 0 ) + foreach( var e in errorTypes ) + customMessages.Add( e, message ); + else + foreach( var e in EnumTools.GetValues() ) + customMessages.Add( e, message ); + } - /// - /// Invokes the appropriate behavior according to how this error handler was created. - /// - internal string HandleError( ValidationError error ) { - // if there is a custom handler, run it and do nothing else - if( customHandler is not null ) { - customHandler( error.Type ); - return ""; - } + /// + /// Invokes the appropriate behavior according to how this error handler was created. + /// + internal string HandleError( ValidationError error ) { + // if there is a custom handler, run it and do nothing else + if( customHandler is not null ) { + customHandler( error.Type ); + return ""; + } - // build the error message - if( !customMessages.TryGetValue( error.Type, out var message ) ) - // NOTE: Do we really need custom message, or can the custom handler manage that? - message = error.GetMessage( Subject ); + // build the error message + if( !customMessages.TryGetValue( error.Type, out var message ) ) + // NOTE: Do we really need custom message, or can the custom handler manage that? + message = error.GetMessage( Subject ); - return message; - } + return message; } } \ No newline at end of file From 6d24a082c546cfc7d48f946b2739a326309dae71 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 17:55:45 -0400 Subject: [PATCH 16/28] Made ExecuteValidation handler parameter nullable --- Tewl/InputValidation/Validator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 313d284..be8b07b 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -885,7 +885,9 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler h /// /// The result value that will be used if the input value is empty or if there is a validation error. internal ValidationResult ExecuteValidation( - ValidationErrorHandler handler, InputType input, bool allowEmpty, ValidationMethod validationMethod, ValType? emptyValue = default ) { + ValidationErrorHandler? handler, InputType input, bool allowEmpty, ValidationMethod validationMethod, ValType? emptyValue = default ) { + handler ??= new ValidationErrorHandler( "value" ); + if( isEmpty( input, out var trimmedInput ) ) return allowEmpty ? new ValidationResult( emptyValue, null ) : handleError( ValidationError.Empty() ); @@ -907,6 +909,6 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler h } private ValidationResult handleEmptyAndReturnEmptyStringIfInvalid( - ValidationErrorHandler handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => + ValidationErrorHandler? handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => ExecuteValidation( handler, valueAsObject, allowEmpty, method, emptyValue: "" )!; } \ No newline at end of file From dbb5511eaf8437f69e7c24209c07a458103f0bad Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 18:02:32 -0400 Subject: [PATCH 17/28] Made ValidationErrorHandler parameters nullable --- Tewl/InputValidation/Validator.cs | 98 +++++++++++++++---------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index be8b07b..c241896 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -125,7 +125,7 @@ public void NoteErrorAndAddMessages( params string[] messages ) { /// Returns the validated boolean type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetBoolean( ValidationErrorHandler errorHandler, string input ) => + public ValidationResult GetBoolean( ValidationErrorHandler? errorHandler, string input ) => ExecuteValidation( errorHandler, input, false, validateBoolean ); /// @@ -133,7 +133,7 @@ public ValidationResult GetBoolean( ValidationErrorHandler errorHandler, s /// Returns the validated boolean type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableBoolean( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableBoolean( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => ExecuteValidation( errorHandler, input, @@ -152,13 +152,13 @@ public ValidationResult GetBoolean( ValidationErrorHandler errorHandler, s /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetByte( ValidationErrorHandler errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); + public ValidationResult GetByte( ValidationErrorHandler? errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); /// /// Returns the validated byte type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetByte( ValidationErrorHandler errorHandler, string input, byte min, byte max ) => + public ValidationResult GetByte( ValidationErrorHandler? errorHandler, string input, byte min, byte max ) => ExecuteValidation( errorHandler, input, @@ -169,7 +169,7 @@ public ValidationResult GetByte( ValidationErrorHandler errorHandler, stri /// Returns the validated byte type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableByte( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableByte( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => ExecuteValidation( errorHandler, input, @@ -180,14 +180,14 @@ public ValidationResult GetByte( ValidationErrorHandler errorHandler, stri /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetShort( ValidationErrorHandler errorHandler, string input ) => + public ValidationResult GetShort( ValidationErrorHandler? errorHandler, string input ) => GetShort( errorHandler, input, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetShort( ValidationErrorHandler errorHandler, string input, short min, short max ) => + public ValidationResult GetShort( ValidationErrorHandler? errorHandler, string input, short min, short max ) => ExecuteValidation( errorHandler, input, @@ -198,14 +198,14 @@ public ValidationResult GetShort( ValidationErrorHandler errorHandler, st /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableShort( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => GetNullableShort( errorHandler, input, allowEmpty, short.MinValue, short.MaxValue ); /// /// Returns the validated short type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableShort( ValidationErrorHandler errorHandler, string input, bool allowEmpty, short min, short max ) => + public ValidationResult GetNullableShort( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, short min, short max ) => ExecuteValidation( errorHandler, input, @@ -216,14 +216,14 @@ public ValidationResult GetShort( ValidationErrorHandler errorHandler, st /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetInt( ValidationErrorHandler errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); + public ValidationResult GetInt( ValidationErrorHandler? errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); /// /// Returns the validated int type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public ValidationResult GetInt( ValidationErrorHandler errorHandler, string input, int min, int max ) => + public ValidationResult GetInt( ValidationErrorHandler? errorHandler, string input, int min, int max ) => ExecuteValidation( errorHandler, input, @@ -235,7 +235,7 @@ public ValidationResult GetInt( ValidationErrorHandler errorHandler, string /// If allowEmpty is true and the given string is empty, null will be returned. /// public ValidationResult GetNullableInt( - ValidationErrorHandler errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => + ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => ExecuteValidation( errorHandler, input, @@ -247,7 +247,7 @@ public ValidationResult GetInt( ValidationErrorHandler errorHandler, string /// Passing an empty string or null will result in ErrorCondition.Empty. /// and are inclusive. /// - public ValidationResult GetLong( ValidationErrorHandler errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => + public ValidationResult GetLong( ValidationErrorHandler? errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => ExecuteValidation( errorHandler, input, @@ -259,7 +259,7 @@ public ValidationResult GetLong( ValidationErrorHandler errorHandler, stri /// If allowEmpty is true and the given string is empty, null will be returned. /// public ValidationResult GetNullableLong( - ValidationErrorHandler errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => + ValidationErrorHandler? errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => ExecuteValidation( errorHandler, input, @@ -292,14 +292,14 @@ public ValidationResult GetLong( ValidationErrorHandler errorHandler, stri /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetFloat( ValidationErrorHandler errorHandler, string input, float min, float max ) => + public ValidationResult GetFloat( ValidationErrorHandler? errorHandler, string input, float min, float max ) => ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated float type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableFloat( ValidationErrorHandler errorHandler, string input, bool allowEmpty, float min, float max ) => + public ValidationResult GetNullableFloat( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, float min, float max ) => ExecuteValidation( errorHandler, input, @@ -331,28 +331,28 @@ public ValidationResult GetFloat( ValidationErrorHandler errorHandler, st /// Returns a validated decimal type from the given string and validation package. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetDecimal( ValidationErrorHandler errorHandler, string input ) => + public ValidationResult GetDecimal( ValidationErrorHandler? errorHandler, string input ) => GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetDecimal( ValidationErrorHandler errorHandler, string input, decimal min, decimal max ) => + public ValidationResult GetDecimal( ValidationErrorHandler? errorHandler, string input, decimal min, decimal max ) => ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); /// /// Returns a validated decimal type from the given string and validation package. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableDecimal( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => GetNullableDecimal( errorHandler, input, allowEmpty, decimal.MinValue, decimal.MaxValue ); /// /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableDecimal( ValidationErrorHandler errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => + public ValidationResult GetNullableDecimal( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => ExecuteValidation( errorHandler, input, @@ -381,7 +381,7 @@ public ValidationResult GetDecimal( ValidationErrorHandler errorHandler /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => GetString( errorHandler, input, allowEmpty, int.MaxValue ); /// @@ -389,7 +389,7 @@ public ValidationResult GetString( ValidationErrorHandler errorHandler, /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength ) => + public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength ) => GetString( errorHandler, input, allowEmpty, 0, maxLength ); /// @@ -397,13 +397,13 @@ public ValidationResult GetString( ValidationErrorHandler errorHandler, /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// Automatically trims whitespace from edges of returned string. /// - public ValidationResult GetString( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => + public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, allowEmpty, ( valueSetter, trimmedInput ) => { - var errorMessage = "The length of the " + errorHandler.Subject + " must be between " + minLength + " and " + maxLength + " characters."; + var errorMessage = "The length of the {0} must be between " + minLength + " and " + maxLength + " characters."; if( trimmedInput.Length > maxLength ) return ValidationError.Custom( ValidationErrorType.TooLong, errorMessage ); if( trimmedInput.Length < minLength ) @@ -420,7 +420,7 @@ public ValidationResult GetString( ValidationErrorHandler errorHandler, /// The maxLength defaults to 254 per this source: http://en.wikipedia.org/wiki/E-mail_address#Syntax /// If you pass a different value for maxLength, you'd better have a good reason. /// - public ValidationResult GetEmailAddress( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => + public ValidationResult GetEmailAddress( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -453,7 +453,7 @@ public ValidationResult GetEmailAddress( ValidationErrorHandler errorHan /// /// Returns a validated URL. /// - public ValidationResult GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetUrl( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => GetUrl( errorHandler, input, allowEmpty, MaxUrlLength ); private static readonly string[] validSchemes = { "http", "https", "ftp" }; @@ -462,7 +462,7 @@ public ValidationResult GetUrl( ValidationErrorHandler errorHandler, str /// Returns a validated URL. Note that you may run into problems with certain browsers if you pass a length longer than /// 2048. /// - public ValidationResult GetUrl( ValidationErrorHandler errorHandler, string input, bool allowEmpty, int maxUrlLength ) => + public ValidationResult GetUrl( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxUrlLength ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -513,7 +513,7 @@ public ValidationResult GetUrl( ValidationErrorHandler errorHandler, str /// This is useful when working with data that had the area code omitted because the number was local. /// public ValidationResult GetPhoneNumberWithDefaultAreaCode( - ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { + ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { if( new Validator().GetPhoneNumber( null, input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is not null ) // If the phone number was invalid without the area code, but is valid with the area code, we really validate using the default // area code and then return. In all other cases, we return what would have happened without tacking on the default area code. @@ -531,7 +531,7 @@ public ValidationResult GetPhoneNumberWithDefaultAreaCode( /// and count as a valid phone number. /// public ValidationResult GetPhoneNumber( - ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => + ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => GetPhoneWithLastFiveMapping( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, null ); /// @@ -541,7 +541,7 @@ public ValidationResult GetPhoneNumber( /// string or null is given, the empty string is returned. /// public ValidationResult GetPhoneWithLastFiveMapping( - ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, + ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, Dictionary? firstFives ) { return handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, @@ -558,14 +558,14 @@ public ValidationResult GetPhoneWithLastFiveMapping( } internal ValidationResult GetPhoneNumberAsObject( - ValidationErrorHandler errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, + ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, Dictionary? firstFives ) { return ExecuteValidation( errorHandler, input, allowEmpty, ( valueSetter, trimmedInput ) => { - var invalidPrefix = "The " + errorHandler.Subject + " (" + trimmedInput + ") is invalid."; + var invalidPrefix = "The {0} (" + trimmedInput + ") is invalid."; // Remove all of the valid delimiter characters so we can just deal with numbers and whitespace trimmedInput = trimmedInput.RemoveCharacters( '-', '(', ')', '.' ).Trim(); @@ -622,7 +622,7 @@ internal ValidationResult GetPhoneNumberAsObject( /// Returns a validated phone number extension as a string. /// If allow empty is true and the empty string or null is given, the empty string is returned. /// - public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -639,7 +639,7 @@ public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler /// Returns a validated social security number from the given string and restrictions. /// If allowEmpty true and an empty string or null is given, the empty string is returned. /// - public ValidationResult GetSocialSecurityNumber( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetSocialSecurityNumber( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => GetNumber( errorHandler, input, 9, allowEmpty, "-" ); /// @@ -648,7 +648,7 @@ public ValidationResult GetSocialSecurityNumber( ValidationErrorHandler /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). /// public ValidationResult GetNumber( - ValidationErrorHandler errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => + ValidationErrorHandler? errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => handleEmptyAndReturnEmptyStringIfInvalid( errorHandler, input, @@ -666,13 +666,13 @@ public ValidationResult GetNumber( /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// - public ValidationResult GetZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetZipCode( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, emptyValue: new ZipCode() )!; /// /// Gets a validated US or Canadian zip code. /// - public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, emptyValue: new ZipCode() )!; /// @@ -680,7 +680,7 @@ public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// Passing an empty string or null will result in ErrorCondition.Empty. /// - public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler errorHandler, string input ) => + public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler? errorHandler, string input ) => ExecuteValidation( errorHandler, input, @@ -692,7 +692,7 @@ public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler er /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. /// If allowEmpty is true and the given string is empty, null will be returned. /// - public ValidationResult GetNullableSqlSmallDateTime( ValidationErrorHandler errorHandler, string input, bool allowEmpty ) => + public ValidationResult GetNullableSqlSmallDateTime( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => ExecuteValidation( errorHandler, input, @@ -710,7 +710,7 @@ public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler er /// Passing an empty string or null for each date part will result in ErrorCondition.Empty. /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. /// - public ValidationResult GetSqlSmallDateTimeFromParts( ValidationErrorHandler errorHandler, string month, string day, string year ) => + public ValidationResult GetSqlSmallDateTimeFromParts( ValidationErrorHandler? errorHandler, string month, string day, string year ) => GetSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ) ); /// @@ -720,7 +720,7 @@ public ValidationResult GetSqlSmallDateTimeFromParts( ValidationErrorH /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. /// public ValidationResult GetNullableSqlSmallDateTimeFromParts( - ValidationErrorHandler errorHandler, string month, string day, string year, bool allowEmpty ) => + ValidationErrorHandler? errorHandler, string month, string day, string year, bool allowEmpty ) => GetNullableSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ), allowEmpty ); private static string makeDateFromParts( string month, string day, string year ) { @@ -734,7 +734,7 @@ private static string makeDateFromParts( string month, string day, string year ) /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern ) => + public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandler? errorHandler, string input, string pattern ) => ExecuteValidation( errorHandler, input, @@ -745,7 +745,7 @@ public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandl /// Returns the validated DateTime type from a date string and an exact match pattern.' /// Pattern specifies the date format, such as "MM/dd/yyyy". /// - public ValidationResult GetNullableSqlSmallDateTimeExact( ValidationErrorHandler errorHandler, string input, string pattern, bool allowEmpty ) => + public ValidationResult GetNullableSqlSmallDateTimeExact( ValidationErrorHandler? errorHandler, string input, string pattern, bool allowEmpty ) => ExecuteValidation( errorHandler, input, @@ -803,7 +803,7 @@ public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandl /// Validates the date using given allowEmpty, min, and max constraints. /// public ValidationResult GetNullableDateTime( - ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => + ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => ExecuteValidation( handler, input, @@ -813,7 +813,7 @@ public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandl /// /// Validates the date using given min and max constraints. /// - public ValidationResult GetDateTime( ValidationErrorHandler handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => + public ValidationResult GetDateTime( ValidationErrorHandler? handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => ExecuteValidation( handler, input, @@ -823,7 +823,7 @@ public ValidationResult GetDateTime( ValidationErrorHandler handler, s /// /// Validates the given time span. /// - public ValidationResult GetNullableTimeSpan( ValidationErrorHandler handler, TimeSpan? input, bool allowEmpty ) => + public ValidationResult GetNullableTimeSpan( ValidationErrorHandler? handler, TimeSpan? input, bool allowEmpty ) => ExecuteValidation( handler, input, @@ -836,7 +836,7 @@ public ValidationResult GetDateTime( ValidationErrorHandler handler, s /// /// Validates the given time span. /// - public ValidationResult GetTimeSpan( ValidationErrorHandler handler, TimeSpan? input ) => + public ValidationResult GetTimeSpan( ValidationErrorHandler? handler, TimeSpan? input ) => ExecuteValidation( handler, input, @@ -849,7 +849,7 @@ public ValidationResult GetTimeSpan( ValidationErrorHandler handler, T /// /// Validates the given time span. /// - public ValidationResult GetNullableTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats, bool allowEmpty ) => + public ValidationResult GetNullableTimeOfDayTimeSpan( ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty ) => ExecuteValidation( handler, input, @@ -864,7 +864,7 @@ public ValidationResult GetTimeSpan( ValidationErrorHandler handler, T /// /// Validates the given time span. /// - public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler handler, string input, string[]? formats ) => + public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler? handler, string input, string[]? formats ) => ExecuteValidation( handler, input, From fe70e4cbc654bf9276295a3a032e0ef87eacbae7 Mon Sep 17 00:00:00 2001 From: William Gross Date: Thu, 20 Mar 2025 18:15:32 -0400 Subject: [PATCH 18/28] Documented ExecuteValidation handler parameter --- Tewl/InputValidation/Validator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index c241896..eaae915 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Text.RegularExpressions; namespace Tewl.InputValidation; @@ -879,7 +879,7 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler? /// /// Executes a validation and returns the result. /// - /// + /// Pass null if you’re only validating a single value and don’t need to distinguish it from others in error messages. /// /// /// From 4428eb12b58f0182e30201660a44e86df4e36869 Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 10:38:51 -0400 Subject: [PATCH 19/28] Moved text validations to their own file --- .../Validation Methods/Text.cs | 175 +++++++++++++++++ Tewl/InputValidation/Validator.cs | 176 +----------------- Tewl/Tewl.csproj.DotSettings | 2 + 3 files changed, 183 insertions(+), 170 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/Text.cs create mode 100644 Tewl/Tewl.csproj.DotSettings diff --git a/Tewl/InputValidation/Validation Methods/Text.cs b/Tewl/InputValidation/Validation Methods/Text.cs new file mode 100644 index 0000000..a944aef --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/Text.cs @@ -0,0 +1,175 @@ +using System.Text.RegularExpressions; + +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + /// + /// Returns a validated string from the given string and restrictions. + /// If allowEmpty true and an empty string or null is given, the empty string is returned. + /// Automatically trims whitespace from edges of returned string. + /// + public static ValidationResult GetString( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.GetString( errorHandler, input, allowEmpty, int.MaxValue ); + + /// + /// Returns a validated string from the given string and restrictions. + /// If allowEmpty true and an empty string or null is given, the empty string is returned. + /// Automatically trims whitespace from edges of returned string. + /// + public static ValidationResult GetString( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength ) => + validator.GetString( errorHandler, input, allowEmpty, 0, maxLength ); + + /// + /// Returns a validated string from the given string and restrictions. + /// If allowEmpty true and an empty string or null is given, the empty string is returned. + /// Automatically trims whitespace from edges of returned string. + /// + public static ValidationResult GetString( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => + validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + var errorMessage = "The length of the {0} must be between " + minLength + " and " + maxLength + " characters."; + if( trimmedInput.Length > maxLength ) + return ValidationError.Custom( ValidationErrorType.TooLong, errorMessage ); + if( trimmedInput.Length < minLength ) + return ValidationError.Custom( ValidationErrorType.TooShort, errorMessage ); + + valueSetter( trimmedInput ); + return null; + } ); + + /// + /// Returns a validated email address from the given string and restrictions. + /// If allowEmpty true and the empty string or null is given, the empty string is returned. + /// Automatically trims whitespace from edges of returned string. + /// The maxLength defaults to 254 per this source: http://en.wikipedia.org/wiki/E-mail_address#Syntax + /// If you pass a different value for maxLength, you'd better have a good reason. + /// + public static ValidationResult GetEmailAddress( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => + validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + // Validate as a string with same restrictions - if it fails on that, return + if( new Validator().GetString( null, trimmedInput, allowEmpty, maxLength ).Error( out _ ) is {} error ) + return error; + + // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. + //([^@ \n\.]+\.)+ forces any positive number of (anything.)s to exist in a row. Doesn't allow "..". + // Allows anything.anything123-anything@anything.anything123.anything + const string localPartUnconditionallyPermittedCharacters = @"[a-z0-9!#\$%&'\*\+\-/=\?\^_`\{\|}~]"; + const string localPart = "(" + localPartUnconditionallyPermittedCharacters + @"+\.?)*" + localPartUnconditionallyPermittedCharacters + "+"; + const string domainUnconditionallyPermittedCharacters = "[a-z0-9-]"; + const string domain = "(" + domainUnconditionallyPermittedCharacters + @"+\.)+" + domainUnconditionallyPermittedCharacters + "+"; + // The first two conditions are for performance only. + if( !trimmedInput.Contains( "@" ) || !trimmedInput.Contains( "." ) || !Regex.IsMatch( + trimmedInput, + "^" + localPart + "@" + domain + "$", + RegexOptions.IgnoreCase ) ) + return ValidationError.Invalid(); + // Max length is already checked by the string validation + // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. + + valueSetter( trimmedInput ); + return null; + } ); + + /// + /// Returns a validated URL. + /// + public static ValidationResult GetUrl( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.GetUrl( errorHandler, input, allowEmpty, 2048 ); + + /// + /// Returns a validated URL. Note that you may run into problems with certain browsers if you pass a length longer than + /// 2048. + /// + public static ValidationResult GetUrl( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxUrlLength ) => + validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + /* If the string is just a number, reject it right out. */ + if( int.TryParse( trimmedInput, out _ ) || double.TryParse( trimmedInput, out _ ) ) + return ValidationError.Invalid(); + + /* If it's an email, it's not an URL. */ + if( new Validator().GetEmailAddress( null, trimmedInput, allowEmpty ).Error( out _ ) is null ) + return ValidationError.Invalid(); + + /* If it doesn't start with one of our whitelisted schemes, add in the best guess. */ + var validSchemes = new[] { "http", "https", "ftp" }; + if( new Validator().GetString( + null, + validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, + true, + maxUrlLength ) + .Error( out trimmedInput ) is {} error ) + return error; + + /* If the getstring didn't fail, keep on keepin keepin on. */ + try { + if( !Uri.IsWellFormedUriString( trimmedInput, UriKind.Absolute ) ) + throw new UriFormatException(); + + // Don't allow relative URLs + var uri = new Uri( trimmedInput, UriKind.Absolute ); + + // Must be a valid DNS-style hostname or IP address + // Must contain at least one '.', to prevent just host names + // Must be one of the common web browser-accessible schemes + if( uri.HostNameType != UriHostNameType.Dns && uri.HostNameType != UriHostNameType.IPv4 && uri.HostNameType != UriHostNameType.IPv6 || + uri.Host.All( c => c != '.' ) || validSchemes.All( s => s != uri.Scheme ) ) + throw new UriFormatException(); + } + catch( UriFormatException ) { + return ValidationError.Invalid(); + } + + valueSetter( trimmedInput ); + return null; + } ); + + /// + /// Returns a validated social security number from the given string and restrictions. + /// If allowEmpty true and an empty string or null is given, the empty string is returned. + /// + public static ValidationResult GetSocialSecurityNumber( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.GetNumber( errorHandler, input, 9, allowEmpty, "-" ); + + /// + /// Gets a string of the given length whose characters are only numeric values, after throwing out all acceptable garbage + /// characters. + /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). + /// + public static ValidationResult GetNumber( + this Validator validator, ValidationErrorHandler? errorHandler, string input, int numberOfDigits, bool allowEmpty, + params string[] acceptableGarbageStrings ) => + validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + foreach( var garbageString in acceptableGarbageStrings ) + trimmedInput = trimmedInput.Replace( garbageString, "" ); + trimmedInput = trimmedInput.Trim(); + if( !Regex.IsMatch( trimmedInput, @"^\d{" + numberOfDigits + "}$" ) ) + return ValidationError.Invalid(); + valueSetter( trimmedInput ); + return null; + } ); + + private static ValidationResult executeStringValidation( + this Validator validator, ValidationErrorHandler? handler, InputType input, bool allowEmpty, + Validator.ValidationMethod validationMethod ) => + validator.ExecuteValidation( handler, input, allowEmpty, validationMethod, emptyValue: "" )!; +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index eaae915..85a8f88 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -44,12 +44,6 @@ private static bool isEmpty( InputType input, out InputType trimmedIn private readonly List errors = new List(); - /// - /// The maximum length for a URL as dictated by the limitations of Internet Explorer. This is safely the maximum size for a - /// URL. - /// - public const int MaxUrlLength = 2048; - /// /// Returns true if any errors have been encountered during validation so far. This can be true even while /// ErrorMessages.Count == 0 @@ -376,138 +370,6 @@ public ValidationResult GetDecimal( ValidationErrorHandler? errorHandle return null; } - /// - /// Returns a validated string from the given string and restrictions. - /// If allowEmpty true and an empty string or null is given, the empty string is returned. - /// Automatically trims whitespace from edges of returned string. - /// - public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - GetString( errorHandler, input, allowEmpty, int.MaxValue ); - - /// - /// Returns a validated string from the given string and restrictions. - /// If allowEmpty true and an empty string or null is given, the empty string is returned. - /// Automatically trims whitespace from edges of returned string. - /// - public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength ) => - GetString( errorHandler, input, allowEmpty, 0, maxLength ); - - /// - /// Returns a validated string from the given string and restrictions. - /// If allowEmpty true and an empty string or null is given, the empty string is returned. - /// Automatically trims whitespace from edges of returned string. - /// - public ValidationResult GetString( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int minLength, int maxLength ) => - handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - var errorMessage = "The length of the {0} must be between " + minLength + " and " + maxLength + " characters."; - if( trimmedInput.Length > maxLength ) - return ValidationError.Custom( ValidationErrorType.TooLong, errorMessage ); - if( trimmedInput.Length < minLength ) - return ValidationError.Custom( ValidationErrorType.TooShort, errorMessage ); - - valueSetter( trimmedInput ); - return null; - } ); - - /// - /// Returns a validated email address from the given string and restrictions. - /// If allowEmpty true and the empty string or null is given, the empty string is returned. - /// Automatically trims whitespace from edges of returned string. - /// The maxLength defaults to 254 per this source: http://en.wikipedia.org/wiki/E-mail_address#Syntax - /// If you pass a different value for maxLength, you'd better have a good reason. - /// - public ValidationResult GetEmailAddress( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxLength = 254 ) => - handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - // Validate as a string with same restrictions - if it fails on that, return - if( new Validator().GetString( null, trimmedInput, allowEmpty, maxLength ).Error( out _ ) is {} error ) - return error; - - // [^@ \n] means any character but a @ or a newline or a space. This forces only one @ to exist. - //([^@ \n\.]+\.)+ forces any positive number of (anything.)s to exist in a row. Doesn't allow "..". - // Allows anything.anything123-anything@anything.anything123.anything - const string localPartUnconditionallyPermittedCharacters = @"[a-z0-9!#\$%&'\*\+\-/=\?\^_`\{\|}~]"; - const string localPart = "(" + localPartUnconditionallyPermittedCharacters + @"+\.?)*" + localPartUnconditionallyPermittedCharacters + "+"; - const string domainUnconditionallyPermittedCharacters = "[a-z0-9-]"; - const string domain = "(" + domainUnconditionallyPermittedCharacters + @"+\.)+" + domainUnconditionallyPermittedCharacters + "+"; - // The first two conditions are for performance only. - if( !trimmedInput.Contains( "@" ) || !trimmedInput.Contains( "." ) || !Regex.IsMatch( - trimmedInput, - "^" + localPart + "@" + domain + "$", - RegexOptions.IgnoreCase ) ) - return ValidationError.Invalid(); - // Max length is already checked by the string validation - // NOTE: We should really enforce the max length of the domain portion and the local portion individually as well. - - valueSetter( trimmedInput ); - return null; - } ); - - /// - /// Returns a validated URL. - /// - public ValidationResult GetUrl( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - GetUrl( errorHandler, input, allowEmpty, MaxUrlLength ); - - private static readonly string[] validSchemes = { "http", "https", "ftp" }; - - /// - /// Returns a validated URL. Note that you may run into problems with certain browsers if you pass a length longer than - /// 2048. - /// - public ValidationResult GetUrl( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int maxUrlLength ) => - handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - /* If the string is just a number, reject it right out. */ - if( int.TryParse( trimmedInput, out _ ) || double.TryParse( trimmedInput, out _ ) ) - return ValidationError.Invalid(); - - /* If it's an email, it's not an URL. */ - if( new Validator().GetEmailAddress( null, trimmedInput, allowEmpty ).Error( out _ ) is null ) - return ValidationError.Invalid(); - - /* If it doesn't start with one of our whitelisted schemes, add in the best guess. */ - if( new Validator().GetString( - null, - validSchemes.Any( s => trimmedInput.StartsWithIgnoreCase( s ) ) ? trimmedInput : "http://" + trimmedInput, - true, - maxUrlLength ) - .Error( out trimmedInput ) is {} error ) - return error; - - /* If the getstring didn't fail, keep on keepin keepin on. */ - try { - if( !Uri.IsWellFormedUriString( trimmedInput, UriKind.Absolute ) ) - throw new UriFormatException(); - - // Don't allow relative URLs - var uri = new Uri( trimmedInput, UriKind.Absolute ); - - // Must be a valid DNS-style hostname or IP address - // Must contain at least one '.', to prevent just host names - // Must be one of the common web browser-accessible schemes - if( uri.HostNameType != UriHostNameType.Dns && uri.HostNameType != UriHostNameType.IPv4 && uri.HostNameType != UriHostNameType.IPv6 || - uri.Host.All( c => c != '.' ) || validSchemes.All( s => s != uri.Scheme ) ) - throw new UriFormatException(); - } - catch( UriFormatException ) { - return ValidationError.Invalid(); - } - - valueSetter( trimmedInput ); - return null; - } ); - /// /// The same as GetPhoneNumber, except the given default area code will be prepended on the phone number if necessary. /// This is useful when working with data that had the area code omitted because the number was local. @@ -635,34 +497,6 @@ public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler? return null; } ); - /// - /// Returns a validated social security number from the given string and restrictions. - /// If allowEmpty true and an empty string or null is given, the empty string is returned. - /// - public ValidationResult GetSocialSecurityNumber( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - GetNumber( errorHandler, input, 9, allowEmpty, "-" ); - - /// - /// Gets a string of the given length whose characters are only numeric values, after throwing out all acceptable garbage - /// characters. - /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). - /// - public ValidationResult GetNumber( - ValidationErrorHandler? errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => - handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - foreach( var garbageString in acceptableGarbageStrings ) - trimmedInput = trimmedInput.Replace( garbageString, "" ); - trimmedInput = trimmedInput.Trim(); - if( !Regex.IsMatch( trimmedInput, @"^\d{" + numberOfDigits + "}$" ) ) - return ValidationError.Invalid(); - valueSetter( trimmedInput ); - return null; - } ); - /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// @@ -907,8 +741,10 @@ public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler? return new ValidationResult( emptyValue, error ); } } +} - private ValidationResult handleEmptyAndReturnEmptyStringIfInvalid( - ValidationErrorHandler? handler, InputType valueAsObject, bool allowEmpty, ValidationMethod method ) => - ExecuteValidation( handler, valueAsObject, allowEmpty, method, emptyValue: "" )!; -} \ No newline at end of file +/// +/// Validation methods for . +/// +[ PublicAPI ] +public static partial class ValidatorExtensions; \ No newline at end of file diff --git a/Tewl/Tewl.csproj.DotSettings b/Tewl/Tewl.csproj.DotSettings new file mode 100644 index 0000000..bd89b5d --- /dev/null +++ b/Tewl/Tewl.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 197cfabaa04b37e26ded9a8488003068fac974c0 Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 10:46:02 -0400 Subject: [PATCH 20/28] Moved phone validations to their own file --- .../Validation Methods/Phone.cs | 134 ++++++++++++++++++ Tewl/InputValidation/Validator.cs | 128 ----------------- 2 files changed, 134 insertions(+), 128 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/Phone.cs diff --git a/Tewl/InputValidation/Validation Methods/Phone.cs b/Tewl/InputValidation/Validation Methods/Phone.cs new file mode 100644 index 0000000..f3d8c52 --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/Phone.cs @@ -0,0 +1,134 @@ +using System.Text.RegularExpressions; + +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + /// + /// The same as GetPhoneNumber, except the given default area code will be prepended on the phone number if necessary. + /// This is useful when working with data that had the area code omitted because the number was local. + /// + public static ValidationResult GetPhoneNumberWithDefaultAreaCode( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, + string defaultAreaCode ) { + if( new Validator().GetPhoneNumber( null, input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is not null ) + // If the phone number was invalid without the area code, but is valid with the area code, we really validate using the default + // area code and then return. In all other cases, we return what would have happened without tacking on the default area code. + if( new Validator().GetPhoneNumber( null, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is null ) + return validator.GetPhoneNumber( errorHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); + + return validator.GetPhoneNumber( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); + } + + /// + /// Returns a validated phone number as a standard phone number string given the complete phone number with optional + /// extension as a string. If allow empty is true and an empty string or null is given, the empty string is returned. + /// Pass true for allow surrounding garbage if you want to allow "The phone number is 585-455-6476yadayada." to be parsed + /// into 585-455-6476 + /// and count as a valid phone number. + /// + public static ValidationResult GetPhoneNumber( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => + validator.GetPhoneWithLastFiveMapping( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, null ); + + /// + /// Returns a validated phone number as a standard phone number string given the complete phone number with optional + /// extension or the last five digits of the number and a dictionary of single + /// digits to five-digit groups that become the first five digits of the full number. If allow empty is true and an empty + /// string or null is given, the empty string is returned. + /// + public static ValidationResult GetPhoneWithLastFiveMapping( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, + Dictionary? firstFives ) { + return validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + if( new Validator().GetPhoneNumberAsObject( null, trimmedInput, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ) + .Error( out var value ) is {} error ) + return error; + + valueSetter( value.StandardPhoneString ); + return null; + } ); + } + + internal static ValidationResult GetPhoneNumberAsObject( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, + Dictionary? firstFives ) { + return validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + var invalidPrefix = "The {0} (" + trimmedInput + ") is invalid."; + // Remove all of the valid delimiter characters so we can just deal with numbers and whitespace + trimmedInput = trimmedInput.RemoveCharacters( '-', '(', ')', '.' ).Trim(); + + var invalidMessage = invalidPrefix + + " Phone numbers may be entered in any format, such as or xxx-xxx-xxxx, with an optional extension up to 5 digits long. International numbers should begin with a '+' sign."; + var phoneNumber = PhoneNumber.CreateFromParts( "", "", "" ); + + // NOTE: AllowSurroundingGarbage does not apply to first five or international numbers. + + // First-five shortcut (intra-org phone numbers) + if( firstFives != null && Regex.IsMatch( trimmedInput, @"^\d{5}$" ) ) { + if( firstFives.ContainsKey( trimmedInput.Substring( 0, 1 ) ) ) { + var firstFive = firstFives[ trimmedInput.Substring( 0, 1 ) ]; + phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + trimmedInput, "" ); + } + else + return ValidationError.Custom( ValidationErrorType.Invalid, "The five digit phone number you entered isn't recognized." ); + } + // International phone numbers + // We require a country code and then at least 7 digits (but if country code is more than one digit, we require fewer subsequent digits). + // We feel this is a reasonable limit to ensure that they are entering an actual phone number, but there is no source for this limit. + // We have no idea why we ever began accepting letters, but it's risky to stop accepting them and the consequences of accepting them are small. + else if( Regex.IsMatch( trimmedInput, @"\+\s*[0|2-9]([a-zA-Z,#/ \.\(\)\*]*[0-9]){7}" ) ) + phoneNumber = PhoneNumber.CreateInternational( trimmedInput ); + // Validated it as a North American Numbering Plan phone number + else { + var regex = @"(?\+?1)?\s*(?\d{3})\s*(?\d{3})\s*(?\d{4})\s*?(?:(?:x|\s|ext|ext\.|extension)\s*(?\d{1,5}))?\s*"; + if( !allowSurroundingGarbage ) + regex = "^" + regex + "$"; + + var match = Regex.Match( trimmedInput, regex ); + + if( match.Success ) { + var areaCode = match.Groups[ "ac" ].Value; + var number = match.Groups[ "num1" ].Value + match.Groups[ "num2" ].Value; + var extension = match.Groups[ "ext" ].Value; + phoneNumber = PhoneNumber.CreateFromParts( areaCode, number, extension ); + if( !allowExtension && phoneNumber.Extension.Length > 0 ) + return ValidationError.Custom( + ValidationErrorType.Invalid, + invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ); + } + else + return ValidationError.Custom( ValidationErrorType.Invalid, invalidMessage ); + } + + valueSetter( phoneNumber ); + return null; + }, + emptyValue: PhoneNumber.CreateFromParts( "", "", "" ) )!; + } + + /// + /// Returns a validated phone number extension as a string. + /// If allow empty is true and the empty string or null is given, the empty string is returned. + /// + public static ValidationResult GetPhoneNumberExtension( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.executeStringValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + if( !Regex.IsMatch( trimmedInput, @"^ *(?\d{1,5}) *$" ) ) + return ValidationError.Invalid(); + + valueSetter( trimmedInput ); + return null; + } ); +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 85a8f88..0f82c45 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Text.RegularExpressions; namespace Tewl.InputValidation; @@ -370,133 +369,6 @@ public ValidationResult GetDecimal( ValidationErrorHandler? errorHandle return null; } - /// - /// The same as GetPhoneNumber, except the given default area code will be prepended on the phone number if necessary. - /// This is useful when working with data that had the area code omitted because the number was local. - /// - public ValidationResult GetPhoneNumberWithDefaultAreaCode( - ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, string defaultAreaCode ) { - if( new Validator().GetPhoneNumber( null, input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is not null ) - // If the phone number was invalid without the area code, but is valid with the area code, we really validate using the default - // area code and then return. In all other cases, we return what would have happened without tacking on the default area code. - if( new Validator().GetPhoneNumber( null, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ).Error( out _ ) is null ) - return GetPhoneNumber( errorHandler, defaultAreaCode + input, allowExtension, allowEmpty, allowSurroundingGarbage ); - - return GetPhoneNumber( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage ); - } - - /// - /// Returns a validated phone number as a standard phone number string given the complete phone number with optional - /// extension as a string. If allow empty is true and an empty string or null is given, the empty string is returned. - /// Pass true for allow surrounding garbage if you want to allow "The phone number is 585-455-6476yadayada." to be parsed - /// into 585-455-6476 - /// and count as a valid phone number. - /// - public ValidationResult GetPhoneNumber( - ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage ) => - GetPhoneWithLastFiveMapping( errorHandler, input, allowExtension, allowEmpty, allowSurroundingGarbage, null ); - - /// - /// Returns a validated phone number as a standard phone number string given the complete phone number with optional - /// extension or the last five digits of the number and a dictionary of single - /// digits to five-digit groups that become the first five digits of the full number. If allow empty is true and an empty - /// string or null is given, the empty string is returned. - /// - public ValidationResult GetPhoneWithLastFiveMapping( - ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - Dictionary? firstFives ) { - return handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - if( new Validator().GetPhoneNumberAsObject( null, trimmedInput, allowExtension, allowEmpty, allowSurroundingGarbage, firstFives ) - .Error( out var value ) is {} error ) - return error; - - valueSetter( value.StandardPhoneString ); - return null; - } ); - } - - internal ValidationResult GetPhoneNumberAsObject( - ValidationErrorHandler? errorHandler, string input, bool allowExtension, bool allowEmpty, bool allowSurroundingGarbage, - Dictionary? firstFives ) { - return ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - var invalidPrefix = "The {0} (" + trimmedInput + ") is invalid."; - // Remove all of the valid delimiter characters so we can just deal with numbers and whitespace - trimmedInput = trimmedInput.RemoveCharacters( '-', '(', ')', '.' ).Trim(); - - var invalidMessage = invalidPrefix + - " Phone numbers may be entered in any format, such as or xxx-xxx-xxxx, with an optional extension up to 5 digits long. International numbers should begin with a '+' sign."; - var phoneNumber = PhoneNumber.CreateFromParts( "", "", "" ); - - // NOTE: AllowSurroundingGarbage does not apply to first five or international numbers. - - // First-five shortcut (intra-org phone numbers) - if( firstFives != null && Regex.IsMatch( trimmedInput, @"^\d{5}$" ) ) { - if( firstFives.ContainsKey( trimmedInput.Substring( 0, 1 ) ) ) { - var firstFive = firstFives[ trimmedInput.Substring( 0, 1 ) ]; - phoneNumber = PhoneNumber.CreateFromParts( firstFive.Substring( 0, 3 ), firstFive.Substring( 3 ) + trimmedInput, "" ); - } - else - return ValidationError.Custom( ValidationErrorType.Invalid, "The five digit phone number you entered isn't recognized." ); - } - // International phone numbers - // We require a country code and then at least 7 digits (but if country code is more than one digit, we require fewer subsequent digits). - // We feel this is a reasonable limit to ensure that they are entering an actual phone number, but there is no source for this limit. - // We have no idea why we ever began accepting letters, but it's risky to stop accepting them and the consequences of accepting them are small. - else if( Regex.IsMatch( trimmedInput, @"\+\s*[0|2-9]([a-zA-Z,#/ \.\(\)\*]*[0-9]){7}" ) ) - phoneNumber = PhoneNumber.CreateInternational( trimmedInput ); - // Validated it as a North American Numbering Plan phone number - else { - var regex = @"(?\+?1)?\s*(?\d{3})\s*(?\d{3})\s*(?\d{4})\s*?(?:(?:x|\s|ext|ext\.|extension)\s*(?\d{1,5}))?\s*"; - if( !allowSurroundingGarbage ) - regex = "^" + regex + "$"; - - var match = Regex.Match( trimmedInput, regex ); - - if( match.Success ) { - var areaCode = match.Groups[ "ac" ].Value; - var number = match.Groups[ "num1" ].Value + match.Groups[ "num2" ].Value; - var extension = match.Groups[ "ext" ].Value; - phoneNumber = PhoneNumber.CreateFromParts( areaCode, number, extension ); - if( !allowExtension && phoneNumber.Extension.Length > 0 ) - return ValidationError.Custom( - ValidationErrorType.Invalid, - invalidPrefix + " Extensions are not permitted in this field. Use the separate extension field." ); - } - else - return ValidationError.Custom( ValidationErrorType.Invalid, invalidMessage ); - } - - valueSetter( phoneNumber ); - return null; - }, - emptyValue: PhoneNumber.CreateFromParts( "", "", "" ) )!; - } - - /// - /// Returns a validated phone number extension as a string. - /// If allow empty is true and the empty string or null is given, the empty string is returned. - /// - public ValidationResult GetPhoneNumberExtension( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - handleEmptyAndReturnEmptyStringIfInvalid( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - if( !Regex.IsMatch( trimmedInput, @"^ *(?\d{1,5}) *$" ) ) - return ValidationError.Invalid(); - - valueSetter( trimmedInput ); - return null; - } ); - /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// From 2537e6f26bbd429058f2e8ff734aaa3f78d65e29 Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 10:54:22 -0400 Subject: [PATCH 21/28] Moved numeric validations to their own file --- .../Validation Methods/Numeric.cs | 247 ++++++++++++++++++ Tewl/InputValidation/Validator.cs | 228 ---------------- 2 files changed, 247 insertions(+), 228 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/Numeric.cs diff --git a/Tewl/InputValidation/Validation Methods/Numeric.cs b/Tewl/InputValidation/Validation Methods/Numeric.cs new file mode 100644 index 0000000..859d371 --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/Numeric.cs @@ -0,0 +1,247 @@ +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + /// + /// Returns the validated byte type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetByte( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); + + /// + /// Returns the validated byte type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetByte( this Validator validator, ValidationErrorHandler? errorHandler, string input, byte min, byte max ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns the validated byte type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableByte( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, byte.MinValue, byte.MaxValue ) ); + + /// + /// Returns the validated short type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetShort( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.GetShort( errorHandler, input, short.MinValue, short.MaxValue ); + + /// + /// Returns the validated short type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetShort( this Validator validator, ValidationErrorHandler? errorHandler, string input, short min, short max ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns the validated short type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableShort( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.GetNullableShort( errorHandler, input, allowEmpty, short.MinValue, short.MaxValue ); + + /// + /// Returns the validated short type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableShort( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, short min, short max ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); + + /// + /// Returns the validated int type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetInt( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.GetInt( errorHandler, input, int.MinValue, int.MaxValue ); + + /// + /// Returns the validated int type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// and are inclusive. + /// + public static ValidationResult GetInt( this Validator validator, ValidationErrorHandler? errorHandler, string input, int min, int max ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns the validated int type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableInt( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); + + /// + /// Returns the validated long type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// and are inclusive. + /// + public static ValidationResult GetLong( + this Validator validator, ValidationErrorHandler? errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns the validated long type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableLong( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); + + private static ValidationError? validateGenericIntegerType( Action valueSetter, string trimmedInput, long minValue, long maxValue ) { + long intResult; + + try { + intResult = Convert.ToInt64( trimmedInput ); + + if( intResult > maxValue ) + return ValidationError.TooLarge( minValue, maxValue ); + if( intResult < minValue ) + return ValidationError.TooSmall( minValue, maxValue ); + } + catch( FormatException ) { + return ValidationError.Invalid(); + } + catch( OverflowException ) { + return ValidationError.Invalid(); + } + + valueSetter( (T)Convert.ChangeType( intResult, typeof( T ) ) ); + return null; + } + + /// + /// Returns a validated float type from the given string, validation package, and min/max restrictions. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetFloat( this Validator validator, ValidationErrorHandler? errorHandler, string input, float min, float max ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns a validated float type from the given string, validation package, and min/max restrictions. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableFloat( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, float min, float max ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateFloat( value => valueSetter( value ), trimmedInput, min, max ) ); + + private static ValidationError? validateFloat( Action valueSetter, string trimmedInput, float min, float max ) { + float floatValue; + try { + floatValue = float.Parse( trimmedInput ); + } + catch( FormatException ) { + return ValidationError.Invalid(); + } + catch( OverflowException ) { + return ValidationError.Invalid(); + } + + if( floatValue < min ) + return ValidationError.TooSmall( min, max ); + if( floatValue > max ) + return ValidationError.TooLarge( min, max ); + + valueSetter( floatValue ); + return null; + } + + /// + /// Returns a validated decimal type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetDecimal( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); + + /// + /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult + GetDecimal( this Validator validator, ValidationErrorHandler? errorHandler, string input, decimal min, decimal max ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); + + /// + /// Returns a validated decimal type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult + GetNullableDecimal( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.GetNullableDecimal( errorHandler, input, allowEmpty, decimal.MinValue, decimal.MaxValue ); + + /// + /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableDecimal( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateDecimal( value => valueSetter( value ), trimmedInput, min, max ) ); + + private static ValidationError? validateDecimal( Action valueSetter, string trimmedInput, decimal min, decimal max ) { + decimal decimalVal; + try { + decimalVal = decimal.Parse( trimmedInput ); + if( decimalVal < min ) + return ValidationError.TooSmall( min, max ); + if( decimalVal > max ) + return ValidationError.TooLarge( min, max ); + } + catch { + return ValidationError.Invalid(); + } + + valueSetter( decimalVal ); + return null; + } +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 0f82c45..b1038cb 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -141,234 +141,6 @@ public ValidationResult GetBoolean( ValidationErrorHandler? errorHandler, return null; } - /// - /// Returns the validated byte type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetByte( ValidationErrorHandler? errorHandler, string input ) => GetByte( errorHandler, input, byte.MinValue, byte.MaxValue ); - - /// - /// Returns the validated byte type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetByte( ValidationErrorHandler? errorHandler, string input, byte min, byte max ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns the validated byte type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableByte( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, byte.MinValue, byte.MaxValue ) ); - - /// - /// Returns the validated short type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetShort( ValidationErrorHandler? errorHandler, string input ) => - GetShort( errorHandler, input, short.MinValue, short.MaxValue ); - - /// - /// Returns the validated short type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetShort( ValidationErrorHandler? errorHandler, string input, short min, short max ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns the validated short type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableShort( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - GetNullableShort( errorHandler, input, allowEmpty, short.MinValue, short.MaxValue ); - - /// - /// Returns the validated short type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableShort( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, short min, short max ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); - - /// - /// Returns the validated int type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetInt( ValidationErrorHandler? errorHandler, string input ) => GetInt( errorHandler, input, int.MinValue, int.MaxValue ); - - /// - /// Returns the validated int type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// and are inclusive. - /// - public ValidationResult GetInt( ValidationErrorHandler? errorHandler, string input, int min, int max ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns the validated int type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableInt( - ValidationErrorHandler? errorHandler, string input, bool allowEmpty, int min = int.MinValue, int max = int.MaxValue ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); - - /// - /// Returns the validated long type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// and are inclusive. - /// - public ValidationResult GetLong( ValidationErrorHandler? errorHandler, string input, long min = long.MinValue, long max = long.MaxValue ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns the validated long type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableLong( - ValidationErrorHandler? errorHandler, string input, bool allowEmpty, long min = long.MinValue, long max = long.MaxValue ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateGenericIntegerType( value => valueSetter( value ), trimmedInput, min, max ) ); - - private static ValidationError? validateGenericIntegerType( Action valueSetter, string trimmedInput, long minValue, long maxValue ) { - long intResult; - - try { - intResult = Convert.ToInt64( trimmedInput ); - - if( intResult > maxValue ) - return ValidationError.TooLarge( minValue, maxValue ); - if( intResult < minValue ) - return ValidationError.TooSmall( minValue, maxValue ); - } - catch( FormatException ) { - return ValidationError.Invalid(); - } - catch( OverflowException ) { - return ValidationError.Invalid(); - } - - valueSetter( (T)Convert.ChangeType( intResult, typeof( T ) ) ); - return null; - } - - /// - /// Returns a validated float type from the given string, validation package, and min/max restrictions. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetFloat( ValidationErrorHandler? errorHandler, string input, float min, float max ) => - ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateFloat( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns a validated float type from the given string, validation package, and min/max restrictions. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableFloat( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, float min, float max ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateFloat( value => valueSetter( value ), trimmedInput, min, max ) ); - - private static ValidationError? validateFloat( Action valueSetter, string trimmedInput, float min, float max ) { - float floatValue; - try { - floatValue = float.Parse( trimmedInput ); - } - catch( FormatException ) { - return ValidationError.Invalid(); - } - catch( OverflowException ) { - return ValidationError.Invalid(); - } - - if( floatValue < min ) - return ValidationError.TooSmall( min, max ); - if( floatValue > max ) - return ValidationError.TooLarge( min, max ); - - valueSetter( floatValue ); - return null; - } - - /// - /// Returns a validated decimal type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetDecimal( ValidationErrorHandler? errorHandler, string input ) => - GetDecimal( errorHandler, input, decimal.MinValue, decimal.MaxValue ); - - /// - /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetDecimal( ValidationErrorHandler? errorHandler, string input, decimal min, decimal max ) => - ExecuteValidation( errorHandler, input, false, ( valueSetter, trimmedInput ) => validateDecimal( valueSetter, trimmedInput, min, max ) ); - - /// - /// Returns a validated decimal type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableDecimal( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - GetNullableDecimal( errorHandler, input, allowEmpty, decimal.MinValue, decimal.MaxValue ); - - /// - /// Returns a validated decimal type from the given string, validation package, and min/max restrictions. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableDecimal( ValidationErrorHandler? errorHandler, string input, bool allowEmpty, decimal min, decimal max ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateDecimal( value => valueSetter( value ), trimmedInput, min, max ) ); - - private static ValidationError? validateDecimal( Action valueSetter, string trimmedInput, decimal min, decimal max ) { - decimal decimalVal; - try { - decimalVal = decimal.Parse( trimmedInput ); - if( decimalVal < min ) - return ValidationError.TooSmall( min, max ); - if( decimalVal > max ) - return ValidationError.TooLarge( min, max ); - } - catch { - return ValidationError.Invalid(); - } - - valueSetter( decimalVal ); - return null; - } - /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// From 1625544767a6b5bd2a75c2c1839c1fdf8ad6f7da Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 10:56:37 -0400 Subject: [PATCH 22/28] Moved boolean validations to their own file --- .../Validation Methods/Boolean.cs | 31 +++++++++++++++++++ Tewl/InputValidation/Validator.cs | 28 ----------------- 2 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/Boolean.cs diff --git a/Tewl/InputValidation/Validation Methods/Boolean.cs b/Tewl/InputValidation/Validation Methods/Boolean.cs new file mode 100644 index 0000000..3152897 --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/Boolean.cs @@ -0,0 +1,31 @@ +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + /// + /// Accepts either true/false (case-sensitive) or 1/0. + /// Returns the validated boolean type from the given string and validation package. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetBoolean( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.ExecuteValidation( errorHandler, input, false, validateBoolean ); + + /// + /// Accepts either true/false (case-sensitive) or 1/0. + /// Returns the validated boolean type from the given string and validation package. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableBoolean( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateBoolean( value => valueSetter( value ), trimmedInput ) ); + + private static ValidationError? validateBoolean( Action valueSetter, string trimmedInput ) { + if( trimmedInput != "1" && trimmedInput != "0" && trimmedInput != true.ToString() && trimmedInput != false.ToString() ) + return ValidationError.Invalid(); + + valueSetter( trimmedInput == "1" || trimmedInput == true.ToString() ); + return null; + } +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index b1038cb..1a24e56 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -113,34 +113,6 @@ public void NoteErrorAndAddMessages( params string[] messages ) { NoteErrorAndAddMessage( message ); } - /// - /// Accepts either true/false (case-sensitive) or 1/0. - /// Returns the validated boolean type from the given string and validation package. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetBoolean( ValidationErrorHandler? errorHandler, string input ) => - ExecuteValidation( errorHandler, input, false, validateBoolean ); - - /// - /// Accepts either true/false (case-sensitive) or 1/0. - /// Returns the validated boolean type from the given string and validation package. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableBoolean( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateBoolean( value => valueSetter( value ), trimmedInput ) ); - - private static ValidationError? validateBoolean( Action valueSetter, string trimmedInput ) { - if( trimmedInput != "1" && trimmedInput != "0" && trimmedInput != true.ToString() && trimmedInput != false.ToString() ) - return ValidationError.Invalid(); - - valueSetter( trimmedInput == "1" || trimmedInput == true.ToString() ); - return null; - } - /// /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. /// From f50778e835c5da5aaf410d5d8da033f63252c2dd Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 10:59:09 -0400 Subject: [PATCH 23/28] Moved ZIP code validations to their own file --- .../Validation Methods/ZipCode.cs | 16 ++++++++++++++++ Tewl/InputValidation/Validator.cs | 12 ------------ 2 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/ZipCode.cs diff --git a/Tewl/InputValidation/Validation Methods/ZipCode.cs b/Tewl/InputValidation/Validation Methods/ZipCode.cs new file mode 100644 index 0000000..5ea1a08 --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/ZipCode.cs @@ -0,0 +1,16 @@ +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + /// + /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. + /// + public static ValidationResult GetZipCode( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, emptyValue: new ZipCode() )!; + + /// + /// Gets a validated US or Canadian zip code. + /// + public static ValidationResult GetUsOrCanadianZipCode( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, emptyValue: new ZipCode() )!; +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 1a24e56..1049873 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -113,18 +113,6 @@ public void NoteErrorAndAddMessages( params string[] messages ) { NoteErrorAndAddMessage( message ); } - /// - /// Gets a validated United States zip code object given the complete zip code with optional +4 digits. - /// - public ValidationResult GetZipCode( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsZipCode, emptyValue: new ZipCode() )!; - - /// - /// Gets a validated US or Canadian zip code. - /// - public ValidationResult GetUsOrCanadianZipCode( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( errorHandler, input, allowEmpty, ZipCode.CreateUsOrCanadianZipCode, emptyValue: new ZipCode() )!; - /// /// Returns the validated DateTime type from the given string and validation package. /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. From 195de52d280a3a2eb13bbeb10319bd3615e4e21e Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 11:08:03 -0400 Subject: [PATCH 24/28] Moved date/time validations to their own file --- .../Validation Methods/DateAndTime.cs | 216 ++++++++++++++++++ Tewl/InputValidation/Validator.cs | 208 +---------------- 2 files changed, 217 insertions(+), 207 deletions(-) create mode 100644 Tewl/InputValidation/Validation Methods/DateAndTime.cs diff --git a/Tewl/InputValidation/Validation Methods/DateAndTime.cs b/Tewl/InputValidation/Validation Methods/DateAndTime.cs new file mode 100644 index 0000000..3197976 --- /dev/null +++ b/Tewl/InputValidation/Validation Methods/DateAndTime.cs @@ -0,0 +1,216 @@ +using System.Globalization; + +namespace Tewl.InputValidation; + +partial class ValidatorExtensions { + private static readonly DateTime sqlSmallDateTimeMinValue = new( 1900, 1, 1 ); + private static readonly DateTime sqlSmallDateTimeMaxValue = new( 2079, 6, 6 ); + + /// + /// Returns the validated DateTime type from the given string and validation package. + /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. + /// Passing an empty string or null will result in ErrorCondition.Empty. + /// + public static ValidationResult GetSqlSmallDateTime( this Validator validator, ValidationErrorHandler? errorHandler, string input ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, null, sqlSmallDateTimeMinValue, sqlSmallDateTimeMaxValue ) ); + + /// + /// Returns the validated DateTime type from the given string and validation package. + /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. + /// If allowEmpty is true and the given string is empty, null will be returned. + /// + public static ValidationResult GetNullableSqlSmallDateTime( + this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value ), + trimmedInput, + null, + sqlSmallDateTimeMinValue, + sqlSmallDateTimeMaxValue ) ); + + /// + /// Returns the validated DateTime type from the given date part strings and validation package. + /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. + /// Passing an empty string or null for each date part will result in ErrorCondition.Empty. + /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. + /// + public static ValidationResult GetSqlSmallDateTimeFromParts( + this Validator validator, ValidationErrorHandler? errorHandler, string month, string day, string year ) => + validator.GetSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ) ); + + /// + /// Returns the validated DateTime type from the given date part strings and validation package. + /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. + /// If allowEmpty is true and each date part string is empty, null will be returned. + /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. + /// + public static ValidationResult GetNullableSqlSmallDateTimeFromParts( + this Validator validator, ValidationErrorHandler? errorHandler, string month, string day, string year, bool allowEmpty ) => + validator.GetNullableSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ), allowEmpty ); + + private static string makeDateFromParts( string month, string day, string year ) { + var date = month + '/' + day + '/' + year; + if( date == "//" ) + date = ""; + return date; + } + + /// + /// Returns the validated DateTime type from a date string and an exact match pattern. + /// Pattern specifies the date format, such as "MM/dd/yyyy". + /// + public static ValidationResult GetSqlSmallDateTimeExact( + this Validator validator, ValidationErrorHandler? errorHandler, string input, string pattern ) => + validator.ExecuteValidation( + errorHandler, + input, + false, + ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( valueSetter, trimmedInput, pattern ) ); + + /// + /// Returns the validated DateTime type from a date string and an exact match pattern. + /// Pattern specifies the date format, such as "MM/dd/yyyy". + /// + public static ValidationResult GetNullableSqlSmallDateTimeExact( + this Validator validator, ValidationErrorHandler? errorHandler, string input, string pattern, bool allowEmpty ) => + validator.ExecuteValidation( + errorHandler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( value => valueSetter( value ), trimmedInput, pattern ) ); + + private static ValidationError? validateSqlSmallDateTimeExact( Action valueSetter, string trimmedInput, string pattern ) { + if( !DateTime.TryParseExact( trimmedInput, pattern, Cultures.EnglishUnitedStates, DateTimeStyles.None, out var date ) ) + return ValidationError.Invalid(); + if( validateNativeDateTime( date, false, sqlSmallDateTimeMinValue, sqlSmallDateTimeMaxValue ) is {} error ) + return error; + + valueSetter( date ); + return null; + } + + private static ValidationError? validateDateTime( Action valueSetter, string trimmedInput, string[]? formats, DateTime min, DateTime max ) { + DateTime date; + try { + date = formats is not null + ? DateTime.ParseExact( trimmedInput, formats, null, DateTimeStyles.None ) + : DateTime.Parse( trimmedInput, Cultures.EnglishUnitedStates ); + if( validateNativeDateTime( date, false, min, max ) is {} error ) + return error; + } + catch( FormatException ) { + return ValidationError.Invalid(); + } + catch( ArgumentOutOfRangeException ) { + // Undocumented exception that there are reports of being thrown + return ValidationError.Invalid(); + } + catch( ArgumentNullException ) { + return ValidationError.Empty(); + } + + valueSetter( date ); + return null; + } + + private static ValidationError? validateNativeDateTime( DateTime? date, bool allowEmpty, DateTime minDate, DateTime maxDate ) { + if( date is null && !allowEmpty ) + return ValidationError.Empty(); + if( date.HasValue ) { + var minMaxMessage = " It must be between " + minDate.ToDayMonthYearString( false ) + " and " + maxDate.ToDayMonthYearString( false ) + "."; + if( date < minDate ) + return ValidationError.Custom( ValidationErrorType.TooEarly, "The {0} is too early." + minMaxMessage ); + if( date >= maxDate ) + return ValidationError.Custom( ValidationErrorType.TooLate, "The {0} is too late." + minMaxMessage ); + } + return null; + } + + /// + /// Validates the date using given allowEmpty, min, and max constraints. + /// + public static ValidationResult GetNullableDateTime( + this Validator validator, ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => + validator.ExecuteValidation( + handler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value ), trimmedInput, formats, minDate, maxDate ) ); + + /// + /// Validates the date using given min and max constraints. + /// + public static ValidationResult GetDateTime( + this Validator validator, ValidationErrorHandler? handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => + validator.ExecuteValidation( + handler, + input, + false, + ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, formats, minDate, maxDate ) ); + + /// + /// Validates the given time span. + /// + public static ValidationResult + GetNullableTimeSpan( this Validator validator, ValidationErrorHandler? handler, TimeSpan? input, bool allowEmpty ) => + validator.ExecuteValidation( + handler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => { + valueSetter( trimmedInput!.Value ); + return null; + } ); + + /// + /// Validates the given time span. + /// + public static ValidationResult GetTimeSpan( this Validator validator, ValidationErrorHandler? handler, TimeSpan? input ) => + validator.ExecuteValidation( + handler, + input, + false, + ( valueSetter, trimmedInput ) => { + valueSetter( trimmedInput!.Value ); + return null; + } ); + + /// + /// Validates the given time span. + /// + public static ValidationResult GetNullableTimeOfDayTimeSpan( + this Validator validator, ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty ) => + validator.ExecuteValidation( + handler, + input, + allowEmpty, + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value.TimeOfDay ), + trimmedInput, + formats, + DateTime.MinValue, + DateTime.MaxValue ) ); + + /// + /// Validates the given time span. + /// + public static ValidationResult GetTimeOfDayTimeSpan( this Validator validator, ValidationErrorHandler? handler, string input, string[]? formats ) => + validator.ExecuteValidation( + handler, + input, + false, + ( valueSetter, trimmedInput ) => validateDateTime( + value => valueSetter( value.TimeOfDay ), + trimmedInput, + formats, + DateTime.MinValue, + DateTime.MaxValue ) ); +} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 1049873..6e5b69d 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -1,6 +1,4 @@ -using System.Globalization; - -namespace Tewl.InputValidation; +namespace Tewl.InputValidation; /// /// Contains high-level validation methods. Each validation method returns an object that is the value of the validated @@ -11,9 +9,6 @@ namespace Tewl.InputValidation; /// [ PublicAPI ] public class Validator { - internal static readonly DateTime SqlSmallDateTimeMinValue = new DateTime( 1900, 1, 1 ); - internal static readonly DateTime SqlSmallDateTimeMaxValue = new DateTime( 2079, 6, 6 ); - /// /// Most of our SQL Server decimal columns are specified as (9,2). This is the minimum value that will fit in such a /// column. @@ -113,207 +108,6 @@ public void NoteErrorAndAddMessages( params string[] messages ) { NoteErrorAndAddMessage( message ); } - /// - /// Returns the validated DateTime type from the given string and validation package. - /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. - /// Passing an empty string or null will result in ErrorCondition.Empty. - /// - public ValidationResult GetSqlSmallDateTime( ValidationErrorHandler? errorHandler, string input ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, null, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) ); - - /// - /// Returns the validated DateTime type from the given string and validation package. - /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. - /// If allowEmpty is true and the given string is empty, null will be returned. - /// - public ValidationResult GetNullableSqlSmallDateTime( ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateDateTime( - value => valueSetter( value ), - trimmedInput, - null, - SqlSmallDateTimeMinValue, - SqlSmallDateTimeMaxValue ) ); - - /// - /// Returns the validated DateTime type from the given date part strings and validation package. - /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. - /// Passing an empty string or null for each date part will result in ErrorCondition.Empty. - /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. - /// - public ValidationResult GetSqlSmallDateTimeFromParts( ValidationErrorHandler? errorHandler, string month, string day, string year ) => - GetSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ) ); - - /// - /// Returns the validated DateTime type from the given date part strings and validation package. - /// It is restricted to the Sql SmallDateTime range of 1/1/1900 up to 6/6/2079. - /// If allowEmpty is true and each date part string is empty, null will be returned. - /// Passing an empty string or null for only some date parts will result in ErrorCondition.Invalid. - /// - public ValidationResult GetNullableSqlSmallDateTimeFromParts( - ValidationErrorHandler? errorHandler, string month, string day, string year, bool allowEmpty ) => - GetNullableSqlSmallDateTime( errorHandler, makeDateFromParts( month, day, year ), allowEmpty ); - - private static string makeDateFromParts( string month, string day, string year ) { - var date = month + '/' + day + '/' + year; - if( date == "//" ) - date = ""; - return date; - } - - /// - /// Returns the validated DateTime type from a date string and an exact match pattern.' - /// Pattern specifies the date format, such as "MM/dd/yyyy". - /// - public ValidationResult GetSqlSmallDateTimeExact( ValidationErrorHandler? errorHandler, string input, string pattern ) => - ExecuteValidation( - errorHandler, - input, - false, - ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( valueSetter, trimmedInput, pattern ) ); - - /// - /// Returns the validated DateTime type from a date string and an exact match pattern.' - /// Pattern specifies the date format, such as "MM/dd/yyyy". - /// - public ValidationResult GetNullableSqlSmallDateTimeExact( ValidationErrorHandler? errorHandler, string input, string pattern, bool allowEmpty ) => - ExecuteValidation( - errorHandler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateSqlSmallDateTimeExact( value => valueSetter( value ), trimmedInput, pattern ) ); - - private static ValidationError? validateSqlSmallDateTimeExact( Action valueSetter, string trimmedInput, string pattern ) { - if( !DateTime.TryParseExact( trimmedInput, pattern, Cultures.EnglishUnitedStates, DateTimeStyles.None, out var date ) ) - return ValidationError.Invalid(); - if( validateNativeDateTime( date, false, SqlSmallDateTimeMinValue, SqlSmallDateTimeMaxValue ) is {} error ) - return error; - - valueSetter( date ); - return null; - } - - private static ValidationError? validateDateTime( Action valueSetter, string trimmedInput, string[]? formats, DateTime min, DateTime max ) { - DateTime date; - try { - date = formats is not null - ? DateTime.ParseExact( trimmedInput, formats, null, DateTimeStyles.None ) - : DateTime.Parse( trimmedInput, Cultures.EnglishUnitedStates ); - if( validateNativeDateTime( date, false, min, max ) is {} error ) - return error; - } - catch( FormatException ) { - return ValidationError.Invalid(); - } - catch( ArgumentOutOfRangeException ) { - // Undocumented exception that there are reports of being thrown - return ValidationError.Invalid(); - } - catch( ArgumentNullException ) { - return ValidationError.Empty(); - } - - valueSetter( date ); - return null; - } - - private static ValidationError? validateNativeDateTime( DateTime? date, bool allowEmpty, DateTime minDate, DateTime maxDate ) { - if( date is null && !allowEmpty ) - return ValidationError.Empty(); - if( date.HasValue ) { - var minMaxMessage = " It must be between " + minDate.ToDayMonthYearString( false ) + " and " + maxDate.ToDayMonthYearString( false ) + "."; - if( date < minDate ) - return ValidationError.Custom( ValidationErrorType.TooEarly, "The {0} is too early." + minMaxMessage ); - if( date >= maxDate ) - return ValidationError.Custom( ValidationErrorType.TooLate, "The {0} is too late." + minMaxMessage ); - } - return null; - } - - /// - /// Validates the date using given allowEmpty, min, and max constraints. - /// - public ValidationResult GetNullableDateTime( - ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty, DateTime minDate, DateTime maxDate ) => - ExecuteValidation( - handler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateDateTime( value => valueSetter( value ), trimmedInput, formats, minDate, maxDate ) ); - - /// - /// Validates the date using given min and max constraints. - /// - public ValidationResult GetDateTime( ValidationErrorHandler? handler, string input, string[]? formats, DateTime minDate, DateTime maxDate ) => - ExecuteValidation( - handler, - input, - false, - ( valueSetter, trimmedInput ) => validateDateTime( valueSetter, trimmedInput, formats, minDate, maxDate ) ); - - /// - /// Validates the given time span. - /// - public ValidationResult GetNullableTimeSpan( ValidationErrorHandler? handler, TimeSpan? input, bool allowEmpty ) => - ExecuteValidation( - handler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => { - valueSetter( trimmedInput!.Value ); - return null; - } ); - - /// - /// Validates the given time span. - /// - public ValidationResult GetTimeSpan( ValidationErrorHandler? handler, TimeSpan? input ) => - ExecuteValidation( - handler, - input, - false, - ( valueSetter, trimmedInput ) => { - valueSetter( trimmedInput!.Value ); - return null; - } ); - - /// - /// Validates the given time span. - /// - public ValidationResult GetNullableTimeOfDayTimeSpan( ValidationErrorHandler? handler, string input, string[]? formats, bool allowEmpty ) => - ExecuteValidation( - handler, - input, - allowEmpty, - ( valueSetter, trimmedInput ) => validateDateTime( - value => valueSetter( value.TimeOfDay ), - trimmedInput, - formats, - DateTime.MinValue, - DateTime.MaxValue ) ); - - /// - /// Validates the given time span. - /// - public ValidationResult GetTimeOfDayTimeSpan( ValidationErrorHandler? handler, string input, string[]? formats ) => - ExecuteValidation( - handler, - input, - false, - ( valueSetter, trimmedInput ) => validateDateTime( - value => valueSetter( value.TimeOfDay ), - trimmedInput, - formats, - DateTime.MinValue, - DateTime.MaxValue ) ); - /// /// Executes a validation and returns the result. /// From 3167f840a985a293835516c01910a138760d149c Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 11:24:16 -0400 Subject: [PATCH 25/28] Cleaned up Validator --- Tewl/InputValidation/Validator.cs | 62 ++++++++++--------------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 6e5b69d..9f07962 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -10,14 +10,12 @@ [ PublicAPI ] public class Validator { /// - /// Most of our SQL Server decimal columns are specified as (9,2). This is the minimum value that will fit in such a - /// column. + /// Most of our SQL Server decimal columns are specified as (9,2). This is the minimum value that will fit in such a column. /// public const decimal SqlDecimalDefaultMin = -9999999.99m; /// - /// Most of our SQL Server decimal columns are specified as (9,2). This is the maximum value that will fit in such a - /// column. + /// Most of our SQL Server decimal columns are specified as (9,2). This is the maximum value that will fit in such a column. /// public const decimal SqlDecimalDefaultMax = 9999999.99m; @@ -36,52 +34,32 @@ private static bool isEmpty( InputType input, out InputType trimmedIn return input is null; } - private readonly List errors = new List(); - /// - /// Returns true if any errors have been encountered during validation so far. This can be true even while - /// ErrorMessages.Count == 0 - /// and Errors.Count == 0, since NoteError may have been called. + /// Returns true if any errors have been encountered during validation so far. This can be true even while ErrorMessages.Count == 0 and Errors.Count == 0, + /// since NoteError may have been called. /// public bool ErrorsOccurred { get; private set; } + private readonly List errors = [ ]; + /// - /// Returns true if at least one unusable value has been returned since this Validator was created. An unusable value - /// return - /// is defined as any time a Get... fails validation and the Validator is forced to return something other than - /// a good default value. An example of an unusable value would be a call to GetInt that fails validation. An - /// example of a usable value is a call to GetNullableInt, with allowEmpty = true, that fails validation. + /// Returns true if at least one unusable value has been returned since this Validator was created. An unusable value return is defined as any time a Get… + /// fails validation and the Validator is forced to return something other than an allowed empty value. An example of an unusable value would be a call to + /// GetInt that fails validation. An example of a usable value is a call to GetNullableInt, with allowEmpty = true, that fails validation. /// - public bool UnusableValuesReturned { - get { - foreach( var error in errors ) - if( error.UnusableValueReturned ) - return true; - - return false; - } - } + public bool UnusableValuesReturned => errors.Any( i => i.UnusableValueReturned ); /// - /// Returns a deep copy of the list of error messages associated with the validation performed by this validator so far. - /// It's possible for ErrorsOccurred to + /// Returns a deep copy of the list of error messages associated with the validation performed by this validator so far. It’s possible for ErrorsOccurred to /// be true while ErrorsMessages.Count == 0, since NoteError may have been called. /// - public List ErrorMessages { - get { - var errorMessages = new List(); - foreach( var error in errors ) - errorMessages.Add( error.Message ); - return errorMessages; - } - } + public IReadOnlyCollection ErrorMessages => errors.Select( i => i.Message ).Materialize(); /// - /// Returns a deep copy of the list of errors associated with the validation performed by this validator so far. It's - /// possible for ErrorsOccurred to - /// be true while Errors.Count == 0, since NoteError may have been called. + /// Returns a deep copy of the list of errors associated with the validation performed by this validator so far. It’s possible for ErrorsOccurred to be true + /// while Errors.Count == 0, since NoteError may have been called. /// - public List Errors => new List( errors ); + public IReadOnlyCollection Errors => [ ..errors ]; /// /// Sets the ErrorsOccurred flag. @@ -89,9 +67,8 @@ public List ErrorMessages { public void NoteError() => ErrorsOccurred = true; /// - /// Sets the ErrorsOccurred flag and adds an error message to this validator. Use this if you want to add your own error - /// message to the same collection - /// that the error handlers use. + /// Sets the ErrorsOccurred flag and adds an error message to this validator. Use this if you want to add your own error message to the same collection that + /// the error handlers use. /// public void NoteErrorAndAddMessage( string message ) { NoteError(); @@ -99,9 +76,8 @@ public void NoteErrorAndAddMessage( string message ) { } /// - /// Sets the ErrorsOccurred flag and add the given error messages to this validator. Use this if you want to add your own - /// error messages to the same collection - /// that the error handlers use. + /// Sets the ErrorsOccurred flag and add the given error messages to this validator. Use this if you want to add your own error messages to the same + /// collection that the error handlers use. /// public void NoteErrorAndAddMessages( params string[] messages ) { foreach( var message in messages ) From c2f4cba79ea140ff81242ff6929d85cdaf523a02 Mon Sep 17 00:00:00 2001 From: William Gross Date: Fri, 21 Mar 2025 11:36:54 -0400 Subject: [PATCH 26/28] Converted Error class to a nested record --- Tewl/InputValidation/Error.cs | 24 ------------------------ Tewl/InputValidation/Validator.cs | 7 +++++++ 2 files changed, 7 insertions(+), 24 deletions(-) delete mode 100644 Tewl/InputValidation/Error.cs diff --git a/Tewl/InputValidation/Error.cs b/Tewl/InputValidation/Error.cs deleted file mode 100644 index 0b4364f..0000000 --- a/Tewl/InputValidation/Error.cs +++ /dev/null @@ -1,24 +0,0 @@ -using JetBrains.Annotations; - -namespace Tewl.InputValidation { - /// - /// Represents a validation error. - /// - [ PublicAPI ] - public class Error { - internal Error( string message, bool unusableValueReturned ) { - Message = message; - UnusableValueReturned = unusableValueReturned; - } - - /// - /// The error message. - /// - public string Message { get; } - - /// - /// Returns true if the error resulted in an unusable value being returned. - /// - public bool UnusableValueReturned { get; } - } -} \ No newline at end of file diff --git a/Tewl/InputValidation/Validator.cs b/Tewl/InputValidation/Validator.cs index 9f07962..bd1442f 100644 --- a/Tewl/InputValidation/Validator.cs +++ b/Tewl/InputValidation/Validator.cs @@ -19,6 +19,13 @@ public class Validator { /// public const decimal SqlDecimalDefaultMax = 9999999.99m; + /// + /// An error in a . + /// + /// The error message. + /// Whether the error resulted in an unusable value being returned. + public record Error( string Message, bool UnusableValueReturned ); + internal delegate ValidationError? ValidationMethod( Action valueSetter, InputType trimmedInput ); private static bool isEmpty( InputType input, out InputType trimmedInput ) { From 8987d50f2d4b9c977065e57d1b5c042326a32e54 Mon Sep 17 00:00:00 2001 From: William Gross Date: Mon, 24 Mar 2025 13:57:38 -0400 Subject: [PATCH 27/28] Renamed GetNumber to GetNumericString This clarifies that it is a text-related validation method and should not be lumped in with GetInt, GetShort, etc. --- Tewl/InputValidation/Validation Methods/Text.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tewl/InputValidation/Validation Methods/Text.cs b/Tewl/InputValidation/Validation Methods/Text.cs index a944aef..b588494 100644 --- a/Tewl/InputValidation/Validation Methods/Text.cs +++ b/Tewl/InputValidation/Validation Methods/Text.cs @@ -144,14 +144,14 @@ public static ValidationResult GetUrl( /// public static ValidationResult GetSocialSecurityNumber( this Validator validator, ValidationErrorHandler? errorHandler, string input, bool allowEmpty ) => - validator.GetNumber( errorHandler, input, 9, allowEmpty, "-" ); + validator.GetNumericString( errorHandler, input, 9, allowEmpty, "-" ); /// /// Gets a string of the given length whose characters are only numeric values, after throwing out all acceptable garbage /// characters. /// Example: A social security number (987-65-4321) would be GetNumber( errorHandler, ssn, 9, true, "-" ). /// - public static ValidationResult GetNumber( + public static ValidationResult GetNumericString( this Validator validator, ValidationErrorHandler? errorHandler, string input, int numberOfDigits, bool allowEmpty, params string[] acceptableGarbageStrings ) => validator.executeStringValidation( From e2b28f15f058268a0e422ea34519135b79fd79a3 Mon Sep 17 00:00:00 2001 From: William Gross Date: Wed, 26 Mar 2025 16:21:15 -0400 Subject: [PATCH 28/28] Fixed doc typo in ValidationResult --- Tewl/InputValidation/ValidationResult.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tewl/InputValidation/ValidationResult.cs b/Tewl/InputValidation/ValidationResult.cs index c3753c5..c1c4509 100644 --- a/Tewl/InputValidation/ValidationResult.cs +++ b/Tewl/InputValidation/ValidationResult.cs @@ -6,8 +6,8 @@ [ PublicAPI ] public class ValidationResult { /// - /// Gets the validated value. This is sometimes unusable if there was a validation error, and in that case the - /// will be true. + /// Gets the validated value. This is sometimes unusable if there was a validation error, and in that case will + /// be true. /// public T Value { get; }