Skip to content

Commit

Permalink
Merge pull request #76 from zadykian/feature/params-validation
Browse files Browse the repository at this point in the history
Parameters validation via attributes
  • Loading branch information
neuecc authored May 16, 2022
2 parents d79a945 + ec736de commit 70aa69e
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/ConsoleAppFramework/ConsoleAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ IHostBuilder AddConsoleAppFramework(IHostBuilder builder, string[] args, Console
configureOptions?.Invoke(ctx, options);
options.CommandLineArguments = args;
services.AddSingleton(options);
services.AddSingleton<IParamsValidator, ParamsValidator>();

if (options.ReplaceToUseSimpleConsoleLogger)
{
Expand Down
18 changes: 17 additions & 1 deletion src/ConsoleAppFramework/ConsoleAppEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Text.Json;
Expand All @@ -18,13 +20,20 @@ internal class ConsoleAppEngine
readonly CancellationToken cancellationToken;
readonly ConsoleAppOptions options;
readonly IServiceProviderIsService isService;
readonly IParamsValidator paramsValidator;
readonly bool isStrict;

public ConsoleAppEngine(ILogger<ConsoleApp> logger, IServiceProvider provider, ConsoleAppOptions options, IServiceProviderIsService isService, CancellationToken cancellationToken)
public ConsoleAppEngine(ILogger<ConsoleApp> logger,
IServiceProvider provider,
ConsoleAppOptions options,
IServiceProviderIsService isService,
IParamsValidator paramsValidator,
CancellationToken cancellationToken)
{
this.logger = logger;
this.provider = provider;
this.cancellationToken = cancellationToken;
this.paramsValidator = paramsValidator;
this.options = options;
this.isService = isService;
this.isStrict = options.StrictOption;
Expand Down Expand Up @@ -168,6 +177,13 @@ async Task RunCore(Type type, MethodInfo methodInfo, object? instance, string?[]
invokeArgs = newInvokeArgs;
}

var validationResult = paramsValidator.ValidateParameters(originalParameters.Zip(invokeArgs));
if (validationResult != ValidationResult.Success)
{
await SetFailAsync(validationResult!.ErrorMessage!);
return;
}

try
{
if (instance == null && !type.IsAbstract && !methodInfo.IsStatic)
Expand Down
75 changes: 75 additions & 0 deletions src/ConsoleAppFramework/ParamsValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;

namespace ConsoleAppFramework
{
/// <summary>
/// Validator of command parameters.
/// </summary>
public interface IParamsValidator
{
/// <summary>
/// Validate <paramref name="parameters"/> of command based on validation attributes
/// applied to method's parameters.
/// </summary>
ValidationResult? ValidateParameters(IEnumerable<(ParameterInfo Parameter, object? Value)> parameters);
}

/// <inheritdoc />
public class ParamsValidator : IParamsValidator
{
private readonly ConsoleAppOptions options;

public ParamsValidator(ConsoleAppOptions options) => this.options = options;

/// <inheritdoc />
ValidationResult? IParamsValidator.ValidateParameters(
IEnumerable<(ParameterInfo Parameter, object? Value)> parameters)
{
var invalidParameters = parameters
.Select(tuple => (tuple.Parameter, tuple.Value, Result: Validate(tuple.Parameter, tuple.Value)))
.Where(tuple => tuple.Result != ValidationResult.Success)
.ToImmutableArray();

if (!invalidParameters.Any())
{
return ValidationResult.Success;
}

var errorMessage = string.Join(Environment.NewLine,
invalidParameters
.Select(tuple =>
$"{options.NameConverter(tuple.Parameter.Name!)} " +
$"({tuple.Value}): " +
$"{tuple.Result!.ErrorMessage}")
);

return new ValidationResult($"Some parameters have invalid values:{Environment.NewLine}{errorMessage}");
}

private static ValidationResult? Validate(ParameterInfo parameterInfo, object? value)
{
if (value is null) return ValidationResult.Success;

var validationContext = new ValidationContext(value, null, null);

var failedResults = GetValidationAttributes(parameterInfo)
.Select(attribute => attribute.GetValidationResult(value, validationContext))
.Where(result => result != ValidationResult.Success)
.ToImmutableArray();

return failedResults.Any()
? new ValidationResult(string.Join("; ", failedResults.Select(res => res?.ErrorMessage)))
: ValidationResult.Success;
}

private static IEnumerable<ValidationAttribute> GetValidationAttributes(ParameterInfo parameterInfo)
=> parameterInfo
.GetCustomAttributes()
.OfType<ValidationAttribute>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.ComponentModel.DataAnnotations;
using FluentAssertions;
using Xunit;

// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Global

namespace ConsoleAppFramework.Integration.Test;

public class ValidationAttributeTests
{
/// <summary>
/// Try to execute command with invalid option value.
/// </summary>
[Fact]
public void Validate_String_Length_Test()
{
using var console = new CaptureConsoleOutput();

const string optionName = "arg";
const string optionValue = "too-large-string-value";

var args = new[] { nameof(AppWithValidationAttributes.StrLength), $"--{optionName}", optionValue };
ConsoleApp.Run<AppWithValidationAttributes>(args);

// Validation should fail, so StrLength command should not be executed.
console.Output.Should().NotContain(AppWithValidationAttributes.Output);

console.Output.Should().Contain(optionName);
console.Output.Should().Contain(optionValue);
}

[Fact]
public void Command_With_Multiple_Params()
{
using var console = new CaptureConsoleOutput();

var args = new[]
{
nameof(AppWithValidationAttributes.MultipleParams),
"--second-arg", "10",
"--first-arg", "invalid-email-address"
};

ConsoleApp.Run<AppWithValidationAttributes>(args);

// Validation should fail, so StrLength command should not be executed.
console.Output.Should().NotContain(AppWithValidationAttributes.Output);
}

/// <inheritdoc />
internal class AppWithValidationAttributes : ConsoleAppBase
{
public const string Output = $"hello from {nameof(AppWithValidationAttributes)}";

[Command(nameof(StrLength))]
public void StrLength([StringLength(maximumLength: 8)] string arg) => Console.WriteLine(Output);

[Command(nameof(MultipleParams))]
public void MultipleParams(
[EmailAddress] string firstArg,
[Range(0, 2)] int secondArg) => Console.WriteLine(Output);
}
}

0 comments on commit 70aa69e

Please sign in to comment.