From 569a30ac5efdd4547c0798226b96c09e8cfd23c4 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 17:20:14 +0200 Subject: [PATCH] #196 Fixed resolving, added support for multiple asyncAPI documents, default all. Added support for env vars --- src/AsyncAPI.Saunter.Generator.Cli/Args.cs | 4 +- .../AsyncAPI.Saunter.Generator.Cli.csproj | 5 +- .../Commands/Tofile.cs | 18 +-- .../Commands/TofileInternal.cs | 118 +++++++++--------- .../Internal/DependencyResolver.cs | 28 +++++ src/AsyncAPI.Saunter.Generator.Cli/Program.cs | 21 ++-- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 12 +- 7 files changed, 118 insertions(+), 88 deletions(-) create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs index 243315da..cafc79b3 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs @@ -6,7 +6,9 @@ public static partial class Program { internal const string StartupAssemblyArgument = "startupassembly"; - internal const string DocArgument = "doc"; + internal const string DocOption = "--doc"; internal const string FormatOption = "--format"; + internal const string FileNameOption = "--filename"; internal const string OutputOption = "--output"; + internal const string EnvOption = "--env"; } 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 b5539990..63a8b166 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -1,7 +1,6 @@  - net8.0 Exe enable 12 @@ -11,8 +10,8 @@ Exe true AsyncAPI.Saunter.Generator.Cli - asyncapi - true + AsyncAPI.NET + net8.0;net6.0 diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs index dda464ff..9faa30c9 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs @@ -27,24 +27,16 @@ internal static Func, int> Run(string[] args) => nam Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); } + var assembly = typeof(Program).GetTypeInfo().Assembly; var subProcessCommandLine = $"exec --depsfile {EscapePath(depsFile)} " + $"--runtimeconfig {EscapePath(runtimeConfig)} " + - $"--additional-deps AsyncAPI.Saunter.Generator.Cli.deps.json " + - //$"--additionalprobingpath {EscapePath(typeof(Program).GetTypeInfo().Assembly.Location)} " + - $"{EscapePath(typeof(Program).GetTypeInfo().Assembly.Location)} " + + $"{EscapePath(assembly.Location)} " + $"_{commandName} {string.Join(" ", subProcessArguments.Select(EscapePath))}"; - try - { - var subProcess = Process.Start("dotnet", subProcessCommandLine); - subProcess.WaitForExit(); - return subProcess.ExitCode; - } - catch (Exception e) - { - throw new Exception("Running internal _tofile failed.", e); - } + var subProcess = Process.Start("dotnet", subProcessCommandLine); + subProcess.WaitForExit(); + return subProcess.ExitCode; }; private static string EscapePath(string path) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs index 616769b4..00ea898f 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs @@ -27,6 +27,18 @@ internal static int Run(IDictionary namedArgs) var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(Directory.GetCurrentDirectory(), namedArgs[StartupAssemblyArgument])); // 2) Build a service container that's based on the startup assembly + var envVars = namedArgs.TryGetValue(EnvOption, out var x) ? x.Split(',').Select(x => x.Trim()) : Array.Empty(); + foreach (var envVar in envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList())) + { + if (envVar.Count == 2) + { + Environment.SetEnvironmentVariable(envVar[0], envVar[1], EnvironmentVariableTarget.Process); + } + else + { + throw new ArgumentOutOfRangeException(EnvOption, namedArgs[EnvOption], "Environment variable should be in the format: env1=value1,env2=value2"); + } + } var serviceProvider = GetServiceProvider(startupAssembly); // 3) Retrieve AsyncAPI via configured provider @@ -34,55 +46,60 @@ internal static int Run(IDictionary namedArgs) var asyncapiOptions = serviceProvider.GetService>(); var documentSerializer = serviceProvider.GetRequiredService(); - if (!asyncapiOptions.Value.NamedApis.TryGetValue(namedArgs[DocArgument], out var prototype)) + var documentNames = namedArgs.TryGetValue(DocOption, out var doc) ? [doc] : asyncapiOptions.Value.NamedApis.Keys; + var fileTemplate = namedArgs.TryGetValue(FileNameOption, out var template) ? template : "{document}_asyncapi.{extension}"; + foreach (var documentName in documentNames) { - throw new ArgumentOutOfRangeException(DocArgument, namedArgs[DocArgument], $"Requested AsyncAPI document not found: '{namedArgs[DocArgument]}'. Known document(s): {string.Join(", ", asyncapiOptions.Value.NamedApis.Keys)}."); - } - var asyncApiSchema = documentProvider.GetDocument(asyncapiOptions.Value, prototype); - var asyncApiSchemaJson = documentSerializer.Serialize(asyncApiSchema); - var asyncApiDocument = new AsyncApiStringReader().Read(asyncApiSchemaJson, out var diagnostic); - if (diagnostic.Errors.Any()) - { - Console.Error.WriteLine($"AsyncAPI Schema is not valid ({diagnostic.Errors.Count} Error(s), {diagnostic.Warnings.Count} Warning(s)):" + - $"{Environment.NewLine}{string.Join(Environment.NewLine, diagnostic.Errors.Select(x => $"- {x}"))}"); - } + if (!asyncapiOptions.Value.NamedApis.TryGetValue(documentName, out var prototype)) + { + throw new ArgumentOutOfRangeException(DocOption, documentName, $"Requested AsyncAPI document not found: '{documentName}'. Known document(s): {string.Join(", ", asyncapiOptions.Value.NamedApis.Keys)}."); + } - // 4) Serialize to specified output location or stdout - var outputPath = namedArgs.TryGetValue(OutputOption, out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; + var asyncApiSchema = documentProvider.GetDocument(asyncapiOptions.Value, prototype); + var asyncApiSchemaJson = documentSerializer.Serialize(asyncApiSchema); + var asyncApiDocument = new AsyncApiStringReader().Read(asyncApiSchemaJson, out var diagnostic); + if (diagnostic.Errors.Any()) + { + Console.Error.WriteLine($"AsyncAPI Schema '{documentName}' is not valid ({diagnostic.Errors.Count} Error(s), {diagnostic.Warnings.Count} Warning(s)):" + + $"{Environment.NewLine}{string.Join(Environment.NewLine, diagnostic.Errors.Select(x => $"- {x}"))}"); + } - if (!string.IsNullOrEmpty(outputPath)) - { - var directoryPath = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + // 4) Serialize to specified output location or stdout + var outputPath = namedArgs.TryGetValue(OutputOption, out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; + if (!string.IsNullOrEmpty(outputPath)) { - Directory.CreateDirectory(directoryPath); + var directoryPath = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } } - } - var exportJson = true; - var exportYml = false; - var exportYaml = false; - if (namedArgs.TryGetValue(FormatOption, out var format)) - { - var splitted = format.Split(',').Select(x => x.Trim()).ToList(); - exportJson = splitted.Any(x => x.Equals("json", StringComparison.OrdinalIgnoreCase)); - exportYml = splitted.Any(x => x.Equals("yml", StringComparison.OrdinalIgnoreCase)); - exportYaml = splitted.Any(x => x.Equals("yaml", StringComparison.OrdinalIgnoreCase)); - } + var exportJson = true; + var exportYml = false; + var exportYaml = false; + if (namedArgs.TryGetValue(FormatOption, out var format)) + { + var splitted = format.Split(',').Select(x => x.Trim()).ToList(); + exportJson = splitted.Any(x => x.Equals("json", StringComparison.OrdinalIgnoreCase)); + exportYml = splitted.Any(x => x.Equals("yml", StringComparison.OrdinalIgnoreCase)); + exportYaml = splitted.Any(x => x.Equals("yaml", StringComparison.OrdinalIgnoreCase)); + } - if (exportJson) - { - WriteFile(AddFileExtension(outputPath, "json"), stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0)); - } + if (exportJson) + { + WriteFile(AddFileExtension(outputPath, fileTemplate, documentName, "json"), stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0)); + } - if (exportYml) - { - WriteFile(AddFileExtension(outputPath, "yml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); - } + if (exportYml) + { + WriteFile(AddFileExtension(outputPath, fileTemplate, documentName, "yml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } - if (exportYaml) - { - WriteFile(AddFileExtension(outputPath, "yaml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + if (exportYaml) + { + WriteFile(AddFileExtension(outputPath, fileTemplate, documentName, "yaml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } } return 0; @@ -99,31 +116,14 @@ private static void WriteFile(string outputPath, Action writeAction) } } - private static string AddFileExtension(string outputPath, string extension) + private static string AddFileExtension(string outputPath, string fileTemplate, string documentName, string extension) { if (outputPath == null) { return outputPath; } - if (outputPath.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) - { - return outputPath; - } - - return $"{TrimEnd(outputPath, ".json", ".yml", ".yaml")}.{extension}"; - } - - private static string TrimEnd(string str, params string[] trims) - { - foreach (var trim in trims) - { - if (str.EndsWith(trim, StringComparison.OrdinalIgnoreCase)) - { - str = str[..^trim.Length]; - } - } - return str; + return Path.Combine(outputPath, fileTemplate.Replace("{document}", documentName).Replace("{extension}", extension)); } private static IServiceProvider GetServiceProvider(Assembly startupAssembly) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs new file mode 100644 index 00000000..e6ef1bc3 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Reflection; + +namespace AsyncAPI.Saunter.Generator.Cli.Internal; + +internal static class DependencyResolver +{ + public static void Init() + { + var basePath = Path.GetDirectoryName(typeof(Program).GetTypeInfo().Assembly.Location); + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var requestedAssembly = new AssemblyName(args.Name); + var fullPath = Path.Combine(basePath, $"{requestedAssembly.Name}.dll"); + if (File.Exists(fullPath)) + { + var assembly = Assembly.LoadFile(fullPath); + return assembly; + } + + Console.WriteLine($"Could not resolve assembly: {args.Name}, requested by {args.RequestingAssembly?.FullName}"); + return default; + }; + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs index 33471b73..4d174c60 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs @@ -1,31 +1,38 @@ using AsyncApi.Saunter.Generator.Cli.Commands; using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; +using AsyncAPI.Saunter.Generator.Cli.Internal; + +DependencyResolver.Init(); // Helper to simplify command line parsing etc. -var runner = new CommandRunner("dotnet asyncapi", "AsyncAPI Command Line Tools", Console.Out); +var runner = new CommandRunner("dotnet asyncapi.net", "AsyncAPI Command Line Tools", Console.Out); // NOTE: The "dotnet asyncapi tofile" command does not serve the request directly. Instead, it invokes a corresponding // command (called _tofile) via "dotnet exec" so that the runtime configuration (*.runtimeconfig & *.deps.json) of the // provided startupassembly can be used instead of the tool's. This is neccessary to successfully load the // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more. -// > dotnet asyncapi tofile ... +// > dotnet asyncapi.net tofile ... runner.SubCommand("tofile", "retrieves AsyncAPI from a startup assembly, and writes to file ", c => { c.Argument(StartupAssemblyArgument, "relative path to the application's startup assembly"); - c.Argument(DocArgument, "name of the AsyncAPI doc you want to retrieve, as configured in your startup class"); - c.Option(OutputOption, "relative path where the AsyncAPI will be output, defaults to stdout"); - c.Option(FormatOption, "exports AsyncAPI in json and/or yml format [Default json]"); + c.Option(DocOption, "name(s) of the AsyncAPI documents you want to retrieve, as configured in your startup class [defaults to all documents]"); + c.Option(OutputOption, "relative path where the AsyncAPI will be output [defaults to stdout]"); + c.Option(FileNameOption, "defines the file name template, {document} and {extension} template variables can be used [defaults to \"{document}_asyncapi.{extension}\"]"); + c.Option(FormatOption, "exports AsyncAPI in json and/or yml format [defaults to json]"); + c.Option(EnvOption, "define environment variable(s) for the application during generation of the AsyncAPI files [defaults to empty, can be used to define for example ASPNETCORE_ENVIRONMENT]"); c.OnRun(Tofile.Run(args)); }); -// > dotnet asyncapi _tofile ... (* should only be invoked via "dotnet exec") +// > dotnet asyncapi.net _tofile ... (* should only be invoked via "dotnet exec") runner.SubCommand("_tofile", "", c => { c.Argument(StartupAssemblyArgument, ""); - c.Argument(DocArgument, ""); + c.Option(DocOption, ""); c.Option(OutputOption, ""); + c.Option(FileNameOption, ""); c.Option(FormatOption, ""); + c.Option(EnvOption, ""); c.OnRun(TofileInternal.Run); }); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index afb7f457..6c73c716 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -1,13 +1,15 @@ # AsyncApi Generator.Cli Tool +A dotnet tool to generate AsyncAPI specification files based of dotnet DLL (The application itself). ## Tool usage ``` -dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] [startup-assembly] [asyncapi-document-name] +dotnet asyncapi.net tofile --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] [startup-assembly] ``` - -## Tool options startup-assembly: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). -asyncapi-document-name: (optional) The name of the AsyncAPI document as defined in the startup class by the ```.ConfigureNamedAsyncApi()```-method. If not specified, all documents will be exported. ---output: the output path or the file name. File extension can be omitted, as the --format file determine the file extension. +## Tool options +--doc: The name of the AsyncAPI document as defined in the startup class by the ```.ConfigureNamedAsyncApi()```-method. If not specified, all documents will be exported. +--output: relative path where the AsyncAPI will be output [defaults to stdout] +--filename: the template for the outputted file names. Default: "{document}_asyncapi.{extension}" --format: the output formats to generate, can be a combination of json, yml and/or yaml. File extension is appended to the output path. +--env: define environment variable(s) for the application