From ec9651e5e9eb65296c1503ee1a3bc55b1bbd32ee Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Wed, 12 Feb 2025 16:10:49 -0600
Subject: [PATCH] feat: implement generate command in console app
---
.../AltGen.Console.Tests.csproj | 2 +
.../Unit/AltGenServiceTests.cs | 87 ++++++++++
src/AltGen.Console.Tests/UnitTest1.cs | 10 --
src/AltGen.Console.Tests/Usings.cs | 3 +
src/AltGen.Console/AltGen.Console.csproj | 30 ++++
src/AltGen.Console/Generate/AltGenService.cs | 44 +++++
.../Generate/GenerateAltTextRequest.cs | 9 +
.../Generate/GenerateCommand.cs | 88 ++++++++++
src/AltGen.Console/Generate/IAltGenService.cs | 4 +
src/AltGen.Console/Generated/Constants.cs | 6 +
src/AltGen.Console/Program.cs | 163 ++----------------
src/AltGen.Console/Usings.cs | 11 ++
12 files changed, 296 insertions(+), 161 deletions(-)
create mode 100644 src/AltGen.Console.Tests/Unit/AltGenServiceTests.cs
delete mode 100644 src/AltGen.Console.Tests/UnitTest1.cs
create mode 100644 src/AltGen.Console.Tests/Usings.cs
create mode 100644 src/AltGen.Console/Generate/AltGenService.cs
create mode 100644 src/AltGen.Console/Generate/GenerateAltTextRequest.cs
create mode 100644 src/AltGen.Console/Generate/GenerateCommand.cs
create mode 100644 src/AltGen.Console/Generate/IAltGenService.cs
create mode 100644 src/AltGen.Console/Generated/Constants.cs
diff --git a/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj b/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj
index ffec1a6..7995a27 100644
--- a/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj
+++ b/src/AltGen.Console.Tests/AltGen.Console.Tests.csproj
@@ -12,6 +12,8 @@
+
+
diff --git a/src/AltGen.Console.Tests/Unit/AltGenServiceTests.cs b/src/AltGen.Console.Tests/Unit/AltGenServiceTests.cs
new file mode 100644
index 0000000..bd700f4
--- /dev/null
+++ b/src/AltGen.Console.Tests/Unit/AltGenServiceTests.cs
@@ -0,0 +1,87 @@
+using System.Net;
+
+using FluentAssertions;
+namespace AltGen.Console.Tests.Unit;
+
+public class AltGenServiceTests : IDisposable
+{
+ readonly MockHttpMessageHandler _mockHttpMessageHandler = new();
+ readonly AltGenService _altGenService;
+
+ public AltGenServiceTests()
+ {
+ _altGenService = new AltGenService(_mockHttpMessageHandler.ToHttpClient());
+ }
+
+ [Fact]
+ public async Task GenerateAltTextAsync_WhenCalledAndRequestSucceeds_ItShouldReturnAltText()
+ {
+ var req = new GenerateAltTextRequest(
+ "provider",
+ "key",
+ "file.jpg",
+ [1, 2, 3],
+ "image/jpeg"
+ );
+
+ var altText = "alt text";
+
+ _mockHttpMessageHandler
+ .When(HttpMethod.Post, "*/generate")
+ .Respond(
+ "application/json",
+ /*lang=json,strict*/
+ $@"{{""AltText"":""{altText}""}}"
+ );
+
+ var result = await _altGenService.GenerateAltTextAsync(req);
+
+ result.Should().Be(altText);
+ }
+
+ [Fact]
+ public async Task GenerateAltTextAsync_WhenCalledAndRequestFails_ItShouldThrowException()
+ {
+ var req = new GenerateAltTextRequest(
+ "provider",
+ "key",
+ "file.jpg",
+ [1, 2, 3],
+ "image/jpeg"
+ );
+
+ _mockHttpMessageHandler
+ .When(HttpMethod.Post, "*/generate")
+ .Respond(HttpStatusCode.InternalServerError);
+
+ var action = async () => await _altGenService.GenerateAltTextAsync(req);
+
+ await action.Should().ThrowAsync();
+ }
+
+ [Fact]
+ public async Task GenerateAltTextAsync_WhenCalledAndResponseIsNull_ItShouldThrowException()
+ {
+ var req = new GenerateAltTextRequest(
+ "provider",
+ "key",
+ "file.jpg",
+ [1, 2, 3],
+ "image/jpeg"
+ );
+
+ _mockHttpMessageHandler
+ .When(HttpMethod.Post, "*/generate")
+ .Respond("application/json", "null");
+
+ var action = async () => await _altGenService.GenerateAltTextAsync(req);
+
+ await action.Should().ThrowAsync();
+ }
+
+ public void Dispose()
+ {
+ _mockHttpMessageHandler.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file
diff --git a/src/AltGen.Console.Tests/UnitTest1.cs b/src/AltGen.Console.Tests/UnitTest1.cs
deleted file mode 100644
index 39b4247..0000000
--- a/src/AltGen.Console.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace AltGen.Console.Tests;
-
-public class UnitTest1
-{
- [Fact]
- public void Test1()
- {
-
- }
-}
diff --git a/src/AltGen.Console.Tests/Usings.cs b/src/AltGen.Console.Tests/Usings.cs
new file mode 100644
index 0000000..a5fd333
--- /dev/null
+++ b/src/AltGen.Console.Tests/Usings.cs
@@ -0,0 +1,3 @@
+global using RichardSzalay.MockHttp;
+
+global using AltGen.Console.Generate;
\ No newline at end of file
diff --git a/src/AltGen.Console/AltGen.Console.csproj b/src/AltGen.Console/AltGen.Console.csproj
index d51b937..f192040 100644
--- a/src/AltGen.Console/AltGen.Console.csproj
+++ b/src/AltGen.Console/AltGen.Console.csproj
@@ -16,4 +16,34 @@
+
+
+
+
+
+ Generated
+ $(GeneratedCodeDirectory)\Constants.cs
+ http://localhost:7297
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AltGen.Console/Generate/AltGenService.cs b/src/AltGen.Console/Generate/AltGenService.cs
new file mode 100644
index 0000000..9576aee
--- /dev/null
+++ b/src/AltGen.Console/Generate/AltGenService.cs
@@ -0,0 +1,44 @@
+namespace AltGen.Console.Generate;
+
+sealed class AltGenService(HttpClient client) : IAltGenService
+{
+ static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ readonly HttpClient _client = client;
+
+ public async Task GenerateAltTextAsync(GenerateAltTextRequest req)
+ {
+ var byteContent = new ByteArrayContent(req.Image);
+ byteContent.Headers.ContentType = new MediaTypeHeaderValue(req.ContentType);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{Constants.AltGenApiUri}/generate")
+ {
+ Content = new MultipartFormDataContent
+ {
+ { new StringContent(req.Provider), "provider" },
+ { new StringContent(req.ProviderKey), "providerKey" },
+ { new ByteArrayContent(req.Image), "file", req.FileName }
+ }
+ };
+
+ var response = await _client.SendAsync(request);
+ var content = await response.Content.ReadAsStringAsync();
+
+ if (response.IsSuccessStatusCode is false)
+ {
+ throw new AltTextException("Failed to generate alt text.");
+ }
+
+ var altTextResponse = JsonSerializer.Deserialize(content, JsonOptions) ?? throw new AltTextException("Failed to deserialize response.");
+
+ return altTextResponse.AltText;
+ }
+}
+
+record AltTextResponse(string AltText);
+
+class AltTextException(string message) : Exception(message)
+{
+}
diff --git a/src/AltGen.Console/Generate/GenerateAltTextRequest.cs b/src/AltGen.Console/Generate/GenerateAltTextRequest.cs
new file mode 100644
index 0000000..b1f432f
--- /dev/null
+++ b/src/AltGen.Console/Generate/GenerateAltTextRequest.cs
@@ -0,0 +1,9 @@
+namespace AltGen.Console.Generate;
+
+record GenerateAltTextRequest(
+ string Provider,
+ string ProviderKey,
+ string FileName,
+ byte[] Image,
+ string ContentType
+);
diff --git a/src/AltGen.Console/Generate/GenerateCommand.cs b/src/AltGen.Console/Generate/GenerateCommand.cs
new file mode 100644
index 0000000..d4b7259
--- /dev/null
+++ b/src/AltGen.Console/Generate/GenerateCommand.cs
@@ -0,0 +1,88 @@
+namespace AltGen.Console.Generate;
+
+sealed class GenerateCommand(
+ IAnsiConsole console,
+ IFileSystem fileSystem,
+ IAltGenService altGenService
+) : AsyncCommand
+{
+
+ readonly IAnsiConsole _console = console;
+ readonly IFileSystem _fileSystem = fileSystem;
+ readonly IAltGenService _altGenService = altGenService;
+
+ public class Settings(IFileSystem fileSystem) : CommandSettings
+ {
+ readonly Dictionary _imageTypes = new()
+ {
+ [".jpeg"] = "image/jpeg",
+ [".jpg"] = "image/jpeg",
+ [".png"] = "image/png"
+ };
+
+ readonly List _providers = [
+ "gemini",
+ ];
+
+ readonly IFileSystem _fileSystem = fileSystem;
+
+ [CommandArgument(1, "")]
+ [Description("The provider to use for generating alt text.")]
+ public string Provider { get; init; } = string.Empty;
+
+ [CommandArgument(2, "")]
+ [Description("The key for the provider.")]
+ public string Key { get; init; } = string.Empty;
+
+ [CommandArgument(3, "")]
+ [Description("The path to the image to generate alt text for.")]
+ public string Path { get; init; } = string.Empty;
+
+ public string ContentType => _imageTypes[_fileSystem.Path.GetExtension(Path)];
+
+ public override ValidationResult Validate()
+ {
+ // TODO: We should allow provider and key to be optional
+ // if they are not passed on the command line then
+ // we should try to resolve them from configuration
+ if (_providers.Contains(Provider) is false)
+ {
+ return ValidationResult.Error($"The provider '{Provider}' is not supported.");
+ }
+
+ var pathExists = _fileSystem.File.Exists(Path);
+
+ if (pathExists is false)
+ {
+ return ValidationResult.Error($"The path '{Path}' does not exist.");
+ }
+
+ var extension = _fileSystem.Path.GetExtension(Path);
+
+ if (_imageTypes.ContainsKey(extension) is false)
+ {
+ return ValidationResult.Error($"The file '{Path}' is not a valid image file.");
+ }
+
+ return ValidationResult.Success();
+ }
+ }
+
+ public override async Task ExecuteAsync(CommandContext context, Settings settings)
+ {
+ var fileName = _fileSystem.Path.GetFileName(settings.Path);
+ var image = await _fileSystem.File.ReadAllBytesAsync(settings.Path);
+
+ var altText = await _altGenService.GenerateAltTextAsync(new(
+ settings.Provider,
+ settings.Key,
+ fileName,
+ image,
+ settings.ContentType
+ ));
+
+ _console.MarkupLine($"[bold]{altText}[/]");
+
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/src/AltGen.Console/Generate/IAltGenService.cs b/src/AltGen.Console/Generate/IAltGenService.cs
new file mode 100644
index 0000000..1b2b14b
--- /dev/null
+++ b/src/AltGen.Console/Generate/IAltGenService.cs
@@ -0,0 +1,4 @@
+interface IAltGenService
+{
+ Task GenerateAltTextAsync(GenerateAltTextRequest req);
+}
diff --git a/src/AltGen.Console/Generated/Constants.cs b/src/AltGen.Console/Generated/Constants.cs
new file mode 100644
index 0000000..c9ed236
--- /dev/null
+++ b/src/AltGen.Console/Generated/Constants.cs
@@ -0,0 +1,6 @@
+namespace AltGen.Console.Generated;
+
+static class Constants
+{
+ public const string AltGenApiUri = "http://localhost:7297";
+}
diff --git a/src/AltGen.Console/Program.cs b/src/AltGen.Console/Program.cs
index e31f069..ac7c446 100644
--- a/src/AltGen.Console/Program.cs
+++ b/src/AltGen.Console/Program.cs
@@ -1,18 +1,17 @@
-// TODO: We want to be able
-// to provide a path to an image
-// read the image
-// and then post the image to our API
-// get the response and display the alt text
-
-using System.ComponentModel;
-using System.IO.Abstractions;
-using System.Net.Http.Headers;
-
-using AltGen.Console.Common;
-
-using Spectre.Console;
+// TODO: Need to implement config
+// command. This should allow
+// the user to set default values
+// for provider keys and a default
+// provider.
+//
+// For example setting a providers key
+// i.e. altgen config gemini mykey
+//
+// Or setting provider as default
+// i.e. altgen config gemini mykey --default
await Host.CreateDefaultBuilder(args)
+ .ConfigureLogging(static l => l.ClearProviders())
.ConfigureServices(static (_, services) =>
{
services.AddSingleton();
@@ -22,141 +21,3 @@ await Host.CreateDefaultBuilder(args)
})
.BuildApp()
.RunAsync(args);
-
-sealed class GenerateCommand(
- IAnsiConsole console,
- IFileSystem fileSystem,
- IAltGenService altGenService
-) : AsyncCommand
-{
-
- readonly IAnsiConsole _console = console;
- readonly IFileSystem _fileSystem = fileSystem;
- readonly IAltGenService _altGenService = altGenService;
-
- public class Settings(IFileSystem fileSystem) : CommandSettings
- {
- readonly Dictionary _imageTypes = new()
- {
- [".jpeg"] = "image/jpeg",
- [".jpg"] = "image/jpeg",
- [".png"] = "image/png"
- };
-
- readonly List _providers = [
- "gemini",
- ];
-
- readonly IFileSystem _fileSystem = fileSystem;
-
- [CommandArgument(1, "")]
- [Description("The provider to use for generating alt text.")]
- public string Provider { get; init; } = string.Empty;
-
- [CommandArgument(2, "")]
- [Description("The key for the provider.")]
- public string Key { get; init; } = string.Empty;
-
- [CommandArgument(3, "")]
- [Description("The path to the image to generate alt text for.")]
- public string Path { get; init; } = string.Empty;
-
- public string ContentType => _imageTypes[_fileSystem.Path.GetExtension(Path)];
-
- public override ValidationResult Validate()
- {
- if (_providers.Contains(Provider) is false)
- {
- return ValidationResult.Error($"The provider '{Provider}' is not supported.");
- }
-
- var pathExists = _fileSystem.File.Exists(Path);
-
- if (pathExists is false)
- {
- return ValidationResult.Error($"The path '{Path}' does not exist.");
- }
-
- var extension = _fileSystem.Path.GetExtension(Path);
-
- if (_imageTypes.ContainsKey(extension) is false)
- {
- return ValidationResult.Error($"The file '{Path}' is not a valid image file.");
- }
-
- return ValidationResult.Success();
- }
- }
-
- public override async Task ExecuteAsync(CommandContext context, Settings settings)
- {
- var fileName = _fileSystem.Path.GetFileName(settings.Path);
- var image = await _fileSystem.File.ReadAllBytesAsync(settings.Path);
-
- var altText = await _altGenService.GenerateAltTextAsync(
- settings.Provider,
- settings.Key,
- fileName,
- image,
- settings.ContentType
- );
-
- _console.MarkupLine($"[bold]Alt Text:[/] {altText}");
-
- return 0;
- }
-}
-
-interface IAltGenService
-{
- Task GenerateAltTextAsync(
- string provider,
- string key,
- string fileName,
- byte[] image,
- string contentType
- );
-}
-
-sealed class AltGenService(HttpClient client) : IAltGenService
-{
- readonly HttpClient _client = client;
-
- public async Task GenerateAltTextAsync(
- string provider,
- string key,
- string fileName,
- byte[] image,
- string contentType
- )
- {
- var byteContent = new ByteArrayContent(image);
- byteContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
-
- // TODO: Need to have proper URL
- // have constants generated at build time
- // that point to proper API URL
- var request = new HttpRequestMessage(HttpMethod.Post, "altgen")
- {
- Content = new MultipartFormDataContent
- {
- { new StringContent(provider), "provider" },
- { new StringContent(key), "providerKey" },
- { new ByteArrayContent(image), "file", fileName }
- }
- };
-
- var response = await _client.SendAsync(request);
-
- if (response.IsSuccessStatusCode is false)
- {
- // TODO: Use accurate exception
- throw new InvalidDataException("Failed to generate alt text.");
- }
-
- // TODO: Deserialize the response
- var altText = await response.Content.ReadAsStringAsync();
-
- return altText;
- }
-}
\ No newline at end of file
diff --git a/src/AltGen.Console/Usings.cs b/src/AltGen.Console/Usings.cs
index fdd2bfc..8dab214 100644
--- a/src/AltGen.Console/Usings.cs
+++ b/src/AltGen.Console/Usings.cs
@@ -1,4 +1,15 @@
+global using System.ComponentModel;
+global using System.IO.Abstractions;
+global using System.Net.Http.Headers;
+global using System.Text.Json;
+
+global using AltGen.Console.Common;
+global using AltGen.Console.Generated;
+global using AltGen.Console.Generate;
+
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
+global using Microsoft.Extensions.Logging;
+global using Spectre.Console;
global using Spectre.Console.Cli;