Skip to content

Commit

Permalink
Support Skipping Validations (#88)
Browse files Browse the repository at this point in the history
* use `[AllowNull]` to skip null check
* Add support for `SkipSelf` validations
  • Loading branch information
viceroypenguin authored Jul 31, 2024
1 parent 5ca282f commit 7a17711
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 3 deletions.
19 changes: 19 additions & 0 deletions src/Common/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,23 @@ typeSymbol is
},
},
};

public static bool IsAllowNullAttribute(this ITypeSymbol? typeSymbol) =>
typeSymbol is INamedTypeSymbol
{
Name: "AllowNullAttribute",
ContainingNamespace:
{
Name: "CodeAnalysis",
ContainingNamespace:
{
Name: "Diagnostics",
ContainingNamespace:
{
Name: "System",
ContainingNamespace.IsGlobalNamespace: true,
}
}
}
};
}
1 change: 1 addition & 0 deletions src/Immediate.Validations.Generators/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public sealed record ValidationTarget
public required Class Class { get; init; }
public required bool HasAdditionalValidationsMethod { get; init; }
public required bool IsReferenceType { get; init; }
public required bool SkipSelf { get; init; }
public required EquatableReadOnlyList<string> BaseValidationTargets { get; init; }
public required EquatableReadOnlyList<ValidationTargetProperty> Properties { get; init; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ partial {{ class.type }} {{ class.name }}
errors.AddRange({{ bc }}.Validate(t));
{{~ end ~}}

{{~ if !skip_self ~}}
{{~ for p in properties ~}}
__Validate{{ p.property_name }}(errors, t, t.{{ p.property_name }});
{{~ end ~}}

{{~ if has_additional_validations_method ~}}
AdditionalValidations(errors, t);
{{~ end ~}}
{{~ end ~}}

return errors;
}
Expand Down
19 changes: 17 additions & 2 deletions src/Immediate.Validations.Generators/ValidateTargetTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ or IMethodSymbol
var @class = GetClass(_symbol);
var hasAdditionalValidationsMethod = _symbol.HasAdditionalValidationsMethod();
var baseValidationTargets = GetBaseValidationTargets();
var properties = GetProperties();
var skipSelf = GetSkipSelf();
var properties = skipSelf ? [] : GetProperties();

return new()
{
Expand All @@ -65,6 +66,7 @@ or IMethodSymbol
Class = @class,
HasAdditionalValidationsMethod = hasAdditionalValidationsMethod,
IsReferenceType = _symbol.IsReferenceType,
SkipSelf = skipSelf,
BaseValidationTargets = baseValidationTargets,
Properties = properties,
};
Expand Down Expand Up @@ -121,6 +123,18 @@ private EquatableReadOnlyList<string> GetBaseValidationTargets()
return baseValidationTargets.ToEquatableReadOnlyList();
}

private bool GetSkipSelf()
{
var attribute = _context.Attributes[0];

var skipSelf = attribute.NamedArguments
.Where(na => na.Key is "SkipSelf")
.Select(na => na.Value.Value is bool b && b)
.FirstOrDefault();

return skipSelf;
}

private EquatableReadOnlyList<ValidationTargetProperty> GetProperties()
{
_token.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -172,7 +186,8 @@ ImmutableArray<AttributeData> attributes

var isReferenceType = propertyType.IsReferenceType;
var isNullable = isReferenceType
? nullableAnnotation is NullableAnnotation.Annotated
? (nullableAnnotation is NullableAnnotation.Annotated
|| attributes.Any(a => a.AttributeClass.IsAllowNullAttribute()))
: propertyType.IsNullableType();

var baseType = !isReferenceType && isNullable
Expand Down
11 changes: 10 additions & 1 deletion src/Immediate.Validations.Shared/ValidateAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ namespace Immediate.Validations.Shared;
/// Applied to a class to indicate that validation methods should be generated.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
public sealed class ValidateAttribute : Attribute;
public sealed class ValidateAttribute : Attribute
{
/// <summary>
/// Set to <see langword="true" /> in order to skip validating local properties.
/// </summary>
/// <remarks>
/// Base class and interface validators will still be called.
/// </remarks>
public bool SkipSelf { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Immediate.Validations.Shared;
using Xunit;

namespace Immediate.Validations.FunctionalTests.IntegrationTests;

public sealed partial class SkipSelfTests
{
[Validate]
public partial record BaseClass : IValidationTarget<BaseClass>
{
public required string BaseString { get; init; }
}

[Validate(SkipSelf = true)]
public sealed partial record SubClass : BaseClass, IValidationTarget<SubClass>
{
public required string IgnoredString { get; init; }
}

[Fact]
public void SkipSelfOperatesCorrectly()
{
var instance = new SubClass { BaseString = null!, IgnoredString = null!, };

var errors = SubClass.Validate(instance);

Assert.Equal(
[
new()
{
PropertyName = nameof(BaseClass.BaseString),
ErrorMessage = "'Base String' must not be null.",
}
],
errors
);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Validations.Shared;
using Xunit;

Expand All @@ -11,6 +12,13 @@ public partial record StringRecord : IValidationTarget<StringRecord>
public required string StringValue { get; init; }
}

[Validate]
public partial record AllowNullRecord : IValidationTarget<AllowNullRecord>
{
[AllowNull]
public required string StringValue { get; init; }
}

[Fact]
public void StringNotNullTestWhenNotNull()
{
Expand Down Expand Up @@ -39,4 +47,24 @@ public void StringNotNullTestWhenNull()
errors
);
}

[Fact]
public void StringAllowedNullTestWhenNotNull()
{
var instance = new AllowNullRecord { StringValue = "Hello World!" };

var errors = AllowNullRecord.Validate(instance);

Assert.Empty(errors);
}

[Fact]
public void StringAllowedNullTestWhenNull()
{
var instance = new AllowNullRecord { StringValue = null };

var errors = AllowNullRecord.Validate(instance);

Assert.Empty(errors);
}
}

0 comments on commit 7a17711

Please sign in to comment.