From 4ab6af535c9414be68ece1221aa49b9546ec2068 Mon Sep 17 00:00:00 2001 From: KhaosVoid Date: Fri, 29 Nov 2024 02:09:04 -0600 Subject: [PATCH 1/2] Changed field named validation logic to go through a "generator" class. This class can either be used "as-is", or be overridden to alter/enhance the valid field name generation. --- .../Search/ValidFieldNameGenerator.cs | 124 ++++++++++++++++++ .../Queries/SearchQuery.cs | 7 +- .../Validators/DateFieldFilterValidator.cs | 4 +- .../DateTimeFieldFilterValidator.cs | 4 +- .../Validators/FieldFilterValidator.cs | 4 +- .../Validators/FieldSortInfoValidator.cs | 4 +- .../Queries/Validators/FiltersValidator.cs | 12 +- .../Validators/NumberFieldFilterValidator.cs | 4 +- .../Validators/OperatorFilterValidator.cs | 4 +- .../Validators/TextFieldFilterValidator.cs | 4 +- 10 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 src/Rested.Core.Data/Search/ValidFieldNameGenerator.cs diff --git a/src/Rested.Core.Data/Search/ValidFieldNameGenerator.cs b/src/Rested.Core.Data/Search/ValidFieldNameGenerator.cs new file mode 100644 index 0000000..5dbd6cd --- /dev/null +++ b/src/Rested.Core.Data/Search/ValidFieldNameGenerator.cs @@ -0,0 +1,124 @@ +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +namespace Rested.Core.Data.Search; + +public partial class ValidFieldNameGenerator +{ + #region Properties + + public List ValidFieldNames { get; protected set; } = []; + public List IgnoredFieldNames { get; protected set; } = []; + + #endregion Properties + + #region Ctor + + public ValidFieldNameGenerator(Type inputType) + { + GenerateValidFieldNames(inputType); + } + + #endregion Ctor + + #region Methods + + private void GenerateValidFieldNames(Type inputType) + { + var validFieldNames = new List(); + var ignoredFieldNames = new List(); + + GetFieldNamesFromTypeProperties(inputType, validFieldNames, ignoredFieldNames); + + ValidFieldNames = validFieldNames; + IgnoredFieldNames = ignoredFieldNames; + } + + protected virtual void GetFieldNamesFromTypeProperties(Type type, List validFieldNames = null, List ignoredFieldNames = null, string lastFieldName = null) + { + var properties = type.GetProperties(); + + validFieldNames ??= []; + ignoredFieldNames ??= []; + + foreach (var property in properties) + { + if (property.GetCustomAttribute() is not null) + ignoredFieldNames.Add($"^{CalculateFieldName(property.Name, lastFieldName)}$"); + + else if (property.GetCustomAttribute() is not null) + ignoredFieldNames.Add($"^{CalculateFieldName(property.Name, lastFieldName)}$"); + + else + OnAddValidFieldName(property, validFieldNames, lastFieldName); + + if (property.PropertyType.IsClass) + { + if (property.PropertyType.IsGenericType) + { + if (property.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) + OnAddValidListFieldName(property, validFieldNames, ignoredFieldNames, lastFieldName); + + else if (property.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + GetFieldNamesFromTypeProperties(property.PropertyType.GenericTypeArguments[1], validFieldNames, ignoredFieldNames, CalculateFieldName(property.Name, lastFieldName)); + } + + else if (!property.PropertyType.IsPrimitive && property.PropertyType != typeof(string)) + GetFieldNamesFromTypeProperties(property.PropertyType, validFieldNames, ignoredFieldNames, CalculateFieldName(property.Name, lastFieldName)); + } + + else if (property.PropertyType.IsArray) + OnAddValidArrayFieldName(property, validFieldNames, ignoredFieldNames, lastFieldName); + } + } + + protected virtual void OnAddValidFieldName(PropertyInfo property, List validFieldNames, string lastFieldName = null) + { + var propertyName = property.Name; + + if (propertyName is "Id") + OnAddValidIdFieldName(property, validFieldNames, lastFieldName); + + else + validFieldNames.Add($"^{CalculateFieldName(property.Name, lastFieldName)}$"); + } + + protected virtual void OnAddValidIdFieldName(PropertyInfo property, List validFieldNames, string lastFieldName = null) + { + validFieldNames.Add($"^{CalculateFieldName(property.Name, lastFieldName)}$"); + } + + protected string CalculateFieldName(string propertyName, string lastFieldName) + { + return string.IsNullOrWhiteSpace(lastFieldName) ? + propertyName.ToCamelCase() : + $@"{lastFieldName}\.{propertyName.ToCamelCase()}"; + } + + protected virtual void OnAddValidListFieldName(PropertyInfo property, List validFieldNames, List ignoredFieldNames, string lastFieldName = null) + { + var fieldName = $@"{CalculateFieldName(property.Name, lastFieldName)}\[\d*\]"; + + validFieldNames.Add($"^{fieldName}$"); + GetFieldNamesFromTypeProperties(property.PropertyType.GenericTypeArguments[0], validFieldNames, ignoredFieldNames, fieldName); + } + + protected virtual void OnAddValidArrayFieldName(PropertyInfo property, List validFieldNames, List ignoredFieldNames, string lastFieldName = null) + { + var fieldName = $@"{CalculateFieldName(property.Name, lastFieldName)}\[\d*\]"; + + validFieldNames.Add($"^{fieldName}$"); + GetFieldNamesFromTypeProperties(property.PropertyType.GetElementType(), validFieldNames, ignoredFieldNames, fieldName); + } + + public bool IsFieldNameValid(string fieldName) + { + if (IgnoredFieldNames.Any(ignoredFieldName => Regex.IsMatch(fieldName, ignoredFieldName))) + return false; + + return ValidFieldNames.Any(validFieldName => Regex.IsMatch(fieldName, validFieldName)); + } + + #endregion Methods +} \ No newline at end of file diff --git a/src/Rested.Core.MediatR/Queries/SearchQuery.cs b/src/Rested.Core.MediatR/Queries/SearchQuery.cs index 4cd7d6c..3e3cb13 100644 --- a/src/Rested.Core.MediatR/Queries/SearchQuery.cs +++ b/src/Rested.Core.MediatR/Queries/SearchQuery.cs @@ -71,6 +71,7 @@ public abstract class SearchQueryValidator(out var validFieldNames, out var ignoredFieldNames); - RuleFor(query => query.SearchRequest.Page) .GreaterThan(0) .WithServiceErrorCode(ServiceErrorCodes.CommonErrorCodes.PageMustBeGreaterThanZero); @@ -102,11 +101,11 @@ public SearchQueryValidator() .WithServiceErrorCode(ServiceErrorCodes.CommonErrorCodes.PageSizeMustBeLessThanOrEqualToMaxPageSize, query => [query.MaxPageSize]); RuleForEach(query => query.SearchRequest.SortingFields) - .SetValidator(_ => new FieldSortInfoValidator(validFieldNames, ignoredFieldNames, ServiceErrorCodes)) + .SetValidator(_ => new FieldSortInfoValidator(ValidFieldNameGenerator, ServiceErrorCodes)) .When(query => query.SearchRequest.SortingFields is not null && query.SearchRequest.SortingFields.Count > 0); RuleFor(query => query.SearchRequest.Filters) - .SetValidator(_ => new FiltersValidator(validFieldNames, ignoredFieldNames, ServiceErrorCodes)); + .SetValidator(_ => new FiltersValidator(ValidFieldNameGenerator, ServiceErrorCodes)); } #endregion Ctor diff --git a/src/Rested.Core.MediatR/Queries/Validators/DateFieldFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/DateFieldFilterValidator.cs index ec42d08..9b93352 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/DateFieldFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/DateFieldFilterValidator.cs @@ -6,8 +6,8 @@ namespace Rested.Core.MediatR.Queries.Validators; public class DateFieldFilterValidator : FieldFilterValidator { - public DateFieldFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) : - base(validFieldNames, ignoredFieldNames, serviceErrorCodes) + public DateFieldFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) : + base(validFieldNameGenerator, serviceErrorCodes) { When( predicate: dateFieldFilter => dateFieldFilter.FilterOperation diff --git a/src/Rested.Core.MediatR/Queries/Validators/DateTimeFieldFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/DateTimeFieldFilterValidator.cs index 23fed12..89adae2 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/DateTimeFieldFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/DateTimeFieldFilterValidator.cs @@ -6,8 +6,8 @@ namespace Rested.Core.MediatR.Queries.Validators; public class DateTimeFieldFilterValidator : FieldFilterValidator { - public DateTimeFieldFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) : - base(validFieldNames, ignoredFieldNames, serviceErrorCodes) + public DateTimeFieldFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) : + base(validFieldNameGenerator, serviceErrorCodes) { When( predicate: dateTimeFieldFilter => dateTimeFieldFilter.FilterOperation diff --git a/src/Rested.Core.MediatR/Queries/Validators/FieldFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/FieldFilterValidator.cs index 75caed3..87b350a 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/FieldFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/FieldFilterValidator.cs @@ -8,7 +8,7 @@ namespace Rested.Core.MediatR.Queries.Validators; public abstract class FieldFilterValidator : AbstractValidator where TFieldFilter : IFieldFilter { - protected FieldFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) + protected FieldFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) { RuleFor(fieldFilter => fieldFilter.FieldName) .NotEmpty() @@ -19,7 +19,7 @@ protected FieldFilterValidator(IEnumerable validFieldNames, IEnumerable< action: () => { RuleFor(fieldFilter => fieldFilter) - .Must(fieldFilter => DataUtility.IsFieldNameValid(fieldFilter.FieldName, validFieldNames, ignoredFieldNames)) + .Must(fieldFilter => validFieldNameGenerator.IsFieldNameValid(fieldFilter.FieldName)) .WithServiceErrorCode(serviceErrorCodes.CommonErrorCodes.FieldFilterNameIsInvalid); }); } diff --git a/src/Rested.Core.MediatR/Queries/Validators/FieldSortInfoValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/FieldSortInfoValidator.cs index 2088798..8b15320 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/FieldSortInfoValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/FieldSortInfoValidator.cs @@ -7,7 +7,7 @@ namespace Rested.Core.MediatR.Queries.Validators; public class FieldSortInfoValidator : AbstractValidator { - public FieldSortInfoValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) + public FieldSortInfoValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) { RuleFor(fieldSortInfo => fieldSortInfo.FieldName) .NotEmpty() @@ -18,7 +18,7 @@ public FieldSortInfoValidator(IEnumerable validFieldNames, IEnumerable { RuleFor(fieldSortInfo => fieldSortInfo) - .Must(m => DataUtility.IsFieldNameValid(m.FieldName, validFieldNames, ignoredFieldNames)) + .Must(fieldSortInfo => validFieldNameGenerator.IsFieldNameValid(fieldSortInfo.FieldName)) .WithServiceErrorCode(serviceErrorCodes.CommonErrorCodes.SortingFieldNameIsInvalid); }); } diff --git a/src/Rested.Core.MediatR/Queries/Validators/FiltersValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/FiltersValidator.cs index a1814e5..ea7c27e 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/FiltersValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/FiltersValidator.cs @@ -6,16 +6,16 @@ namespace Rested.Core.MediatR.Queries.Validators; public class FiltersValidator : AbstractValidator> { - public FiltersValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) + public FiltersValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) { RuleForEach(filters => filters) .SetInheritanceValidator(v => { - v.Add(_ => new TextFieldFilterValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)); - v.Add(_ => new NumberFieldFilterValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)); - v.Add(_ => new DateFieldFilterValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)); - v.Add(_ => new DateTimeFieldFilterValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)); - v.Add(_ => new OperatorFilterValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)); + v.Add(_ => new TextFieldFilterValidator(validFieldNameGenerator, serviceErrorCodes)); + v.Add(_ => new NumberFieldFilterValidator(validFieldNameGenerator, serviceErrorCodes)); + v.Add(_ => new DateFieldFilterValidator(validFieldNameGenerator, serviceErrorCodes)); + v.Add(_ => new DateTimeFieldFilterValidator(validFieldNameGenerator, serviceErrorCodes)); + v.Add(_ => new OperatorFilterValidator(validFieldNameGenerator, serviceErrorCodes)); }) .When(filters => filters is not null && filters.Count > 0); } diff --git a/src/Rested.Core.MediatR/Queries/Validators/NumberFieldFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/NumberFieldFilterValidator.cs index 24825c6..6a68778 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/NumberFieldFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/NumberFieldFilterValidator.cs @@ -6,8 +6,8 @@ namespace Rested.Core.MediatR.Queries.Validators; public class NumberFieldFilterValidator : FieldFilterValidator { - public NumberFieldFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) : - base(validFieldNames, ignoredFieldNames, serviceErrorCodes) + public NumberFieldFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) : + base(validFieldNameGenerator, serviceErrorCodes) { When( predicate: numberFieldFilter => numberFieldFilter.FilterOperation diff --git a/src/Rested.Core.MediatR/Queries/Validators/OperatorFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/OperatorFilterValidator.cs index 3006c3f..39f40cf 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/OperatorFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/OperatorFilterValidator.cs @@ -6,7 +6,7 @@ namespace Rested.Core.MediatR.Queries.Validators; public class OperatorFilterValidator : AbstractValidator { - public OperatorFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) + public OperatorFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) { RuleFor(operatorFilter => operatorFilter.Operator) .IsInEnum() @@ -17,7 +17,7 @@ public OperatorFilterValidator(IEnumerable validFieldNames, IEnumerable< .WithServiceErrorCode(serviceErrorCodes.CommonErrorCodes.OperatorFilterFiltersIsRequired); RuleFor(operatorFilter => operatorFilter.Filters) - .SetValidator(_ => new FiltersValidator(validFieldNames, ignoredFieldNames, serviceErrorCodes)) + .SetValidator(_ => new FiltersValidator(validFieldNameGenerator, serviceErrorCodes)) .When(operatorFilter => operatorFilter.Filters is not null && operatorFilter.Filters.Count > 0); } } \ No newline at end of file diff --git a/src/Rested.Core.MediatR/Queries/Validators/TextFieldFilterValidator.cs b/src/Rested.Core.MediatR/Queries/Validators/TextFieldFilterValidator.cs index 6534168..40a02ca 100644 --- a/src/Rested.Core.MediatR/Queries/Validators/TextFieldFilterValidator.cs +++ b/src/Rested.Core.MediatR/Queries/Validators/TextFieldFilterValidator.cs @@ -6,8 +6,8 @@ namespace Rested.Core.MediatR.Queries.Validators; public class TextFieldFilterValidator : FieldFilterValidator { - public TextFieldFilterValidator(IEnumerable validFieldNames, IEnumerable ignoredFieldNames, ServiceErrorCodes serviceErrorCodes) : - base(validFieldNames, ignoredFieldNames, serviceErrorCodes) + public TextFieldFilterValidator(ValidFieldNameGenerator validFieldNameGenerator, ServiceErrorCodes serviceErrorCodes) : + base(validFieldNameGenerator, serviceErrorCodes) { When( predicate: textFieldFilter => textFieldFilter.FilterOperation From 16136395503e85ab82da4412436693b2c6bc9125 Mon Sep 17 00:00:00 2001 From: KhaosVoid Date: Fri, 29 Nov 2024 02:22:53 -0600 Subject: [PATCH 2/2] Updated FilterValidatorTest.cs to accomodate new constructor. --- .../Queries/Validators/FilterValidatorTest.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Rested.Core.MediatR.UnitTest/Queries/Validators/FilterValidatorTest.cs b/src/Rested.Core.MediatR.UnitTest/Queries/Validators/FilterValidatorTest.cs index 4526c18..4c6eed1 100644 --- a/src/Rested.Core.MediatR.UnitTest/Queries/Validators/FilterValidatorTest.cs +++ b/src/Rested.Core.MediatR.UnitTest/Queries/Validators/FilterValidatorTest.cs @@ -78,11 +78,9 @@ public void TestCleanup() protected TFilterValidator CreateValidator() { - DataUtility.GenerateValidSearchFieldNames>(out var validFieldNames, out var ignoredFieldNames); - return (TFilterValidator)Activator.CreateInstance( type: typeof(TFilterValidator), - args: [validFieldNames, ignoredFieldNames, ServiceErrorCodes]); + args: [new ValidFieldNameGenerator(typeof(IDocument)), ServiceErrorCodes]); } protected void TestFilterValidationRule(TFilterValidator filterValidator, TFilter filter, ServiceErrorCode serviceErrorCode, params object[] messageFormatArgs)