fix(validation): make Severity meaningful in Validation.IsValid

Validation.IsValid previously counted every problem regardless of severity,
leaving Severity inert. Now IsValid only fails on Error problems, so Warning
and Info no longer invalidate the result, and IsValidFor(Severity) lets
callers set the failing-severity threshold.
This commit is contained in:
Louis Seubert 2026-07-12 18:50:06 +02:00
commit f7263c33a8
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
4 changed files with 75 additions and 3 deletions

View file

@ -53,6 +53,7 @@ To have a consistent experience across all packages, some public interfaces have
- **request.result:** Fold `IsSuccess` into `Result<T>.GetHashCode` to avoid collisions between a failure and a success value whose hash is `0`
- **request.validation:** Guard null comparison bounds in `GreaterThan`/`LessThan`/`Between` etc. so a `null` bound no longer throws (`NullReferenceException`)
- **request.validation:** Return no problems when validating a `null` instance of a reference type instead of dereferencing it (`NullReferenceException`)
- **request.validation:** Make `Severity` meaningful — `Validation.IsValid` now only fails on `Error` problems; `Warning`/`Info` no longer invalidate. Added `Validation.IsValidFor(Severity)` to set the failing-severity threshold (see PLAN.md Inconsistencies#B)
### Removed

View file

@ -0,0 +1,49 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.Request.Validation.Tests;
internal sealed class ValidationSeverityTests
{
[Test]
public async Task I_can_have_warning_problems_not_invalidate_by_default()
{
var validator = new PropertyValidator<Person, string?>(person
=> person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required.")
.WithSeverity(Severity.Warning));
var result = validator.Validate(new Person { Name = "" });
await Assert.That(result.Problems).Count().IsEqualTo(1);
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.IsValidFor(Severity.Warning)).IsFalse();
await Assert.That(result.IsValidFor(Severity.Info)).IsFalse();
}
[Test]
public async Task I_can_have_info_problems_only_invalidate_at_info_threshold()
{
var validator = new PropertyValidator<Person, string?>(person
=> person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required.")
.WithSeverity(Severity.Info));
var result = validator.Validate(new Person { Name = "" });
await Assert.That(result.Problems).Count().IsEqualTo(1);
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.IsValidFor(Severity.Warning)).IsTrue();
await Assert.That(result.IsValidFor(Severity.Info)).IsFalse();
}
[Test]
public async Task I_can_have_error_problems_invalidate_by_default()
{
var validator = new PropertyValidator<Person, string?>(person
=> person.Name, rule => rule.Must(value => !string.IsNullOrWhiteSpace(value), "Name is required."));
var result = validator.Validate(new Person { Name = "" });
await Assert.That(result.IsValid).IsFalse();
await Assert.That(result.IsValidFor(Severity.Error)).IsFalse();
}
}

View file

@ -14,7 +14,8 @@ internal sealed class ValidatorTests
var result = validator.Validate(new Person { Name = "" });
await Assert.That(result.IsValid).IsFalse();
await Assert.That(result.IsValid).IsTrue();
await Assert.That(result.IsValidFor(Severity.Warning)).IsFalse();
await Assert.That(result.Problems).Count().IsEqualTo(1);
var problem = result.Problems.Single();
@ -39,7 +40,8 @@ internal sealed class ValidatorTests
using (Assert.Multiple())
{
await Assert.That(invalid.IsValid).IsFalse();
await Assert.That(invalid.IsValid).IsTrue();
await Assert.That(invalid.IsValidFor(Severity.Warning)).IsFalse();
await Assert.That(valid.IsValid).IsTrue();
await Assert.That(valid.Problems).IsEmpty();
}

View file

@ -23,7 +23,27 @@ public sealed class Validation
/// <summary>
/// Whether the validation was successful.
/// </summary>
public bool IsValid => Problems.Count is 0;
/// <remarks>
/// A validation is considered successful unless it contains at least one problem with
/// <see cref="Severity.Error"/>. Problems of lower severity (<see cref="Severity.Warning"/> or
/// <see cref="Severity.Info"/>) do not invalidate the result. Use <see cref="IsValidFor"/>
/// to control which severities are treated as failures.
/// </remarks>
public bool IsValid => IsValidFor(Severity.Error);
/// <summary>
/// Whether the validation was successful, treating problems at or above the given severity as failures.
/// </summary>
/// <param name="minimum">The minimum severity that should be considered a failure.</param>
/// <remarks>
/// A problem invalidates the validation when its <see cref="Severity"/> is at least as severe as
/// <paramref name="minimum"/>. For example, <c>IsValidFor(Severity.Warning)</c> fails on errors and
/// warnings but not on informational problems.
/// </remarks>
public bool IsValidFor(Severity minimum)
{
return !Problems.Any(problem => problem.Severity <= minimum);
}
/// <summary>
/// The problems that were found during validation.