feat: add property path transformation
This resolves #3 to allow custom property paths to be defined.
This commit is contained in:
parent
ef734ad02e
commit
a7ecf08efb
7 changed files with 167 additions and 14 deletions
|
|
@ -39,6 +39,13 @@ public interface IPropertyRuleBuilder<T, out TProperty>
|
|||
/// <returns>The current rule builder instance for method chaining.</returns>
|
||||
IPropertyRuleBuilder<T, TProperty> WithCode(string code);
|
||||
|
||||
/// <summary>
|
||||
/// Transforms the property path reported by the validation rule.
|
||||
/// </summary>
|
||||
/// <param name="transform">The function used to transform the rule property path.</param>
|
||||
/// <returns>The current rule builder instance for method chaining.</returns>
|
||||
IPropertyRuleBuilder<T, TProperty> WithPropertyPath(Func<PropertyPath, PropertyPath> transform);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the severity of the validation rule.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -6,24 +6,16 @@ using System.Linq.Expressions;
|
|||
namespace Geekeey.Request.Validation;
|
||||
|
||||
internal abstract record Rule
|
||||
{
|
||||
public Severity Severity { get; init; } = Severity.Error;
|
||||
|
||||
public string? Code { get; init; }
|
||||
|
||||
public abstract IEnumerable<Problem> Validate(ValidationContext context);
|
||||
}
|
||||
|
||||
internal abstract record Rule<T, TProperty> : Rule
|
||||
{
|
||||
protected Rule(Expression expression)
|
||||
{
|
||||
PropertyPath = GetPropertyPath(expression);
|
||||
}
|
||||
|
||||
public PropertyPath PropertyPath { get; }
|
||||
|
||||
public IReadOnlyList<IRuleStep<TProperty>> Steps { get; init; } = [];
|
||||
protected PropertyPath GetPropertyPath()
|
||||
{
|
||||
return PropertyPathTransform(PropertyPath);
|
||||
}
|
||||
|
||||
private static PropertyPath GetPropertyPath(Expression expression)
|
||||
{
|
||||
|
|
@ -55,6 +47,26 @@ internal abstract record Rule<T, TProperty> : Rule
|
|||
return string.Join('.', members);
|
||||
}
|
||||
|
||||
public PropertyPath PropertyPath { get; }
|
||||
|
||||
public Severity Severity { get; init; } = Severity.Error;
|
||||
|
||||
public string? Code { get; init; }
|
||||
|
||||
public Func<PropertyPath, PropertyPath> PropertyPathTransform { get; init; } = static path => path;
|
||||
|
||||
public abstract IEnumerable<Problem> Validate(ValidationContext context);
|
||||
}
|
||||
|
||||
internal abstract record Rule<T, TProperty> : Rule
|
||||
{
|
||||
protected Rule(Expression expression)
|
||||
: base(expression)
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<IRuleStep<TProperty>> Steps { get; init; } = [];
|
||||
|
||||
public override IEnumerable<Problem> Validate(ValidationContext context)
|
||||
{
|
||||
if (context.Instance is T instance)
|
||||
|
|
@ -92,7 +104,8 @@ internal sealed record PropertyRule<T, TProperty> : Rule<T, TProperty>
|
|||
}
|
||||
|
||||
var value = _accessor(instance);
|
||||
return Steps.SelectMany(step => step.Validate(value, context, PropertyPath, Code, Severity));
|
||||
var propertyPath = GetPropertyPath();
|
||||
return Steps.SelectMany(step => step.Validate(value, context, propertyPath, Code, Severity));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +133,7 @@ internal sealed record CollectionRule<T, TElement> : Rule<T, TElement>
|
|||
var index = 0;
|
||||
foreach (var element in collection)
|
||||
{
|
||||
var propertyPath = PropertyPath + index;
|
||||
var propertyPath = GetPropertyPath() + index;
|
||||
|
||||
foreach (var step in Steps)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -77,6 +77,21 @@ public static class RuleBuilderExtensions
|
|||
return rule.Must(static value => value is not null && value.Any(), "Value is required.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the current rule property path from emitted validation problems.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the object being validated.</typeparam>
|
||||
/// <typeparam name="TProperty">The type of the property being validated.</typeparam>
|
||||
/// <param name="rule">The rule builder to which the property path transform is applied.</param>
|
||||
/// <returns>The updated rule builder with the property path removed.</returns>
|
||||
public static IPropertyRuleBuilder<T, TProperty> WithoutPropertyPath<T, TProperty>(
|
||||
this IPropertyRuleBuilder<T, TProperty> rule)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(rule);
|
||||
|
||||
return rule.WithPropertyPath(static _ => string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a rule to ensure that the string property value meets the specified minimum length.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -133,6 +133,15 @@ public abstract class Validator<T> : IValidator<T>
|
|||
return this;
|
||||
}
|
||||
|
||||
public IPropertyRuleBuilder<T, TProperty> WithPropertyPath(Func<PropertyPath, PropertyPath> transform)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(transform);
|
||||
|
||||
CurrentRule = CurrentRule with { PropertyPathTransform = transform };
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public IPropertyRuleBuilder<T, TProperty> WithSeverity(Severity severity)
|
||||
{
|
||||
CurrentRule = CurrentRule with { Severity = severity };
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
- **Structured validation output:** Each failure is returned as a `Problem` with a property path, message, severity,
|
||||
code, and attempted value.
|
||||
- **Nested validation:** Reuse validators for complex object graphs with `SetValidator`, including DI-based resolution.
|
||||
- **Configurable paths:** Rewrite or remove rule property paths when the reported path should differ from the CLR
|
||||
member path.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
|
@ -65,6 +67,29 @@ public sealed class CreateUserRequestValidator : Validator<CreateUserRequest>
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record LoginInput(string? Username);
|
||||
|
||||
public sealed record LoginRequest(LoginInput Input);
|
||||
|
||||
public sealed class LoginInputValidator : Validator<LoginInput>
|
||||
{
|
||||
public LoginInputValidator()
|
||||
{
|
||||
RuleFor(input => input.Username)
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LoginRequestValidator : Validator<LoginRequest>
|
||||
{
|
||||
public LoginRequestValidator()
|
||||
{
|
||||
RuleFor(request => request.Input)
|
||||
.WithoutPropertyPath()
|
||||
.SetValidator(new LoginInputValidator());
|
||||
}
|
||||
}
|
||||
|
||||
var validator = new CreateUserRequestValidator();
|
||||
var validation = validator.Validate(new CreateUserRequest(
|
||||
Name: "",
|
||||
|
|
@ -80,3 +105,6 @@ foreach (var problem in validation.Problems)
|
|||
|
||||
The resulting `Problem` entries include full property paths like `Address.Street` and `Tags[0]`, making it easy to
|
||||
surface validation errors back to callers or APIs.
|
||||
|
||||
If the validation path needs to differ from the CLR property path, use `WithPropertyPath(...)` for a custom transform
|
||||
or `WithoutPropertyPath()` to drop the current rule path entirely before nested paths are appended.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue