2026-05-08 20:26:26 +02:00
|
|
|
// Copyright (c) The Geekeey Authors
|
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
|
|
|
|
|
|
using System.Linq.Expressions;
|
|
|
|
|
|
|
|
|
|
namespace Geekeey.Request.Validation;
|
|
|
|
|
|
|
|
|
|
internal abstract record Rule
|
|
|
|
|
{
|
|
|
|
|
protected Rule(Expression expression)
|
|
|
|
|
{
|
|
|
|
|
PropertyPath = PropertyPath.FromExpression(expression);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected PropertyPath GetPropertyPath()
|
|
|
|
|
{
|
|
|
|
|
return PropertyPathTransform(PropertyPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
|
|
|
|
return Validate(instance, context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (context.Instance is null && default(T) is null)
|
|
|
|
|
{
|
2026-07-12 18:43:37 +02:00
|
|
|
return [];
|
2026-05-08 20:26:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var actualType = context.Instance?.GetType().FullName ?? "null";
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Expected validation context instance of type '{typeof(T).FullName}', but got '{actualType}'.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected abstract IEnumerable<Problem> Validate(T instance, ValidationContext context);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed record PropertyRule<T, TProperty> : Rule<T, TProperty>
|
|
|
|
|
{
|
|
|
|
|
private readonly Func<T, TProperty> _accessor;
|
|
|
|
|
|
|
|
|
|
public PropertyRule(Expression<Func<T, TProperty>> expression) : base(expression)
|
|
|
|
|
{
|
|
|
|
|
_accessor = expression.Compile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override IEnumerable<Problem> Validate(T instance, ValidationContext context)
|
|
|
|
|
{
|
|
|
|
|
if (Steps.Count is 0)
|
|
|
|
|
{
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var value = _accessor(instance);
|
|
|
|
|
var propertyPath = GetPropertyPath();
|
|
|
|
|
return Steps.SelectMany(step => step.Validate(value, context, propertyPath, Code, Severity));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal sealed record CollectionRule<T, TElement> : Rule<T, TElement>
|
|
|
|
|
{
|
|
|
|
|
private readonly Func<T, IEnumerable<TElement>> _accessor;
|
|
|
|
|
|
|
|
|
|
public CollectionRule(Expression<Func<T, IEnumerable<TElement>>> expression) : base(expression)
|
|
|
|
|
{
|
|
|
|
|
_accessor = expression.Compile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override IEnumerable<Problem> Validate(T instance, ValidationContext context)
|
|
|
|
|
{
|
|
|
|
|
if (Steps.Count is 0)
|
|
|
|
|
{
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_accessor(instance) is not { } collection)
|
|
|
|
|
{
|
|
|
|
|
yield break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var index = 0;
|
|
|
|
|
foreach (var element in collection)
|
|
|
|
|
{
|
|
|
|
|
var propertyPath = GetPropertyPath() + index;
|
|
|
|
|
|
|
|
|
|
foreach (var step in Steps)
|
|
|
|
|
{
|
|
|
|
|
foreach (var problem in step.Validate(element, context, propertyPath, Code, Severity))
|
|
|
|
|
{
|
|
|
|
|
yield return problem;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|