diff --git a/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj b/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj
index 858e390..73a16aa 100644
--- a/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj
+++ b/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj
@@ -10,8 +10,8 @@
-
-
+
+
@@ -46,7 +46,6 @@
-
@@ -61,7 +60,10 @@
-
+
+
+
+
diff --git a/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.targets b/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.targets
index 858b202..6e35260 100644
--- a/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.targets
+++ b/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.targets
@@ -3,8 +3,8 @@
-
-
+
+
@@ -15,13 +15,18 @@
$([System.IO.Path]::Combine($(AsyncAPIBuildToolRoot), tools, net8.0, AsyncAPI.Saunter.Generator.Cli.dll))
-
-
-
+
+
+
+
+
+
+
+
-
$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath), $(AssemblyTitle).dll))
@@ -30,17 +35,13 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -48,10 +49,9 @@
-
-
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj
index f2483a8..e4eeaae 100644
--- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj
+++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj
@@ -39,11 +39,14 @@
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs
new file mode 100644
index 0000000..48a7de7
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs
@@ -0,0 +1,138 @@
+using System.Reflection;
+using System.Text;
+using CaseConverter;
+using LEGO.AsyncAPI.Models;
+using LEGO.AsyncAPI.Readers;
+
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;
+
+internal interface IAsyncApiGenerator
+{
+ string GenerateAsyncApiInterfaces(GeneratorOptions options, string text, AsyncApiState state);
+
+ string GenerateAsyncApiInterfaces(GeneratorOptions options, AsyncApiDocument asyncApi, AsyncApiState state);
+}
+
+internal class AsyncApiGenerator : IAsyncApiGenerator
+{
+ private readonly string _version;
+ private readonly string _name;
+
+ public AsyncApiGenerator()
+ {
+ var assembly = Assembly.GetExecutingAssembly().GetName();
+
+ this._version = assembly.Version!.ToString();
+ this._name = assembly.Name;
+ }
+
+ private static string MakeGlobalDocumentTopic(string name) => $"TOPIC_{name.ToSnakeCase().ToUpperInvariant()}";
+
+ public string GenerateAsyncApiInterfaces(GeneratorOptions options, string text, AsyncApiState state)
+ {
+ var asyncApi = new AsyncApiStringReader().Read(text, out var diagnostic);
+ return this.GenerateAsyncApiInterfaces(options, asyncApi, state);
+ }
+
+ public string GenerateAsyncApiInterfaces(GeneratorOptions options, AsyncApiDocument asyncApi, AsyncApiState state)
+ {
+ var sb = new StringBuilder(
+ $$"""
+ //----------------------
+ //
+ // Generated using {{this._name}} v{{this._version}}
+ // At: {{DateTime.Now:U}}
+ //
+ //----------------------
+
+ using Saunter.Attributes;
+ using System.CodeDom.Compiler;
+
+ namespace {{options.Namespace}}
+ {
+ {{this.GetGeneratedCodeAttributeLine()}}
+ public static partial class {{options.TopicsClassName}}
+ {
+ """);
+
+ sb.AppendLine();
+ sb.AppendLine($" public const string {MakeGlobalDocumentTopic(options.ClassName)} = \"{options.ClassName.ToPascalCase()}\";");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Commands
+ this.AddInterface(sb, options, options.ClassName, $"{options.ClassName}Commands", new("SubscribeOperation", "command", x => x.Subscribe), asyncApi.Channels.Where(x => x.Value.Subscribe != null));
+ sb.AppendLine();
+
+ // Events
+ this.AddInterface(sb, options, options.ClassName, $"{options.ClassName}Events", new("PublishOperation", "evt", x => x.Publish), asyncApi.Channels.Where(x => x.Value.Publish != null));
+
+ sb.AppendLine("}"); // close namespace
+ sb.AppendLine();
+
+ state.Documents.Add(options.ClassName);
+ var contents = sb.ToString();
+ return contents;
+ }
+
+ private string GetGeneratedCodeAttributeLine() => $" [GeneratedCodeAttribute(\"{this._name}\", \"{this._version}\")]";
+
+ private void AddInterface(StringBuilder sb, GeneratorOptions genOptions, string specName, string className, OperationOptions options, IEnumerable> channels)
+ {
+ sb.AppendLine($" [AsyncApi({genOptions.TopicsClassName}.{MakeGlobalDocumentTopic(specName)})]");
+ sb.AppendLine(this.GetGeneratedCodeAttributeLine());
+ sb.AppendLine($" public interface I{className}");
+ sb.Append(" {");
+
+ foreach (var channel in channels)
+ {
+ AddChannelOperation(sb, options, channel);
+ }
+
+ sb.AppendLine(" }");
+ }
+
+ private record OperationOptions(string OperationAttribute, string VarName, Func OperationProvider);
+
+ private static void AddChannelOperation(StringBuilder sb, OperationOptions options, KeyValuePair channel)
+ {
+ var operation = options.OperationProvider(channel.Value);
+
+ sb.AppendLine();
+ sb.AppendLine($" [Channel(\"{channel.Key}\")]");
+ var channelParametersSb = new StringBuilder();
+ foreach (var channelParameter in channel.Value.Parameters)
+ {
+ var refName = FromReference(channelParameter.Value.Reference.Reference);
+ var typeName = refName.ToPascalCase();
+ if (channelParameter.Value.Schema.Enum.Any())
+ {
+ typeName += "s";
+ }
+ var varName = refName.ToCamelCase();
+ sb.AppendLine($" [ChannelParameter(\"{channelParameter.Key}\", typeof({typeName}), Description = \"{channelParameter.Value.Description}\")]");
+
+ channelParametersSb.Append(", ");
+ channelParametersSb.Append(typeName);
+ channelParametersSb.Append(" ");
+ channelParametersSb.Append(varName);
+ }
+
+ var msg = operation.Message.Single();
+ var msgTypeName = (msg.Name ?? msg.Payload.Reference.Id).ToPascalCase();
+ sb.AppendLine($" [{options.OperationAttribute}(typeof({msgTypeName}), Summary = \"{operation.Summary}\", Description = \"{operation.Description}\")]");
+ sb.Append($" void {operation.OperationId}({msgTypeName} {options.VarName}");
+ sb.Append(channelParametersSb);
+ sb.AppendLine(");");
+ }
+
+ private static string FromReference(string reference)
+ {
+ var prefix = "#/components/parameters/";
+ if (reference.IndexOf(prefix) == 0)
+ {
+ return reference[prefix.Length..];
+ }
+ throw new ArgumentException($"Invalid reference: {reference}");
+ }
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiState.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiState.cs
new file mode 100644
index 0000000..58f2495
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiState.cs
@@ -0,0 +1,6 @@
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;
+
+internal class AsyncApiState
+{
+ public List Documents { get; } = new();
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs
new file mode 100644
index 0000000..95c8c91
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs
@@ -0,0 +1,41 @@
+using NJsonSchema;
+using NSwag.CodeGeneration.CSharp;
+using NSwag;
+using NJsonSchema.CodeGeneration.CSharp;
+
+
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;
+
+internal interface IDataTypesGenerator
+{
+ Task GenerateDataTypesAsync(GeneratorOptions options, string spec, DataTypesGeneratorState state);
+}
+
+internal class NSwagGenerator : IDataTypesGenerator
+{
+ public async Task GenerateDataTypesAsync(GeneratorOptions options, string spec, DataTypesGeneratorState state)
+ {
+ spec = OpenApiCompatibility.PrepareSpecFile(spec);
+
+ var document = await OpenApiDocument.FromJsonAsync(spec).ConfigureAwait(false);
+ var settings = new CSharpClientGeneratorSettings
+ {
+ CSharpGeneratorSettings =
+ {
+ Namespace = options.Namespace,
+ SchemaType = SchemaType.OpenApi3,
+ ClassStyle = CSharpClassStyle.Record,
+ ExcludedTypeNames = state.AlreadyGeneratedDataTypes.ToArray(),
+ },
+ GenerateClientClasses = false,
+ AdditionalNamespaceUsages = state.AlreadyGeneratedNamespaces.ToArray()
+ };
+
+ var generator = new CSharpClientGenerator(document, settings);
+ var contents = generator.GenerateFile();
+ state.AlreadyGeneratedDataTypes.AddRange(document.Definitions.Keys);
+ state.AlreadyGeneratedNamespaces.Add(options.Namespace);
+
+ return contents;
+ }
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs
new file mode 100644
index 0000000..71bdbed
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs
@@ -0,0 +1,8 @@
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;
+
+internal class DataTypesGeneratorState
+{
+ public List AlreadyGeneratedDataTypes { get; } = new();
+
+ public List AlreadyGeneratedNamespaces { get; } = new();
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs
new file mode 100644
index 0000000..9d6f10b
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json.Linq;
+using Newtonsoft.Json;
+
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;
+
+internal static class OpenApiCompatibility
+{
+ internal static string PrepareSpecFile(string spec)
+ {
+ var json = (JObject)JsonConvert.DeserializeObject(spec);
+ // the type is important for NSwag
+ if (!json.ContainsKey("openapi"))
+ {
+ json.Add("openapi", "3.0.1");
+ }
+ // NSwag doesn't understand the servers format of AsyncApi, and it is not needed anyway.
+ if (json.ContainsKey("servers"))
+ {
+ json.Remove("servers");
+ }
+ return JsonConvert.SerializeObject(json);
+ }
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs
index fcb401a..5c365c8 100644
--- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs
@@ -1,39 +1,88 @@
-using ConsoleAppFramework;
-using LEGO.AsyncAPI;
-using LEGO.AsyncAPI.Models;
+using System.Diagnostics;
+using System.Text;
+using AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;
+using AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;
+using CaseConverter;
+using ConsoleAppFramework;
using Microsoft.Extensions.Logging;
namespace AsyncAPI.Saunter.Generator.Cli.FromSpec;
-internal class FromSpecCommand(ILogger logger)
+internal class FromSpecCommand(ILogger logger, IAsyncApiGenerator asyncApiGenerator, IDataTypesGenerator dataTypesGenerator)
{
///
/// Retrieves AsyncAPI spec from a startup assembly and writes to file.
///
- /// the AsyncAPI specification to generate code for. Parameter should include the namespace: namespace,outputPath,asyncapi.json
+ /// the AsyncAPI specification to generate code for. Parameter should include 3 parts: namespace,outputDirectory,asyncapiSpec
[Command("fromspec")]
- public int FromSpec(params string[] specs)
+ public async Task FromSpec(params string[] specs)
{
logger.LogInformation($"FromSpec(#{specs.Length}): --specs {string.Join(';', specs)}");
- foreach (var (namespaceName, output, specName) in Split(specs))
+ var specsToGenerate = Split(specs);
+ var output = specsToGenerate.ToDictionary(x => x, _ => new StringBuilder());
+
+ // Common
+ var topicsClassName = "Topics";
+ foreach (var (spec, _) in output)
+ {
+ Directory.CreateDirectory(spec.OutputDirectory);
+
+ if (!File.Exists(spec.SpecFile))
+ {
+ throw new ArgumentException($"Provided spec does not exist: {Path.GetFullPath(spec.SpecFile)}.");
+ }
+ }
+
+ // AsyncAPI Interface
+ var aaState = new AsyncApiState();
+ foreach (var (spec, sb) in output)
+ {
+ var options = new GeneratorOptions($"{spec.NamespaceName}.{spec.SpecName}.Api", spec.SpecName, $"{topicsClassName}");
+ var contents = asyncApiGenerator.GenerateAsyncApiInterfaces(options, spec.Contents, aaState);
+ sb.Append(contents);
+ }
+
+ // DataTypes
+ var nsState = new DataTypesGeneratorState();
+ foreach (var (spec, sb) in output)
{
- Directory.CreateDirectory(output);
- var outputFile = Path.Combine(output, $"{Path.GetFileNameWithoutExtension(specName)}.g.cs");
- File.Create(outputFile);
- logger.LogInformation($"Created {Path.GetFullPath(outputFile)}");
+ var options = new GeneratorOptions($"{spec.NamespaceName}.{spec.SpecName}.Api", spec.SpecName, $"{topicsClassName}");
+ var contents = await dataTypesGenerator.GenerateDataTypesAsync(options, spec.Contents, nsState).ConfigureAwait(false);
+ sb.Append(contents);
}
+
+ // Write to file
+ foreach (var (spec, sb) in output)
+ {
+ var contents = sb.ToString();
+ var outputFile = spec.OutputFileName;
+ await File.WriteAllTextAsync(outputFile, contents, Encoding.UTF8);
+ logger.LogInformation($"Created {outputFile} (size: {contents.Length:N0} chars)");
+ }
+
return 0;
}
- private static IEnumerable<(string namespaceName, string output, string specName)> Split(IEnumerable input)
+ private record SpecToGenerate(string NamespaceName, string OutputDirectory, string SpecFile)
+ {
+ private string _contents;
+
+ public string Contents => this._contents ??= File.ReadAllText(this.SpecFile);
+
+ public string SpecName => Path.GetFileNameWithoutExtension(this.SpecFile).ToPascalCase();
+
+ public string OutputFileName => Path.GetFullPath(Path.Combine(this.OutputDirectory, $"{this.SpecName}.g.cs"));
+ }
+
+ private static IEnumerable Split(IEnumerable input)
{
foreach (var spec in input)
{
var split = spec.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList();
if (split.Count == 3 && !split.Any(string.IsNullOrWhiteSpace))
{
- yield return (split[0], split[1], split[2]);
+ yield return new SpecToGenerate(NamespaceName: split[0], OutputDirectory: split[1], SpecFile: split[2]);
}
}
}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs
new file mode 100644
index 0000000..6ee53d8
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs
@@ -0,0 +1,3 @@
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec;
+
+internal record GeneratorOptions(string Namespace, string ClassName, string TopicsClassName);
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs
new file mode 100644
index 0000000..e4bb95b
--- /dev/null
+++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs
@@ -0,0 +1,15 @@
+using AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;
+using AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AsyncAPI.Saunter.Generator.Cli.FromSpec;
+
+internal static class ServiceExtensions
+{
+ public static IServiceCollection AddFromSpecCommand(this IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddTransient();
+ return services;
+ }
+}
diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs
index 3c7018d..5cc2552 100644
--- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs
+++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs
@@ -7,6 +7,7 @@
var services = new ServiceCollection();
services.AddLogging(builder => builder.AddSimpleConsole(x => x.SingleLine = true).SetMinimumLevel(LogLevel.Trace));
services.AddToFileCommand();
+services.AddFromSpecCommand();
using var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService>();