Skip to content

Commit

Permalink
feat: validation data annotations support dependency resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
reddogaw-cba authored and reddogaw committed Sep 2, 2024
1 parent 3ff50b0 commit 7243d18
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 14 deletions.
30 changes: 26 additions & 4 deletions samples/InAction.Validation/Program.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
using System.ComponentModel.DataAnnotations;
using Cocona;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;

namespace InAction.Validation;

class Program
{
static void Main(string[] args)
{
CoconaApp.Run<Program>(args);
var builder = CoconaApp.CreateBuilder(args);
builder.Services
.AddTransient<IFileProvider>(serviceProvider => serviceProvider.GetRequiredService<IHostEnvironment>().ContentRootFileProvider);

var app = builder.Build();
app.Run<Program>();
}

public void Run([Range(1, 128)]int width, [Range(1, 128)]int height, [Argument][PathExists]string filePath)
public void Run([Range(1, 128)]int width, [Range(1, 128)]int height, [Argument][PathExists][PathExistsWithDI] string filePath)
{
Console.WriteLine($"Size: {width}x{height}");
Console.WriteLine($"Path: {filePath}");
Expand All @@ -21,11 +29,25 @@ class PathExistsAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value is string path && (Directory.Exists(path) || Directory.Exists(path)))
if (value is string path && (Directory.Exists(path) || File.Exists(path)))
{
return ValidationResult.Success;
}

return new ValidationResult($"The path '{value}' is not found.");
}
}
}

class PathExistsWithDIAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var fileSystem = validationContext.GetRequiredService<IFileProvider>();
if (value is string path && fileSystem.GetFileInfo(path).Exists)
{
return ValidationResult.Success;
}

return new ValidationResult($"The path '{value}' is not found.");
}
}
2 changes: 1 addition & 1 deletion src/Cocona.Core/Command/Binder/CoconaParameterBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ private object CreateParameterSetWithMembers(CommandParameterSetDescriptor param

private object? Validate(ICommandParameterDescriptor commandParameter, object? value)
{
var ctx = new CoconaParameterValidationContext(commandParameter, value);
var ctx = new CoconaParameterValidationContext(_serviceProvider, commandParameter, value);
foreach (var validator in _validatorProvider.CreateValidators(commandParameter))
{
var validationFailed = validator.Validate(ctx).FirstOrDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

public struct CoconaParameterValidationContext
{
public IServiceProvider ServiceProvider { get; }
public ICommandParameterDescriptor Parameter { get; }
public object? Value { get; }

public CoconaParameterValidationContext(ICommandParameterDescriptor parameter, object? value)
public CoconaParameterValidationContext(IServiceProvider serviceProvider, ICommandParameterDescriptor parameter, object? value)
{
ServiceProvider = serviceProvider;
Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter));
Value = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public IEnumerable<CoconaParameterValidationResult> Validate(CoconaParameterVali
return new[] { new CoconaParameterValidationResult(ctx.Parameter.Name, "The value must not be null.") };
}

var validationCtx = new ValidationContext(ctx.Value);
var validationCtx = new ValidationContext(ctx.Value, serviceProvider: ctx.ServiceProvider, items: null);
validationCtx.DisplayName = ctx.Parameter.Name;
var result = _attribute.GetValidationResult(ctx.Value, validationCtx);
if (result is not null && result != ValidationResult.Success)
Expand All @@ -28,4 +28,4 @@ public IEnumerable<CoconaParameterValidationResult> Validate(CoconaParameterVali

return Array.Empty<CoconaParameterValidationResult>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ public IEnumerable<ICoconaParameterValidator> CreateValidators(ICommandParameter
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
public interface ICoconaParameterValidatorProvider
{
IEnumerable<ICoconaParameterValidator> CreateValidators(ICommandParameterDescriptor parameter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ private CommandDescriptor CreateCommand(ICommandParameterDescriptor[] parameterD
);
}

private static CoconaParameterBinder CreateCoconaParameterBinder()
private static CoconaParameterBinder CreateCoconaParameterBinder(Action<IServiceCollection>? registerDependencies = null)
{
return new CoconaParameterBinder(new ServiceCollection().BuildServiceProvider(), new CoconaValueConverter(), new DataAnnotationsParameterValidatorProvider());
var services = new ServiceCollection();
registerDependencies?.Invoke(services);
return new CoconaParameterBinder(services.BuildServiceProvider(), new CoconaValueConverter(), new DataAnnotationsParameterValidatorProvider());
}

[Fact]
Expand Down Expand Up @@ -168,8 +170,20 @@ public void Bind_Argument_DataAnnotationsParameterValidator_UnknownAttribute()
var result = binder.Bind(command, Array.Empty<CommandOption>(), new[] { new CommandArgument("0", 0) });
result.Should().HaveCount(1);
}

[Fact]
public void Bind_Argument_DataAnnotationsParameterValidator_UsingDependencyInjection()
{
var command = CreateCommand(new[]
{
new CommandArgumentDescriptor(typeof(int), "arg0", 0, "", CoconaDefaultValue.None, new [] { new IsEvenUsingDependencyInjectionAttribute() } )
});


var binder = CreateCoconaParameterBinder(services => services.AddSingleton<Calculator>());
var result = binder.Bind(command, Array.Empty<CommandOption>(), new[] { new CommandArgument("2", 0) });
result.Should().HaveCount(1);
}

class MyAttribute : Attribute
{
}
Expand All @@ -188,6 +202,27 @@ class IsEvenEnumerableAttribute : ValidationAttribute
: new ValidationResult("List contains uneven numbers.");
}
}

class IsEvenUsingDependencyInjectionAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is not int number)
{
return new ValidationResult($"Could not validate value, values's type is {value?.GetType()}");
}

var calculator = validationContext.GetRequiredService<Calculator>();
return calculator.IsEven(number)
? ValidationResult.Success
: new ValidationResult("Value is an uneven number.");
}
}

class Calculator
{
public bool IsEven(int number) => number % 2 == 0;
}

class CommandParameterValidationTest
{
Expand Down

0 comments on commit 7243d18

Please sign in to comment.