Skip to content

Commit

Permalink
feat: Add validation attributes for files and directories that must n…
Browse files Browse the repository at this point in the history
…ot exist. (#230)
  • Loading branch information
IanG authored and natemcmaster committed May 9, 2019
1 parent 1370b3a commit be23040
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/CommandLineUtils/Attributes/DirectoryNotExistsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Validation;

namespace McMaster.Extensions.CommandLineUtils
{
/// <summary>
/// Specifies that the data must not be an already existing directory, not a file.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class DirectoryNotExistsAttribute : FilePathNotExistsAttributeBase
{
/// <summary>
/// Initializes an instance of <see cref="DirectoryNotExistsAttribute"/>.
/// </summary>
public DirectoryNotExistsAttribute()
: base(FilePathType.Directory)
{
}
}
}
24 changes: 24 additions & 0 deletions src/CommandLineUtils/Attributes/FileNotExistsAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Validation;

namespace McMaster.Extensions.CommandLineUtils
{
/// <summary>
/// Specifies that the data must not be an already existing file, not a directory.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class FileNotExistsAttribute : FilePathNotExistsAttributeBase
{
/// <summary>
/// Initializes an instance of <see cref="FileNotExistsAttribute"/>.
/// </summary>
public FileNotExistsAttribute()
: base(FilePathType.File)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Validation;

namespace McMaster.Extensions.CommandLineUtils
{
/// <summary>
/// Specifies that the data must not be an already existing file or directory.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class FileOrDirectoryNotExistsAttribute : FilePathNotExistsAttributeBase
{
/// <summary>
/// Initializes an instance of <see cref="FileOrDirectoryNotExistsAttribute"/>.
/// </summary>
public FileOrDirectoryNotExistsAttribute()
: base(FilePathType.Any)
{
}
}
}
76 changes: 76 additions & 0 deletions src/CommandLineUtils/Attributes/FilePathNotExistsAttributeBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using McMaster.Extensions.CommandLineUtils.Abstractions;

namespace McMaster.Extensions.CommandLineUtils.Validation
{
/// <summary>
/// Base type for attributes that check for files or directories not existing.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public abstract class FilePathNotExistsAttributeBase : ValidationAttribute
{
private readonly FilePathType _filePathType;

/// <summary>
/// Initializes an instance of <see cref="FilePathNotExistsAttributeBase"/>.
/// </summary>
/// <param name="filePathType">Acceptable file path types</param>
internal FilePathNotExistsAttributeBase(FilePathType filePathType)
: base(GetDefaultErrorMessage(filePathType))
{
_filePathType = filePathType;
}

/// <inheritdoc />
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!(value is string path) || path.Length == 0 || path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
return new ValidationResult(FormatErrorMessage(value as string));
}

if (!Path.IsPathRooted(path)
&& validationContext.GetService(typeof(CommandLineContext)) is CommandLineContext context)
{
path = Path.Combine(context.WorkingDirectory, path);
}

if ((_filePathType == FilePathType.File) && !File.Exists(path))
{
return ValidationResult.Success;
}

if ((_filePathType == FilePathType.Directory) && !Directory.Exists(path))
{
return ValidationResult.Success;
}

if ((_filePathType == FilePathType.Any) && (!File.Exists(path) && !Directory.Exists(path)))
{
return ValidationResult.Success;
}

return new ValidationResult(FormatErrorMessage(value as string));
}

private static string GetDefaultErrorMessage(FilePathType filePathType)
{
if (filePathType == FilePathType.File)
{
return "The file '{0}' already exists.";
}

if (filePathType == FilePathType.Directory)
{
return "The directory '{0}' already exists.";
}

return "The file path '{0}' already exists.";
}
}
}
27 changes: 27 additions & 0 deletions src/CommandLineUtils/Validation/ValidationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,15 @@ public static IValidationBuilder EmailAddress(this IValidationBuilder builder, s
public static IValidationBuilder ExistingFile(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<FileExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be a path to a file that does not already exist.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="errorMessage">A custom error message to display.</param>
/// <returns>The builder.</returns>
public static IValidationBuilder NonExistingFile(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<FileNotExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be a path to a directory that already exists.
/// </summary>
Expand All @@ -270,6 +279,15 @@ public static IValidationBuilder ExistingFile(this IValidationBuilder builder, s
public static IValidationBuilder ExistingDirectory(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<DirectoryExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be a path to a directory that does not already exist.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="errorMessage">A custom error message to display.</param>
/// <returns>The builder.</returns>
public static IValidationBuilder NonExistingDirectory(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<DirectoryNotExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be a valid file path or directory, and the file path must already exist.
/// </summary>
Expand All @@ -279,6 +297,15 @@ public static IValidationBuilder ExistingDirectory(this IValidationBuilder build
public static IValidationBuilder ExistingFileOrDirectory(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<FileOrDirectoryExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be a valid file path or directory, and the file path must not already exist.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="errorMessage">A custom error message to display.</param>
/// <returns>The builder.</returns>
public static IValidationBuilder NonExistingFileOrDirectory(this IValidationBuilder builder, string errorMessage = null)
=> builder.Satisfies<FileOrDirectoryNotExistsAttribute>(errorMessage);

/// <summary>
/// Specifies that values must be legal file paths.
/// </summary>
Expand Down
186 changes: 186 additions & 0 deletions test/CommandLineUtils.Tests/FilePathNotExistsAttributeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using McMaster.Extensions.CommandLineUtils.Internal;
using Xunit;
using Xunit.Abstractions;

namespace McMaster.Extensions.CommandLineUtils.Tests
{
public class FilePathNotExistsAttributeTests
{
private readonly ITestOutputHelper _output;

public FilePathNotExistsAttributeTests(ITestOutputHelper output)
{
_output = output;
}

private class App
{
[Argument(0)]
[FileOrDirectoryNotExists]
public string File { get; }

private void OnExecute() { }
}

[Theory]
[InlineData("exists.txt")]
public void ValidatesFilesMustNotExist(string filePath)
{
var exists = Path.Combine(AppContext.BaseDirectory, filePath);
if (!File.Exists(exists))
{
File.WriteAllText(exists, "");
}

var app = new CommandLineApplication(
new TestConsole(_output),
AppContext.BaseDirectory, false);

app.Argument("Files", "Files")
.Accepts().NonExistingFileOrDirectory();

var result = app
.Parse(filePath)
.SelectedCommand
.GetValidationResult();

Assert.NotEqual(ValidationResult.Success, result);
Assert.Equal($"The file path '{filePath}' already exists.", result.ErrorMessage);

var console = new TestConsole(_output);
Assert.NotEqual(0, CommandLineApplication.Execute<App>(console, filePath));
}

public static TheoryData<string> BadFilePaths
=> new TheoryData<string>
{
"notfound.txt",
"\0",
null,
string.Empty,
};

[Fact]
public void ValidatesFilesRelativeToAppContext()
{
var exists = Path.Combine(AppContext.BaseDirectory, "exists.txt");
if (!File.Exists(exists))
{
File.WriteAllText(exists, "");
}

var appInBaseDir = new CommandLineApplication(
new TestConsole(_output),
AppContext.BaseDirectory,
false);
var notFoundDir = Path.Combine(AppContext.BaseDirectory, "notfound");
var appNotInBaseDir = new CommandLineApplication(
new TestConsole(_output),
notFoundDir,
false);

appInBaseDir.Argument("Files", "Files")
.Accepts(v => v.NonExistingFileOrDirectory());
appNotInBaseDir.Argument("Files", "Files")
.Accepts(v => v.NonExistingFileOrDirectory());

var fails = appInBaseDir
.Parse("exists.txt")
.SelectedCommand
.GetValidationResult();

var success = appNotInBaseDir
.Parse("exists.txt")
.SelectedCommand
.GetValidationResult();

Assert.NotEqual(ValidationResult.Success, fails);
Assert.Equal("The file path 'exists.txt' already exists.", fails.ErrorMessage);

Assert.Equal(ValidationResult.Success, success);

var console = new TestConsole(_output);
var context = new DefaultCommandLineContext(console, appInBaseDir.WorkingDirectory, new[] { "exists.txt" });
Assert.NotEqual(0, CommandLineApplication.Execute<App>(context));

context = new DefaultCommandLineContext(console, appNotInBaseDir.WorkingDirectory, new[] { "exists.txt" });
Assert.Equal(0, CommandLineApplication.Execute<App>(context));
}

[Theory]
[InlineData("./dir")]
[InlineData("./")]
[InlineData("../")]
public void ValidatesDirectories(string dirPath)
{
Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, dirPath));

var context = new DefaultCommandLineContext(
new TestConsole(_output),
AppContext.BaseDirectory,
new[] { dirPath });

Assert.NotEqual(0, CommandLineApplication.Execute<App>(context));
}

private class OnlyDir
{
[Argument(0)]
[DirectoryNotExists]
public string Dir { get; }

private void OnExecute() { }
}

private class OnlyFile
{
[Argument(0)]
[FileNotExists]
public string Path { get; }

private void OnExecute() { }
}

[Theory]
[InlineData("./dir")]
[InlineData("./")]
[InlineData("../")]
public void ValidatesOnlyDirectories(string dirPath)
{
Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, dirPath));

var context = new DefaultCommandLineContext(
new TestConsole(_output),
AppContext.BaseDirectory,
new[] { dirPath });

Assert.Equal(0, CommandLineApplication.Execute<OnlyFile>(context));
Assert.NotEqual(0, CommandLineApplication.Execute<OnlyDir>(context));
}

[Fact]
public void ValidatesOnlyFiles()
{
var filePath = "exists.txt";
var fullPath = Path.Combine(AppContext.BaseDirectory, filePath);
if (!File.Exists(fullPath))
{
File.WriteAllText(fullPath, "");
}

var context = new DefaultCommandLineContext(
new TestConsole(_output),
AppContext.BaseDirectory,
new[] { filePath });

Assert.NotEqual(0, CommandLineApplication.Execute<OnlyFile>(context));
Assert.Equal(0, CommandLineApplication.Execute<OnlyDir>(context));
}
}
}

0 comments on commit be23040

Please sign in to comment.