Skip to content

Commit 6b7de68

Browse files
committed
feat: Improve command validation
1 parent 1d5c235 commit 6b7de68

34 files changed

+358
-72
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
using Discord.WebSocket;
2+
using FluentValidation.Results;
23
using MediatR;
34

45
namespace DockerDiscordBot.Commands;
56

67
public abstract class Command : IRequest
78
{
89
public SocketMessage Message { get; }
10+
public ValidationResult? ValidationResult { get; protected set; }
911

1012
protected Command(SocketMessage message)
1113
{
1214
Message = message;
1315
}
16+
17+
public abstract bool IsValid();
1418
}

DockerDiscordBot/Commands/CommandHandler.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,25 @@ protected CommandHandler(ILogger<CommandHandler<T>> logger)
1313
}
1414

1515
public abstract Task Handle(T request, CancellationToken cancellationToken);
16+
17+
protected async ValueTask<bool> TestValidityAsync(Command command)
18+
{
19+
if (command.IsValid())
20+
{
21+
return true;
22+
}
23+
24+
if (command.ValidationResult is null)
25+
{
26+
throw new InvalidOperationException("Command is invalid and should therefore have a validation result");
27+
}
28+
29+
foreach (var error in command.ValidationResult!.Errors)
30+
{
31+
Logger.LogWarning("Validation error: {ErrorMessage}", error.ErrorMessage);
32+
await command.Message.Channel.SendMessageAsync(error.ErrorMessage);
33+
}
34+
35+
return false;
36+
}
1637
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using FluentValidation;
2+
3+
namespace DockerDiscordBot.Commands;
4+
5+
public abstract class CommandValidation<T> : AbstractValidator<T> where T : Command
6+
{
7+
protected void AddRuleForMessageContent(
8+
int? parameterMinAmount = null,
9+
int? parameterMaxAmount = null)
10+
{
11+
if (parameterMinAmount is null && parameterMaxAmount is null)
12+
{
13+
return;
14+
}
15+
16+
RuleFor(x => x.Message.Content)
17+
.NotEmpty()
18+
.WithMessage("Message content cannot be empty.");
19+
20+
if (parameterMinAmount is not null && parameterMaxAmount is not null)
21+
{
22+
var message = parameterMinAmount == parameterMaxAmount
23+
? $"Message content must have exactly {parameterMinAmount} parameter(s)."
24+
: $"Message content must have between {parameterMinAmount} and {parameterMaxAmount} parameter(s).";
25+
26+
RuleFor(x => x.Message.Content.Split(" ", StringSplitOptions.None))
27+
.Must(x => x.Length >= parameterMinAmount + 1 && x.Length <= parameterMaxAmount + 1)
28+
.WithMessage(message);
29+
}
30+
else if (parameterMinAmount is not null)
31+
{
32+
RuleFor(x => x.Message.Content.Split(" ", StringSplitOptions.None))
33+
.Must(x => x.Length >= parameterMinAmount + 1)
34+
.WithMessage($"Message content must have at least {parameterMinAmount} parameter(s).");
35+
}
36+
else if (parameterMaxAmount is not null)
37+
{
38+
RuleFor(x => x.Message.Content.Split(" ", StringSplitOptions.None))
39+
.Must(x => x.Length <= parameterMaxAmount + 1)
40+
.WithMessage($"Message content must have at most {parameterMaxAmount} parameter(s).");
41+
}
42+
}
43+
}

DockerDiscordBot/Commands/CreateContainer/CreateContainerCommand.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,19 @@ namespace DockerDiscordBot.Commands.CreateContainer;
44

55
public sealed class CreateContainerCommand : Command
66
{
7+
private static readonly CreateContainerCommandValidation s_validation = new();
8+
9+
public string ImageId => Message.Content.Split(" ").ElementAtOrDefault(1) ?? string.Empty;
10+
public string ContainerName => Message.Content.Split(" ").ElementAtOrDefault(2) ?? string.Empty;
11+
public string Ports => Message.Content.Split(" ").ElementAtOrDefault(3) ?? string.Empty;
12+
713
public CreateContainerCommand(SocketMessage message) : base(message)
814
{
915
}
16+
17+
public override bool IsValid()
18+
{
19+
ValidationResult = s_validation.Validate(this);
20+
return ValidationResult.IsValid;
21+
}
1022
}

DockerDiscordBot/Commands/CreateContainer/CreateContainerCommandHandler.cs

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,15 @@ public override async Task Handle(CreateContainerCommand request, CancellationTo
1818
{
1919
Logger.LogInformation("Executing {Command}", nameof(CreateContainerCommand));
2020

21-
var parameters = request.Message.Content.Split(" ");
22-
23-
var image = parameters.ElementAtOrDefault(1);
24-
var containerName = parameters.ElementAtOrDefault(2);
25-
var ports = parameters.ElementAtOrDefault(3);
26-
27-
if (string.IsNullOrWhiteSpace(image))
28-
{
29-
await request.Message.Channel.SendMessageAsync("Please provide a container id.");
30-
return;
31-
}
32-
33-
if (string.IsNullOrWhiteSpace(containerName))
21+
if (!await TestValidityAsync(request))
3422
{
35-
await request.Message.Channel.SendMessageAsync("Please provide a container id.");
3623
return;
3724
}
3825

39-
var imageWithoutTag = image.Contains(":") ? image.Split(":")[0] : image;
40-
var tag = image.Contains(":") ? image.Split(":")[^1] : "latest";
26+
var imageWithoutTag = request.ImageId.Contains(":") ? request.ImageId.Split(":")[0] : request.ImageId;
27+
var tag = request.ImageId.Contains(":") ? request.ImageId.Split(":")[^1] : "latest";
4128

42-
var imageExists = await _dockerService.ImageExistsAsync(image, cancellationToken);
29+
var imageExists = await _dockerService.ImageExistsAsync(request.ImageId, cancellationToken);
4330

4431
if (!imageExists)
4532
{
@@ -55,9 +42,9 @@ public override async Task Handle(CreateContainerCommand request, CancellationTo
5542

5643
var portMapping = new Dictionary<string, string>();
5744

58-
if (!string.IsNullOrWhiteSpace(ports))
45+
if (!string.IsNullOrWhiteSpace(request.Ports))
5946
{
60-
var portPairs = ports.Split(",");
47+
var portPairs = request.Ports.Split(",");
6148

6249
foreach (var portPair in portPairs)
6350
{
@@ -68,13 +55,13 @@ public override async Task Handle(CreateContainerCommand request, CancellationTo
6855

6956
var containerId = await _dockerService.CreateContainerAsync(
7057
imageWithoutTag + ":" + tag,
71-
containerName,
58+
request.ContainerName,
7259
portMapping,
7360
cancellationToken);
7461

7562
if (containerId is null)
7663
{
77-
await request.Message.Channel.SendMessageAsync($"Failed to create container {containerName}.");
64+
await request.Message.Channel.SendMessageAsync($"Failed to create container {request.ContainerName}.");
7865
return;
7966
}
8067

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using FluentValidation;
2+
3+
namespace DockerDiscordBot.Commands.CreateContainer;
4+
5+
public sealed class CreateContainerCommandValidation : CommandValidation<CreateContainerCommand>
6+
{
7+
public CreateContainerCommandValidation()
8+
{
9+
AddRuleForMessageContent(2, 3);
10+
AddRuleForImageId();
11+
AddRuleForContainerName();
12+
}
13+
14+
private void AddRuleForImageId()
15+
{
16+
RuleFor(x => x.ImageId)
17+
.NotEmpty()
18+
.WithMessage("Image id cannot be empty.");
19+
}
20+
21+
private void AddRuleForContainerName()
22+
{
23+
RuleFor(x => x.ContainerName)
24+
.NotEmpty()
25+
.WithMessage("Container name cannot be empty.");
26+
}
27+
}

DockerDiscordBot/Commands/GetContainers/GetContainersCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public sealed class GetContainersCommand : Command
77
public GetContainersCommand(SocketMessage message) : base(message)
88
{
99
}
10+
11+
public override bool IsValid() => true;
1012
}

DockerDiscordBot/Commands/GetDockerInfo/GetDockerInfoCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public sealed class GetDockerInfoCommand : Command
77
public GetDockerInfoCommand(SocketMessage message) : base(message)
88
{
99
}
10+
11+
public override bool IsValid() => true;
1012
}

DockerDiscordBot/Commands/GetDockerInfo/GetDockerInfoCommandHandler.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ public override async Task Handle(GetDockerInfoCommand request, CancellationToke
1919
{
2020
Logger.LogInformation("Executing {Command}", nameof(GetDockerInfoCommand));
2121

22-
2322
var result = await _dockerService.GetDockerInfoAsync(cancellationToken);
2423

2524
if (result is null)

DockerDiscordBot/Commands/GetImages/GetImagesCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ public sealed class GetImagesCommand : Command
77
public GetImagesCommand(SocketMessage message) : base(message)
88
{
99
}
10+
11+
public override bool IsValid() => true;
1012
}

0 commit comments

Comments
 (0)