Skip to content

Commit

Permalink
Update Documentation (#66)
Browse files Browse the repository at this point in the history
* Update readme
* Add individual diagnostic documentation
* Condense diagnostic codes
  • Loading branch information
viceroypenguin authored Jun 19, 2024
1 parent 447ae67 commit 2660024
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 7 deletions.
123 changes: 123 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,130 @@ public partial record Query : IValidationTarget<Query>
}
```

### 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<Query>
{
[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<Query>
{
[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<Query>
{
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;
};

```
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion src/Immediate.Validations.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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";
}
200 changes: 200 additions & 0 deletions src/Immediate.Validations.Analyzers/Immediate.Validations.Analyzers.md
Original file line number Diff line number Diff line change
@@ -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<T>` 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 |
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,7 @@ await AnalyzerTestHelpers.CreateAnalyzerTest<ValidateClassAnalyzer>(
[Validate]
public sealed partial record Target : IValidationTarget<Target>
{
[Equal({|IV0018:nameof(DateTime)|})]
[Equal({|IV0017:nameof(DateTime)|})]
public required string Id { get; init; }
public static ValidationResult Validate(Target target) => [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ await AnalyzerTestHelpers.CreateAnalyzerTest<ValidatorClassAnalyzer>(
"""
using Immediate.Validations.Shared;
public sealed class {|IV0019:GreaterThanAttribute|} : ValidatorAttribute
public sealed class {|IV0010:GreaterThanAttribute|} : ValidatorAttribute
{
public required int Operand { get; init; }
Expand Down

0 comments on commit 2660024

Please sign in to comment.