Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: validation data annotations support dependency resolution #158

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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