Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Benchmarks/Benchmarks.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,10 +2,10 @@
<PropertyGroup>
<AnalysisLevel>latest-all</AnalysisLevel>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<RootNamespace>ptr727.ProjectTemplate.Benchmarks</RootNamespace>
<IsPackable>false</IsPackable>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
Expand Down
47 changes: 47 additions & 0 deletions CodeGen/ApiNinjas.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ptr727.ProjectTemplate.CodeGen;

internal sealed class ApiNinjas(string apiKey, CancellationToken cancellationToken)
{
internal async Task<string> GetQuoteOfTheDayAsync()
{
// https://api-ninjas.com/api/quotes#v2-quoteoftheday
using HttpRequestMessage request = new(
HttpMethod.Get,
"https://api.api-ninjas.com/v2/quotes?categories=philosophy"
);
request.Headers.Add("X-Api-Key", apiKey);

using HttpResponseMessage response = await HttpClientFactory
.GetHttpClient()
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
_ = response.EnsureSuccessStatusCode();

using Stream responseStream = await response
.Content.ReadAsStreamAsync(cancellationToken)
.ConfigureAwait(false);
QuoteOfTheDayItem[]? items = await JsonSerializer
.DeserializeAsync(
responseStream,
QuoteOfTheDayJsonContext.Default.QuoteOfTheDayItemArray,
cancellationToken
)
.ConfigureAwait(false);

string? quote = items?.FirstOrDefault()?.Quote;
return string.IsNullOrWhiteSpace(quote)
? throw new InvalidOperationException(
"Quote of the day response did not include a quote."
)
: quote;
}
}

internal sealed record QuoteOfTheDayItem([property: JsonPropertyName("quote")] string Quote);

[JsonSerializable(typeof(QuoteOfTheDayItem[]))]
internal sealed partial class QuoteOfTheDayJsonContext : JsonSerializerContext;
10 changes: 5 additions & 5 deletions CodeGen/CodeGen.csproj
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,18 +2,18 @@
<PropertyGroup>
<AnalysisLevel>latest-all</AnalysisLevel>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<InformationalVersion>1.0.0-pre</InformationalVersion>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<PublishAot>false</PublishAot>
<RootNamespace>ptr727.ProjectTemplate.CodeGen</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<Version>1.0.0</Version>
<InformationalVersion Condition="'$(SourceRevisionId)' != ''"
>$(Version)+$(SourceRevisionId)</InformationalVersion
>
</PropertyGroup>
<PropertyGroup Condition="'$(IsPublish)' == 'true'">
<PublishAot>true</PublishAot>
<PropertyGroup Condition="'$(PublishAot)' == 'true'">
<InvariantGlobalization>true</InvariantGlobalization>
<SelfContained>true</SelfContained>
<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>
</PropertyGroup>
<ItemGroup>
Expand Down
62 changes: 62 additions & 0 deletions CodeGen/CodeGenBuilder.cs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
using System.IO;
using System.Text;

namespace ptr727.ProjectTemplate.CodeGen;

internal sealed class CodeGenBuilder(string outputPath, CancellationToken cancellationToken)
{
internal async Task CodeGenAsync(string quote)
{
// Codegen example
string codeGen = $$"""
namespace ptr727.ProjectTemplate.CodeGen;

internal static class CodeGen
{
private const string QuoteOfTheDay = {{ToCSharpStringLiteral(quote)}};

[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Globalization",
"CA1303:Do not pass literals as localized parameters",
Justification = "Demonstration code."
)]
internal static void Quote()
{
string dateTime = $"{{DateTime.UtcNow:o}}";
Console.WriteLine($"{dateTime} : {QuoteOfTheDay}");
Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay);
}
}
""";

// Write code to file
await File.WriteAllTextAsync(outputPath, codeGen, cancellationToken).ConfigureAwait(false);
}

private static string ToCSharpStringLiteral(string value)
{
StringBuilder sb = new(value.Length + 2);
_ = sb.Append('"');
foreach (char c in value)
{
_ = sb.Append(
c switch
{
'\\' => "\\\\",
'\"' => "\\\"",
'\r' => "\\r",
'\n' => "\\n",
'\t' => "\\t",
'\0' => "\\0",
'\b' => "\\b",
'\f' => "\\f",
'\u2019' => "'",
_ when char.IsControl(c) => $"\\u{(int)c:X4}",
_ => c.ToString(),
}
);
}
_ = sb.Append('"');
return sb.ToString();
}
}
29 changes: 15 additions & 14 deletions CodeGen/CommandLine.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,35 +6,36 @@ namespace ptr727.ProjectTemplate.CodeGen;

internal sealed class CommandLine
{
internal sealed class Context
internal CommandLine(string[] args)
{
Root = CreateRootCommand();
Result = Root.Parse(args);
}

internal sealed class Options
{
internal required DirectoryInfo CodePath { get; init; }
internal required string APIKey { get; init; }
}

internal static async Task<(
CommandLine commandLine,
RootCommand rootCommand
)> CreateRootCommandWithCommandLine()
internal RootCommand Root { get; init; }
internal ParseResult Result { get; init; }

internal RootCommand CreateRootCommand()
{
CommandLine commandLine = new();
RootCommand rootCommand = new("C# .NET codegen project")
{
commandLine._codePathOption,
commandLine._apiKeyOption,
};
RootCommand rootCommand = new("C# .NET codegen project") { _codePathOption, _apiKeyOption };
rootCommand.SetAction(
(parseResult, cancellationToken) =>
{
Program program = new(commandLine.CreateContext(parseResult), cancellationToken);
Program program = new(CreateOptions(parseResult), cancellationToken);
return program.ExecuteAsync();
}
);

return (commandLine, rootCommand);
return rootCommand;
}

internal Context CreateContext(ParseResult parseResult) =>
internal Options CreateOptions(ParseResult parseResult) =>
new()
{
CodePath = parseResult.GetValue(_codePathOption)!,
Expand Down
136 changes: 20 additions & 116 deletions CodeGen/Program.cs
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,26 @@
using System.CommandLine;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog.Sinks.SystemConsole.Themes;

namespace ptr727.ProjectTemplate.CodeGen;

internal sealed class Program(
CommandLine.Context commandLineContext,
CommandLine.Options commandLineOptions,
CancellationToken cancellationToken
)
{
internal CommandLine.Context GetCommandLineContext() => commandLineContext;
internal CommandLine.Options GetCommandLineOptions() => commandLineOptions;

internal CancellationToken GetCancellationToken() => cancellationToken;

private readonly HttpClient _httpClient = HttpClientFactory.GetHttpClient();

internal HttpClient GetHttpClient() => _httpClient;

internal static async Task<int> Main(string[] args)
{
// Parse commandline
(CommandLine _, RootCommand rootCommand) = await CommandLine
.CreateRootCommandWithCommandLine()
.ConfigureAwait(false);
ParseResult parseResult = rootCommand.Parse(args);
CommandLine commandLine = new(args);

// Bypass startup for help and version commands
if (CommandLine.BypassStartup(parseResult))
// Bypass startup for errors or help and version commands
if (CommandLine.BypassStartup(commandLine.Result))
{
return await parseResult.InvokeAsync().ConfigureAwait(false);
return await commandLine.Result.InvokeAsync().ConfigureAwait(false);
}

// Configure logging
Expand All@@ -44,119 +33,34 @@ internal static async Task<int> Main(string[] args)
Log.Logger = loggerConfiguration.CreateLogger();

// Invoke command
return await parseResult.InvokeAsync().ConfigureAwait(false);
return await commandLine.Result.InvokeAsync().ConfigureAwait(false);
}

internal async Task<int> ExecuteAsync()
{
try
{
Log.Information("Executing codegen command...");

string quoteoftheday = "No API key provided.";
if (!string.IsNullOrEmpty(commandLineContext.APIKey))
if (!string.IsNullOrEmpty(commandLineOptions.APIKey))
{
quoteoftheday = await GetQuoteOfTheDayAsync().ConfigureAwait(false);
Log.Information("Retrieving quote from API Ninjas...");
ApiNinjas apiNinjas = new(commandLineOptions.APIKey, cancellationToken);
quoteoftheday = await apiNinjas.GetQuoteOfTheDayAsync().ConfigureAwait(false);
}
await CodeGenAsync(quoteoftheday).ConfigureAwait(false);
Log.Information("Quote: {Quote}", quoteoftheday);

string outputPath = Path.Combine(commandLineOptions.CodePath.FullName, "CodeGen.cs");
Log.Information("Writing quote to {OutputPath}", outputPath);
CodeGenBuilder codegenBuilder = new(outputPath, cancellationToken);
await codegenBuilder.CodeGenAsync(quoteoftheday).ConfigureAwait(false);

return 0;
}
catch (Exception ex) when (Log.Logger.LogAndHandle(ex))
{
return 1;
}
}

private async Task<string> GetQuoteOfTheDayAsync()
{
// https://api-ninjas.com/api/quotes#v2-quoteoftheday
using HttpRequestMessage request = new(
HttpMethod.Get,
"https://api.api-ninjas.com/v2/quotes?categories=philosophy"
);
request.Headers.Add("X-Api-Key", commandLineContext.APIKey);

using HttpResponseMessage response = await GetHttpClient()
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, GetCancellationToken())
.ConfigureAwait(false);
_ = response.EnsureSuccessStatusCode();

using Stream responseStream = await response
.Content.ReadAsStreamAsync(GetCancellationToken())
.ConfigureAwait(false);
QuoteOfTheDayItem[]? items = await JsonSerializer
.DeserializeAsync(
responseStream,
QuoteOfTheDayJsonContext.Default.QuoteOfTheDayItemArray,
GetCancellationToken()
)
.ConfigureAwait(false);

string? quote = items?.FirstOrDefault()?.Quote;
return string.IsNullOrWhiteSpace(quote)
? throw new InvalidOperationException(
"Quote of the day response did not include a quote."
)
: quote;
}

private async Task CodeGenAsync(string quote)
{
// Codegen example
string codeGen = $$"""
namespace ptr727.ProjectTemplate.CodeGen;

internal static class CodeGen
{
private const string QuoteOfTheDay = {{ToCSharpStringLiteral(quote)}};

[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Globalization",
"CA1303:Do not pass literals as localized parameters",
Justification = "Demonstration code."
)]
internal static void Quote()
{
string dateTime = $"{{DateTime.UtcNow:o}}";
Console.WriteLine($"{dateTime} : {QuoteOfTheDay}");
Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay);
}
}
""";

// Write code to file
string outputPath = Path.Combine(commandLineContext.CodePath.FullName, "CodeGen.cs");
await File.WriteAllTextAsync(outputPath, codeGen, GetCancellationToken())
.ConfigureAwait(false);
}

private static string ToCSharpStringLiteral(string value)
{
StringBuilder sb = new(value.Length + 2);
_ = sb.Append('"');
foreach (char c in value)
{
_ = sb.Append(
c switch
{
'\\' => "\\\\",
'\"' => "\\\"",
'\r' => "\\r",
'\n' => "\\n",
'\t' => "\\t",
'\0' => "\\0",
'\b' => "\\b",
'\f' => "\\f",
'\u2019' => "'",
_ when char.IsControl(c) => $"\\u{(int)c:X4}",
_ => c.ToString(),
}
);
}
_ = sb.Append('"');
return sb.ToString();
}
}

internal sealed record QuoteOfTheDayItem([property: JsonPropertyName("quote")] string Quote);

[JsonSerializable(typeof(QuoteOfTheDayItem[]))]
internal sealed partial class QuoteOfTheDayJsonContext : JsonSerializerContext;
Loading