diff --git a/readme.md b/readme.md index 76fda5d..9a4980d 100644 --- a/readme.md +++ b/readme.md @@ -57,7 +57,130 @@ public partial record Query : IValidationTarget } ``` +### Referencing Other Properties + +Since attributes cannot reference anything other than constant strings, the way to reference static and instance +properties, fields, and methods is to use the `nameof()` to identify which property, field, or method should be used. Example: + +```cs +[Validate] +public partial record Query : IValidationTarget +{ + [GeneratedRegex(@"^\d+$")] + private static partial Regex AllDigitsRegex(); + + [Match(regex: nameof(AllDigitsRegex))] + public required string Id { get; init; } +} +``` + +### Custom Messages + +Provide a custom message to any validation using the `Message` property of the attribute. This message will be parsed +for template parameters, which will be applied to the message before rendering to the validation result. The target property +name is available as `{PropertyName}`, and it's value via `{PropertyValue}`. + +Other parameter values will be added using their property name suffixed with `Value` (for example, the +`GreaterThanAttribute` uses a `comparison` parameter, so the value is available via `ComparisonValue`). If another +property on the target class is referenced via `nameof(Property)`, the name of that property will be available using the +`Name` suffix (for example, `ComparisonName` for the `comparison` property). + +```cs +[Validate] +public partial record Query : IValidationTarget +{ + [GreaterThan(0, Message = "'{PropertyName}' must be greater than '{ComparisonValue}'")] + public required int Id { get; init; } +} +``` + +### Extending Validation Classes + +If attributes are not enough to specify how to validate a class, an `AdditionalValidations` method can be used to write +additional validations for the class. + +```cs +[Validate] +public partial record Query : IValidationTarget +{ + public required bool Enabled { get; init; } + public required int Id { get; init; } + + private static void AdditionalValidations( + ValidationResult errors, + Query target + ) + { + if (target.Enabled) + { + // Use a lambda to use the default message or override message; + // the message will be templated in the same way as attribute validations. + errors.Add( + () => GreaterThanAttribute.ValidateProperty( + target.Id, + 0 + ) + ); + } + + if (false) + { + // Manually create a `ValidationError` and add it to the `ValidationResult`. + errors.Add( + new ValidationError() + { + PropertyName = "ExampleProperty", + ErrorMessage = "Example Message", + } + ) + } + } +} +``` + ### Results The result of doing the above is that when a parameter fails one or more validations, a `ValidationException` is thrown, which can be handled via ProblemDetails or any other infrastructure mechanism. + +Example using ProblemDetails: +```cs +builder.Services.AddProblemDetails(ConfigureProblemDetails); + +public static void ConfigureProblemDetails(ProblemDetailsOptions options) => + options.CustomizeProblemDetails = c => + { + if (c.Exception is null) + return; + + c.ProblemDetails = c.Exception switch + { + ValidationException ex => new ValidationProblemDetails( + ex + .Errors + .GroupBy(x => x.PropertyName, StringComparer.OrdinalIgnoreCase) + .ToDictionary( + x => x.Key, + x => x.Select(x => x.ErrorMessage).ToArray(), + StringComparer.OrdinalIgnoreCase + ) + ) + { + Status = StatusCodes.Status400BadRequest, + }, + + // other exception handling as desired + + var ex => new ProblemDetails + { + Detail = "An error has occurred.", + Status = StatusCodes.Status500InternalServerError, + }, + }; + + c.HttpContext.Response.StatusCode = + c.ProblemDetails.Status + ?? StatusCodes.Status500InternalServerError; + }; + +``` diff --git a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md index 9357bad..18f5889 100644 --- a/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Immediate.Validations.Analyzers/AnalyzerReleases.Unshipped.md @@ -11,11 +11,11 @@ IV0006 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0007 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0008 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0009 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0010 | ImmediateValidations | Error | ValidatorClassAnalyzer IV0011 | ImmediateValidations | Warning | AssemblyBehaviorAnalyzer IV0012 | ImmediateValidations | Error | ValidateClassAnalyzer IV0013 | ImmediateValidations | Warning | ValidateClassAnalyzer IV0014 | ImmediateValidations | Warning | ValidateClassAnalyzer IV0015 | ImmediateValidations | Warning | ValidateClassAnalyzer IV0016 | ImmediateValidations | Warning | ValidateClassAnalyzer -IV0018 | ImmediateValidations | Error | ValidateClassAnalyzer -IV0019 | ImmediateValidations | Error | ValidatorClassAnalyzer +IV0017 | ImmediateValidations | Error | ValidateClassAnalyzer diff --git a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs index 8146a8e..154980b 100644 --- a/src/Immediate.Validations.Analyzers/DiagnosticIds.cs +++ b/src/Immediate.Validations.Analyzers/DiagnosticIds.cs @@ -12,6 +12,7 @@ public static class DiagnosticIds public const string IV0008ValidatePropertyMustBeRequired = "IV0008"; public const string IV0009ValidatorHasTooManyConstructors = "IV0009"; public const string IV0019ValidatorIsMissingDefaultMessage = "IV0019"; + public const string IV0010ValidatorIsMissingDefaultMessage = "IV0010"; public const string IV0011AssemblyBehaviorsShouldUseValidation = "IV0011"; @@ -20,5 +21,5 @@ public static class DiagnosticIds public const string IV0014ValidatePropertyIncompatibleType = "IV0014"; public const string IV0015ValidateParameterIncompatibleType = "IV0015"; public const string IV0016ValidateParameterPropertyIncompatibleType = "IV0016"; - public const string IV0018ValidateParameterNameofInvalid = "IV0018"; + public const string IV0017ValidateParameterNameofInvalid = "IV0017"; } diff --git a/src/Immediate.Validations.Analyzers/Immediate.Validations.Analyzers.md b/src/Immediate.Validations.Analyzers/Immediate.Validations.Analyzers.md index e69de29..7ab97c3 100644 --- a/src/Immediate.Validations.Analyzers/Immediate.Validations.Analyzers.md +++ b/src/Immediate.Validations.Analyzers/Immediate.Validations.Analyzers.md @@ -0,0 +1,200 @@ +# Immediate.Validations.Analyzers + +## IV0001: Validators must have a valid `ValidateProperty` method + +A Validator that inherits from `ValidatorAttribute` must have a `public static bool ValidateProperty()` in order to +validate an attached property. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0002: `ValidateProperty` method must be static + +The `ValidateProperty()` for a particular validator must be `static`. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0003: `ValidateProperty` method must be unique + +IV only supports a single `public static void ValidateProperty()` in the validator class. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0004: `ValidateProperty` method must have a valid return + +The `ValidateProperty()` for a particular validator must return a `bool`. A `true` should represent a successful +validation, while a `false` represents an invalid property. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0005: `ValidateProperty` method is missing parameters + +The validator class has a property or constructor parameter that does not have a matching parameter on the +`ValidateProperty()` method. All properties or constructor parameters should have a matching parameter, which can be +used provide attribute values to the `ValidateProperty()` method. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0006: `ValidateProperty` method has extra parameters + +The `ValidatorProperty()` method has a parameter that does not have a matching property or constructor parameter. All +properties or constructor parameters should have a matching parameter, which can be used provide attribute values to the +`ValidateProperty()` method. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0007: `ValidateProperty` parameters and Validator properties must match + +The type of the constructor parameter or the property must be assignable to the parameter. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0008: Validator property must be `required` + +The property or the constructor parameter must be required if the `ValidateProperty()` parameter is required. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0009: Validator has too many constructors + +IV only supports a single constructor. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0010: Validator is missing `DefaultMessage` + +A Validator that inherits from `ValidatorAttribute` must have a `public static string DefaultMessage => "";` or `public +const string DefaultMessage = "";` to provide a default validation message when the property is invalid. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0011: Assembly-wide `Behaviors` attribute should use `ValidationBehavior<,>` + +The `ValidationBehavior<,>` behavior should be registered as part of the assembly-wide Immediate.Handlers pipeline. This +will ensure that all request types that should be validated are validated as part of the IH command handling. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | + +## IV0012: Validation targets must be marked `[Validate]` + +The `[Validate]` attribute is necessary for the IV Source Generator to operate, and must be applied to a type in order +to implement `IValidationTarget` and use the validator attributes attached to any properties. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | + +## IV0013: Validation targets should implement the interface `IValidationTarget<>` + +The `IValidationTarget<>` interface is necessary for the `ValidationBehavior<,>` behavior to be enabled and validate the +class when part of an IH command handler. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | + +## IV0014: Validator will not be used + +A validator can only be used on properties that have compatible types. For example, the `[Length]` validator can only be +used on `string`s. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Warning | +| CodeFix | | + +## IV0015: Parameter is incompatible type + +A value of invalid type was provided to the validator. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | + +## IV0016: Parameter is incompatible type + +A `nameof()` reference of invalid type was provided to the validator. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Warning | +| CodeFix | False | + +## IV0017: nameof() target is invalid + +An invalid `nameof()` destination was used. Only immediate properties, fields, and methods are allowed to be used as a +`nameof()` value for the validator. + +| Item | Value | +|----------|----------------------| +| Category | ImmediateValidations | +| Enabled | True | +| Severity | Error | +| CodeFix | False | diff --git a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs index cab176a..7b24fb2 100644 --- a/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs +++ b/src/Immediate.Validations.Analyzers/ValidateClassAnalyzer.cs @@ -67,7 +67,7 @@ public sealed class ValidateClassAnalyzer : DiagnosticAnalyzer public static readonly DiagnosticDescriptor ValidateParameterNameofInvalid = new( - id: DiagnosticIds.IV0018ValidateParameterNameofInvalid, + id: DiagnosticIds.IV0017ValidateParameterNameofInvalid, title: "nameof() target is invalid", messageFormat: "nameof({0}) must refer to a property or method on the class `{1}`", category: "ImmediateValidations", diff --git a/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs b/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs index d2963eb..961654f 100644 --- a/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs +++ b/src/Immediate.Validations.Analyzers/ValidatorClassAnalyzer.cs @@ -109,7 +109,7 @@ public sealed class ValidatorClassAnalyzer : DiagnosticAnalyzer public static readonly DiagnosticDescriptor ValidatorIsMissingDefaultMessage = new( - id: DiagnosticIds.IV0019ValidatorIsMissingDefaultMessage, + id: DiagnosticIds.IV0010ValidatorIsMissingDefaultMessage, title: "Validator is missing `DefaultMessage`", messageFormat: "Validator `{0}` must have a `DefaultMessage` property", category: "ImmediateValidations", diff --git a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs index bd4e87e..4a75afa 100644 --- a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs +++ b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidateClassAnalyzerTests.cs @@ -835,7 +835,7 @@ await AnalyzerTestHelpers.CreateAnalyzerTest( [Validate] public sealed partial record Target : IValidationTarget { - [Equal({|IV0018:nameof(DateTime)|})] + [Equal({|IV0017:nameof(DateTime)|})] public required string Id { get; init; } public static ValidationResult Validate(Target target) => []; diff --git a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs index e4e1773..b2be5ca 100644 --- a/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs +++ b/tests/Immediate.Validations.Tests/AnalyzerTests/ValidatorClassAnalyzerTests.cs @@ -446,7 +446,7 @@ await AnalyzerTestHelpers.CreateAnalyzerTest( """ using Immediate.Validations.Shared; - public sealed class {|IV0019:GreaterThanAttribute|} : ValidatorAttribute + public sealed class {|IV0010:GreaterThanAttribute|} : ValidatorAttribute { public required int Operand { get; init; }