diff --git a/Saunter.sln b/Saunter.sln index 15fcbc0..f72daf9 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -74,6 +74,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreetlightsAPI.AsyncApiSpe {A320E670-5CB0-4815-AF67-D8D09FC92A2A} = {A320E670-5CB0-4815-AF67-D8D09FC92A2A} EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.SourceGenerator", "src\AsyncAPI.Saunter.Generator.SourceGenerator\AsyncAPI.Saunter.Generator.SourceGenerator.csproj", "{6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator", "src\AsyncAPI.Saunter.Generator\AsyncAPI.Saunter.Generator.csproj", "{47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -216,6 +220,30 @@ Global {19A30A6D-1E91-44FD-BB5D-428D12D0160D}.Release|x64.Build.0 = Release|Any CPU {19A30A6D-1E91-44FD-BB5D-428D12D0160D}.Release|x86.ActiveCfg = Release|Any CPU {19A30A6D-1E91-44FD-BB5D-428D12D0160D}.Release|x86.Build.0 = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|x64.Build.0 = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Debug|x86.Build.0 = Debug|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|Any CPU.Build.0 = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|x64.ActiveCfg = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|x64.Build.0 = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|x86.ActiveCfg = Release|Any CPU + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0}.Release|x86.Build.0 = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|x64.ActiveCfg = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|x64.Build.0 = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|x86.ActiveCfg = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Debug|x86.Build.0 = Debug|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|Any CPU.Build.0 = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|x64.ActiveCfg = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|x64.Build.0 = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|x86.ActiveCfg = Release|Any CPU + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -234,6 +262,8 @@ Global {A320E670-5CB0-4815-AF67-D8D09FC92A2A} = {28D4C365-FDED-49AE-A97D-36202E24A55A} {61142B10-7B49-436E-AE32-2737658BD1E5} = {6491E321-2D02-44AB-9116-D722FE169595} {19A30A6D-1E91-44FD-BB5D-428D12D0160D} = {6ABD4842-47AF-49A5-B057-0EBA64416789} + {6ADD07FE-B6D1-42A9-A618-E7AA102E38A0} = {28D4C365-FDED-49AE-A97D-36202E24A55A} + {47763E73-0B9B-4B11-BB03-5FDB0B8A0C5E} = {28D4C365-FDED-49AE-A97D-36202E24A55A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E} diff --git a/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj b/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj index 7a2249f..21137df 100644 --- a/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj +++ b/examples/StreetlightsAPI.AsyncApiSpecFirst/StreetlightsAPI.AsyncApiSpecFirst.csproj @@ -4,14 +4,13 @@ net8.0 false - - false + true - - - + + + @@ -27,7 +26,12 @@ - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,7 +39,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,24 +56,11 @@ PreserveNewest - - - - - - - - - - - - - diff --git a/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.props b/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.props index 9f8adb8..e8179b5 100644 --- a/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.props +++ b/src/AsyncAPI.Saunter.Generator.Build/build/AsyncAPI.Saunter.Generator.Build.props @@ -11,9 +11,4 @@ - - - - - \ No newline at end of file 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 6f7f826..dfcebc9 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 @@ -1,11 +1,5 @@ - - - - - - - + @@ -15,14 +9,14 @@ $([System.IO.Path]::Combine($(AsyncAPIBuildToolRoot), tools, net8.0, AsyncAPI.Saunter.Generator.Cli.dll)) - - - + + + - - - - + + + + @@ -47,19 +41,4 @@ WorkingDirectory="$(AsyncAPIBuildToolRoot)" /> - - - - - - - - - - - - \ No newline at end of file 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 c43abba..bf45435 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -39,9 +39,6 @@ - - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,6 +49,7 @@ + diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs index 5c365c8..8b76144 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/FromSpecCommand.cs @@ -1,14 +1,11 @@ -using System.Diagnostics; -using System.Text; -using AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface; -using AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes; -using CaseConverter; +using System.Text; +using AsyncAPI.Saunter.Generator.FromSpec; using ConsoleAppFramework; using Microsoft.Extensions.Logging; namespace AsyncAPI.Saunter.Generator.Cli.FromSpec; -internal class FromSpecCommand(ILogger logger, IAsyncApiGenerator asyncApiGenerator, IDataTypesGenerator dataTypesGenerator) +internal class FromSpecCommand(ILogger logger, IAsyncApiCodeGenerator codeGenerator) { /// /// Retrieves AsyncAPI spec from a startup assembly and writes to file. @@ -20,58 +17,23 @@ public async Task FromSpec(params string[] specs) logger.LogInformation($"FromSpec(#{specs.Length}): --specs {string.Join(';', specs)}"); var specsToGenerate = Split(specs); - var output = specsToGenerate.ToDictionary(x => x, _ => new StringBuilder()); - - // Common - var topicsClassName = "Topics"; - foreach (var (spec, _) in output) + var output = await codeGenerator.FromSpecs(specsToGenerate).ConfigureAwait(false); + + // Write to file + foreach (var (spec, contents) 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) - { - 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); + await File.WriteAllTextAsync(outputFile, contents, Encoding.UTF8).ConfigureAwait(false); logger.LogInformation($"Created {outputFile} (size: {contents.Length:N0} chars)"); } return 0; } - private record SpecToGenerate(string NamespaceName, string OutputDirectory, string SpecFile) + private record SpecToGenerate(string NamespaceName, string OutputDirectory, string SpecFilePath) : Generator.FromSpec.SpecToGenerate(NamespaceName, SpecFilePath) { - 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")); } @@ -82,7 +44,7 @@ private static IEnumerable Split(IEnumerable input) var split = spec.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToList(); if (split.Count == 3 && !split.Any(string.IsNullOrWhiteSpace)) { - yield return new SpecToGenerate(NamespaceName: split[0], OutputDirectory: split[1], SpecFile: split[2]); + yield return new SpecToGenerate(NamespaceName: split[0], OutputDirectory: split[1], SpecFilePath: split[2]); } } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs deleted file mode 100644 index e4bb95b..0000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/ServiceExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -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 5cc2552..f93f804 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs @@ -1,13 +1,14 @@ -using AsyncAPI.Saunter.Generator.Cli.FromSpec; +using AsyncAPI.Saunter.Generator; +using AsyncAPI.Saunter.Generator.Cli.FromSpec; using AsyncAPI.Saunter.Generator.Cli.ToFile; +using AsyncAPI.Saunter.Generator.FromSpec; using ConsoleAppFramework; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -var services = new ServiceCollection(); -services.AddLogging(builder => builder.AddSimpleConsole(x => x.SingleLine = true).SetMinimumLevel(LogLevel.Trace)); +var services = GeneratorServiceCollection.Create(); services.AddToFileCommand(); -services.AddFromSpecCommand(); +services.AddFromSpecCodeGenerator(); using var serviceProvider = services.BuildServiceProvider(); var logger = serviceProvider.GetRequiredService>(); diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Shipped.md b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..af490de --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Shipped.md @@ -0,0 +1,6 @@ +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Unshipped.md b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..ff9342a --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,5 @@ +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +AA0001 | SourceGenerator | Error | AA001 \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/AsyncAPI.Saunter.Generator.SourceGenerator.csproj b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AsyncAPI.Saunter.Generator.SourceGenerator.csproj new file mode 100644 index 0000000..2e7f676 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/AsyncAPI.Saunter.Generator.SourceGenerator.csproj @@ -0,0 +1,66 @@ + + + + + + netstandard2.0 + enable + 12 + true + true + true + $(NoWarn);NU5128 + + AsyncAPI Roslyn Source Generator: Generate data classes and AsyncAPI interface from AsyncAPI spec file(s). + AsyncAPI Initiative + AsyncAPI.Saunter.Generator.SourceGenerator + asyncapi;aspnetcore;openapi;documentation;amqp;source generator;build;generator + readme.md + logo.png + https://github.com/asyncapi/saunter + true + true + false + false + https://github.com/asyncapi/saunter + MIT + false + true + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/Properties/launchSettings.json b/src/AsyncAPI.Saunter.Generator.SourceGenerator/Properties/launchSettings.json new file mode 100644 index 0000000..a9d17f6 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "SourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\examples\\StreetlightsAPI.AsyncApiSpecFirst\\StreetlightsAPI.AsyncApiSpecFirst.csproj" + } + } +} \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/SpecFirstCodeGenerator.cs b/src/AsyncAPI.Saunter.Generator.SourceGenerator/SpecFirstCodeGenerator.cs new file mode 100644 index 0000000..f3ecb45 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/SpecFirstCodeGenerator.cs @@ -0,0 +1,53 @@ +using System.Collections.Immutable; +using System.Reflection; +using AsyncAPI.Saunter.Generator.FromSpec; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.DependencyInjection; + +namespace AsyncAPI.Saunter.Generator.SourceGenerator; + +[Generator] +public class SpecFirstCodeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var asyncApiSpecInputs = context.AdditionalTextsProvider.Where(x => x.Path.EndsWith(".json") || x.Path.EndsWith(".yml") || x.Path.EndsWith(".yaml")).Collect(); + + context.RegisterSourceOutput(context.AnalyzerConfigOptionsProvider.Combine(asyncApiSpecInputs), GenerateCode); + } + + private static async void GenerateCode(SourceProductionContext context, (AnalyzerConfigOptionsProvider options, ImmutableArray specs) args) + { + var services = GeneratorServiceCollection.Create(); + services.AddFromSpecCodeGenerator(); + using var provider = services.BuildServiceProvider(); + var codeGen = provider.GetRequiredService(); + + var basePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + DependencyResolver.Init(basePath); + var output = await codeGen.FromSpecs(args.specs.Select(specFile => + { + var fileOptions = args.options.GetOptions(specFile); + if (!fileOptions.TryGetValue("build_metadata.AdditionalFiles.Namespace", out var namespaceName)) + { + context.ReportDiagnostic(Diagnostic.Create(MissingNamespace, null, Path.GetFileName(specFile.Path))); + } + + return new SpecToGenerate(namespaceName, specFile.Path); + })); + + foreach (var (spec, contents) in output) + { + context.AddSource($"{spec.SpecFileName}.g.cs", contents); + } + } + + public static readonly DiagnosticDescriptor MissingNamespace = new( + "AA0001", + "No namespace provided for AsyncAPI code generator", + "Missing 'Namespace' for {0}", + "SourceGenerator", + DiagnosticSeverity.Error, + true); +} diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/build/AsyncAPI.Saunter.Generator.SourceGenerator.props b/src/AsyncAPI.Saunter.Generator.SourceGenerator/build/AsyncAPI.Saunter.Generator.SourceGenerator.props new file mode 100644 index 0000000..1b03b95 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/build/AsyncAPI.Saunter.Generator.SourceGenerator.props @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator.SourceGenerator/readme.md b/src/AsyncAPI.Saunter.Generator.SourceGenerator/readme.md new file mode 100644 index 0000000..60b2614 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.SourceGenerator/readme.md @@ -0,0 +1,30 @@ +# AsyncApi Generator.SourceGenerator Nuget Package +A nuget package to generate AsyncAPI specification files at build time, based on code-first attributes. This nuget package requires .NET8.0 runtime in order to work. The consuming csproj doesn't need to target .NET8.0. + +This nuget packages can help to better control API changes by commiting the AsyncAPI spec to source control. By always generating spec files at build, it will be clear when the api changes. + +# Customization Properties +The AsyncAPI spec generation can be configured through project properties in the csproj-file (or included via [.props files](https://learn.microsoft.com/en-us/visualstudio/msbuild/customize-your-build)): +``` + + + + + + + + +``` + +Defaults are the same as the underlying [Generator.Cli tool](https://www.nuget.org/packages/AsyncAPI.Saunter.Generator.Cli). + +If the ```AsyncAPI.Saunter.Generator.Build``` Nuget package is referenced, the default is to generate AsyncAPI spec files at build time. + +- _AsyncAPIGenerateDocumentsOnBuild_: Whether to actually generate AsyncAPI spec files on build (true or false, default: true) +- _AsyncAPIDocumentFormats_: the output formats to generate, can be a combination of json, yml and/or yaml. +- _AsyncAPIDocumentOutputPath_: relative path where the AsyncAPI will be output (default is the csproj root path: ./) +- _AsyncAPIDocumentNames_: The AsyncAPI documents to generate. (default: generate all known documents) +- _AsyncAPIDocumentFilename_: the template for the outputted file names. Default: "{document}_asyncapi.{extension}" +- _AsyncAPIDocumentEnvVars_: define environment variable(s) for the application. Formatted as a comma separated list of _key=value_ pairs, example: ```ASPNETCORE_ENVIRONMENT=AsyncAPI,CONNECT_TO_DATABASE=false```. + +None of these properties are mandatory. Only referencing the [AsyncAPI.Saunter.Generator.Build](https://www.nuget.org/packages/AsyncAPI.Saunter.Generator.Build) Nuget package will generate a json AsyncAPI spec file for all AsyncAPI documents. \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator/AsyncAPI.Saunter.Generator.csproj b/src/AsyncAPI.Saunter.Generator/AsyncAPI.Saunter.Generator.csproj new file mode 100644 index 0000000..7cc0e9a --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator/AsyncAPI.Saunter.Generator.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + enable + 12 + AsyncAPI.Saunter.Generator + false + + + + true + + + + + + + + + + + + + + diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs b/src/AsyncAPI.Saunter.Generator/DependencyResolver.cs similarity index 88% rename from src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs rename to src/AsyncAPI.Saunter.Generator/DependencyResolver.cs index 3d6d010..79e4499 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs +++ b/src/AsyncAPI.Saunter.Generator/DependencyResolver.cs @@ -1,8 +1,8 @@ using System.Reflection; -namespace AsyncAPI.Saunter.Generator.Cli.ToFile; +namespace AsyncAPI.Saunter.Generator; -internal static class DependencyResolver +public static class DependencyResolver { public static void Init(string startupAssemblyBasePath) { diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs similarity index 96% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs index de6a8ed..19a25b6 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/AsyncApiInterface/AsyncApiGenerator.cs @@ -4,7 +4,7 @@ using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface; +namespace AsyncAPI.Saunter.Generator.FromSpec.AsyncApiInterface; internal interface IAsyncApiGenerator { @@ -102,7 +102,8 @@ private StringBuilder CreateHeader(GeneratorOptions options) // At: {{DateTime.Now.ToLocalTime():U}} // //---------------------- - + + using Saunter; using Saunter.Attributes; using Saunter.AsyncApiSchema.v2; using System.CodeDom.Compiler; @@ -175,10 +176,10 @@ private static void AddChannelOperation(StringBuilder sb, OperationOptions optio private static string FromReference(string reference) { - var prefix = "#/components/parameters/"; - if (reference.IndexOf(prefix) == 0) + const string prefix = "#/components/parameters/"; + if (reference.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) == 0) { - return reference[prefix.Length..]; + return reference.Substring(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/FromSpec/AsyncApiInterface/AsyncApiState.cs similarity index 54% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiState.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/AsyncApiInterface/AsyncApiState.cs index 58f2495..01f9719 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/AsyncApiInterface/AsyncApiState.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/AsyncApiInterface/AsyncApiState.cs @@ -1,4 +1,4 @@ -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface; +namespace AsyncAPI.Saunter.Generator.FromSpec.AsyncApiInterface; internal class AsyncApiState { diff --git a/src/AsyncAPI.Saunter.Generator/FromSpec/CodeGenerator.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/CodeGenerator.cs new file mode 100644 index 0000000..30a337b --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/CodeGenerator.cs @@ -0,0 +1,49 @@ +using System.Text; +using AsyncAPI.Saunter.Generator.FromSpec.AsyncApiInterface; +using AsyncAPI.Saunter.Generator.FromSpec.DataTypes; +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.FromSpec; + +public interface IAsyncApiCodeGenerator +{ + Task> FromSpecs(IEnumerable specsToGenerate) where TSpecToGenerate : SpecToGenerate; +} + +internal class CodeGenerator(ILogger logger, IAsyncApiGenerator asyncApiGenerator, IDataTypesGenerator dataTypesGenerator) : IAsyncApiCodeGenerator +{ + public async Task> FromSpecs(IEnumerable specsToGenerate) where TSpecToGenerate : SpecToGenerate + { + var output = specsToGenerate.Select(x => (x, new StringBuilder())).ToList(); + + // Common + var topicsClassName = "Topics"; + foreach (var (spec, _) in output) + { + if (!File.Exists(spec.SpecFilePath)) + { + throw new ArgumentException($"Provided spec does not exist: {Path.GetFullPath(spec.SpecFilePath)}."); + } + } + + // AsyncAPI Interface + var aaState = new AsyncApiState(); + foreach (var (spec, sb) in output) + { + var options = new GeneratorOptions($"{spec.NamespaceName}.{spec.SpecName}.Api".TrimStart('.'), spec.SpecName, $"{topicsClassName}"); + var contents = asyncApiGenerator.GenerateAsyncApiInterfaces(options, spec.FileContents, aaState); + sb.Append(contents); + } + + // DataTypes + var nsState = new DataTypesGeneratorState(); + foreach (var (spec, sb) in output) + { + var options = new GeneratorOptions($"{spec.NamespaceName}.{spec.SpecName}.Api".TrimStart('.'), spec.SpecName, $"{topicsClassName}"); + var contents = await dataTypesGenerator.GenerateDataTypesAsync(options, spec.FileContents, nsState).ConfigureAwait(false); + sb.Append(contents); + } + + return output.Select(x => (x.Item1, x.Item2.ToString())).ToList(); + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGenerator.cs similarity index 90% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGenerator.cs index 95c8c91..4c95a5c 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGenerator.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGenerator.cs @@ -4,7 +4,7 @@ using NJsonSchema.CodeGeneration.CSharp; -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes; +namespace AsyncAPI.Saunter.Generator.FromSpec.DataTypes; internal interface IDataTypesGenerator { @@ -28,7 +28,7 @@ public async Task GenerateDataTypesAsync(GeneratorOptions options, strin ExcludedTypeNames = state.AlreadyGeneratedDataTypes.ToArray(), }, GenerateClientClasses = false, - AdditionalNamespaceUsages = state.AlreadyGeneratedNamespaces.ToArray() + AdditionalNamespaceUsages = [], }; var generator = new CSharpClientGenerator(document, settings); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGeneratorState.cs similarity index 73% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGeneratorState.cs index 71bdbed..54a45cf 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/DataTypesGeneratorState.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/DataTypesGeneratorState.cs @@ -1,4 +1,4 @@ -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes; +namespace AsyncAPI.Saunter.Generator.FromSpec.DataTypes; internal class DataTypesGeneratorState { diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/OpenApiCompatibility.cs similarity index 85% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/OpenApiCompatibility.cs index 3c876f3..55a5941 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/DataTypes/OpenApiCompatibility.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/DataTypes/OpenApiCompatibility.cs @@ -1,16 +1,14 @@ -using System.Diagnostics; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using YamlDotNet.RepresentationModel; using Yaml2JsonNode; -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes; +namespace AsyncAPI.Saunter.Generator.FromSpec.DataTypes; internal static class OpenApiCompatibility { internal static string PrepareSpecFile(string spec) { - Debugger.Launch(); var reader = new StringReader(spec); var yamlStream = new YamlStream(); yamlStream.Load(reader); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/GeneratorOptions.cs similarity index 63% rename from src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs rename to src/AsyncAPI.Saunter.Generator/FromSpec/GeneratorOptions.cs index 6ee53d8..6035c23 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/FromSpec/GeneratorOptions.cs +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/GeneratorOptions.cs @@ -1,3 +1,3 @@ -namespace AsyncAPI.Saunter.Generator.Cli.FromSpec; +namespace AsyncAPI.Saunter.Generator.FromSpec; internal record GeneratorOptions(string Namespace, string ClassName, string TopicsClassName); diff --git a/src/AsyncAPI.Saunter.Generator/FromSpec/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/ServiceExtensions.cs new file mode 100644 index 0000000..62e490c --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/ServiceExtensions.cs @@ -0,0 +1,16 @@ +using AsyncAPI.Saunter.Generator.FromSpec.AsyncApiInterface; +using AsyncAPI.Saunter.Generator.FromSpec.DataTypes; +using Microsoft.Extensions.DependencyInjection; + +namespace AsyncAPI.Saunter.Generator.FromSpec; + +public static class ServiceExtensions +{ + public static IServiceCollection AddFromSpecCodeGenerator(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; + } +} diff --git a/src/AsyncAPI.Saunter.Generator/FromSpec/SpecToGenerate.cs b/src/AsyncAPI.Saunter.Generator/FromSpec/SpecToGenerate.cs new file mode 100644 index 0000000..a3031a9 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator/FromSpec/SpecToGenerate.cs @@ -0,0 +1,17 @@ +using CaseConverter; + +namespace AsyncAPI.Saunter.Generator.FromSpec; + +public record SpecToGenerate(string NamespaceName, string SpecFilePath) +{ + private string _fileContents; + + /// AsyncAPI spec file contents. + public string FileContents => this._fileContents ??= File.ReadAllText(this.SpecFilePath); + + /// AsyncAPI spec file name, Pascal Cased, without file extensions. + public string SpecName => Path.GetFileNameWithoutExtension(this.SpecFilePath).ToPascalCase(); + + /// AsyncAPI spec file name, including file extension. + public string SpecFileName => Path.GetFileName(this.SpecFilePath); +} diff --git a/src/AsyncAPI.Saunter.Generator/ServiceCollection.cs b/src/AsyncAPI.Saunter.Generator/ServiceCollection.cs new file mode 100644 index 0000000..bc77bae --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator/ServiceCollection.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +namespace AsyncAPI.Saunter.Generator; + +public static class GeneratorServiceCollection +{ + public static IServiceCollection Create() + { + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddSimpleConsole(x => x.SingleLine = true).SetMinimumLevel(LogLevel.Trace)); + return services; + } +} diff --git a/src/Saunter-src.slnf b/src/Saunter-src.slnf index c5c2729..799c5cf 100644 --- a/src/Saunter-src.slnf +++ b/src/Saunter-src.slnf @@ -2,8 +2,10 @@ "solution": { "path": "..\\Saunter.sln", "projects": [ + "src\\AsyncAPI.Saunter.Generator\\AsyncAPI.Saunter.Generator.csproj", "src\\AsyncAPI.Saunter.Generator.Build\\AsyncAPI.Saunter.Generator.Build.csproj", "src\\AsyncAPI.Saunter.Generator.Cli\\AsyncAPI.Saunter.Generator.Cli.csproj", + "src\\AsyncAPI.Saunter.Generator.SourceGenerator\\AsyncAPI.Saunter.Generator.SourceGenerator.csproj", "src\\Saunter\\Saunter.csproj" ] }