From 6318f80c42cc56809ac291d4bc185e521e9e7d2e Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 14:27:12 +0200 Subject: [PATCH 01/34] #196 Publish a global dotnet tool --- .editorconfig | 4 +- .gitattributes | 1 + Saunter.sln | 16 + src/AsyncApi.Saunter.Generator.Cli/Args.cs | 12 + .../AsyncApi.Saunter.Generator.Cli.csproj | 31 ++ .../Commands/Tofile.cs | 54 +++ .../Commands/TofileInternal.cs | 183 ++++++++++ src/AsyncApi.Saunter.Generator.Cli/Program.cs | 32 ++ .../SwashbuckleImport/CommandRunner.cs | 145 ++++++++ .../SwashbuckleImport/HostFactoryResolver.cs | 325 ++++++++++++++++++ .../SwashbuckleImport/HostingApplication.cs | 118 +++++++ .../SwashbuckleImport/readme.md | 3 + src/AsyncApi.Saunter.Generator.Cli/readme.md | 13 + 13 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 src/AsyncApi.Saunter.Generator.Cli/Args.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj create mode 100644 src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/Program.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs create mode 100644 src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md create mode 100644 src/AsyncApi.Saunter.Generator.Cli/readme.md diff --git a/.editorconfig b/.editorconfig index fe3bd5da..5209be73 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,10 +14,12 @@ spelling_exclusion_path = SpellingExclusions.dic indent_size = 4 insert_final_newline = true charset = utf-8-bom +end_of_line = lf # XML project files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 4 +indent_size = 2 +end_of_line = lf # XML config files [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..07764a78 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/Saunter.sln b/Saunter.sln index 46277661..bd69f05e 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -18,6 +18,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E0D34C77-924E-4F6B-9289-5A2F07D125A8}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .gitattributes = .gitattributes CHANGELOG.md = CHANGELOG.md .github\workflows\ci.yaml = .github\workflows\ci.yaml README.md = README.md @@ -28,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.IntegrationTests.Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Tests.MarkerTypeTests", "test\Saunter.Tests.MarkerTypeTests\Saunter.Tests.MarkerTypeTests.csproj", "{02284473-6DE7-4EE0-8433-2AC295045549}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncApi.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,6 +101,18 @@ Global {02284473-6DE7-4EE0-8433-2AC295045549}.Release|x64.Build.0 = Release|Any CPU {02284473-6DE7-4EE0-8433-2AC295045549}.Release|x86.ActiveCfg = Release|Any CPU {02284473-6DE7-4EE0-8433-2AC295045549}.Release|x86.Build.0 = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x64.Build.0 = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Debug|x86.Build.0 = Debug|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|Any CPU.Build.0 = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x64.ActiveCfg = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x64.Build.0 = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.ActiveCfg = Release|Any CPU + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -108,6 +123,7 @@ Global {F188D4A7-BBCB-464F-A370-2BD84D18EA79} = {6ABD4842-47AF-49A5-B057-0EBA64416789} {7CD09B89-130A-41AF-ADAE-2166C4ED695B} = {6491E321-2D02-44AB-9116-D722FE169595} {02284473-6DE7-4EE0-8433-2AC295045549} = {6491E321-2D02-44AB-9116-D722FE169595} + {6C102D4D-3DA4-4763-B75E-C15E33E7E94A} = {28D4C365-FDED-49AE-A97D-36202E24A55A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E} diff --git a/src/AsyncApi.Saunter.Generator.Cli/Args.cs b/src/AsyncApi.Saunter.Generator.Cli/Args.cs new file mode 100644 index 00000000..243315da --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/Args.cs @@ -0,0 +1,12 @@ +// 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. + +// ReSharper disable once CheckNamespace +public static partial class Program +{ + internal const string StartupAssemblyArgument = "startupassembly"; + internal const string DocArgument = "doc"; + internal const string FormatOption = "--format"; + internal const string OutputOption = "--output"; +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj b/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj new file mode 100644 index 00000000..bf10ee04 --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + Exe + enable + 12 + AsyncAPI.Saunter.Generator.Cli + + AsyncAPI Command Line Tools + Exe + true + AsyncAPI.Saunter.Generator.Cli + + asyncapi + true + + + + + + + + + + + + + + + diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs new file mode 100644 index 00000000..dda464ff --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs @@ -0,0 +1,54 @@ +// 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.Diagnostics; +using System.Reflection; +using static Program; + +namespace AsyncApi.Saunter.Generator.Cli.Commands; + +internal class Tofile +{ + internal static Func, int> Run(string[] args) => namedArgs => + { + if (!File.Exists(namedArgs[StartupAssemblyArgument])) + { + throw new FileNotFoundException(namedArgs[StartupAssemblyArgument]); + } + + var depsFile = namedArgs[StartupAssemblyArgument].Replace(".dll", ".deps.json"); + var runtimeConfig = namedArgs[StartupAssemblyArgument].Replace(".dll", ".runtimeconfig.json"); + var commandName = args[0]; + + var subProcessArguments = new string[args.Length - 1]; + if (subProcessArguments.Length > 0) + { + Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); + } + + 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)} " + + $"_{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); + } + }; + + private static string EscapePath(string path) + { + return path.Contains(' ') ? "\"" + path + "\"" : path; + } +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs new file mode 100644 index 00000000..616769b4 --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs @@ -0,0 +1,183 @@ +// 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 LEGO.AsyncAPI.Readers; +using Microsoft.Extensions.Options; +using Saunter.Serialization; +using Saunter; +using System.Runtime.Loader; +using System.Reflection; +using LEGO.AsyncAPI; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.DependencyInjection; +using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; +using static Program; + +namespace AsyncApi.Saunter.Generator.Cli.Commands; + +internal class TofileInternal +{ + internal static int Run(IDictionary namedArgs) + { + // 1) Configure host with provided startupassembly + var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.Combine(Directory.GetCurrentDirectory(), namedArgs[StartupAssemblyArgument])); + + // 2) Build a service container that's based on the startup assembly + var serviceProvider = GetServiceProvider(startupAssembly); + + // 3) Retrieve AsyncAPI via configured provider + var documentProvider = serviceProvider.GetService(); + var asyncapiOptions = serviceProvider.GetService>(); + var documentSerializer = serviceProvider.GetRequiredService(); + + if (!asyncapiOptions.Value.NamedApis.TryGetValue(namedArgs[DocArgument], out var prototype)) + { + 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}"))}"); + } + + // 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)) + { + 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)); + } + + if (exportJson) + { + WriteFile(AddFileExtension(outputPath, "json"), stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0)); + } + + if (exportYml) + { + WriteFile(AddFileExtension(outputPath, "yml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } + + if (exportYaml) + { + WriteFile(AddFileExtension(outputPath, "yaml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } + + return 0; + } + + private static void WriteFile(string outputPath, Action writeAction) + { + using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + writeAction(stream); + + if (outputPath != null) + { + Console.WriteLine($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); + } + } + + private static string AddFileExtension(string outputPath, 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; + } + + private static IServiceProvider GetServiceProvider(Assembly startupAssembly) + { + if (TryGetCustomHost(startupAssembly, "AsyncAPIHostFactory", "CreateHost", out IHost host)) + { + return host.Services; + } + + if (TryGetCustomHost(startupAssembly, "AsyncAPIWebHostFactory", "CreateWebHost", out IWebHost webHost)) + { + return webHost.Services; + } + + try + { + return WebHost.CreateDefaultBuilder().UseStartup(startupAssembly.GetName().Name).Build().Services; + } + catch + { + var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly); + + if (serviceProvider != null) + { + return serviceProvider; + } + + throw; + } + } + + private static bool TryGetCustomHost(Assembly startupAssembly, string factoryClassName, string factoryMethodName, out THost host) + { + // Scan the assembly for any types that match the provided naming convention + var factoryTypes = startupAssembly.DefinedTypes.Where(t => t.Name == factoryClassName).ToList(); + + if (factoryTypes.Count == 0) + { + host = default; + return false; + } + else if (factoryTypes.Count > 1) + { + throw new InvalidOperationException($"Multiple {factoryClassName} classes detected"); + } + + var factoryMethod = factoryTypes.Single().GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static); + + if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost)) + { + throw new InvalidOperationException($"{factoryClassName} class detected but does not contain a public static method called {factoryMethodName} with return type {typeof(THost).Name}"); + } + + host = (THost)factoryMethod.Invoke(null, null); + return true; + } +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/Program.cs b/src/AsyncApi.Saunter.Generator.Cli/Program.cs new file mode 100644 index 00000000..33471b73 --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/Program.cs @@ -0,0 +1,32 @@ +using AsyncApi.Saunter.Generator.Cli.Commands; +using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; + +// Helper to simplify command line parsing etc. +var runner = new CommandRunner("dotnet asyncapi", "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 ... +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.OnRun(Tofile.Run(args)); +}); + +// > dotnet asyncapi _tofile ... (* should only be invoked via "dotnet exec") +runner.SubCommand("_tofile", "", c => +{ + c.Argument(StartupAssemblyArgument, ""); + c.Argument(DocArgument, ""); + c.Option(OutputOption, ""); + c.Option(FormatOption, ""); + c.OnRun(TofileInternal.Run); +}); + +return runner.Run(args); diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs new file mode 100644 index 00000000..c3c8eca0 --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs @@ -0,0 +1,145 @@ +namespace AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; + +internal class CommandRunner +{ + private readonly Dictionary _argumentDescriptors; + private readonly Dictionary _optionDescriptors; + private Func, int> _runFunc; + private readonly List _subRunners; + private readonly TextWriter _output; + + public CommandRunner(string commandName, string commandDescription, TextWriter output) + { + CommandName = commandName; + CommandDescription = commandDescription; + _argumentDescriptors = []; + _optionDescriptors = []; + _runFunc = (_) => 1; // no-op + _subRunners = []; + _output = output; + } + + public string CommandName { get; private set; } + + public string CommandDescription { get; private set; } + + public void Argument(string name, string description) + { + _argumentDescriptors.Add(name, description); + } + + public void Option(string name, string description, bool isFlag = false) + { + if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --"); + _optionDescriptors.Add(name, new OptionDescriptor { Description = description, IsFlag = isFlag }); + } + + public void OnRun(Func, int> runFunc) + { + _runFunc = runFunc; + } + + public void SubCommand(string name, string description, Action configAction) + { + var runner = new CommandRunner($"{CommandName} {name}", description, _output); + configAction(runner); + _subRunners.Add(runner); + } + + public int Run(IEnumerable args) + { + if (args.Any()) + { + var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First()); + if (subRunner != null) return subRunner.Run(args.Skip(1)); + } + + if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs)) + { + PrintUsage(); + return 1; + } + + return _runFunc(namedArgs); + } + + private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs) + { + namedArgs = new Dictionary(); + var argsQueue = new Queue(args); + + // Process options first + while (argsQueue.Any() && argsQueue.Peek().StartsWith("--")) + { + // Ensure it's a known option + var name = argsQueue.Dequeue(); + if (!_optionDescriptors.TryGetValue(name, out OptionDescriptor optionDescriptor)) + return false; + + // If it's not a flag, ensure it's followed by a corresponding value + if (!optionDescriptor.IsFlag && (!argsQueue.Any() || argsQueue.Peek().StartsWith("--"))) + return false; + + namedArgs.Add(name, (!optionDescriptor.IsFlag ? argsQueue.Dequeue() : null)); + } + + // Process required args - ensure corresponding values are provided + foreach (var name in _argumentDescriptors.Keys) + { + if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false; + namedArgs.Add(name, argsQueue.Dequeue()); + } + + return argsQueue.Count() == 0; + } + + private void PrintUsage() + { + if (_subRunners.Any()) + { + // List sub commands + _output.WriteLine(CommandDescription); + _output.WriteLine("Commands:"); + foreach (var runner in _subRunners) + { + var shortName = runner.CommandName.Split(' ').Last(); + if (shortName.StartsWith("_")) continue; // convention to hide commands + _output.WriteLine($" {shortName}: {runner.CommandDescription}"); + } + _output.WriteLine(); + } + else + { + // Usage for this command + var optionsPart = _optionDescriptors.Any() ? "[options] " : ""; + var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]"); + _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}"); + _output.WriteLine(); + + // Arguments + foreach (var entry in _argumentDescriptors) + { + _output.WriteLine($"{entry.Key}:"); + _output.WriteLine($" {entry.Value}"); + _output.WriteLine(); + } + + // Options + if (_optionDescriptors.Any()) + { + _output.WriteLine("options:"); + foreach (var entry in _optionDescriptors) + { + _output.WriteLine($" {entry.Key}: {entry.Value.Description}"); + } + _output.WriteLine(); + } + } + } + + private struct OptionDescriptor + { + public string Description; + public bool IsFlag; + } +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs new file mode 100644 index 00000000..29d3e96e --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs @@ -0,0 +1,325 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Reflection; + +namespace Microsoft.Extensions.Hosting; + +internal sealed class HostFactoryResolver +{ + private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + + public const string BuildWebHost = nameof(BuildWebHost); + public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); + public const string CreateHostBuilder = nameof(CreateHostBuilder); + + // The amount of time we wait for the diagnostic source events to fire + private static readonly TimeSpan s_defaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(30); + + public static Func ResolveWebHostFactory(Assembly assembly) + { + return ResolveFactory(assembly, BuildWebHost); + } + + public static Func ResolveWebHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateWebHostBuilder); + } + + public static Func ResolveHostBuilderFactory(Assembly assembly) + { + return ResolveFactory(assembly, CreateHostBuilder); + } + + // This helpers encapsulates all of the complex logic required to: + // 1. Execute the entry point of the specified assembly in a different thread. + // 2. Wait for the diagnostic source events to fire + // 3. Give the caller a chance to execute logic to mutate the IHostBuilder + // 4. Resolve the instance of the applications's IHost + // 5. Allow the caller to determine if the entry point has completed + public static Func ResolveHostFactory(Assembly assembly, + TimeSpan? waitTimeout = null, + bool stopApplication = true, + Action configureHostBuilder = null, + Action entrypointCompleted = null) + { + if (assembly.EntryPoint is null) + { + return null; + } + + try + { + // Attempt to load hosting and check the version to make sure the events + // even have a chance of firing (they were added in .NET >= 6) + var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); + if (hostingAssembly.GetName().Version is Version version && version.Major < 6) + { + return null; + } + + // We're using a version >= 6 so the events can fire. If they don't fire + // then it's because the application isn't using the hosting APIs + } + catch + { + // There was an error loading the extensions assembly, return null. + return null; + } + + return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); + } + + private static Func ResolveFactory(Assembly assembly, string name) + { + var programType = assembly?.EntryPoint?.DeclaringType; + if (programType == null) + { + return null; + } + + var factory = programType.GetMethod(name, DeclaredOnlyLookup); + if (!IsFactory(factory)) + { + return null; + } + + return args => (T)factory.Invoke(null, [args]); + } + + // TReturn Factory(string[] args); + private static bool IsFactory(MethodInfo factory) + { + return factory != null + && typeof(TReturn).IsAssignableFrom(factory.ReturnType) + && factory.GetParameters().Length == 1 + && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); + } + + // Used by EF tooling without any Hosting references. Looses some return type safety checks. + public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null) + { + // Prefer the older patterns by default for back compat. + var webHostFactory = ResolveWebHostFactory(assembly); + if (webHostFactory != null) + { + return args => + { + var webHost = webHostFactory(args); + return GetServiceProvider(webHost); + }; + } + + var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); + if (webHostBuilderFactory != null) + { + return args => + { + var webHostBuilder = webHostBuilderFactory(args); + var webHost = Build(webHostBuilder); + return GetServiceProvider(webHost); + }; + } + + var hostBuilderFactory = ResolveHostBuilderFactory(assembly); + if (hostBuilderFactory != null) + { + return args => + { + var hostBuilder = hostBuilderFactory(args); + var host = Build(hostBuilder); + return GetServiceProvider(host); + }; + } + + var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); + if (hostFactory != null) + { + return args => + { + var host = hostFactory(args); + return GetServiceProvider(host); + }; + } + + return null; + } + + private static object Build(object builder) + { + var buildMethod = builder.GetType().GetMethod("Build"); + return buildMethod?.Invoke(builder, []); + } + + private static IServiceProvider GetServiceProvider(object host) + { + if (host == null) + { + return null; + } + var hostType = host.GetType(); + var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); + return (IServiceProvider)servicesProperty?.GetValue(host); + } + + private sealed class HostingListener : IObserver, IObserver> + { + private readonly string[] _args; + private readonly MethodInfo _entryPoint; + private readonly TimeSpan _waitTimeout; + private readonly bool _stopApplication; + + private readonly TaskCompletionSource _hostTcs = new(); + private IDisposable _disposable; + private readonly Action _configure; + private readonly Action _entrypointCompleted; + private static readonly AsyncLocal _currentListener = new(); + + public HostingListener( + string[] args, + MethodInfo entryPoint, + TimeSpan waitTimeout, + bool stopApplication, + Action configure, + Action entrypointCompleted) + { + _args = args; + _entryPoint = entryPoint; + _waitTimeout = waitTimeout; + _stopApplication = stopApplication; + _configure = configure; + _entrypointCompleted = entrypointCompleted; + } + + public object CreateHost() + { + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + + // Kick off the entry point on a new thread so we don't block the current one + // in case we need to timeout the execution + var thread = new Thread(() => + { + Exception exception = null; + + try + { + // Set the async local to the instance of the HostingListener so we can filter events that + // aren't scoped to this execution of the entry point. + _currentListener.Value = this; + + var parameters = _entryPoint.GetParameters(); + if (parameters.Length == 0) + { + _entryPoint.Invoke(null, []); + } + else + { + _entryPoint.Invoke(null, [_args]); + } + + // Try to set an exception if the entry point returns gracefully, this will force + // build to throw + _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); + } + catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) + { + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + exception = tie.InnerException ?? tie; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(exception); + } + catch (Exception ex) + { + exception = ex; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); + } + finally + { + // Signal that the entry point is completed + _entrypointCompleted?.Invoke(exception); + } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; + + // Start the thread + thread.Start(); + + try + { + // Wait before throwing an exception + if (!_hostTcs.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException("Unable to build IHost"); + } + } + catch (AggregateException) when (_hostTcs.Task.IsCompleted) + { + // Lets this propagate out of the call to GetAwaiter().GetResult() + } + + Debug.Assert(_hostTcs.Task.IsCompleted); + + return _hostTcs.Task.GetAwaiter().GetResult(); + } + + public void OnCompleted() + { + _disposable?.Dispose(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(DiagnosticListener value) + { + if (_currentListener.Value != this) + { + // Ignore events that aren't for this listener + return; + } + + if (value.Name == "Microsoft.Extensions.Hosting") + { + _disposable = value.Subscribe(this); + } + } + + public void OnNext(KeyValuePair value) + { + if (_currentListener.Value != this) + { + // Ignore events that aren't for this listener + return; + } + + if (value.Key == "HostBuilding") + { + _configure?.Invoke(value.Value); + } + + if (value.Key == "HostBuilt") + { + _hostTcs.TrySetResult(value.Value); + + if (_stopApplication) + { + // Stop the host from running further + throw new StopTheHostException(); + } + } + } + + private sealed class StopTheHostException : Exception; + } +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs new file mode 100644 index 00000000..e4635aff --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs @@ -0,0 +1,118 @@ +using System.Reflection; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http.Features; +#if NETCOREAPP3_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +#endif +using Microsoft.Extensions.Hosting; + +namespace AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; + +// Represents an application that uses Microsoft.Extensions.Hosting and supports +// the various entry point flavors. The final model *does not* have an explicit CreateHost entry point and thus inverts the typical flow where the +// execute Main and we wait for events to fire in order to access the appropriate state. +// This is what allows top level statements to work, but getting the IServiceProvider is slightly more complex. +internal class HostingApplication +{ + internal static IServiceProvider GetServiceProvider(Assembly assembly) + { +#if NETCOREAPP2_1 + return null; +#else + // We're disabling the default server and the console host lifetime. This will disable: + // 1. Listening on ports + // 2. Logging to the console from the default host. + // This is essentially what the test server does in order to get access to the application's + // IServicerProvider *and* middleware pipeline. + void ConfigureHostBuilder(object hostBuilder) + { + ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + + for (var i = services.Count - 1; i >= 0; i--) + { + // exclude all implementations of IHostedService + // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure + // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. + var registration = services[i]; + if (registration.ServiceType == typeof(IHostedService) + && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) + { + services.RemoveAt(i); + } + } + }); + } + + var waitForStartTcs = new TaskCompletionSource(); + + void OnEntryPointExit(Exception exception) + { + // If the entry point exited, we'll try to complete the wait + if (exception != null) + { + waitForStartTcs.TrySetException(exception); + } + else + { + waitForStartTcs.TrySetResult(null); + } + } + + // If all of the existing techniques fail, then try to resolve the ResolveHostFactory + var factory = HostFactoryResolver.ResolveHostFactory(assembly, + stopApplication: false, + configureHostBuilder: ConfigureHostBuilder, + entrypointCompleted: OnEntryPointExit); + + // We're unable to resolve the factory. This could mean the application wasn't referencing the right + // version of hosting. + if (factory == null) + { + return null; + } + + try + { + // Get the IServiceProvider from the host + var assemblyName = assembly.GetName()?.FullName ?? string.Empty; + // We set the application name in the hosting environment to the startup assembly + // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our + // application. + var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; + + // Wait for the application to start so that we know it's fully configured. This is important because + // we need the middleware pipeline to be configured before we access the ISwaggerProvider in + // in the IServiceProvider + var applicationLifetime = services.GetRequiredService(); + + using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); + waitForStartTcs.Task.Wait(); + + return services; + } + catch (InvalidOperationException) + { + // We're unable to resolve the host, swallow the exception and return null + } + + return null; +#endif + } + + private class NoopHostLifetime : IHostLifetime + { + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } + + private class NoopServer : IServer + { + public IFeatureCollection Features { get; } = new FeatureCollection(); + public void Dispose() { } + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md new file mode 100644 index 00000000..babea97b --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md @@ -0,0 +1,3 @@ +This code is taken from [Swashbuckle.AspNetCore.Cli](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/src/Swashbuckle.AspNetCore.Cli) + +Since Swashbuckle.AspNetCore.Cli is delivered as a tool, code cannot be reference through Nuget. \ No newline at end of file diff --git a/src/AsyncApi.Saunter.Generator.Cli/readme.md b/src/AsyncApi.Saunter.Generator.Cli/readme.md new file mode 100644 index 00000000..afb7f457 --- /dev/null +++ b/src/AsyncApi.Saunter.Generator.Cli/readme.md @@ -0,0 +1,13 @@ +# AsyncApi Generator.Cli Tool + +## Tool usage +``` +dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] [startup-assembly] [asyncapi-document-name] +``` + +## 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. +--format: the output formats to generate, can be a combination of json, yml and/or yaml. File extension is appended to the output path. From 2162d72adb996d85d5a14f0dd4b07673cf5ffb73 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 14:36:00 +0200 Subject: [PATCH 02/34] #196 Publish a global dotnet tool -- Api/API casing fix --- Saunter.sln | 2 +- .../Args.cs | 0 .../AsyncAPI.Saunter.Generator.Cli.csproj} | 0 .../Commands/Tofile.cs | 0 .../Commands/TofileInternal.cs | 0 .../Program.cs | 0 .../SwashbuckleImport/CommandRunner.cs | 0 .../SwashbuckleImport/HostFactoryResolver.cs | 0 .../SwashbuckleImport/HostingApplication.cs | 0 .../SwashbuckleImport/readme.md | 0 .../readme.md | 0 11 files changed, 1 insertion(+), 1 deletion(-) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/Args.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj => AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj} (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/Commands/Tofile.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/Commands/TofileInternal.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/Program.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/SwashbuckleImport/CommandRunner.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/SwashbuckleImport/HostFactoryResolver.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/SwashbuckleImport/HostingApplication.cs (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/SwashbuckleImport/readme.md (100%) rename src/{AsyncApi.Saunter.Generator.Cli => AsyncAPI.Saunter.Generator.Cli}/readme.md (100%) diff --git a/Saunter.sln b/Saunter.sln index bd69f05e..2aff09fa 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -29,7 +29,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.IntegrationTests.Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Tests.MarkerTypeTests", "test\Saunter.Tests.MarkerTypeTests\Saunter.Tests.MarkerTypeTests.csproj", "{02284473-6DE7-4EE0-8433-2AC295045549}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncApi.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AsyncApi.Saunter.Generator.Cli/Args.cs b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/Args.cs rename to src/AsyncAPI.Saunter.Generator.Cli/Args.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj rename to src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/Commands/Tofile.cs rename to src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs rename to src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/Program.cs b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/Program.cs rename to src/AsyncAPI.Saunter.Generator.Cli/Program.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs rename to src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs rename to src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs rename to src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs diff --git a/src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/SwashbuckleImport/readme.md rename to src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md diff --git a/src/AsyncApi.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md similarity index 100% rename from src/AsyncApi.Saunter.Generator.Cli/readme.md rename to src/AsyncAPI.Saunter.Generator.Cli/readme.md From 55895a2afc5dd17acd0eac6161a2b289388ac4fa Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 14:43:36 +0200 Subject: [PATCH 03/34] #196 .NET8 --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf470245..0cfc8f79 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: node-version: 'lts/*' # latest LTS version - uses: actions/setup-dotnet@v2 with: - dotnet-version: '6.0.x' # SDK Version to use; x will use the latest version of the channel + dotnet-version: '8.0.x' # SDK Version to use; x will use the latest version of the channel - name: Run NPM install run: npm ci From 6ba3409ef277262b518045921850df6184c2b2f3 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 14:46:59 +0200 Subject: [PATCH 04/34] #196 .NET6 --- .github/workflows/ci.yaml | 2 +- .../AsyncAPI.Saunter.Generator.Cli.csproj | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0cfc8f79..bf470245 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: node-version: 'lts/*' # latest LTS version - uses: actions/setup-dotnet@v2 with: - dotnet-version: '8.0.x' # SDK Version to use; x will use the latest version of the channel + dotnet-version: '6.0.x' # SDK Version to use; x will use the latest version of the channel - name: Run NPM install run: npm ci 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 bf10ee04..0181dacd 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,7 @@  - net8.0 + net6.0 Exe enable 12 @@ -11,7 +11,6 @@ Exe true AsyncAPI.Saunter.Generator.Cli - asyncapi true From 74c9cff2ef59a98d152cce29c1dea628e44b11f2 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 14:52:06 +0200 Subject: [PATCH 05/34] #196 I'd like .NET8 more, request both for ci --- .github/workflows/ci.yaml | 4 +++- .../AsyncAPI.Saunter.Generator.Cli.csproj | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf470245..8eb5bb44 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,9 @@ jobs: node-version: 'lts/*' # latest LTS version - uses: actions/setup-dotnet@v2 with: - dotnet-version: '6.0.x' # SDK Version to use; x will use the latest version of the channel + dotnet-version: | + 6.0.x # SDK Version to use; x will use the latest version of the channel + 8.0.x - name: Run NPM install run: npm ci 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 0181dacd..b5539990 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,7 @@  - net6.0 + net8.0 Exe enable 12 From cc5d95213b31791140fc771b770142038cf8787d Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 15:01:07 +0200 Subject: [PATCH 06/34] #196 I'd like .NET8 more, request both for ci? --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8eb5bb44..d262e704 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-dotnet@v2 with: dotnet-version: | - 6.0.x # SDK Version to use; x will use the latest version of the channel + 6.0.x 8.0.x - name: Run NPM install From 569a30ac5efdd4547c0798226b96c09e8cfd23c4 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 17:20:14 +0200 Subject: [PATCH 07/34] #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 From 0419101890e3db925295aa449a331517bb19718d Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 18:28:04 +0200 Subject: [PATCH 08/34] #196 Test for dotnet cli tool, added support for default asyncapi document --- Saunter.sln | 17 +- .../Commands/TofileInternal.cs | 37 ++- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 10 +- ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 28 +++ .../DotnetCliToolTests.cs | 234 ++++++++++++++++++ 5 files changed, 312 insertions(+), 14 deletions(-) create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs diff --git a/Saunter.sln b/Saunter.sln index 2aff09fa..43967dd8 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -29,7 +29,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.IntegrationTests.Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Tests.MarkerTypeTests", "test\Saunter.Tests.MarkerTypeTests\Saunter.Tests.MarkerTypeTests.csproj", "{02284473-6DE7-4EE0-8433-2AC295045549}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -113,6 +115,18 @@ Global {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x64.Build.0 = Release|Any CPU {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.ActiveCfg = Release|Any CPU {6C102D4D-3DA4-4763-B75E-C15E33E7E94A}.Release|x86.Build.0 = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|x64.ActiveCfg = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|x64.Build.0 = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|x86.ActiveCfg = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Debug|x86.Build.0 = Debug|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|Any CPU.Build.0 = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x64.ActiveCfg = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x64.Build.0 = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x86.ActiveCfg = Release|Any CPU + {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -124,6 +138,7 @@ Global {7CD09B89-130A-41AF-ADAE-2166C4ED695B} = {6491E321-2D02-44AB-9116-D722FE169595} {02284473-6DE7-4EE0-8433-2AC295045549} = {6491E321-2D02-44AB-9116-D722FE169595} {6C102D4D-3DA4-4763-B75E-C15E33E7E94A} = {28D4C365-FDED-49AE-A97D-36202E24A55A} + {18AD0249-0436-4A26-9972-B97BA6905A54} = {6491E321-2D02-44AB-9116-D722FE169595} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs index 00ea898f..a3477c7a 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs @@ -15,12 +15,16 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore; using Microsoft.Extensions.Hosting; +using Saunter.AsyncApiSchema.v2; using static Program; +using AsyncApiDocument = Saunter.AsyncApiSchema.v2.AsyncApiDocument; namespace AsyncApi.Saunter.Generator.Cli.Commands; internal class TofileInternal { + private const string defaultDocumentName = null; + internal static int Run(IDictionary namedArgs) { // 1) Configure host with provided startupassembly @@ -43,24 +47,41 @@ internal static int Run(IDictionary namedArgs) // 3) Retrieve AsyncAPI via configured provider var documentProvider = serviceProvider.GetService(); - var asyncapiOptions = serviceProvider.GetService>(); + var asyncapiOptions = serviceProvider.GetService>().Value; var documentSerializer = serviceProvider.GetRequiredService(); - var documentNames = namedArgs.TryGetValue(DocOption, out var doc) ? [doc] : asyncapiOptions.Value.NamedApis.Keys; + var documentNames = namedArgs.TryGetValue(DocOption, out var doc) ? [doc] : asyncapiOptions.NamedApis.Keys; var fileTemplate = namedArgs.TryGetValue(FileNameOption, out var template) ? template : "{document}_asyncapi.{extension}"; + if (documentNames.Count == 0) + { + if (asyncapiOptions.AssemblyMarkerTypes.Any()) + { + documentNames = [defaultDocumentName]; + } + else + { + throw new ArgumentOutOfRangeException(DocOption, $"No AsyncAPI documents found: {DocOption} = '{doc}'. Known document(s): {string.Join(", ", asyncapiOptions.NamedApis.Keys)}."); + } + } + foreach (var documentName in documentNames) { - if (!asyncapiOptions.Value.NamedApis.TryGetValue(documentName, out var prototype)) + AsyncApiDocument prototype; + if (documentName == defaultDocumentName) + { + prototype = asyncapiOptions.AsyncApi; + } + else if (!asyncapiOptions.NamedApis.TryGetValue(documentName, out prototype)) { - throw new ArgumentOutOfRangeException(DocOption, documentName, $"Requested AsyncAPI document not found: '{documentName}'. Known document(s): {string.Join(", ", asyncapiOptions.Value.NamedApis.Keys)}."); + throw new ArgumentOutOfRangeException(DocOption, documentName, $"Requested AsyncAPI document not found: '{documentName}'. Known document(s): {string.Join(", ", asyncapiOptions.NamedApis.Keys)}."); } - var asyncApiSchema = documentProvider.GetDocument(asyncapiOptions.Value, prototype); - var asyncApiSchemaJson = documentSerializer.Serialize(asyncApiSchema); + var schema = documentProvider.GetDocument(asyncapiOptions, prototype); + var asyncApiSchemaJson = documentSerializer.Serialize(schema); 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)):" + + Console.Error.WriteLine($"AsyncAPI Schema '{documentName ?? "default"}' is not valid ({diagnostic.Errors.Count} Error(s), {diagnostic.Warnings.Count} Warning(s)):" + $"{Environment.NewLine}{string.Join(Environment.NewLine, diagnostic.Errors.Select(x => $"- {x}"))}"); } @@ -123,7 +144,7 @@ private static string AddFileExtension(string outputPath, string fileTemplate, s return outputPath; } - return Path.Combine(outputPath, fileTemplate.Replace("{document}", documentName).Replace("{extension}", extension)); + return Path.Combine(outputPath, fileTemplate.Replace("{document}", documentName == defaultDocumentName ? "" : documentName).Replace("{extension}", extension).TrimStart('_')); } private static IServiceProvider GetServiceProvider(Assembly startupAssembly) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index 6c73c716..11d3c9b4 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -8,8 +8,8 @@ dotnet asyncapi.net tofile --output [output-path] --format [json,yml,yaml] --doc startup-assembly: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). ## 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 +--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 diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj new file mode 100644 index 00000000..8d3e969c --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs new file mode 100644 index 00000000..3d6c7324 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs @@ -0,0 +1,234 @@ +// 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.Diagnostics; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public class DotnetCliToolTests(ITestOutputHelper output) +{ + private string RunTool(string args, int expectedExitCode = 0) + { + var process = Process.Start(new ProcessStartInfo("dotnet") + { + Arguments = $"../../../../../src/AsyncAPI.Saunter.Generator.Cli/bin/Debug/net6.0/AsyncAPI.Saunter.Generator.Cli.dll tofile {args}", + RedirectStandardOutput = true, + RedirectStandardError = true, + }); + process.WaitForExit(); + var stdOut = process.StandardOutput.ReadToEnd().Trim(); + var stdError = process.StandardError.ReadToEnd().Trim(); + output.WriteLine(stdOut); + output.WriteLine(stdError); + + process.ExitCode.ShouldBe(expectedExitCode); + //stdError.ShouldBeEmpty(); LEGO lib doesn't like id: "id is not a valid property at #/components/schemas/lightMeasuredEvent"" + return stdOut; + } + + [Fact] + public void DefaultCallPrintsCommandInfo() + { + var stdOut = RunTool("", 1); + + stdOut.ShouldBe(""" + Usage: dotnet asyncapi.net tofile [options] [startupassembly] + + startupassembly: + relative path to the application's startup assembly + + options: + --doc: name(s) of the AsyncAPI documents you want to retrieve, as configured in your startup class [defaults to all documents] + --output: relative path where the AsyncAPI will be output [defaults to stdout] + --filename: defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}"] + --format: exports AsyncAPI in json and/or yml format [defaults to json] + --env: 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] + """, StringCompareShould.IgnoreLineEndings); + } + + [Fact] + public void StreetlightsAPIExportSpecTest() + { + var path = Directory.GetCurrentDirectory(); + output.WriteLine($"Output path: {path}"); + var stdOut = RunTool($"--output {path} --format json,yml,yaml ../../../../../examples/StreetlightsAPI/bin/Debug/net6.0/StreetlightsAPI.dll"); + + stdOut.ShouldNotBeEmpty(); + stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); + stdOut.ShouldContain($"AsyncAPI yml successfully written to {Path.Combine(path, "asyncapi.yml")}"); + stdOut.ShouldContain($"AsyncAPI json successfully written to {Path.Combine(path, "asyncapi.json")}"); + + File.Exists("asyncapi.yml").ShouldBeTrue("asyncapi.yml"); + File.Exists("asyncapi.yaml").ShouldBeTrue("asyncapi.yaml"); + File.Exists("asyncapi.json").ShouldBeTrue("asyncapi.json"); + + var yml = File.ReadAllText("asyncapi.yml"); + yml.ShouldBe(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 + servers: + mosquitto: + url: test.mosquitto.org + protocol: mqtt + webapi: + url: localhost:5000 + protocol: http + defaultContentType: application/json + channels: + publish/light/measured: + servers: + - webapi + publish: + operationId: MeasureLight + summary: Inform about environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + $ref: '#/components/messages/lightMeasuredEvent' + subscribe/light/measured: + servers: + - mosquitto + subscribe: + operationId: PublishLightMeasurement + summary: Subscribe to environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + payload: + $ref: '#/components/schemas/lightMeasuredEvent' + components: + schemas: + lightMeasuredEvent: + type: object + properties: + id: + type: integer + format: int32 + description: Id of the streetlight. + lumens: + type: integer + format: int32 + description: Light intensity measured in lumens. + sentAt: + type: string + format: date-time + description: Light intensity measured in lumens. + additionalProperties: false + messages: + lightMeasuredEvent: + payload: + $ref: '#/components/schemas/lightMeasuredEvent' + name: lightMeasuredEvent + """, "yaml"); + + var yaml = File.ReadAllText("asyncapi.yaml"); + yaml.ShouldBe(yml, "yml"); + + var json = File.ReadAllText("asyncapi.json"); + json.ShouldBe(""" + { + "asyncapi": "2.6.0", + "info": { + "title": "Streetlights API", + "version": "1.0.0", + "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + "servers": { + "mosquitto": { + "url": "test.mosquitto.org", + "protocol": "mqtt" + }, + "webapi": { + "url": "localhost:5000", + "protocol": "http" + } + }, + "defaultContentType": "application/json", + "channels": { + "publish/light/measured": { + "servers": [ + "webapi" + ], + "publish": { + "operationId": "MeasureLight", + "summary": "Inform about environmental lighting conditions for a particular streetlight.", + "tags": [ + { + "name": "Light" + } + ], + "message": { + "$ref": "#/components/messages/lightMeasuredEvent" + } + } + }, + "subscribe/light/measured": { + "servers": [ + "mosquitto" + ], + "subscribe": { + "operationId": "PublishLightMeasurement", + "summary": "Subscribe to environmental lighting conditions for a particular streetlight.", + "tags": [ + { + "name": "Light" + } + ], + "message": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredEvent" + } + } + } + } + }, + "components": { + "schemas": { + "lightMeasuredEvent": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Id of the streetlight." + }, + "lumens": { + "type": "integer", + "format": "int32", + "description": "Light intensity measured in lumens." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Light intensity measured in lumens." + } + }, + "additionalProperties": false + } + }, + "messages": { + "lightMeasuredEvent": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredEvent" + }, + "name": "lightMeasuredEvent" + } + } + } + } + """, "json"); + } +} From b29a1ab720d7ed962b527bbf12bd43251b9982c2 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 19:31:17 +0200 Subject: [PATCH 09/34] #196 Pack_Install_Run_Uninstall Test --- ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 6 ++ .../PackAndInstallLocalTests.cs | 56 +++++++++++++++++++ .../asyncapi.cmd | 1 + 3 files changed, 63 insertions(+) create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 8d3e969c..c1b63ddd 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -25,4 +25,10 @@ + + + PreserveNewest + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs new file mode 100644 index 00000000..fe082871 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs @@ -0,0 +1,56 @@ +// 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.Diagnostics; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public class PackAndInstallLocalTests(ITestOutputHelper output) +{ + private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) + { + var process = Process.Start(new ProcessStartInfo(file) + { + Arguments = args, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + process.WaitForExit(TimeSpan.FromSeconds(20)); + var stdOut = process.StandardOutput.ReadToEnd().Trim(); + var stdError = process.StandardError.ReadToEnd().Trim(); + output.WriteLine($"### Output of \"{file} {args}\""); + output.WriteLine(stdOut); + output.WriteLine(stdError); + + process.ExitCode.ShouldBe(expectedExitCode); + return stdOut; + } + + [Fact] + public void Pack_Install_Run_Uninstall_Test() + { + var stdOut = this.Run("dotnet", "pack", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); + stdOut.ShouldContain("Successfully created package"); + + stdOut = this.Run("dotnet", "tool install --global --add-source ./bin/release AsyncAPI.Saunter.Generator.Cli", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); + stdOut.ShouldBeOneOf("You can invoke the tool using the following command: AsyncAPI.NET\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.0') was successfully installed.", + "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.0')."); + + stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", ""); + stdOut.ShouldContain("AsyncAPI.NET"); + + stdOut = this.Run("asyncapi.cmd", "", "", 1); + stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); + + stdOut = this.Run("dotnet", "tool uninstall -g asyncapi.saunter.generator.cli", ""); + stdOut.ShouldContain(" was successfully uninstalled."); + + stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", "", 1); + stdOut.ShouldNotContain("AsyncAPI.NET"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd new file mode 100644 index 00000000..b75bbd67 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd @@ -0,0 +1 @@ +AsyncAPI.NET \ No newline at end of file From 12593dce086296dce363b6384889077ceca13f5b Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 19:31:17 +0200 Subject: [PATCH 10/34] #196 Pack_Install_Run_Uninstall fix casing in test, add meta data to package --- .../AsyncAPI.Saunter.Generator.Cli.csproj | 19 ++++++- ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 6 ++ .../PackAndInstallLocalTests.cs | 56 +++++++++++++++++++ .../asyncapi.cmd | 1 + 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd 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 63a8b166..3b07d834 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -2,16 +2,24 @@ Exe + net8.0;net6.0 enable 12 AsyncAPI.Saunter.Generator.Cli AsyncAPI Command Line Tools - Exe + AsyncAPI Initiative true AsyncAPI.Saunter.Generator.Cli AsyncAPI.NET - net8.0;net6.0 + asyncapi;aspnetcore;openapi;documentation;amqp;generator;cli;tool + readme.md + logo.png + https://github.com/asyncapi/saunter + true + https://github.com/asyncapi/saunter + MIT + false @@ -25,5 +33,10 @@ - + + + + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 8d3e969c..c1b63ddd 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -25,4 +25,10 @@ + + + PreserveNewest + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs new file mode 100644 index 00000000..34930f30 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs @@ -0,0 +1,56 @@ +// 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.Diagnostics; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public class PackAndInstallLocalTests(ITestOutputHelper output) +{ + private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) + { + var process = Process.Start(new ProcessStartInfo(file) + { + Arguments = args, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + process.WaitForExit(TimeSpan.FromSeconds(20)); + var stdOut = process.StandardOutput.ReadToEnd().Trim(); + var stdError = process.StandardError.ReadToEnd().Trim(); + output.WriteLine($"### Output of \"{file} {args}\""); + output.WriteLine(stdOut); + output.WriteLine(stdError); + + process.ExitCode.ShouldBe(expectedExitCode); + return stdOut; + } + + [Fact] + public void Pack_Install_Run_Uninstall_Test() + { + var stdOut = this.Run("dotnet", "pack", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); + stdOut.ShouldContain("Successfully created package"); + + stdOut = this.Run("dotnet", "tool install --global --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); + stdOut.ShouldBeOneOf("You can invoke the tool using the following command: AsyncAPI.NET\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.0') was successfully installed.", + "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.0')."); + + stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", ""); + stdOut.ShouldContain("AsyncAPI.NET"); + + stdOut = this.Run("asyncapi.cmd", "", "", 1); + stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); + + stdOut = this.Run("dotnet", "tool uninstall -g asyncapi.saunter.generator.cli", ""); + stdOut.ShouldContain(" was successfully uninstalled."); + + stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", "", 1); + stdOut.ShouldNotContain("AsyncAPI.NET"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd new file mode 100644 index 00000000..b75bbd67 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd @@ -0,0 +1 @@ +AsyncAPI.NET \ No newline at end of file From e70166076b8e3fff1108c4e963ff87876c66610a Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 19:53:57 +0200 Subject: [PATCH 11/34] #196 fix warnings, fix permission --- .../AsyncAPI.Saunter.Generator.Cli.Tests.csproj | 1 - test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd | 0 2 files changed, 1 deletion(-) mode change 100644 => 100755 test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index c1b63ddd..3a1e115b 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -3,7 +3,6 @@ net8.0 enable - enable false true diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd old mode 100644 new mode 100755 From d9b558c24435ac03fb74e81b27a97026fd48fedb Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 19:57:40 +0200 Subject: [PATCH 12/34] #196 unix needs a shebang --- test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd index b75bbd67..1f61f620 100755 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd @@ -1 +1,2 @@ +#!/bin/bash AsyncAPI.NET \ No newline at end of file From f0af6d1ce920145af1a0bb43db5a60398bef476d Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 6 Jul 2024 20:04:13 +0200 Subject: [PATCH 13/34] #196 sourcelink properties --- .../AsyncAPI.Saunter.Generator.Cli.csproj | 6 ++++++ 1 file changed, 6 insertions(+) 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 3b07d834..0400c5ab 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -17,9 +17,12 @@ logo.png https://github.com/asyncapi/saunter true + true https://github.com/asyncapi/saunter MIT false + true + snupkg @@ -28,6 +31,9 @@ + + + From 1ebb77c984c194947d0c9c00738e9570bb59d3bd Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sun, 7 Jul 2024 11:39:54 +0200 Subject: [PATCH 14/34] #196 renamed tool to make up a more logical name, fixed empty parameter handling --- .../AsyncAPI.Saunter.Generator.Cli.csproj | 5 ++-- .../Commands/Tofile.cs | 2 +- .../Commands/TofileInternal.cs | 19 +++++++-------- src/AsyncAPI.Saunter.Generator.Cli/Program.cs | 6 ++--- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 23 +++++++++++++------ ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 6 ----- .../DotnetCliToolTests.cs | 2 +- .../PackAndInstallLocalTests.cs | 10 ++++---- .../asyncapi.cmd | 2 -- 9 files changed, 37 insertions(+), 38 deletions(-) delete mode 100755 test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd 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 0400c5ab..472cc32e 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net6.0 + net6.0;net8.0 enable 12 AsyncAPI.Saunter.Generator.Cli @@ -11,7 +11,7 @@ AsyncAPI Initiative true AsyncAPI.Saunter.Generator.Cli - AsyncAPI.NET + dotnet-asyncapi asyncapi;aspnetcore;openapi;documentation;amqp;generator;cli;tool readme.md logo.png @@ -23,6 +23,7 @@ false true snupkg + 1.0.1 diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs index 9faa30c9..a447ce43 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs @@ -41,6 +41,6 @@ internal static Func, int> Run(string[] args) => nam private static string EscapePath(string path) { - return path.Contains(' ') ? "\"" + path + "\"" : path; + return (path.Contains(' ') || string.IsNullOrWhiteSpace(path)) ? "\"" + path + "\"" : path; } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs index a3477c7a..5889e5d2 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs @@ -18,6 +18,7 @@ using Saunter.AsyncApiSchema.v2; using static Program; using AsyncApiDocument = Saunter.AsyncApiSchema.v2.AsyncApiDocument; +using System.IO; namespace AsyncApi.Saunter.Generator.Cli.Commands; @@ -31,7 +32,7 @@ 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(); + var envVars = (namedArgs.TryGetValue(EnvOption, out var x) && !string.IsNullOrWhiteSpace(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) @@ -50,9 +51,9 @@ internal static int Run(IDictionary namedArgs) var asyncapiOptions = serviceProvider.GetService>().Value; var documentSerializer = serviceProvider.GetRequiredService(); - var documentNames = namedArgs.TryGetValue(DocOption, out var doc) ? [doc] : asyncapiOptions.NamedApis.Keys; - var fileTemplate = namedArgs.TryGetValue(FileNameOption, out var template) ? template : "{document}_asyncapi.{extension}"; - if (documentNames.Count == 0) + var documentNames = (namedArgs.TryGetValue(DocOption, out var doc) && !string.IsNullOrWhiteSpace(doc)) ? [doc] : asyncapiOptions.NamedApis.Keys; + var fileTemplate = (namedArgs.TryGetValue(FileNameOption, out var template) && !string.IsNullOrWhiteSpace(template)) ? template : "{document}_asyncapi.{extension}"; + if (documentNames.Count == 0) { if (asyncapiOptions.AssemblyMarkerTypes.Any()) { @@ -86,20 +87,16 @@ internal static int Run(IDictionary namedArgs) } // 4) Serialize to specified output location or stdout - var outputPath = namedArgs.TryGetValue(OutputOption, out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; + var outputPath = (namedArgs.TryGetValue(OutputOption, out var path) && !string.IsNullOrWhiteSpace(path)) ? Path.Combine(Directory.GetCurrentDirectory(), path) : null; if (!string.IsNullOrEmpty(outputPath)) { - var directoryPath = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } + Directory.CreateDirectory(outputPath); } var exportJson = true; var exportYml = false; var exportYaml = false; - if (namedArgs.TryGetValue(FormatOption, out var format)) + if (namedArgs.TryGetValue(FormatOption, out var format) && !string.IsNullOrWhiteSpace(format)) { var splitted = format.Split(',').Select(x => x.Trim()).ToList(); exportJson = splitted.Any(x => x.Equals("json", StringComparison.OrdinalIgnoreCase)); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs index 4d174c60..b9facd2b 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs @@ -5,14 +5,14 @@ DependencyResolver.Init(); // Helper to simplify command line parsing etc. -var runner = new CommandRunner("dotnet asyncapi.net", "AsyncAPI Command Line Tools", Console.Out); +var runner = new CommandRunner("dotnet asyncapi", "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.net tofile ... +// > dotnet asyncapi 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"); @@ -24,7 +24,7 @@ c.OnRun(Tofile.Run(args)); }); -// > dotnet asyncapi.net _tofile ... (* should only be invoked via "dotnet exec") +// > dotnet asyncapi _tofile ... (* should only be invoked via "dotnet exec") runner.SubCommand("_tofile", "", c => { c.Argument(StartupAssemblyArgument, ""); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index 11d3c9b4..74d5c81b 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -1,15 +1,24 @@ # AsyncApi Generator.Cli Tool -A dotnet tool to generate AsyncAPI specification files based of dotnet DLL (The application itself). +A dotnet tool to generate AsyncAPI specification files based of a dotnet DLL (The application itself). ## Tool usage ``` -dotnet asyncapi.net tofile --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] [startup-assembly] +dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] [startup-assembly] ``` startup-assembly: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). ## 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 +- _--doc_: The name of the AsyncAPI document as defined in the startup class by the ```.ConfigureNamedAsyncApi()```-method. If only ```.AddAsyncApiSchemaGeneration()``` is used, the document is unnamed and will always be exported. 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 + +## Install the Generator.Cli dotnet Tool +``` +dotnet tool install --global AsyncAPI.Saunter.Generator.Cli +``` +After installing the tool globally, it is available using commands: ```dotnet asyncapi``` or ```dotnet-asyncapi``` + +Want to learn more about .NET tools? Or want to install it local using a manifest? +(Check out this Microsoft page on how to manage .NET tools)[https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools] \ No newline at end of file diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 3a1e115b..355fdd59 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -24,10 +24,4 @@ - - - PreserveNewest - - - diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs index 3d6c7324..97258c87 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs @@ -35,7 +35,7 @@ public void DefaultCallPrintsCommandInfo() var stdOut = RunTool("", 1); stdOut.ShouldBe(""" - Usage: dotnet asyncapi.net tofile [options] [startupassembly] + Usage: dotnet asyncapi tofile [options] [startupassembly] startupassembly: relative path to the application's startup assembly diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs index 34930f30..79d9844d 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs @@ -38,19 +38,19 @@ public void Pack_Install_Run_Uninstall_Test() stdOut.ShouldContain("Successfully created package"); stdOut = this.Run("dotnet", "tool install --global --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); - stdOut.ShouldBeOneOf("You can invoke the tool using the following command: AsyncAPI.NET\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.0') was successfully installed.", - "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.0')."); + stdOut.ShouldBeOneOf("You can invoke the tool using the following command: dotnet-asyncapi\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.1') was successfully installed.", + "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.1')."); stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", ""); - stdOut.ShouldContain("AsyncAPI.NET"); + stdOut.ShouldContain("dotnet-asyncapi"); - stdOut = this.Run("asyncapi.cmd", "", "", 1); + stdOut = this.Run("dotnet", "asyncapi", "", 1); stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); stdOut = this.Run("dotnet", "tool uninstall -g asyncapi.saunter.generator.cli", ""); stdOut.ShouldContain(" was successfully uninstalled."); stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", "", 1); - stdOut.ShouldNotContain("AsyncAPI.NET"); + stdOut.ShouldNotContain("dotnet-asyncapi"); } } diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd b/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd deleted file mode 100755 index 1f61f620..00000000 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/asyncapi.cmd +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -AsyncAPI.NET \ No newline at end of file From ba1e8566cd3f2c2690d503dda90fa6baa36987c2 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sun, 7 Jul 2024 11:48:39 +0200 Subject: [PATCH 15/34] #196 fix readme error --- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index 74d5c81b..8eb9a503 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -21,4 +21,4 @@ dotnet tool install --global AsyncAPI.Saunter.Generator.Cli After installing the tool globally, it is available using commands: ```dotnet asyncapi``` or ```dotnet-asyncapi``` Want to learn more about .NET tools? Or want to install it local using a manifest? -(Check out this Microsoft page on how to manage .NET tools)[https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools] \ No newline at end of file +[Check out this Microsoft page on how to manage .NET tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) \ No newline at end of file From a746e4d0db912f915d11a4a82b84af60fac37858 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Tue, 9 Jul 2024 20:51:52 +0200 Subject: [PATCH 16/34] #196 Fixed formatting etc + some PR remarks --- .editorconfig | 2 - src/AsyncAPI.Saunter.Generator.Cli/Args.cs | 6 +- .../AsyncAPI.Saunter.Generator.Cli.csproj | 7 +- .../Commands/Tofile.cs | 6 +- .../Commands/TofileInternal.cs | 30 +-- .../Internal/DependencyResolver.cs | 6 +- .../SwashbuckleImport/HostFactoryResolver.cs | 5 +- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 6 +- src/Saunter/AsyncApiOptions.cs | 14 +- .../AsyncApiServiceCollectionExtensions.cs | 10 +- .../DotnetCliToolTests.cs | 6 +- .../PackAndInstallLocalTests.cs | 6 +- .../InterfaceAttributeTests.cs | 214 +++++++++--------- .../AsyncApiTypesTests.cs | 1 - .../SchemaGeneration/SchemaGenerationTests.cs | 30 +-- 15 files changed, 160 insertions(+), 189 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5209be73..20f2dfdc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -144,8 +144,6 @@ dotnet_naming_symbols.all_members.applicable_kinds = * dotnet_naming_style.pascal_case_style.capitalization = pascal_case -file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license.\nSee the LICENSE file in the project root for more information. - # RS0016: Only enable if API files are present dotnet_public_api_analyzer.require_api_files = true diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs index cafc79b3..1100a58b 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs @@ -1,8 +1,4 @@ -// 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. - -// ReSharper disable once CheckNamespace +// ReSharper disable once CheckNamespace public static partial class Program { internal const string StartupAssemblyArgument = "startupassembly"; 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 472cc32e..0ece5488 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -7,7 +7,7 @@ 12 AsyncAPI.Saunter.Generator.Cli - AsyncAPI Command Line Tools + AsyncAPI Command Line Tools: Dotnet tool to generate AsyncAPI spec file from dotnet startup assembly. AsyncAPI Initiative true AsyncAPI.Saunter.Generator.Cli @@ -23,7 +23,10 @@ false true snupkg - 1.0.1 + + + + true diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs index a447ce43..1c90a9b9 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs @@ -1,8 +1,4 @@ -// 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.Diagnostics; +using System.Diagnostics; using System.Reflection; using static Program; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs index 5889e5d2..b833d741 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs @@ -1,24 +1,18 @@ -// 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 LEGO.AsyncAPI.Readers; -using Microsoft.Extensions.Options; -using Saunter.Serialization; -using Saunter; +using System.Reflection; using System.Runtime.Loader; -using System.Reflection; +using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; using LEGO.AsyncAPI; using LEGO.AsyncAPI.Models; -using Microsoft.Extensions.DependencyInjection; -using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; -using Microsoft.AspNetCore.Hosting; +using LEGO.AsyncAPI.Readers; using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Saunter.AsyncApiSchema.v2; +using Microsoft.Extensions.Options; +using Saunter; +using Saunter.Serialization; using static Program; using AsyncApiDocument = Saunter.AsyncApiSchema.v2.AsyncApiDocument; -using System.IO; namespace AsyncApi.Saunter.Generator.Cli.Commands; @@ -35,13 +29,13 @@ internal static int Run(IDictionary namedArgs) var envVars = (namedArgs.TryGetValue(EnvOption, out var x) && !string.IsNullOrWhiteSpace(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) + if (envVar.Count is 1 or 2) { - Environment.SetEnvironmentVariable(envVar[0], envVar[1], EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); } else { - throw new ArgumentOutOfRangeException(EnvOption, namedArgs[EnvOption], "Environment variable should be in the format: env1=value1,env2=value2"); + throw new ArgumentOutOfRangeException(EnvOption, namedArgs[EnvOption], "Environment variable should be in the format: env1=value1,env2=value2,env3"); } } var serviceProvider = GetServiceProvider(startupAssembly); @@ -51,7 +45,7 @@ internal static int Run(IDictionary namedArgs) var asyncapiOptions = serviceProvider.GetService>().Value; var documentSerializer = serviceProvider.GetRequiredService(); - var documentNames = (namedArgs.TryGetValue(DocOption, out var doc) && !string.IsNullOrWhiteSpace(doc)) ? [doc] : asyncapiOptions.NamedApis.Keys; + var documentNames = (namedArgs.TryGetValue(DocOption, out var doc) && !string.IsNullOrWhiteSpace(doc)) ? [doc] : asyncapiOptions.NamedApis.Keys; var fileTemplate = (namedArgs.TryGetValue(FileNameOption, out var template) && !string.IsNullOrWhiteSpace(template)) ? template : "{document}_asyncapi.{extension}"; if (documentNames.Count == 0) { diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs index e6ef1bc3..d136cf1f 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs @@ -1,8 +1,4 @@ -// 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; +using System.Reflection; namespace AsyncAPI.Saunter.Generator.Cli.Internal; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs index 29d3e96e..266e47f4 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs @@ -1,7 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; +using System.Diagnostics; using System.Reflection; namespace Microsoft.Extensions.Hosting; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index 8eb9a503..05c42764 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -5,14 +5,14 @@ A dotnet tool to generate AsyncAPI specification files based of a dotnet DLL (Th ``` dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] [startup-assembly] ``` -startup-assembly: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). +- _startup-assembly_: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). ## Tool options - _--doc_: The name of the AsyncAPI document as defined in the startup class by the ```.ConfigureNamedAsyncApi()```-method. If only ```.AddAsyncApiSchemaGeneration()``` is used, the document is unnamed and will always be exported. 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 +- _--format_: the output formats to generate, can be a combination of json, yml and/or yaml. +- _--env_: define environment variable(s) for the application. Formatted as a comma separated list of _key=value_ pairs or just _key_ for flags, example: ```ASPNETCORE_ENVIRONMENT=AsyncAPI,CONNECT_TO_DATABASE=false,GENERATOR_FLAG```. ## Install the Generator.Cli dotnet Tool ``` diff --git a/src/Saunter/AsyncApiOptions.cs b/src/Saunter/AsyncApiOptions.cs index cc64cc55..cdd410fb 100644 --- a/src/Saunter/AsyncApiOptions.cs +++ b/src/Saunter/AsyncApiOptions.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; - +using System.Collections.Generic; + using Newtonsoft.Json; using Newtonsoft.Json.Serialization; - + using NJsonSchema; using NJsonSchema.NewtonsoftJson.Generation; - + using Saunter.AsyncApiSchema.v2; using Saunter.Generation.Filters; using Saunter.Generation.SchemaGeneration; @@ -90,10 +90,10 @@ public void AddOperationFilter() where T : IOperationFilter public class AsyncApiSchemaOptions : NewtonsoftJsonSchemaGeneratorSettings { - public AsyncApiSchemaOptions() + public AsyncApiSchemaOptions() { SchemaType = SchemaType.JsonSchema; // AsyncAPI uses json-schema, see https://github.com/tehmantra/saunter/pull/103#issuecomment-893267360 - TypeNameGenerator = new CamelCaseTypeNameGenerator(); + TypeNameGenerator = new CamelCaseTypeNameGenerator(); SerializerSettings = new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver(), @@ -120,4 +120,4 @@ public class AsyncApiMiddlewareOptions /// public string UiTitle { get; set; } = "AsyncAPI"; } -} +} diff --git a/src/Saunter/AsyncApiServiceCollectionExtensions.cs b/src/Saunter/AsyncApiServiceCollectionExtensions.cs index 288001cb..cf2ac39c 100644 --- a/src/Saunter/AsyncApiServiceCollectionExtensions.cs +++ b/src/Saunter/AsyncApiServiceCollectionExtensions.cs @@ -1,8 +1,8 @@ -using System; - +using System; + using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - +using Microsoft.Extensions.DependencyInjection.Extensions; + using Saunter.AsyncApiSchema.v2; using Saunter.Generation; using Saunter.Serialization; @@ -57,4 +57,4 @@ public static IServiceCollection ConfigureNamedAsyncApi(this IServiceCollection return services; } } -} +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs index 97258c87..47746fc8 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs @@ -1,8 +1,4 @@ -// 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.Diagnostics; +using System.Diagnostics; using Shouldly; using Xunit.Abstractions; diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs index 79d9844d..2bf87baf 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs @@ -1,8 +1,4 @@ -// 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.Diagnostics; +using System.Diagnostics; using Shouldly; using Xunit.Abstractions; diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs index a5183890..f520bd10 100644 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs +++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs @@ -1,107 +1,107 @@ -// 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; -using System.Reflection; -using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; -using Saunter.Generation; -using Shouldly; -using Xunit; -using System.Linq; - -namespace Saunter.Tests.Generation.DocumentGeneratorTests -{ - public class InterfaceAttributeTests - { - [Theory] - [InlineData(typeof(IServiceEvents))] - [InlineData(typeof(ServiceEventsFromInterface))] - [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] // Check that annotations are not inherited from the interface - public void NonAnnotatedTypesTest(Type type) - { - // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); - - // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - - // Assert - document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(0); - } - - [Theory] - [InlineData(typeof(IAnnotatedServiceEvents), "interface")] - [InlineData(typeof(AnnotatedServiceEventsFromInterface), "class")] - [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class")] // Check that the actual type's annotation takes precedence of the inherited interface - public void AnnotatedTypesTest(Type type, string source) - { - // Arrange - var options = new AsyncApiOptions(); - var documentGenerator = new DocumentGenerator(); - - // Act - var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); - - // Assert - document.ShouldNotBeNull(); - document.Channels.Count.ShouldBe(1); - - var channel = document.Channels.First(); - channel.Key.ShouldBe($"{source}.event"); - channel.Value.Description.ShouldBeNull(); - - var publish = channel.Value.Publish; - publish.ShouldNotBeNull(); - publish.OperationId.ShouldBe("PublishEvent"); - publish.Description.ShouldBe($"({source}) Subscribe to domains events about a tenant."); - - var messageRef = publish.Message.ShouldBeOfType(); - messageRef.Id.ShouldBe("tenantEvent"); - } - - [AsyncApi] - private interface IAnnotatedServiceEvents - { - [Channel("interface.event")] - [PublishOperation(typeof(TenantEvent), Description = "(interface) Subscribe to domains events about a tenant.")] - void PublishEvent(TenantEvent evt); - } - - private interface IServiceEvents - { - void PublishEvent(TenantEvent evt); - } - - private class ServiceEventsFromInterface : IServiceEvents - { - public void PublishEvent(TenantEvent evt) { } - } - - private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents - { - public void PublishEvent(TenantEvent evt) { } - } - - [AsyncApi] - private class AnnotatedServiceEventsFromInterface : IAnnotatedServiceEvents - { - [Channel("class.event")] - [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] - public void PublishEvent(TenantEvent evt) { } - } - - [AsyncApi] - private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents - { - [Channel("class.event")] - [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] - public void PublishEvent(TenantEvent evt) { } - } - - private class TenantEvent { } - } -} +// 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; +using System.Reflection; +using Saunter.AsyncApiSchema.v2; +using Saunter.Attributes; +using Saunter.Generation; +using Shouldly; +using Xunit; +using System.Linq; + +namespace Saunter.Tests.Generation.DocumentGeneratorTests +{ + public class InterfaceAttributeTests + { + [Theory] + [InlineData(typeof(IServiceEvents))] + [InlineData(typeof(ServiceEventsFromInterface))] + [InlineData(typeof(ServiceEventsFromAnnotatedInterface))] // Check that annotations are not inherited from the interface + public void NonAnnotatedTypesTest(Type type) + { + // Arrange + var options = new AsyncApiOptions(); + var documentGenerator = new DocumentGenerator(); + + // Act + var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + + // Assert + document.ShouldNotBeNull(); + document.Channels.Count.ShouldBe(0); + } + + [Theory] + [InlineData(typeof(IAnnotatedServiceEvents), "interface")] + [InlineData(typeof(AnnotatedServiceEventsFromInterface), "class")] + [InlineData(typeof(AnnotatedServiceEventsFromAnnotatedInterface), "class")] // Check that the actual type's annotation takes precedence of the inherited interface + public void AnnotatedTypesTest(Type type, string source) + { + // Arrange + var options = new AsyncApiOptions(); + var documentGenerator = new DocumentGenerator(); + + // Act + var document = documentGenerator.GenerateDocument(new[] { type.GetTypeInfo() }, options, options.AsyncApi, ActivatorServiceProvider.Instance); + + // Assert + document.ShouldNotBeNull(); + document.Channels.Count.ShouldBe(1); + + var channel = document.Channels.First(); + channel.Key.ShouldBe($"{source}.event"); + channel.Value.Description.ShouldBeNull(); + + var publish = channel.Value.Publish; + publish.ShouldNotBeNull(); + publish.OperationId.ShouldBe("PublishEvent"); + publish.Description.ShouldBe($"({source}) Subscribe to domains events about a tenant."); + + var messageRef = publish.Message.ShouldBeOfType(); + messageRef.Id.ShouldBe("tenantEvent"); + } + + [AsyncApi] + private interface IAnnotatedServiceEvents + { + [Channel("interface.event")] + [PublishOperation(typeof(TenantEvent), Description = "(interface) Subscribe to domains events about a tenant.")] + void PublishEvent(TenantEvent evt); + } + + private interface IServiceEvents + { + void PublishEvent(TenantEvent evt); + } + + private class ServiceEventsFromInterface : IServiceEvents + { + public void PublishEvent(TenantEvent evt) { } + } + + private class ServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents + { + public void PublishEvent(TenantEvent evt) { } + } + + [AsyncApi] + private class AnnotatedServiceEventsFromInterface : IAnnotatedServiceEvents + { + [Channel("class.event")] + [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] + public void PublishEvent(TenantEvent evt) { } + } + + [AsyncApi] + private class AnnotatedServiceEventsFromAnnotatedInterface : IAnnotatedServiceEvents + { + [Channel("class.event")] + [PublishOperation(typeof(TenantEvent), Description = "(class) Subscribe to domains events about a tenant.")] + public void PublishEvent(TenantEvent evt) { } + } + + private class TenantEvent { } + } +} diff --git a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs b/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs index c3692640..c7806ae5 100644 --- a/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs +++ b/test/Saunter.Tests/Generation/DocumentProviderTests/AsyncApiTypesTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Saunter.AsyncApiSchema.v2; -using Saunter.Attributes; using Saunter.Tests.MarkerTypeTests; using Shouldly; using Xunit; diff --git a/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs b/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs index 41d11a8c..b836fa00 100644 --- a/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs +++ b/test/Saunter.Tests/Generation/SchemaGeneration/SchemaGenerationTests.cs @@ -1,23 +1,23 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Runtime.Serialization; - +using System.Runtime.Serialization; + using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - +using Newtonsoft.Json.Serialization; + using NJsonSchema; using NJsonSchema.Generation; -using NJsonSchema.NewtonsoftJson.Converters; - +using NJsonSchema.NewtonsoftJson.Converters; + using Saunter.AsyncApiSchema.v2; using Saunter.Generation.SchemaGeneration; -using Saunter.Tests.Utils; - -using Shouldly; - -using Xunit; - +using Saunter.Tests.Utils; + +using Shouldly; + +using Xunit; + using JsonInheritanceAttribute = NJsonSchema.NewtonsoftJson.Converters.JsonInheritanceAttribute; namespace Saunter.Tests.Generation.SchemaGeneration @@ -97,8 +97,8 @@ public void GenerateSchema_GenerateSchemaFromClassWithDiscriminator_GeneratesSch var schema = _schemaGenerator.Generate(type, _schemaResolver); - schema.ShouldNotBeNull(); - + schema.ShouldNotBeNull(); + _schemaResolver.Schemas.ShouldNotBeNull(); var petSchema = _schemaResolver.Schemas.FirstOrDefault(s => s.Id == "pet"); petSchema.Discriminator.ShouldBe("petType"); @@ -242,4 +242,4 @@ public class Dog : Pet { public string PackSize { get; set; } } -} +} From 8e7a01fd65d7566b6eacdc54b7daaaf09ec59927 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Wed, 10 Jul 2024 00:13:28 +0200 Subject: [PATCH 17/34] #196 Tool rewrite to make its components testable, removed Swachbuckle copy/paste code --- src/AsyncAPI.Saunter.Generator.Cli/Args.cs | 10 - .../AsyncAPI.Saunter.Generator.Cli.csproj | 14 +- .../Commands/Tofile.cs | 42 --- .../Commands/TofileInternal.cs | 195 ----------- .../Internal/DependencyResolver.cs | 24 -- src/AsyncAPI.Saunter.Generator.Cli/Program.cs | 50 +-- .../SwashbuckleImport/CommandRunner.cs | 145 -------- .../SwashbuckleImport/HostFactoryResolver.cs | 322 ------------------ .../SwashbuckleImport/HostingApplication.cs | 118 ------- .../SwashbuckleImport/readme.md | 3 - .../ToFile/AsyncApiDocumentExtractor.cs | 67 ++++ .../ToFile/Environment.cs | 28 ++ .../ToFile/FileWriter.cs | 34 ++ .../ToFile/ServiceExtensions.cs | 15 + .../ToFile/ServiceProviderBuilder.cs | 20 ++ .../ToFile/ToFileCommand.cs | 77 +++++ .../AsyncApiSchema/v2/AsyncApiDocument.cs | 14 +- .../DotnetCliToolTests.cs | 32 +- .../InterfaceAttributeTests.cs | 8 +- 19 files changed, 290 insertions(+), 928 deletions(-) delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/Args.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs delete mode 100644 src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs b/src/AsyncAPI.Saunter.Generator.Cli/Args.cs deleted file mode 100644 index 1100a58b..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/Args.cs +++ /dev/null @@ -1,10 +0,0 @@ -// ReSharper disable once CheckNamespace -public static partial class Program -{ - internal const string StartupAssemblyArgument = "startupassembly"; - 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 0ece5488..e510838e 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net8.0 + net8.0 enable 12 AsyncAPI.Saunter.Generator.Cli @@ -11,6 +11,7 @@ AsyncAPI Initiative true AsyncAPI.Saunter.Generator.Cli + DotnetTool dotnet-asyncapi asyncapi;aspnetcore;openapi;documentation;amqp;generator;cli;tool readme.md @@ -34,10 +35,13 @@ - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -45,7 +49,7 @@ - + diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs deleted file mode 100644 index 1c90a9b9..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using static Program; - -namespace AsyncApi.Saunter.Generator.Cli.Commands; - -internal class Tofile -{ - internal static Func, int> Run(string[] args) => namedArgs => - { - if (!File.Exists(namedArgs[StartupAssemblyArgument])) - { - throw new FileNotFoundException(namedArgs[StartupAssemblyArgument]); - } - - var depsFile = namedArgs[StartupAssemblyArgument].Replace(".dll", ".deps.json"); - var runtimeConfig = namedArgs[StartupAssemblyArgument].Replace(".dll", ".runtimeconfig.json"); - var commandName = args[0]; - - var subProcessArguments = new string[args.Length - 1]; - if (subProcessArguments.Length > 0) - { - Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length); - } - - var assembly = typeof(Program).GetTypeInfo().Assembly; - var subProcessCommandLine = - $"exec --depsfile {EscapePath(depsFile)} " + - $"--runtimeconfig {EscapePath(runtimeConfig)} " + - $"{EscapePath(assembly.Location)} " + - $"_{commandName} {string.Join(" ", subProcessArguments.Select(EscapePath))}"; - - var subProcess = Process.Start("dotnet", subProcessCommandLine); - subProcess.WaitForExit(); - return subProcess.ExitCode; - }; - - private static string EscapePath(string path) - { - return (path.Contains(' ') || string.IsNullOrWhiteSpace(path)) ? "\"" + path + "\"" : path; - } -} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs b/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs deleted file mode 100644 index b833d741..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System.Reflection; -using System.Runtime.Loader; -using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; -using LEGO.AsyncAPI; -using LEGO.AsyncAPI.Models; -using LEGO.AsyncAPI.Readers; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using Saunter; -using Saunter.Serialization; -using static Program; -using AsyncApiDocument = Saunter.AsyncApiSchema.v2.AsyncApiDocument; - -namespace AsyncApi.Saunter.Generator.Cli.Commands; - -internal class TofileInternal -{ - private const string defaultDocumentName = null; - - internal static int Run(IDictionary namedArgs) - { - // 1) Configure host with provided startupassembly - 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) && !string.IsNullOrWhiteSpace(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 is 1 or 2) - { - Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); - } - else - { - throw new ArgumentOutOfRangeException(EnvOption, namedArgs[EnvOption], "Environment variable should be in the format: env1=value1,env2=value2,env3"); - } - } - var serviceProvider = GetServiceProvider(startupAssembly); - - // 3) Retrieve AsyncAPI via configured provider - var documentProvider = serviceProvider.GetService(); - var asyncapiOptions = serviceProvider.GetService>().Value; - var documentSerializer = serviceProvider.GetRequiredService(); - - var documentNames = (namedArgs.TryGetValue(DocOption, out var doc) && !string.IsNullOrWhiteSpace(doc)) ? [doc] : asyncapiOptions.NamedApis.Keys; - var fileTemplate = (namedArgs.TryGetValue(FileNameOption, out var template) && !string.IsNullOrWhiteSpace(template)) ? template : "{document}_asyncapi.{extension}"; - if (documentNames.Count == 0) - { - if (asyncapiOptions.AssemblyMarkerTypes.Any()) - { - documentNames = [defaultDocumentName]; - } - else - { - throw new ArgumentOutOfRangeException(DocOption, $"No AsyncAPI documents found: {DocOption} = '{doc}'. Known document(s): {string.Join(", ", asyncapiOptions.NamedApis.Keys)}."); - } - } - - foreach (var documentName in documentNames) - { - AsyncApiDocument prototype; - if (documentName == defaultDocumentName) - { - prototype = asyncapiOptions.AsyncApi; - } - else if (!asyncapiOptions.NamedApis.TryGetValue(documentName, out prototype)) - { - throw new ArgumentOutOfRangeException(DocOption, documentName, $"Requested AsyncAPI document not found: '{documentName}'. Known document(s): {string.Join(", ", asyncapiOptions.NamedApis.Keys)}."); - } - - var schema = documentProvider.GetDocument(asyncapiOptions, prototype); - var asyncApiSchemaJson = documentSerializer.Serialize(schema); - var asyncApiDocument = new AsyncApiStringReader().Read(asyncApiSchemaJson, out var diagnostic); - if (diagnostic.Errors.Any()) - { - Console.Error.WriteLine($"AsyncAPI Schema '{documentName ?? "default"}' is not valid ({diagnostic.Errors.Count} Error(s), {diagnostic.Warnings.Count} Warning(s)):" + - $"{Environment.NewLine}{string.Join(Environment.NewLine, diagnostic.Errors.Select(x => $"- {x}"))}"); - } - - // 4) Serialize to specified output location or stdout - var outputPath = (namedArgs.TryGetValue(OutputOption, out var path) && !string.IsNullOrWhiteSpace(path)) ? Path.Combine(Directory.GetCurrentDirectory(), path) : null; - if (!string.IsNullOrEmpty(outputPath)) - { - Directory.CreateDirectory(outputPath); - } - - var exportJson = true; - var exportYml = false; - var exportYaml = false; - if (namedArgs.TryGetValue(FormatOption, out var format) && !string.IsNullOrWhiteSpace(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, fileTemplate, documentName, "json"), stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0)); - } - - if (exportYml) - { - WriteFile(AddFileExtension(outputPath, fileTemplate, documentName, "yml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); - } - - if (exportYaml) - { - WriteFile(AddFileExtension(outputPath, fileTemplate, documentName, "yaml"), stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); - } - } - - return 0; - } - - private static void WriteFile(string outputPath, Action writeAction) - { - using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); - writeAction(stream); - - if (outputPath != null) - { - Console.WriteLine($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); - } - } - - private static string AddFileExtension(string outputPath, string fileTemplate, string documentName, string extension) - { - if (outputPath == null) - { - return outputPath; - } - - return Path.Combine(outputPath, fileTemplate.Replace("{document}", documentName == defaultDocumentName ? "" : documentName).Replace("{extension}", extension).TrimStart('_')); - } - - private static IServiceProvider GetServiceProvider(Assembly startupAssembly) - { - if (TryGetCustomHost(startupAssembly, "AsyncAPIHostFactory", "CreateHost", out IHost host)) - { - return host.Services; - } - - if (TryGetCustomHost(startupAssembly, "AsyncAPIWebHostFactory", "CreateWebHost", out IWebHost webHost)) - { - return webHost.Services; - } - - try - { - return WebHost.CreateDefaultBuilder().UseStartup(startupAssembly.GetName().Name).Build().Services; - } - catch - { - var serviceProvider = HostingApplication.GetServiceProvider(startupAssembly); - - if (serviceProvider != null) - { - return serviceProvider; - } - - throw; - } - } - - private static bool TryGetCustomHost(Assembly startupAssembly, string factoryClassName, string factoryMethodName, out THost host) - { - // Scan the assembly for any types that match the provided naming convention - var factoryTypes = startupAssembly.DefinedTypes.Where(t => t.Name == factoryClassName).ToList(); - - if (factoryTypes.Count == 0) - { - host = default; - return false; - } - else if (factoryTypes.Count > 1) - { - throw new InvalidOperationException($"Multiple {factoryClassName} classes detected"); - } - - var factoryMethod = factoryTypes.Single().GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static); - - if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost)) - { - throw new InvalidOperationException($"{factoryClassName} class detected but does not contain a public static method called {factoryMethodName} with return type {typeof(THost).Name}"); - } - - host = (THost)factoryMethod.Invoke(null, null); - return true; - } -} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs deleted file mode 100644 index d136cf1f..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -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 b9facd2b..3502ea7d 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs @@ -1,39 +1,17 @@ -using AsyncApi.Saunter.Generator.Cli.Commands; -using AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; -using AsyncAPI.Saunter.Generator.Cli.Internal; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using ConsoleAppFramework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; -DependencyResolver.Init(); +var services = new ServiceCollection(); +services.AddLogging(builder => builder.AddSimpleConsole(x => x.SingleLine = true).SetMinimumLevel(LogLevel.Trace)); +services.AddToFileCommand(); -// Helper to simplify command line parsing etc. -var runner = new CommandRunner("dotnet asyncapi", "AsyncAPI Command Line Tools", Console.Out); +using var serviceProvider = services.BuildServiceProvider(); +var logger = serviceProvider.GetRequiredService>(); +ConsoleApp.LogError = msg => logger.LogError(msg); +ConsoleApp.ServiceProvider = serviceProvider; -// 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 ... -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.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") -runner.SubCommand("_tofile", "", c => -{ - c.Argument(StartupAssemblyArgument, ""); - c.Option(DocOption, ""); - c.Option(OutputOption, ""); - c.Option(FileNameOption, ""); - c.Option(FormatOption, ""); - c.Option(EnvOption, ""); - c.OnRun(TofileInternal.Run); -}); - -return runner.Run(args); +var app = ConsoleApp.Create(); +app.Add(); +app.Run(args); diff --git a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs deleted file mode 100644 index c3c8eca0..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/CommandRunner.cs +++ /dev/null @@ -1,145 +0,0 @@ -namespace AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; - -internal class CommandRunner -{ - private readonly Dictionary _argumentDescriptors; - private readonly Dictionary _optionDescriptors; - private Func, int> _runFunc; - private readonly List _subRunners; - private readonly TextWriter _output; - - public CommandRunner(string commandName, string commandDescription, TextWriter output) - { - CommandName = commandName; - CommandDescription = commandDescription; - _argumentDescriptors = []; - _optionDescriptors = []; - _runFunc = (_) => 1; // no-op - _subRunners = []; - _output = output; - } - - public string CommandName { get; private set; } - - public string CommandDescription { get; private set; } - - public void Argument(string name, string description) - { - _argumentDescriptors.Add(name, description); - } - - public void Option(string name, string description, bool isFlag = false) - { - if (!name.StartsWith("--")) throw new ArgumentException("name of option must begin with --"); - _optionDescriptors.Add(name, new OptionDescriptor { Description = description, IsFlag = isFlag }); - } - - public void OnRun(Func, int> runFunc) - { - _runFunc = runFunc; - } - - public void SubCommand(string name, string description, Action configAction) - { - var runner = new CommandRunner($"{CommandName} {name}", description, _output); - configAction(runner); - _subRunners.Add(runner); - } - - public int Run(IEnumerable args) - { - if (args.Any()) - { - var subRunner = _subRunners.FirstOrDefault(r => r.CommandName.Split(' ').Last() == args.First()); - if (subRunner != null) return subRunner.Run(args.Skip(1)); - } - - if (_subRunners.Any() || !TryParseArgs(args, out IDictionary namedArgs)) - { - PrintUsage(); - return 1; - } - - return _runFunc(namedArgs); - } - - private bool TryParseArgs(IEnumerable args, out IDictionary namedArgs) - { - namedArgs = new Dictionary(); - var argsQueue = new Queue(args); - - // Process options first - while (argsQueue.Any() && argsQueue.Peek().StartsWith("--")) - { - // Ensure it's a known option - var name = argsQueue.Dequeue(); - if (!_optionDescriptors.TryGetValue(name, out OptionDescriptor optionDescriptor)) - return false; - - // If it's not a flag, ensure it's followed by a corresponding value - if (!optionDescriptor.IsFlag && (!argsQueue.Any() || argsQueue.Peek().StartsWith("--"))) - return false; - - namedArgs.Add(name, (!optionDescriptor.IsFlag ? argsQueue.Dequeue() : null)); - } - - // Process required args - ensure corresponding values are provided - foreach (var name in _argumentDescriptors.Keys) - { - if (!argsQueue.Any() || argsQueue.Peek().StartsWith("--")) return false; - namedArgs.Add(name, argsQueue.Dequeue()); - } - - return argsQueue.Count() == 0; - } - - private void PrintUsage() - { - if (_subRunners.Any()) - { - // List sub commands - _output.WriteLine(CommandDescription); - _output.WriteLine("Commands:"); - foreach (var runner in _subRunners) - { - var shortName = runner.CommandName.Split(' ').Last(); - if (shortName.StartsWith("_")) continue; // convention to hide commands - _output.WriteLine($" {shortName}: {runner.CommandDescription}"); - } - _output.WriteLine(); - } - else - { - // Usage for this command - var optionsPart = _optionDescriptors.Any() ? "[options] " : ""; - var argParts = _argumentDescriptors.Keys.Select(name => $"[{name}]"); - _output.WriteLine($"Usage: {CommandName} {optionsPart}{string.Join(" ", argParts)}"); - _output.WriteLine(); - - // Arguments - foreach (var entry in _argumentDescriptors) - { - _output.WriteLine($"{entry.Key}:"); - _output.WriteLine($" {entry.Value}"); - _output.WriteLine(); - } - - // Options - if (_optionDescriptors.Any()) - { - _output.WriteLine("options:"); - foreach (var entry in _optionDescriptors) - { - _output.WriteLine($" {entry.Key}: {entry.Value.Description}"); - } - _output.WriteLine(); - } - } - } - - private struct OptionDescriptor - { - public string Description; - public bool IsFlag; - } -} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs deleted file mode 100644 index 266e47f4..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostFactoryResolver.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System.Diagnostics; -using System.Reflection; - -namespace Microsoft.Extensions.Hosting; - -internal sealed class HostFactoryResolver -{ - private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; - - public const string BuildWebHost = nameof(BuildWebHost); - public const string CreateWebHostBuilder = nameof(CreateWebHostBuilder); - public const string CreateHostBuilder = nameof(CreateHostBuilder); - - // The amount of time we wait for the diagnostic source events to fire - private static readonly TimeSpan s_defaultWaitTimeout = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(30); - - public static Func ResolveWebHostFactory(Assembly assembly) - { - return ResolveFactory(assembly, BuildWebHost); - } - - public static Func ResolveWebHostBuilderFactory(Assembly assembly) - { - return ResolveFactory(assembly, CreateWebHostBuilder); - } - - public static Func ResolveHostBuilderFactory(Assembly assembly) - { - return ResolveFactory(assembly, CreateHostBuilder); - } - - // This helpers encapsulates all of the complex logic required to: - // 1. Execute the entry point of the specified assembly in a different thread. - // 2. Wait for the diagnostic source events to fire - // 3. Give the caller a chance to execute logic to mutate the IHostBuilder - // 4. Resolve the instance of the applications's IHost - // 5. Allow the caller to determine if the entry point has completed - public static Func ResolveHostFactory(Assembly assembly, - TimeSpan? waitTimeout = null, - bool stopApplication = true, - Action configureHostBuilder = null, - Action entrypointCompleted = null) - { - if (assembly.EntryPoint is null) - { - return null; - } - - try - { - // Attempt to load hosting and check the version to make sure the events - // even have a chance of firing (they were added in .NET >= 6) - var hostingAssembly = Assembly.Load("Microsoft.Extensions.Hosting"); - if (hostingAssembly.GetName().Version is Version version && version.Major < 6) - { - return null; - } - - // We're using a version >= 6 so the events can fire. If they don't fire - // then it's because the application isn't using the hosting APIs - } - catch - { - // There was an error loading the extensions assembly, return null. - return null; - } - - return args => new HostingListener(args, assembly.EntryPoint, waitTimeout ?? s_defaultWaitTimeout, stopApplication, configureHostBuilder, entrypointCompleted).CreateHost(); - } - - private static Func ResolveFactory(Assembly assembly, string name) - { - var programType = assembly?.EntryPoint?.DeclaringType; - if (programType == null) - { - return null; - } - - var factory = programType.GetMethod(name, DeclaredOnlyLookup); - if (!IsFactory(factory)) - { - return null; - } - - return args => (T)factory.Invoke(null, [args]); - } - - // TReturn Factory(string[] args); - private static bool IsFactory(MethodInfo factory) - { - return factory != null - && typeof(TReturn).IsAssignableFrom(factory.ReturnType) - && factory.GetParameters().Length == 1 - && typeof(string[]).Equals(factory.GetParameters()[0].ParameterType); - } - - // Used by EF tooling without any Hosting references. Looses some return type safety checks. - public static Func ResolveServiceProviderFactory(Assembly assembly, TimeSpan? waitTimeout = null) - { - // Prefer the older patterns by default for back compat. - var webHostFactory = ResolveWebHostFactory(assembly); - if (webHostFactory != null) - { - return args => - { - var webHost = webHostFactory(args); - return GetServiceProvider(webHost); - }; - } - - var webHostBuilderFactory = ResolveWebHostBuilderFactory(assembly); - if (webHostBuilderFactory != null) - { - return args => - { - var webHostBuilder = webHostBuilderFactory(args); - var webHost = Build(webHostBuilder); - return GetServiceProvider(webHost); - }; - } - - var hostBuilderFactory = ResolveHostBuilderFactory(assembly); - if (hostBuilderFactory != null) - { - return args => - { - var hostBuilder = hostBuilderFactory(args); - var host = Build(hostBuilder); - return GetServiceProvider(host); - }; - } - - var hostFactory = ResolveHostFactory(assembly, waitTimeout: waitTimeout); - if (hostFactory != null) - { - return args => - { - var host = hostFactory(args); - return GetServiceProvider(host); - }; - } - - return null; - } - - private static object Build(object builder) - { - var buildMethod = builder.GetType().GetMethod("Build"); - return buildMethod?.Invoke(builder, []); - } - - private static IServiceProvider GetServiceProvider(object host) - { - if (host == null) - { - return null; - } - var hostType = host.GetType(); - var servicesProperty = hostType.GetProperty("Services", DeclaredOnlyLookup); - return (IServiceProvider)servicesProperty?.GetValue(host); - } - - private sealed class HostingListener : IObserver, IObserver> - { - private readonly string[] _args; - private readonly MethodInfo _entryPoint; - private readonly TimeSpan _waitTimeout; - private readonly bool _stopApplication; - - private readonly TaskCompletionSource _hostTcs = new(); - private IDisposable _disposable; - private readonly Action _configure; - private readonly Action _entrypointCompleted; - private static readonly AsyncLocal _currentListener = new(); - - public HostingListener( - string[] args, - MethodInfo entryPoint, - TimeSpan waitTimeout, - bool stopApplication, - Action configure, - Action entrypointCompleted) - { - _args = args; - _entryPoint = entryPoint; - _waitTimeout = waitTimeout; - _stopApplication = stopApplication; - _configure = configure; - _entrypointCompleted = entrypointCompleted; - } - - public object CreateHost() - { - using var subscription = DiagnosticListener.AllListeners.Subscribe(this); - - // Kick off the entry point on a new thread so we don't block the current one - // in case we need to timeout the execution - var thread = new Thread(() => - { - Exception exception = null; - - try - { - // Set the async local to the instance of the HostingListener so we can filter events that - // aren't scoped to this execution of the entry point. - _currentListener.Value = this; - - var parameters = _entryPoint.GetParameters(); - if (parameters.Length == 0) - { - _entryPoint.Invoke(null, []); - } - else - { - _entryPoint.Invoke(null, [_args]); - } - - // Try to set an exception if the entry point returns gracefully, this will force - // build to throw - _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); - } - catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) - { - // The host was stopped by our own logic - } - catch (TargetInvocationException tie) - { - exception = tie.InnerException ?? tie; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(exception); - } - catch (Exception ex) - { - exception = ex; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(ex); - } - finally - { - // Signal that the entry point is completed - _entrypointCompleted?.Invoke(exception); - } - }) - { - // Make sure this doesn't hang the process - IsBackground = true - }; - - // Start the thread - thread.Start(); - - try - { - // Wait before throwing an exception - if (!_hostTcs.Task.Wait(_waitTimeout)) - { - throw new InvalidOperationException("Unable to build IHost"); - } - } - catch (AggregateException) when (_hostTcs.Task.IsCompleted) - { - // Lets this propagate out of the call to GetAwaiter().GetResult() - } - - Debug.Assert(_hostTcs.Task.IsCompleted); - - return _hostTcs.Task.GetAwaiter().GetResult(); - } - - public void OnCompleted() - { - _disposable?.Dispose(); - } - - public void OnError(Exception error) - { - } - - public void OnNext(DiagnosticListener value) - { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } - - if (value.Name == "Microsoft.Extensions.Hosting") - { - _disposable = value.Subscribe(this); - } - } - - public void OnNext(KeyValuePair value) - { - if (_currentListener.Value != this) - { - // Ignore events that aren't for this listener - return; - } - - if (value.Key == "HostBuilding") - { - _configure?.Invoke(value.Value); - } - - if (value.Key == "HostBuilt") - { - _hostTcs.TrySetResult(value.Value); - - if (_stopApplication) - { - // Stop the host from running further - throw new StopTheHostException(); - } - } - } - - private sealed class StopTheHostException : Exception; - } -} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs deleted file mode 100644 index e4635aff..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/HostingApplication.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Reflection; -using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Http.Features; -#if NETCOREAPP3_0_OR_GREATER -using Microsoft.Extensions.DependencyInjection; -#endif -using Microsoft.Extensions.Hosting; - -namespace AsyncApi.Saunter.Generator.Cli.SwashbuckleImport; - -// Represents an application that uses Microsoft.Extensions.Hosting and supports -// the various entry point flavors. The final model *does not* have an explicit CreateHost entry point and thus inverts the typical flow where the -// execute Main and we wait for events to fire in order to access the appropriate state. -// This is what allows top level statements to work, but getting the IServiceProvider is slightly more complex. -internal class HostingApplication -{ - internal static IServiceProvider GetServiceProvider(Assembly assembly) - { -#if NETCOREAPP2_1 - return null; -#else - // We're disabling the default server and the console host lifetime. This will disable: - // 1. Listening on ports - // 2. Logging to the console from the default host. - // This is essentially what the test server does in order to get access to the application's - // IServicerProvider *and* middleware pipeline. - void ConfigureHostBuilder(object hostBuilder) - { - ((IHostBuilder)hostBuilder).ConfigureServices((context, services) => - { - services.AddSingleton(); - services.AddSingleton(); - - for (var i = services.Count - 1; i >= 0; i--) - { - // exclude all implementations of IHostedService - // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure - // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. - var registration = services[i]; - if (registration.ServiceType == typeof(IHostedService) - && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) - { - services.RemoveAt(i); - } - } - }); - } - - var waitForStartTcs = new TaskCompletionSource(); - - void OnEntryPointExit(Exception exception) - { - // If the entry point exited, we'll try to complete the wait - if (exception != null) - { - waitForStartTcs.TrySetException(exception); - } - else - { - waitForStartTcs.TrySetResult(null); - } - } - - // If all of the existing techniques fail, then try to resolve the ResolveHostFactory - var factory = HostFactoryResolver.ResolveHostFactory(assembly, - stopApplication: false, - configureHostBuilder: ConfigureHostBuilder, - entrypointCompleted: OnEntryPointExit); - - // We're unable to resolve the factory. This could mean the application wasn't referencing the right - // version of hosting. - if (factory == null) - { - return null; - } - - try - { - // Get the IServiceProvider from the host - var assemblyName = assembly.GetName()?.FullName ?? string.Empty; - // We set the application name in the hosting environment to the startup assembly - // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our - // application. - var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; - - // Wait for the application to start so that we know it's fully configured. This is important because - // we need the middleware pipeline to be configured before we access the ISwaggerProvider in - // in the IServiceProvider - var applicationLifetime = services.GetRequiredService(); - - using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); - waitForStartTcs.Task.Wait(); - - return services; - } - catch (InvalidOperationException) - { - // We're unable to resolve the host, swallow the exception and return null - } - - return null; -#endif - } - - private class NoopHostLifetime : IHostLifetime - { - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - public Task WaitForStartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } - - private class NoopServer : IServer - { - public IFeatureCollection Features { get; } = new FeatureCollection(); - public void Dispose() { } - public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } -} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md deleted file mode 100644 index babea97b..00000000 --- a/src/AsyncAPI.Saunter.Generator.Cli/SwashbuckleImport/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -This code is taken from [Swashbuckle.AspNetCore.Cli](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/master/src/Swashbuckle.AspNetCore.Cli) - -Since Swashbuckle.AspNetCore.Cli is delivered as a tool, code cannot be reference through Nuget. \ No newline at end of file diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs new file mode 100644 index 00000000..9f070ddf --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs @@ -0,0 +1,67 @@ +using LEGO.AsyncAPI.Models; +using LEGO.AsyncAPI.Readers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Saunter.Serialization; +using Saunter; +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal class AsyncApiDocumentExtractor(ILogger logger) +{ + private IEnumerable GetDocumentNames(string[] requestedDocuments, AsyncApiOptions asyncApiOptions) + { + var documentNames = requestedDocuments ?? asyncApiOptions.NamedApis.Keys; + if (documentNames.Count == 0) + { + if (asyncApiOptions.AssemblyMarkerTypes.Any()) + { + documentNames = [null]; // null marks the default, unnamed, document + } + else + { + logger.LogCritical($"AsyncAPI documents found. Known named document(s): {string.Join(", ", asyncApiOptions.NamedApis.Keys)}."); + } + } + return documentNames; + } + + public IEnumerable<(string name, AsyncApiDocument document)> GetAsyncApiDocument(IServiceProvider serviceProvider, string[] requestedDocuments) + { + var documentProvider = serviceProvider.GetService(); + var asyncApiOptions = serviceProvider.GetService>().Value; + var documentSerializer = serviceProvider.GetRequiredService(); + + foreach (var documentName in GetDocumentNames(requestedDocuments, asyncApiOptions)) + { + if (documentName == null || !asyncApiOptions.NamedApis.TryGetValue(documentName, out var prototype)) + { + prototype = asyncApiOptions.AsyncApi; + } + + var schema = documentProvider.GetDocument(asyncApiOptions, prototype); + var asyncApiSchemaJson = documentSerializer.Serialize(schema); + var asyncApiDocument = new AsyncApiStringReader().Read(asyncApiSchemaJson, out var diagnostic); + if (diagnostic.Errors.Any()) + { + logger.LogError($"AsyncAPI Schema '{documentName ?? "default"}' is not valid ({diagnostic.Errors.Count} Error(s))"); + foreach (var error in diagnostic.Errors) + { + logger.LogError($"- {error}"); + } + } + if (diagnostic.Warnings.Any()) + { + logger.LogWarning($"AsyncAPI Schema '{documentName ?? "default"}' has {diagnostic.Warnings.Count} Warning(s):"); + foreach (var warning in diagnostic.Warnings) + { + logger.LogWarning($"- {warning}"); + } + } + + yield return (documentName, asyncApiDocument); + } + } +} + diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs new file mode 100644 index 00000000..040637a4 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal class EnvironmentBuilder(ILogger logger) +{ + public void SetEnvironmentVariables(string env) + { + var envVars = !string.IsNullOrWhiteSpace(env) ? env.Split(',').Select(x => x.Trim()) : Array.Empty(); + foreach (var envVar in envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList())) + { + if (envVar.Count is 1) + { + Environment.SetEnvironmentVariable(envVar[0], null, EnvironmentVariableTarget.Process); + logger.LogDebug($"Set environment flag: {envVar[0]}"); + } + if (envVar.Count is 2) + { + Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); + logger.LogDebug($"Set environment variable: {envVar[0]} = {envVar[1]}"); + } + else + { + logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2,env3"); + } + } + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs new file mode 100644 index 00000000..62e1bc43 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal class FileWriter(ILogger logger) +{ + public void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter) + { + var fullFileName = AddFileExtension(outputPath, fileTemplate, documentName, format); + this.WriteFile(fullFileName, streamWriter); + } + + private void WriteFile(string outputPath, Action writeAction) + { + using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + writeAction(stream); + + if (outputPath != null) + { + logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); + } + } + + private static string AddFileExtension(string outputPath, string fileTemplate, string documentName, string extension) + { + if (outputPath == null) + { + return outputPath; + } + + return Path.GetFullPath(Path.Combine(outputPath, fileTemplate.Replace("{document}", documentName ?? "") + .Replace("{extension}", extension).TrimStart('_'))); + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs new file mode 100644 index 00000000..041e496e --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal static class ServiceExtensions +{ + public static IServiceCollection AddToFileCommand(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs new file mode 100644 index 00000000..6a2907a5 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using Microsoft.Extensions.Logging; +using System.Runtime.Loader; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal class ServiceProviderBuilder(ILogger logger) +{ + public IServiceProvider BuildServiceProvider(string startupAssembly) + { + var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), startupAssembly)); + logger.LogInformation($"Loading startup assembly: {fullPath}"); + var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(fullPath); + var nswagCommandsAssembly = Assembly.LoadFrom("NSwag.Commands.dll"); + var nswagServiceProvider = nswagCommandsAssembly.GetType("NSwag.Commands.ServiceProviderResolver"); + var serviceProvider = (IServiceProvider)nswagServiceProvider.InvokeMember("GetServiceProvider", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, null, [assembly]); + return serviceProvider; + } +} + diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs new file mode 100644 index 00000000..5a5172b9 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs @@ -0,0 +1,77 @@ +using ConsoleAppFramework; +using LEGO.AsyncAPI; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal class ToFileCommand(ILogger logger, EnvironmentBuilder environment, ServiceProviderBuilder builder, AsyncApiDocumentExtractor docExtractor, FileWriter fileWriter) +{ + private const string DEFAULT_FILENAME = "{document}_asyncapi.{extension}"; + + /// + /// Retrieves AsyncAPI spec from a startup assembly and writes to file. + /// + /// relative path to the application's startup assembly + /// -o,relative path where the AsyncAPI will be output [defaults to stdout] + /// -d,name(s) of the AsyncAPI documents you want to retrieve, as configured in your startup class [defaults to all documents] + /// exports AsyncAPI in json and/or yml format [defaults to json] + /// defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}\"] + /// define environment variable(s) for the application. Formatted as a comma separated list of key=value pairs or just key for flags + [Command("tofile")] + public int ToFile([Argument] string startupassembly, string output = "./", string doc = null, string format = "json", string filename = DEFAULT_FILENAME, string env = "") + { + if (!File.Exists(startupassembly)) + { + throw new FileNotFoundException(startupassembly); + } + + // Prepare environment + environment.SetEnvironmentVariables(env); + + // Get service provider from startup assembly + var serviceProvider = builder.BuildServiceProvider(startupassembly); + + // Retrieve AsyncAPI via service provider + var documents = docExtractor.GetAsyncApiDocument(serviceProvider, !string.IsNullOrWhiteSpace(doc) ? doc.Split(',', StringSplitOptions.RemoveEmptyEntries) : null); + foreach (var (documentName, asyncApiDocument) in documents) + { + // Serialize to specified output location or stdout + var outputPath = !string.IsNullOrWhiteSpace(output) ? Path.Combine(Directory.GetCurrentDirectory(), output) : null; + if (!string.IsNullOrEmpty(outputPath)) + { + Directory.CreateDirectory(outputPath); + } + + var exportJson = true; + var exportYml = false; + var exportYaml = false; + if (!string.IsNullOrWhiteSpace(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)); + } + logger.LogDebug($"Format: exportJson={exportJson}, exportYml={exportYml}, exportYaml={exportYaml}."); + + var fileTemplate = !string.IsNullOrWhiteSpace(filename) ? filename : DEFAULT_FILENAME; + if (exportJson) + { + fileWriter.Write(outputPath, fileTemplate, documentName, "json", stream => asyncApiDocument.SerializeAsJson(stream, AsyncApiVersion.AsyncApi2_0)); + } + + if (exportYml) + { + fileWriter.Write(outputPath, fileTemplate, documentName, "yml", stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } + + if (exportYaml) + { + fileWriter.Write(outputPath, fileTemplate, documentName, "yaml", stream => asyncApiDocument.SerializeAsYaml(stream, AsyncApiVersion.AsyncApi2_0)); + } + } + + return 1; + } +} diff --git a/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs b/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs index ed38829a..a5604b64 100644 --- a/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs +++ b/src/Saunter/AsyncApiSchema/v2/AsyncApiDocument.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Linq; - -using Newtonsoft.Json; - +using System.Linq; + +using Newtonsoft.Json; + using NJsonSchema.NewtonsoftJson.Converters; namespace Saunter.AsyncApiSchema.v2 @@ -15,8 +15,8 @@ public class AsyncApiDocument : ICloneable /// Specifies the AsyncAPI Specification version being used. /// [JsonProperty("asyncapi", NullValueHandling = NullValueHandling.Ignore)] - public string AsyncApi { get; } = "2.4.0"; - + public string AsyncApi { get; } = "2.4.0"; + /// /// Identifier of the application the AsyncAPI document is defining. /// @@ -105,4 +105,4 @@ object ICloneable.Clone() return Clone(); } } -} +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs index 47746fc8..8d9e12d3 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs @@ -6,11 +6,11 @@ namespace AsyncAPI.Saunter.Generator.Cli.Tests; public class DotnetCliToolTests(ITestOutputHelper output) { - private string RunTool(string args, int expectedExitCode = 0) + private string RunTool(string args, int expectedExitCode = 1) { var process = Process.Start(new ProcessStartInfo("dotnet") { - Arguments = $"../../../../../src/AsyncAPI.Saunter.Generator.Cli/bin/Debug/net6.0/AsyncAPI.Saunter.Generator.Cli.dll tofile {args}", + Arguments = $"../../../../../src/AsyncAPI.Saunter.Generator.Cli/bin/Debug/net8.0/AsyncAPI.Saunter.Generator.Cli.dll tofile {args}", RedirectStandardOutput = true, RedirectStandardError = true, }); @@ -28,20 +28,22 @@ private string RunTool(string args, int expectedExitCode = 0) [Fact] public void DefaultCallPrintsCommandInfo() { - var stdOut = RunTool("", 1); + var stdOut = RunTool("", 0).Trim(); stdOut.ShouldBe(""" - Usage: dotnet asyncapi tofile [options] [startupassembly] - - startupassembly: - relative path to the application's startup assembly - - options: - --doc: name(s) of the AsyncAPI documents you want to retrieve, as configured in your startup class [defaults to all documents] - --output: relative path where the AsyncAPI will be output [defaults to stdout] - --filename: defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}"] - --format: exports AsyncAPI in json and/or yml format [defaults to json] - --env: 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] + Usage: tofile [arguments...] [options...] [-h|--help] [--version] + + Retrieves AsyncAPI spec from a startup assembly and writes to file. + + Arguments: + [0] relative path to the application's startup assembly + + Options: + -o|--output relative path where the AsyncAPI will be output [defaults to stdout] (Default: "./") + -d|--doc name(s) of the AsyncAPI documents you want to retrieve as configured in your startup class [defaults to all documents] (Default: null) + --format exports AsyncAPI in json and/or yml format [defaults to json] (Default: "json") + --filename defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}\"] (Default: "{document}_asyncapi.{extension}") + --env define environment variable(s) for the application. Formatted as a comma separated list of key=value pairs or just key for flags (Default: "") """, StringCompareShould.IgnoreLineEndings); } @@ -50,7 +52,7 @@ public void StreetlightsAPIExportSpecTest() { var path = Directory.GetCurrentDirectory(); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"--output {path} --format json,yml,yaml ../../../../../examples/StreetlightsAPI/bin/Debug/net6.0/StreetlightsAPI.dll"); + var stdOut = RunTool($"../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); diff --git a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs index f520bd10..030dbfd4 100644 --- a/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs +++ b/test/Saunter.Tests/Generation/DocumentGeneratorTests/InterfaceAttributeTests.cs @@ -1,15 +1,11 @@ -// 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; +using System; +using System.Linq; using System.Reflection; using Saunter.AsyncApiSchema.v2; using Saunter.Attributes; using Saunter.Generation; using Shouldly; using Xunit; -using System.Linq; namespace Saunter.Tests.Generation.DocumentGeneratorTests { From 240c9ed18be84682b67a18d7778adb284a5b4324 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Wed, 10 Jul 2024 00:24:46 +0200 Subject: [PATCH 18/34] #196 formatting --- .../ToFile/AsyncApiDocumentExtractor.cs | 2 +- .../ToFile/ServiceProviderBuilder.cs | 2 +- src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs index 9f070ddf..aa9852bd 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs @@ -1,10 +1,10 @@ using LEGO.AsyncAPI.Models; using LEGO.AsyncAPI.Readers; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Saunter.Serialization; using Saunter; -using Microsoft.Extensions.Logging; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs index 6a2907a5..5c3c6a69 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -1,6 +1,6 @@ using System.Reflection; -using Microsoft.Extensions.Logging; using System.Runtime.Loader; +using Microsoft.Extensions.Logging; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs index 5a5172b9..98bebc55 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs @@ -8,7 +8,7 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; internal class ToFileCommand(ILogger logger, EnvironmentBuilder environment, ServiceProviderBuilder builder, AsyncApiDocumentExtractor docExtractor, FileWriter fileWriter) { private const string DEFAULT_FILENAME = "{document}_asyncapi.{extension}"; - + /// /// Retrieves AsyncAPI spec from a startup assembly and writes to file. /// From 715a62db60d702b361c3a51518f9bed50e377c5f Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Wed, 10 Jul 2024 18:37:13 +0200 Subject: [PATCH 19/34] #196 add unitTests for tofile classes --- Directory.Build.props | 9 ++ Saunter.sln | 3 +- .../ToFile/AsyncApiDocumentExtractor.cs | 2 +- .../ToFile/Environment.cs | 14 +- .../ToFile/FileWriter.cs | 6 +- .../ToFile/ServiceExtensions.cs | 1 + .../ToFile/ServiceProviderBuilder.cs | 1 - .../ToFile/StreamProvider.cs | 11 ++ ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 5 +- .../ToFile/AsyncApiDocumentExtractorTests.cs | 153 ++++++++++++++++++ .../ToFile/EnvironmentBuilderTests.cs | 95 +++++++++++ .../ToFile/FileWriterTests.cs | 108 +++++++++++++ .../ToFile/StreamProviderTests.cs | 37 +++++ test/Saunter.Tests/Saunter.Tests.csproj | 2 +- 14 files changed, 430 insertions(+), 17 deletions(-) create mode 100644 Directory.Build.props create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..2451c08c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Saunter.sln b/Saunter.sln index 10d433bd..85c435a4 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{E0D34C77-9 .editorconfig = .editorconfig .gitattributes = .gitattributes CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props README.md = README.md EndProjectSection EndProject @@ -45,7 +46,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "npm", "npm", "{E8FACA22-CFE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs index aa9852bd..7cb45ca8 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs @@ -3,8 +3,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Saunter.Serialization; using Saunter; +using Saunter.Serialization; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs index 040637a4..bb69a731 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs @@ -7,21 +7,17 @@ internal class EnvironmentBuilder(ILogger logger) public void SetEnvironmentVariables(string env) { var envVars = !string.IsNullOrWhiteSpace(env) ? env.Split(',').Select(x => x.Trim()) : Array.Empty(); - foreach (var envVar in envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList())) + var keyValues = envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList()); + foreach (var envVar in keyValues) { - if (envVar.Count is 1) + if (envVar.Count == 2 && !string.IsNullOrWhiteSpace(envVar[0])) { - Environment.SetEnvironmentVariable(envVar[0], null, EnvironmentVariableTarget.Process); - logger.LogDebug($"Set environment flag: {envVar[0]}"); - } - if (envVar.Count is 2) - { - Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(envVar[0], envVar[1], EnvironmentVariableTarget.Process); logger.LogDebug($"Set environment variable: {envVar[0]} = {envVar[1]}"); } else { - logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2,env3"); + logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2"); } } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs index 62e1bc43..02e67b8f 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs @@ -2,7 +2,7 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class FileWriter(ILogger logger) +internal class FileWriter(IStreamProvider streamProvider, ILogger logger) { public void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter) { @@ -12,12 +12,12 @@ public void Write(string outputPath, string fileTemplate, string documentName, s private void WriteFile(string outputPath, Action writeAction) { - using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + using var stream = streamProvider.GetStreamFor(outputPath); writeAction(stream); if (outputPath != null) { - logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); + logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath).TrimStart('.')} successfully written to {outputPath}"); } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs index 041e496e..f10b7bbe 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs @@ -9,6 +9,7 @@ public static IServiceCollection AddToFileCommand(this IServiceCollection servic services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); return services; } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs index 5c3c6a69..c5001d5e 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -17,4 +17,3 @@ public IServiceProvider BuildServiceProvider(string startupAssembly) return serviceProvider; } } - diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs new file mode 100644 index 00000000..23e49a20 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -0,0 +1,11 @@ +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal interface IStreamProvider +{ + Stream GetStreamFor(string path); +} + +internal class StreamProvider : IStreamProvider +{ + public Stream GetStreamFor(string path) => path != null ? File.Create(path) : Console.OpenStandardOutput(); +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 355fdd59..954ce53e 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,11 +9,14 @@ + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs new file mode 100644 index 00000000..0dd165c6 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs @@ -0,0 +1,153 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.Community.Logging; +using Saunter; +using Saunter.AsyncApiSchema.v2; +using Saunter.Serialization; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class AsyncApiDocumentExtractorTests +{ + private readonly AsyncApiDocumentExtractor _extractor; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IAsyncApiDocumentProvider _documentProvider; + private readonly IOptions _asyncApiOptions; + private readonly IAsyncApiDocumentSerializer _documentSerializer; + + public AsyncApiDocumentExtractorTests(ITestOutputHelper output) + { + var services = new ServiceCollection(); + this._documentProvider = Substitute.For(); + this._asyncApiOptions = Substitute.For>(); + var options = new AsyncApiOptions(); + this._asyncApiOptions.Value.Returns(options); + this._documentSerializer = Substitute.For(); + services.AddSingleton(this._documentProvider); + services.AddSingleton(this._asyncApiOptions); + services.AddSingleton(this._documentSerializer); + this._serviceProvider = services.BuildServiceProvider(); + + this._logger = Substitute.For>(); + this._extractor = new AsyncApiDocumentExtractor(this._logger); + } + + [Fact] + public void GetAsyncApiDocument_Null_NoMarkerAssemblies() + { + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(1).CallToLog(LogLevel.Critical); + } + + [Fact] + public void GetAsyncApiDocument_Default_WithMarkerAssembly() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_1NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._asyncApiOptions.Value.NamedApis["service 1"] = doc; + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_2NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc1 = new AsyncApiDocument { Id = "1" }; + var doc2 = new AsyncApiDocument { Id = "2" }; + this._asyncApiOptions.Value.NamedApis["service 1"] = doc1; + this._asyncApiOptions.Value.NamedApis["service 2"] = doc2; + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc1)).Returns(doc1); + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc2)).Returns(doc2); + this._documentSerializer.Serialize(doc1).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 1 + """); + this._documentSerializer.Serialize(doc2).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 2 + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).OrderBy(x => x.name).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(2); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API 1"); + documents[1].name.ShouldBe("service 2"); + documents[1].document.Info.Title.ShouldBe("Streetlights API 2"); + } + + [Fact] + public void GetAsyncApiDocument_LogErrors() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + channels: + publish/light/measured: + servers: + - webapi + publish: + operationId: MeasureLight + summary: Inform about environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + $ref: '#/components/messages/lightMeasuredEvent' + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + this._logger.Received(3).CallToLog(LogLevel.Error); + this._logger.Received(0).CallToLog(LogLevel.Warning); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs new file mode 100644 index 00000000..812033a3 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs @@ -0,0 +1,95 @@ +using System.Collections; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Community.Logging; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class EnvironmentBuilderTests : IDisposable +{ + private readonly IDictionary _variablesBefore = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + private readonly EnvironmentBuilder _environment; + private readonly ILogger _logger; + + public EnvironmentBuilderTests() + { + this._logger = Substitute.For>(); + this._environment = new EnvironmentBuilder(this._logger); + } + + private Dictionary GetAddedEnvironmentVariables() + { + var after = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + return after.Cast().ExceptBy(this._variablesBefore.Keys.Cast(), x => x.Key).ToDictionary(x => x.Key.ToString(), x => x.Value?.ToString()); + } + + public void Dispose() + { + foreach (var variable in this.GetAddedEnvironmentVariables()) + { + Environment.SetEnvironmentVariable(variable.Key, null, EnvironmentVariableTarget.Process); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EmptyEnvStringProvided(string env) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.ReceivedCalls().Count().ShouldBe(0); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Theory] + [InlineData("env1=val1", 1)] + [InlineData("a=b,b=c", 2)] + public void ValidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Debug); + this.GetAddedEnvironmentVariables().ShouldNotBeEmpty(); + } + + [Theory] + [InlineData(",", 2)] + [InlineData(",,,,", 5)] + [InlineData("=a", 1)] + [InlineData("b", 1)] + [InlineData("=", 1)] + [InlineData("====", 1)] + public void InvalidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Critical); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Fact] + public void ValidateEnvValues() + { + this._environment.SetEnvironmentVariables("ENV=1,,Test=two"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe("1"); + Environment.GetEnvironmentVariable("Test").ShouldBe("two"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void EmptyValueDeletesEnvValue(string value) + { + this._environment.SetEnvironmentVariables($"ENV=1,,ENV={value}"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe(null); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs new file mode 100644 index 00000000..04ba400d --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs @@ -0,0 +1,108 @@ +using System.Linq; +using System.Text; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class FileWriterTests +{ + private readonly Action _testContextWriter = stream => stream.Write(Encoding.Default.GetBytes("ananas")); + + private readonly FileWriter _writer; + private readonly IStreamProvider _streamProvider; + private readonly ILogger _logger; + private readonly MemoryStream _stream = new(); + + public FileWriterTests(ITestOutputHelper output) + { + this._logger = Substitute.For>(); + this._streamProvider = Substitute.For(); + this._streamProvider.GetStreamFor(default).ReturnsForAnyArgs(x => + { + output.WriteLine($"GetStreamFor({x.Args()[0]})"); + return this._stream; + }); + this._writer = new FileWriter(this._streamProvider, this._logger); + } + + [Fact] + public void CheckStreamContents() + { + this._writer.Write("/", "", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/")); + Encoding.Default.GetString(this._stream.GetBuffer().Take(6).ToArray()).ShouldBe("ananas"); + } + + [Fact] + public void CheckName_NoVariablesInTemplate() + { + this._writer.Write("/some/path", "fixed_name", "doc", "json", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/fixed_name")); + } + + [Theory] + [InlineData("./")] + [InlineData("/")] + [InlineData("/test/")] + [InlineData("/test/1/2/3/4/")] + public void CheckOutputPath_BaseOutputPath_Absolute(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"{path}document.something")); + } + + [Theory] + [InlineData(".")] + [InlineData("")] + [InlineData("asyncApi/")] + [InlineData("service-1/")] + [InlineData("service 1/")] + [InlineData("service 1/spec")] + public void CheckOutputPath_BaseOutputPath_Relative(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path, "document.something"))); + } + + [Theory] + [InlineData("json")] + [InlineData("yml")] + [InlineData("txt")] + public void CheckOutputPath_FormatTemplate(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{format}_name.{format}")); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void CheckOutputPath_FormatTemplate_trimmed(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/name.")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("asyncApi")] + [InlineData("service-1")] + [InlineData("service 1")] + public void CheckOutputPath_DocumentNameTemplate(string documentName) + { + this._writer.Write("/some/path", "{document}.something", documentName, "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{documentName}.something")); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs new file mode 100644 index 00000000..a340b6d4 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs @@ -0,0 +1,37 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class StreamProviderTests +{ + private readonly IStreamProvider _streamProvider = new StreamProvider(); + + [Fact] + public void NullPathIsStdOut() + { + using var stream = this._streamProvider.GetStreamFor(null); + + stream.ShouldNotBeNull(); + Assert.False(stream is FileStream); + } + + [Fact] + public void StringPathIsFileStream() + { + var path = Path.GetFullPath("./test"); + File.Delete(path); + try + { + using var stream = this._streamProvider.GetStreamFor(path); + + stream.ShouldNotBeNull(); + Assert.True(stream is FileStream); + File.Exists(path); + } + finally + { + File.Delete(path); + } + } +} diff --git a/test/Saunter.Tests/Saunter.Tests.csproj b/test/Saunter.Tests/Saunter.Tests.csproj index 8f47b9f1..4c8fc01d 100644 --- a/test/Saunter.Tests/Saunter.Tests.csproj +++ b/test/Saunter.Tests/Saunter.Tests.csproj @@ -17,7 +17,7 @@ - + From e1818508e223ab6e97ede284e740ef5376647231 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Wed, 10 Jul 2024 18:37:13 +0200 Subject: [PATCH 20/34] #196 add unitTests for tofile --- .gitignore | 1 + Directory.Build.props | 9 + Saunter.sln | 3 +- .../ToFile/AsyncApiDocumentExtractor.cs | 9 +- .../ToFile/Environment.cs | 21 +- .../ToFile/FileWriter.cs | 11 +- .../ToFile/ServiceExtensions.cs | 9 +- .../ToFile/ServiceProviderBuilder.cs | 8 +- .../ToFile/StreamProvider.cs | 19 ++ .../ToFile/ToFileCommand.cs | 8 +- ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 12 +- .../E2ETests.cs | 59 +++++ ...netCliToolTests.cs => IntegrationTests.cs} | 33 +-- .../PackAndInstallLocalTests.cs | 52 ----- .../ToFile/AsyncApiDocumentExtractorTests.cs | 152 +++++++++++++ .../ToFile/EnvironmentBuilderTests.cs | 94 ++++++++ .../ToFile/FileWriterTests.cs | 107 +++++++++ .../ToFile/StreamProviderTests.cs | 37 ++++ .../ToFile/ToFileCommandTests.cs | 205 ++++++++++++++++++ test/Saunter.Tests/Saunter.Tests.csproj | 6 +- 20 files changed, 754 insertions(+), 101 deletions(-) create mode 100644 Directory.Build.props create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs rename test/AsyncAPI.Saunter.Generator.Cli.Tests/{DotnetCliToolTests.cs => IntegrationTests.cs} (92%) delete mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs diff --git a/.gitignore b/.gitignore index 758b4e30..dd3e6915 100644 --- a/.gitignore +++ b/.gitignore @@ -206,6 +206,7 @@ PublishScripts/ # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets +dotnet-tools.json # Microsoft Azure Build Output csx/ diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..2451c08c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Saunter.sln b/Saunter.sln index 10d433bd..85c435a4 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{E0D34C77-9 .editorconfig = .editorconfig .gitattributes = .gitattributes CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props README.md = README.md EndProjectSection EndProject @@ -45,7 +46,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "npm", "npm", "{E8FACA22-CFE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli", "src\AsyncAPI.Saunter.Generator.Cli\AsyncAPI.Saunter.Generator.Cli.csproj", "{6C102D4D-3DA4-4763-B75E-C15E33E7E94A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs index aa9852bd..6a1709fb 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/AsyncApiDocumentExtractor.cs @@ -3,12 +3,17 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Saunter.Serialization; using Saunter; +using Saunter.Serialization; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class AsyncApiDocumentExtractor(ILogger logger) +internal interface IAsyncApiDocumentExtractor +{ + IEnumerable<(string name, AsyncApiDocument document)> GetAsyncApiDocument(IServiceProvider serviceProvider, string[] requestedDocuments); +} + +internal class AsyncApiDocumentExtractor(ILogger logger) : IAsyncApiDocumentExtractor { private IEnumerable GetDocumentNames(string[] requestedDocuments, AsyncApiOptions asyncApiOptions) { diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs index 040637a4..2ae66e79 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/Environment.cs @@ -2,26 +2,27 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class EnvironmentBuilder(ILogger logger) +internal interface IEnvironmentBuilder +{ + void SetEnvironmentVariables(string env); +} + +internal class EnvironmentBuilder(ILogger logger) : IEnvironmentBuilder { public void SetEnvironmentVariables(string env) { var envVars = !string.IsNullOrWhiteSpace(env) ? env.Split(',').Select(x => x.Trim()) : Array.Empty(); - foreach (var envVar in envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList())) + var keyValues = envVars.Select(x => x.Split('=').Select(x => x.Trim()).ToList()); + foreach (var envVar in keyValues) { - if (envVar.Count is 1) - { - Environment.SetEnvironmentVariable(envVar[0], null, EnvironmentVariableTarget.Process); - logger.LogDebug($"Set environment flag: {envVar[0]}"); - } - if (envVar.Count is 2) + if (envVar.Count == 2 && !string.IsNullOrWhiteSpace(envVar[0])) { - Environment.SetEnvironmentVariable(envVar[0], envVar.ElementAtOrDefault(1), EnvironmentVariableTarget.Process); + Environment.SetEnvironmentVariable(envVar[0], envVar[1], EnvironmentVariableTarget.Process); logger.LogDebug($"Set environment variable: {envVar[0]} = {envVar[1]}"); } else { - logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2,env3"); + logger.LogCritical("Environment variables should be in the format: env1=value1,env2=value2"); } } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs index 62e1bc43..d58060ea 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/FileWriter.cs @@ -2,7 +2,12 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class FileWriter(ILogger logger) +internal interface IFileWriter +{ + void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter); +} + +internal class FileWriter(IStreamProvider streamProvider, ILogger logger) : IFileWriter { public void Write(string outputPath, string fileTemplate, string documentName, string format, Action streamWriter) { @@ -12,12 +17,12 @@ public void Write(string outputPath, string fileTemplate, string documentName, s private void WriteFile(string outputPath, Action writeAction) { - using var stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + using var stream = streamProvider.GetStreamFor(outputPath); writeAction(stream); if (outputPath != null) { - logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath)[1..]} successfully written to {outputPath}"); + logger.LogInformation($"AsyncAPI {Path.GetExtension(outputPath).TrimStart('.')} successfully written to {outputPath}"); } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs index 041e496e..33201f1e 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceExtensions.cs @@ -6,10 +6,11 @@ internal static class ServiceExtensions { public static IServiceCollection AddToFileCommand(this IServiceCollection services) { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); return services; } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs index 5c3c6a69..69bde4f2 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -4,7 +4,12 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class ServiceProviderBuilder(ILogger logger) +internal interface IServiceProviderBuilder +{ + IServiceProvider BuildServiceProvider(string startupAssembly); +} + +internal class ServiceProviderBuilder(ILogger logger) : IServiceProviderBuilder { public IServiceProvider BuildServiceProvider(string startupAssembly) { @@ -17,4 +22,3 @@ public IServiceProvider BuildServiceProvider(string startupAssembly) return serviceProvider; } } - diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs new file mode 100644 index 00000000..8af5dbd4 --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -0,0 +1,19 @@ +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal interface IStreamProvider +{ + Stream GetStreamFor(string path); +} + +internal class StreamProvider : IStreamProvider +{ + public Stream GetStreamFor(string path) + { + if (!string.IsNullOrEmpty(path)) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + + return path != null ? File.Create(path) : Console.OpenStandardOutput(); + } +} diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs index 98bebc55..0a62c43d 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs @@ -5,7 +5,7 @@ namespace AsyncAPI.Saunter.Generator.Cli.ToFile; -internal class ToFileCommand(ILogger logger, EnvironmentBuilder environment, ServiceProviderBuilder builder, AsyncApiDocumentExtractor docExtractor, FileWriter fileWriter) +internal class ToFileCommand(ILogger logger, IEnvironmentBuilder environment, IServiceProviderBuilder builder, IAsyncApiDocumentExtractor docExtractor, IFileWriter fileWriter) { private const string DEFAULT_FILENAME = "{document}_asyncapi.{extension}"; @@ -37,11 +37,7 @@ public int ToFile([Argument] string startupassembly, string output = "./", strin foreach (var (documentName, asyncApiDocument) in documents) { // Serialize to specified output location or stdout - var outputPath = !string.IsNullOrWhiteSpace(output) ? Path.Combine(Directory.GetCurrentDirectory(), output) : null; - if (!string.IsNullOrEmpty(outputPath)) - { - Directory.CreateDirectory(outputPath); - } + var outputPath = !string.IsNullOrWhiteSpace(output) ? Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), output)) : null; var exportJson = true; var exportYml = false; diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 355fdd59..6b89cdbe 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -9,11 +9,17 @@ + - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs new file mode 100644 index 00000000..0134ce62 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/E2ETests.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public class E2ETests(ITestOutputHelper output) +{ + private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) + { + var process = Process.Start(new ProcessStartInfo(file) + { + Arguments = args, + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + process.WaitForExit(TimeSpan.FromSeconds(20)); + var stdOut = process.StandardOutput.ReadToEnd().Trim(); + var stdError = process.StandardError.ReadToEnd().Trim(); + output.WriteLine($"### Output of \"{file} {args}\""); + output.WriteLine(stdOut); + output.WriteLine(stdError); + + process.ExitCode.ShouldBe(expectedExitCode); + return stdOut; + } + + [Fact(Skip = "Manual verification only")] + public void Pack_Install_Run_Uninstall_Test() + { + var workingDirectory = "../../../../../src/AsyncAPI.Saunter.Generator.Cli"; + var stdOut = this.Run("dotnet", "pack", workingDirectory); + stdOut.ShouldContain("Successfully created package"); + + // use --force flag to ensure the test starts clean every run + stdOut = this.Run("dotnet", "new tool-manifest --force", workingDirectory); + stdOut.ShouldContain("The template \"Dotnet local tool manifest file\" was created successfully"); + + stdOut = this.Run("dotnet", "tool install --local --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", workingDirectory); + stdOut = stdOut.Replace("Skipping NuGet package signature verification.", "").Trim(); + stdOut.ShouldContain("You can invoke the tool from this directory using the following commands: 'dotnet tool run dotnet-asyncapi"); + stdOut.ShouldContain("was successfully installed."); + + stdOut = this.Run("dotnet", "tool list --local asyncapi.saunter.generator.cli", workingDirectory); + stdOut.ShouldContain("dotnet-asyncapi"); + + stdOut = this.Run("dotnet", "tool run dotnet-asyncapi", workingDirectory, 1); + stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); + + stdOut = this.Run("dotnet", "tool uninstall --local asyncapi.saunter.generator.cli", workingDirectory); + stdOut.ShouldContain(" was successfully uninstalled"); + stdOut.ShouldContain("removed from manifest file"); + + stdOut = this.Run("dotnet", "tool list --local asyncapi.saunter.generator.cli", workingDirectory, 1); + stdOut.ShouldNotContain("dotnet-asyncapi"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs similarity index 92% rename from test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs rename to test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 8d9e12d3..105916d2 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/DotnetCliToolTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -1,26 +1,29 @@ -using System.Diagnostics; -using Shouldly; +using Shouldly; using Xunit.Abstractions; namespace AsyncAPI.Saunter.Generator.Cli.Tests; -public class DotnetCliToolTests(ITestOutputHelper output) +public class IntegrationTests(ITestOutputHelper output) { private string RunTool(string args, int expectedExitCode = 1) { - var process = Process.Start(new ProcessStartInfo("dotnet") - { - Arguments = $"../../../../../src/AsyncAPI.Saunter.Generator.Cli/bin/Debug/net8.0/AsyncAPI.Saunter.Generator.Cli.dll tofile {args}", - RedirectStandardOutput = true, - RedirectStandardError = true, - }); - process.WaitForExit(); - var stdOut = process.StandardOutput.ReadToEnd().Trim(); - var stdError = process.StandardError.ReadToEnd().Trim(); + using var outWriter = new StringWriter(); + using var errorWriter = new StringWriter(); + Console.SetOut(outWriter); + Console.SetError(errorWriter); + + var entryPoint = typeof(Program).Assembly.EntryPoint!; + entryPoint.Invoke(null, new object[] { args.Split(' ') }); + + var stdOut = outWriter.ToString(); + var stdError = errorWriter.ToString(); + output.WriteLine($"RUN: {args}"); + output.WriteLine("### STD OUT"); output.WriteLine(stdOut); + output.WriteLine("### STD ERROR"); output.WriteLine(stdError); - process.ExitCode.ShouldBe(expectedExitCode); + Environment.ExitCode.ShouldBe(expectedExitCode); //stdError.ShouldBeEmpty(); LEGO lib doesn't like id: "id is not a valid property at #/components/schemas/lightMeasuredEvent"" return stdOut; } @@ -28,7 +31,7 @@ private string RunTool(string args, int expectedExitCode = 1) [Fact] public void DefaultCallPrintsCommandInfo() { - var stdOut = RunTool("", 0).Trim(); + var stdOut = RunTool("tofile", 0).Trim(); stdOut.ShouldBe(""" Usage: tofile [arguments...] [options...] [-h|--help] [--version] @@ -52,7 +55,7 @@ public void StreetlightsAPIExportSpecTest() { var path = Directory.GetCurrentDirectory(); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); + var stdOut = RunTool($"tofile ../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs deleted file mode 100644 index 2bf87baf..00000000 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/PackAndInstallLocalTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics; -using Shouldly; -using Xunit.Abstractions; - -namespace AsyncAPI.Saunter.Generator.Cli.Tests; - -public class PackAndInstallLocalTests(ITestOutputHelper output) -{ - private string Run(string file, string args, string workingDirectory, int expectedExitCode = 0) - { - var process = Process.Start(new ProcessStartInfo(file) - { - Arguments = args, - WorkingDirectory = workingDirectory, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - }); - process.WaitForExit(TimeSpan.FromSeconds(20)); - var stdOut = process.StandardOutput.ReadToEnd().Trim(); - var stdError = process.StandardError.ReadToEnd().Trim(); - output.WriteLine($"### Output of \"{file} {args}\""); - output.WriteLine(stdOut); - output.WriteLine(stdError); - - process.ExitCode.ShouldBe(expectedExitCode); - return stdOut; - } - - [Fact] - public void Pack_Install_Run_Uninstall_Test() - { - var stdOut = this.Run("dotnet", "pack", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); - stdOut.ShouldContain("Successfully created package"); - - stdOut = this.Run("dotnet", "tool install --global --add-source ./bin/Release AsyncAPI.Saunter.Generator.Cli", "../../../../../src/AsyncAPI.Saunter.Generator.Cli"); - stdOut.ShouldBeOneOf("You can invoke the tool using the following command: dotnet-asyncapi\r\nTool 'asyncapi.saunter.generator.cli' (version '1.0.1') was successfully installed.", - "Tool 'asyncapi.saunter.generator.cli' was reinstalled with the stable version (version '1.0.1')."); - - stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", ""); - stdOut.ShouldContain("dotnet-asyncapi"); - - stdOut = this.Run("dotnet", "asyncapi", "", 1); - stdOut.ShouldContain("tofile: retrieves AsyncAPI from a startup assembly, and writes to file"); - - stdOut = this.Run("dotnet", "tool uninstall -g asyncapi.saunter.generator.cli", ""); - stdOut.ShouldContain(" was successfully uninstalled."); - - stdOut = this.Run("dotnet", "tool list -g asyncapi.saunter.generator.cli", "", 1); - stdOut.ShouldNotContain("dotnet-asyncapi"); - } -} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs new file mode 100644 index 00000000..bf091832 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/AsyncApiDocumentExtractorTests.cs @@ -0,0 +1,152 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using NSubstitute.Community.Logging; +using Saunter; +using Saunter.AsyncApiSchema.v2; +using Saunter.Serialization; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class AsyncApiDocumentExtractorTests +{ + private readonly AsyncApiDocumentExtractor _extractor; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IAsyncApiDocumentProvider _documentProvider; + private readonly IOptions _asyncApiOptions; + private readonly IAsyncApiDocumentSerializer _documentSerializer; + + public AsyncApiDocumentExtractorTests() + { + var services = new ServiceCollection(); + this._documentProvider = Substitute.For(); + this._asyncApiOptions = Substitute.For>(); + var options = new AsyncApiOptions(); + this._asyncApiOptions.Value.Returns(options); + this._documentSerializer = Substitute.For(); + services.AddSingleton(this._documentProvider); + services.AddSingleton(this._asyncApiOptions); + services.AddSingleton(this._documentSerializer); + this._serviceProvider = services.BuildServiceProvider(); + + this._logger = Substitute.For>(); + this._extractor = new AsyncApiDocumentExtractor(this._logger); + } + + [Fact] + public void GetAsyncApiDocument_Null_NoMarkerAssemblies() + { + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(1).CallToLog(LogLevel.Critical); + } + + [Fact] + public void GetAsyncApiDocument_Default_WithMarkerAssembly() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_1NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._asyncApiOptions.Value.NamedApis["service 1"] = doc; + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } + + [Fact] + public void GetAsyncApiDocument_2NamedDocument() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc1 = new AsyncApiDocument { Id = "1" }; + var doc2 = new AsyncApiDocument { Id = "2" }; + this._asyncApiOptions.Value.NamedApis["service 1"] = doc1; + this._asyncApiOptions.Value.NamedApis["service 2"] = doc2; + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc1)).Returns(doc1); + this._documentProvider.GetDocument(Arg.Any(), Arg.Is(doc2)).Returns(doc2); + this._documentSerializer.Serialize(doc1).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 1 + """); + this._documentSerializer.Serialize(doc2).Returns(""" + asyncapi: 2.6.0 + info: + title: Streetlights API 2 + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).OrderBy(x => x.name).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + documents.Count.ShouldBe(2); + documents[0].name.ShouldBe("service 1"); + documents[0].document.Info.Title.ShouldBe("Streetlights API 1"); + documents[1].name.ShouldBe("service 2"); + documents[1].document.Info.Title.ShouldBe("Streetlights API 2"); + } + + [Fact] + public void GetAsyncApiDocument_LogErrors() + { + this._asyncApiOptions.Value.AssemblyMarkerTypes = [typeof(AsyncApiDocumentExtractorTests)]; + var doc = new AsyncApiDocument(); + this._documentProvider.GetDocument(default, default).ReturnsForAnyArgs(doc); + this._documentSerializer.Serialize(doc).ReturnsForAnyArgs(""" + asyncapi: 2.6.0 + info: + title: Streetlights API + channels: + publish/light/measured: + servers: + - webapi + publish: + operationId: MeasureLight + summary: Inform about environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + $ref: '#/components/messages/lightMeasuredEvent' + """); + + var documents = this._extractor.GetAsyncApiDocument(this._serviceProvider, null).ToList(); + + this._logger.Received(0).CallToLog(LogLevel.Critical); + this._logger.Received(3).CallToLog(LogLevel.Error); + this._logger.Received(0).CallToLog(LogLevel.Warning); + documents.Count.ShouldBe(1); + documents[0].name.ShouldBeNull(); + documents[0].document.Info.Title.ShouldBe("Streetlights API"); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs new file mode 100644 index 00000000..de26811d --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/EnvironmentBuilderTests.cs @@ -0,0 +1,94 @@ +using System.Collections; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Community.Logging; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class EnvironmentBuilderTests : IDisposable +{ + private readonly IDictionary _variablesBefore = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + private readonly EnvironmentBuilder _environment; + private readonly ILogger _logger; + + public EnvironmentBuilderTests() + { + this._logger = Substitute.For>(); + this._environment = new EnvironmentBuilder(this._logger); + } + + private Dictionary GetAddedEnvironmentVariables() + { + var after = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); + return after.Cast().ExceptBy(this._variablesBefore.Keys.Cast(), x => x.Key).ToDictionary(x => x.Key.ToString(), x => x.Value?.ToString()); + } + + public void Dispose() + { + foreach (var variable in this.GetAddedEnvironmentVariables()) + { + Environment.SetEnvironmentVariable(variable.Key, null, EnvironmentVariableTarget.Process); + } + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void EmptyEnvStringProvided(string env) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.ReceivedCalls().Count().ShouldBe(0); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Theory] + [InlineData("env1=val1", 1)] + [InlineData("a=b,b=c", 2)] + public void ValidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Debug); + this.GetAddedEnvironmentVariables().ShouldNotBeEmpty(); + } + + [Theory] + [InlineData(",", 2)] + [InlineData(",,,,", 5)] + [InlineData("=a", 1)] + [InlineData("b", 1)] + [InlineData("=", 1)] + [InlineData("====", 1)] + public void InvalidEnvStringProvided(string env, int expectedSets) + { + this._environment.SetEnvironmentVariables(env); + + this._logger.Received(expectedSets).CallToLog(LogLevel.Critical); + this.GetAddedEnvironmentVariables().ShouldBeEmpty(); + } + + [Fact] + public void ValidateEnvValues() + { + this._environment.SetEnvironmentVariables("ENV=1,,Test=two"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe("1"); + Environment.GetEnvironmentVariable("Test").ShouldBe("two"); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void EmptyValueDeletesEnvValue(string value) + { + this._environment.SetEnvironmentVariables($"ENV=1,,ENV={value}"); + + Environment.GetEnvironmentVariable("ENV").ShouldBe(null); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs new file mode 100644 index 00000000..129959b0 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/FileWriterTests.cs @@ -0,0 +1,107 @@ +using System.Text; +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class FileWriterTests +{ + private readonly Action _testContextWriter = stream => stream.Write(Encoding.Default.GetBytes("ananas")); + + private readonly FileWriter _writer; + private readonly IStreamProvider _streamProvider; + private readonly ILogger _logger; + private readonly MemoryStream _stream = new(); + + public FileWriterTests(ITestOutputHelper output) + { + this._logger = Substitute.For>(); + this._streamProvider = Substitute.For(); + this._streamProvider.GetStreamFor(default).ReturnsForAnyArgs(x => + { + output.WriteLine($"GetStreamFor({x.Args()[0]})"); + return this._stream; + }); + this._writer = new FileWriter(this._streamProvider, this._logger); + } + + [Fact] + public void CheckStreamContents() + { + this._writer.Write("/", "", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/")); + Encoding.Default.GetString(this._stream.GetBuffer().Take(6).ToArray()).ShouldBe("ananas"); + } + + [Fact] + public void CheckName_NoVariablesInTemplate() + { + this._writer.Write("/some/path", "fixed_name", "doc", "json", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/fixed_name")); + } + + [Theory] + [InlineData("./")] + [InlineData("/")] + [InlineData("/test/")] + [InlineData("/test/1/2/3/4/")] + public void CheckOutputPath_BaseOutputPath_Absolute(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"{path}document.something")); + } + + [Theory] + [InlineData(".")] + [InlineData("")] + [InlineData("asyncApi/")] + [InlineData("service-1/")] + [InlineData("service 1/")] + [InlineData("service 1/spec")] + public void CheckOutputPath_BaseOutputPath_Relative(string path) + { + this._writer.Write(path, "document.something", "", "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), path, "document.something"))); + } + + [Theory] + [InlineData("json")] + [InlineData("yml")] + [InlineData("txt")] + public void CheckOutputPath_FormatTemplate(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{format}_name.{format}")); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void CheckOutputPath_FormatTemplate_trimmed(string format) + { + this._writer.Write("/some/path", "{extension}_name.{extension}", "doc", format, _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath("/some/path/name.")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("asyncApi")] + [InlineData("service-1")] + [InlineData("service 1")] + public void CheckOutputPath_DocumentNameTemplate(string documentName) + { + this._writer.Write("/some/path", "{document}.something", documentName, "", _testContextWriter); + + this._streamProvider.Received(1).GetStreamFor(Path.GetFullPath($"/some/path/{documentName}.something")); + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs new file mode 100644 index 00000000..a340b6d4 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs @@ -0,0 +1,37 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Shouldly; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class StreamProviderTests +{ + private readonly IStreamProvider _streamProvider = new StreamProvider(); + + [Fact] + public void NullPathIsStdOut() + { + using var stream = this._streamProvider.GetStreamFor(null); + + stream.ShouldNotBeNull(); + Assert.False(stream is FileStream); + } + + [Fact] + public void StringPathIsFileStream() + { + var path = Path.GetFullPath("./test"); + File.Delete(path); + try + { + using var stream = this._streamProvider.GetStreamFor(path); + + stream.ShouldNotBeNull(); + Assert.True(stream is FileStream); + File.Exists(path); + } + finally + { + File.Delete(path); + } + } +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs new file mode 100644 index 00000000..d879cc41 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs @@ -0,0 +1,205 @@ +using AsyncAPI.Saunter.Generator.Cli.ToFile; +using LEGO.AsyncAPI.Models; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Shouldly; +using Xunit.Abstractions; + +namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; + +public class ToFileCommandTests +{ + private readonly ToFileCommand _command; + private readonly IEnvironmentBuilder _environment; + private readonly IServiceProviderBuilder _builder; + private readonly IAsyncApiDocumentExtractor _docExtractor; + private readonly IFileWriter _fileWriter; + private readonly ILogger _logger; + private readonly ITestOutputHelper _output; + + public ToFileCommandTests(ITestOutputHelper output) + { + this._output = output; + this._logger = Substitute.For>(); + this._environment = Substitute.For(); + this._builder = Substitute.For(); + this._docExtractor = Substitute.For(); + this._fileWriter = Substitute.For(); + this._command = new ToFileCommand(this._logger, _environment, _builder, _docExtractor, _fileWriter); + } + + [Fact] + public void StartupAssembly_FileNotFoundException() + { + Assert.Throws(() => this._command.ToFile("")); + } + + [Fact] + public void SetEnvironmentVariables() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + + this._command.ToFile(me, env: "env=value"); + + this._environment.Received(1).SetEnvironmentVariables("env=value"); + } + + [Fact] + public void BuildServiceProvider() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + + this._command.ToFile(me); + + this._builder.Received(1).BuildServiceProvider(me); + } + + [Fact] + public void GetAsyncApiDocument_DefaultDocParam() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, null); + } + + [Fact] + public void GetAsyncApiDocument_DocParam() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me, doc: "a"); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, Arg.Is(x => x.SequenceEqual(new[] { "a" }))); ; + } + + [Fact] + public void GetAsyncApiDocument_DocParamMultiple() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + var sp = Substitute.For(); + this._builder.BuildServiceProvider(default).ReturnsForAnyArgs(sp); + + this._command.ToFile(me, doc: "a,b, c ,,"); + + this._docExtractor.Received(1).GetAsyncApiDocument(sp, Arg.Is(x => x.SequenceEqual(new[] { "a", "b", " c " }))); + } + + [Fact] + public void WriteFile_DefaultParams() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } } )]); + + this._command.ToFile(me); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("json")] + [InlineData("yml")] + [InlineData("yaml")] + public void WriteFile_FormatParam(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, format, Arg.Any>()); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void WriteFile_EmptyFormatParamVariants_FallbackToJson(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("a")] + [InlineData("json1")] + [InlineData(".json")] + public void WriteFile_InvalidFormatParam_FallbackToJson(string format) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: format); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(0); + } + + [Fact] + public void WriteFile_FormatParamMultiple() + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, format: " json , yaml,yml ,,a, "); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(3); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "yml", Arg.Any>()); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), "{document}_asyncapi.{extension}", null, "yaml", Arg.Any>()); + } + + [Theory] + [InlineData("doc")] + [InlineData("{document}")] + [InlineData("{extension}")] + [InlineData("{document}.{extension}")] + public void WriteFile_FileTemplateParam(string template) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, filename: template); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath("./"), template, null, "json", Arg.Any>()); + } + + [Theory] + [InlineData("./")] + [InlineData("/")] + [InlineData("a/")] + [InlineData("/a/b")] + public void WriteFile_OutputPathParam(string path) + { + var me = typeof(ToFileCommandTests).Assembly.Location; + this._output.WriteLine($"Assembly: {me}"); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); + + this._command.ToFile(me, output: path); + + this._fileWriter.ReceivedCalls().Count().ShouldBe(1); + this._fileWriter.Received(1).Write(Path.GetFullPath(path), "{document}_asyncapi.{extension}", null, "json", Arg.Any>()); + } +} diff --git a/test/Saunter.Tests/Saunter.Tests.csproj b/test/Saunter.Tests/Saunter.Tests.csproj index 8f47b9f1..98b03563 100644 --- a/test/Saunter.Tests/Saunter.Tests.csproj +++ b/test/Saunter.Tests/Saunter.Tests.csproj @@ -17,11 +17,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 183a4e8276d205a3d890c8f9a3014ca0170b990c Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Thu, 11 Jul 2024 22:53:43 +0200 Subject: [PATCH 21/34] #196 formatting --- src/AsyncAPI.Saunter.Generator.Cli/readme.md | 9 ++++++--- .../ToFile/ToFileCommandTests.cs | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index 05c42764..b5ee1273 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -3,7 +3,7 @@ A dotnet tool to generate AsyncAPI specification files based of a dotnet DLL (Th ## Tool usage ``` -dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] [startup-assembly] +dotnet asyncapi tofile [startup-assembly] --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] ``` - _startup-assembly_: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). @@ -12,7 +12,7 @@ dotnet asyncapi tofile --output [output-path] --format [json,yml,yaml] --doc [as - _--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. -- _--env_: define environment variable(s) for the application. Formatted as a comma separated list of _key=value_ pairs or just _key_ for flags, example: ```ASPNETCORE_ENVIRONMENT=AsyncAPI,CONNECT_TO_DATABASE=false,GENERATOR_FLAG```. +- _--env_: 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```. ## Install the Generator.Cli dotnet Tool ``` @@ -21,4 +21,7 @@ dotnet tool install --global AsyncAPI.Saunter.Generator.Cli After installing the tool globally, it is available using commands: ```dotnet asyncapi``` or ```dotnet-asyncapi``` Want to learn more about .NET tools? Or want to install it local using a manifest? -[Check out this Microsoft page on how to manage .NET tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) \ No newline at end of file +[Check out this Microsoft page on how to manage .NET tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) + +## Internals +How does the tool work internally? It tries to exact an ```IServiceProvider``` from the provided _startup-assembly_ and exports AsyncApiDocument(s) as registered in the services provider. \ No newline at end of file diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs index d879cc41..44e3c1f3 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/ToFileCommandTests.cs @@ -99,7 +99,7 @@ public void WriteFile_DefaultParams() { var me = typeof(ToFileCommandTests).Assembly.Location; this._output.WriteLine($"Assembly: {me}"); - this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } } )]); + this._docExtractor.GetAsyncApiDocument(default, default).ReturnsForAnyArgs([(null, new AsyncApiDocument { Info = new AsyncApiInfo { Title = "a" } })]); this._command.ToFile(me); From 8d2fd4969a34a4c15b1bb0be21592e362d5e0931 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Thu, 11 Jul 2024 23:00:38 +0200 Subject: [PATCH 22/34] #196 ALL tests should run --- .github/workflows/ci.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9ab2a66..ec47f69c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -35,7 +35,4 @@ jobs: - name: setup build uses: ./.github/npm - name: unit test - run: dotnet test ./test/Saunter.Tests/Saunter.Tests.csproj - # TODO: why there are 2 of them.... - - name: unit mark test - run: dotnet test ./test/Saunter.Tests.MarkerTypeTests/Saunter.Tests.MarkerTypeTests.csproj + run: dotnet test --configuration Debug From a9d544ac7672f8942586555f8856c9ae1eee00ea Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Thu, 11 Jul 2024 23:03:34 +0200 Subject: [PATCH 23/34] #196 Tests use streetlights DLL in integegration tests, add dependency --- .../AsyncAPI.Saunter.Generator.Cli.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 6b89cdbe..6704dac9 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -23,6 +23,7 @@ + From 72dc74205694a29a384ed6532d4041e323a9caa2 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Thu, 11 Jul 2024 23:13:02 +0200 Subject: [PATCH 24/34] #196 release also the CLI tool --- .github/workflows/release.yaml | 2 +- test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs | 2 +- .../Saunter.Tests.MarkerTypeTests.csproj | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c1c33107..452effd8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,7 +16,7 @@ jobs: # Gets the numeric version from a tag (e.g. v1.2.3 -> 1.2.3) run: echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Create Nuget package - run: dotnet pack ./src/Saunter/Saunter.csproj --configuration Release -p:Version="$RELEASE_VERSION" --output ./build + run: dotnet pack --configuration Release -p:Version="$RELEASE_VERSION" --output ./build - name: Push Nuget package to Nuget.org env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 105916d2..e7751ac3 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -55,7 +55,7 @@ public void StreetlightsAPIExportSpecTest() { var path = Directory.GetCurrentDirectory(); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"tofile ../../../../../examples/StreetlightsAPI/bin/Debug/net8.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); + var stdOut = RunTool($"tofile ../../../../../examples/StreetlightsAPI/bin/Debug/net6.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); diff --git a/test/Saunter.Tests.MarkerTypeTests/Saunter.Tests.MarkerTypeTests.csproj b/test/Saunter.Tests.MarkerTypeTests/Saunter.Tests.MarkerTypeTests.csproj index 014abc3c..bd04796d 100644 --- a/test/Saunter.Tests.MarkerTypeTests/Saunter.Tests.MarkerTypeTests.csproj +++ b/test/Saunter.Tests.MarkerTypeTests/Saunter.Tests.MarkerTypeTests.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + false From 7b22ce094be266ffa9c4b793f90948616803c824 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:11:04 +0200 Subject: [PATCH 25/34] #196 Add example project with top level statement, because that failed to generate specs, fixed now. Added an external nuget dependancy to the example project (nlog) because that made spec generation fail, fixed now. And now also testing both .NET6 & .NET8 --- Saunter.sln | 18 ++ examples/.gitignore | 4 + .../Program.cs | 76 +++++++ .../StreetlightsAPI.TopLevelStatement.csproj | 46 +++++ .../appsettings.json | 19 ++ examples/StreetlightsAPI/Program.cs | 7 +- .../StreetlightsAPI/StreetlightsAPI.csproj | 15 ++ examples/StreetlightsAPI/nlog.config | 23 +++ .../AsyncAPI.Saunter.Generator.Cli.csproj | 3 +- .../ToFile/DependencyResolver.cs | 22 ++ .../ToFile/ServiceProviderBuilder.cs | 21 +- ...syncAPI.Saunter.Generator.Cli.Tests.csproj | 9 + .../IntegrationTests.cs | 190 ++---------------- .../Specs/ExpectedSpecFiles.cs | 12 ++ .../Specs/streetlights_v2.6.json | 94 +++++++++ .../Specs/streetlights_v2.6.yml | 61 ++++++ 16 files changed, 442 insertions(+), 178 deletions(-) create mode 100644 examples/.gitignore create mode 100644 examples/StreetlightsAPI.TopLevelStatement/Program.cs create mode 100644 examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj create mode 100644 examples/StreetlightsAPI.TopLevelStatement/appsettings.json create mode 100644 examples/StreetlightsAPI/nlog.config create mode 100644 src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.json create mode 100644 test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.yml diff --git a/Saunter.sln b/Saunter.sln index 85c435a4..60ff6a88 100644 --- a/Saunter.sln +++ b/Saunter.sln @@ -12,6 +12,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Tests", "test\Saunter.Tests\Saunter.Tests.csproj", "{3ADB27EF-7C80-40EB-AFC6-5D06D415FFAB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{6ABD4842-47AF-49A5-B057-0EBA64416789}" + ProjectSection(SolutionItems) = preProject + examples\.gitignore = examples\.gitignore + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreetlightsAPI", "examples\StreetlightsAPI\StreetlightsAPI.csproj", "{F188D4A7-BBCB-464F-A370-2BD84D18EA79}" EndProject @@ -48,6 +51,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAPI.Saunter.Generator.Cli.Tests", "test\AsyncAPI.Saunter.Generator.Cli.Tests\AsyncAPI.Saunter.Generator.Cli.Tests.csproj", "{18AD0249-0436-4A26-9972-B97BA6905A54}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StreetlightsAPI.TopLevelStatement", "examples\StreetlightsAPI.TopLevelStatement\StreetlightsAPI.TopLevelStatement.csproj", "{6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -142,6 +147,18 @@ Global {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x64.Build.0 = Release|Any CPU {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x86.ActiveCfg = Release|Any CPU {18AD0249-0436-4A26-9972-B97BA6905A54}.Release|x86.Build.0 = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|x64.Build.0 = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Debug|x86.Build.0 = Debug|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|Any CPU.Build.0 = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|x64.ActiveCfg = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|x64.Build.0 = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|x86.ActiveCfg = Release|Any CPU + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -156,6 +173,7 @@ Global {E8FACA22-CFED-4710-89E4-D55F31BF96B3} = {D8CB9C0D-9605-457B-979F-C8994B20A926} {6C102D4D-3DA4-4763-B75E-C15E33E7E94A} = {28D4C365-FDED-49AE-A97D-36202E24A55A} {18AD0249-0436-4A26-9972-B97BA6905A54} = {6491E321-2D02-44AB-9116-D722FE169595} + {6F6B8B03-9045-46EC-AE12-E7ADA492F9FA} = {6ABD4842-47AF-49A5-B057-0EBA64416789} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E} diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 00000000..f91a1165 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,4 @@ +specs/ +streetlights.json +streetlights.yml +streetlights.yaml \ No newline at end of file diff --git a/examples/StreetlightsAPI.TopLevelStatement/Program.cs b/examples/StreetlightsAPI.TopLevelStatement/Program.cs new file mode 100644 index 00000000..79aa9229 --- /dev/null +++ b/examples/StreetlightsAPI.TopLevelStatement/Program.cs @@ -0,0 +1,76 @@ +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NLog; +using NLog.Web; +using Saunter; +using Saunter.AsyncApiSchema.v2; +using StreetlightsAPI; + +LogManager.Setup().LoadConfigurationFromAppSettings(); + +var builder = WebApplication.CreateBuilder(args); +builder.Host.ConfigureLogging(logging => logging.AddSimpleConsole(console => console.SingleLine = true)); +builder.Host.UseNLog(); + +// Add Saunter to the application services. +builder.Services.AddAsyncApiSchemaGeneration(options => +{ + options.AssemblyMarkerTypes = [typeof(StreetlightMessageBus)]; + + options.Middleware.UiTitle = "Streetlights API"; + + options.AsyncApi = new AsyncApiDocument + { + Info = new Info("Streetlights API", "1.0.0") + { + Description = "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + License = new License("Apache 2.0") + { + Url = "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + Servers = + { + ["mosquitto"] = new Server("test.mosquitto.org", "mqtt"), + ["webapi"] = new Server("localhost:5000", "http"), + }, + }; +}); + +builder.Services.AddScoped(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseDeveloperExceptionPage(); + +app.UseRouting(); +app.UseCors(configure => configure.AllowAnyOrigin().AllowAnyMethod()); + +app.UseEndpoints(endpoints => +{ + endpoints.MapAsyncApiDocuments(); + endpoints.MapAsyncApiUi(); + + endpoints.MapControllers(); +}); + +await app.StartAsync(); + +// Print the AsyncAPI doc location +var logger = app.Services.GetService().CreateLogger(); +var options = app.Services.GetService>(); +var addresses = app.Urls; +logger.LogInformation("AsyncAPI doc available at: {URL}", $"{addresses.FirstOrDefault()}{options.Value.Middleware.Route}"); +logger.LogInformation("AsyncAPI UI available at: {URL}", $"{addresses.FirstOrDefault()}{options.Value.Middleware.UiBaseRoute}"); + +// Redirect base url to AsyncAPI UI +app.Map("/", () => Results.Redirect("index.html")); +app.Map("/index.html", () => Results.Redirect(options.Value.Middleware.UiBaseRoute)); + +await app.WaitForShutdownAsync(); diff --git a/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj new file mode 100644 index 00000000..4e4dfcc0 --- /dev/null +++ b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj @@ -0,0 +1,46 @@ + + + + + net8.0 + false + + + true + json,yml + streetlights.{extension} + specs + + + + bin\Debug\StreetlightsAPI.TopLevelStatement.xml + 1701;1702;1591 + + + + bin\Release\StreetlightsAPI.TopLevelStatement.xml + 1701;1702;1591 + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + diff --git a/examples/StreetlightsAPI.TopLevelStatement/appsettings.json b/examples/StreetlightsAPI.TopLevelStatement/appsettings.json new file mode 100644 index 00000000..b4f6487b --- /dev/null +++ b/examples/StreetlightsAPI.TopLevelStatement/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "AllowedHosts": "*", + + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://localhost:5001" + } + } + } +} diff --git a/examples/StreetlightsAPI/Program.cs b/examples/StreetlightsAPI/Program.cs index aa3f77ad..9afd805e 100644 --- a/examples/StreetlightsAPI/Program.cs +++ b/examples/StreetlightsAPI/Program.cs @@ -6,6 +6,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog; +using NLog.Web; using Saunter; using Saunter.AsyncApiSchema.v2; @@ -15,6 +17,8 @@ public class Program { public static void Main(string[] args) { + LogManager.Setup().LoadConfigurationFromAppSettings(); + CreateHostBuilder(args).Build().Run(); } @@ -22,10 +26,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureLogging(logging => logging.AddSimpleConsole(console => console.SingleLine = true)) + .UseNLog() .ConfigureWebHostDefaults(web => { web.UseStartup(); - web.UseUrls("http://localhost:5000"); + web.UseUrls("http://localhost:5001"); }); } } diff --git a/examples/StreetlightsAPI/StreetlightsAPI.csproj b/examples/StreetlightsAPI/StreetlightsAPI.csproj index 43f0b54f..ab72e944 100644 --- a/examples/StreetlightsAPI/StreetlightsAPI.csproj +++ b/examples/StreetlightsAPI/StreetlightsAPI.csproj @@ -1,6 +1,8 @@  + net6.0 false @@ -19,4 +21,17 @@ + + + + + PreserveNewest + + + + + + + + diff --git a/examples/StreetlightsAPI/nlog.config b/examples/StreetlightsAPI/nlog.config new file mode 100644 index 00000000..dd855146 --- /dev/null +++ b/examples/StreetlightsAPI/nlog.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + \ 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 e510838e..6ab8e1ae 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -6,6 +6,7 @@ enable 12 AsyncAPI.Saunter.Generator.Cli + $(NoWarn);EF1001 AsyncAPI Command Line Tools: Dotnet tool to generate AsyncAPI spec file from dotnet startup assembly. AsyncAPI Initiative @@ -40,8 +41,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs new file mode 100644 index 00000000..3d6d010a --- /dev/null +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/DependencyResolver.cs @@ -0,0 +1,22 @@ +using System.Reflection; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; + +internal static class DependencyResolver +{ + public static void Init(string startupAssemblyBasePath) + { + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var requestedAssembly = new AssemblyName(args.Name); + var fullPath = Path.Combine(startupAssemblyBasePath, $"{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/ToFile/ServiceProviderBuilder.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs index 69bde4f2..eed4c58b 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ServiceProviderBuilder.cs @@ -1,5 +1,6 @@ -using System.Reflection; -using System.Runtime.Loader; +using System.Runtime.Loader; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.Extensions.Logging; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; @@ -13,12 +14,20 @@ internal class ServiceProviderBuilder(ILogger logger) : { public IServiceProvider BuildServiceProvider(string startupAssembly) { - var fullPath = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), startupAssembly)); + var fullPath = Path.GetFullPath(startupAssembly); + var basePath = Path.GetDirectoryName(fullPath); + DependencyResolver.Init(basePath); + logger.LogInformation($"Loading startup assembly: {fullPath}"); var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(fullPath); - var nswagCommandsAssembly = Assembly.LoadFrom("NSwag.Commands.dll"); - var nswagServiceProvider = nswagCommandsAssembly.GetType("NSwag.Commands.ServiceProviderResolver"); - var serviceProvider = (IServiceProvider)nswagServiceProvider.InvokeMember("GetServiceProvider", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, null, [assembly]); + var reporter = new OperationReporter(new OperationReportHandler( + m => logger.LogError(m), + m => logger.LogWarning(m), + m => logger.LogInformation(m), + m => logger.LogDebug(m))); + var appServiceProvider = new AppServiceProviderFactory(assembly, reporter); + var serviceProvider = appServiceProvider.Create([]); + return serviceProvider; } } diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index 6704dac9..ad8e79fc 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -31,4 +31,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index e7751ac3..52cb62e7 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -50,186 +50,36 @@ Retrieves AsyncAPI spec from a startup assembly and writes to file. """, StringCompareShould.IgnoreLineEndings); } - [Fact] - public void StreetlightsAPIExportSpecTest() + /// + /// Both example projects are used to check whether AsyncAPI spec generation is working because they are targeting different .NET versions and are using different hosting strategies. + /// - StreetlightsAPI project is targeting NET6 using the 'old school' Startup-class hosting mechanism. + /// - StreetlightsAPI.TopLevelStatement project is targeting NET8 using the new Top Level Statement hosting mechanism. + /// + [Theory] + [InlineData("StreetlightsAPI", "net6.0")] + [InlineData("StreetlightsAPI.TopLevelStatement", "net8.0")] + public void Streetlights_ExportSpecTest(string csprojName, string targetFramework) { - var path = Directory.GetCurrentDirectory(); + var path = Path.Combine(Directory.GetCurrentDirectory(), csprojName); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"tofile ../../../../../examples/StreetlightsAPI/bin/Debug/net6.0/StreetlightsAPI.dll --output {path} --format json,yml,yaml"); + var stdOut = RunTool($"tofile ../../../../../examples/{csprojName}/bin/Debug/{targetFramework}/{csprojName}.dll --output {path} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); stdOut.ShouldContain($"AsyncAPI yml successfully written to {Path.Combine(path, "asyncapi.yml")}"); stdOut.ShouldContain($"AsyncAPI json successfully written to {Path.Combine(path, "asyncapi.json")}"); - File.Exists("asyncapi.yml").ShouldBeTrue("asyncapi.yml"); - File.Exists("asyncapi.yaml").ShouldBeTrue("asyncapi.yaml"); - File.Exists("asyncapi.json").ShouldBeTrue("asyncapi.json"); + File.Exists(Path.Combine(csprojName, "asyncapi.yml")).ShouldBeTrue("asyncapi.yml"); + File.Exists(Path.Combine(csprojName, "asyncapi.yaml")).ShouldBeTrue("asyncapi.yaml"); + File.Exists(Path.Combine(csprojName, "asyncapi.json")).ShouldBeTrue("asyncapi.json"); - var yml = File.ReadAllText("asyncapi.yml"); - yml.ShouldBe(""" - asyncapi: 2.6.0 - info: - title: Streetlights API - version: 1.0.0 - description: The Smartylighting Streetlights API allows you to remotely manage the city lights. - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 - servers: - mosquitto: - url: test.mosquitto.org - protocol: mqtt - webapi: - url: localhost:5000 - protocol: http - defaultContentType: application/json - channels: - publish/light/measured: - servers: - - webapi - publish: - operationId: MeasureLight - summary: Inform about environmental lighting conditions for a particular streetlight. - tags: - - name: Light - message: - $ref: '#/components/messages/lightMeasuredEvent' - subscribe/light/measured: - servers: - - mosquitto - subscribe: - operationId: PublishLightMeasurement - summary: Subscribe to environmental lighting conditions for a particular streetlight. - tags: - - name: Light - message: - payload: - $ref: '#/components/schemas/lightMeasuredEvent' - components: - schemas: - lightMeasuredEvent: - type: object - properties: - id: - type: integer - format: int32 - description: Id of the streetlight. - lumens: - type: integer - format: int32 - description: Light intensity measured in lumens. - sentAt: - type: string - format: date-time - description: Light intensity measured in lumens. - additionalProperties: false - messages: - lightMeasuredEvent: - payload: - $ref: '#/components/schemas/lightMeasuredEvent' - name: lightMeasuredEvent - """, "yaml"); + var yml = File.ReadAllText(Path.Combine(csprojName, "asyncapi.yml")); + yml.ShouldBe(ExpectedSpecFiles.Yml_v2_6, "yml"); - var yaml = File.ReadAllText("asyncapi.yaml"); - yaml.ShouldBe(yml, "yml"); + var yaml = File.ReadAllText(Path.Combine(csprojName, "asyncapi.yaml")); + yaml.ShouldBe(yml, "yaml"); - var json = File.ReadAllText("asyncapi.json"); - json.ShouldBe(""" - { - "asyncapi": "2.6.0", - "info": { - "title": "Streetlights API", - "version": "1.0.0", - "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.", - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0" - } - }, - "servers": { - "mosquitto": { - "url": "test.mosquitto.org", - "protocol": "mqtt" - }, - "webapi": { - "url": "localhost:5000", - "protocol": "http" - } - }, - "defaultContentType": "application/json", - "channels": { - "publish/light/measured": { - "servers": [ - "webapi" - ], - "publish": { - "operationId": "MeasureLight", - "summary": "Inform about environmental lighting conditions for a particular streetlight.", - "tags": [ - { - "name": "Light" - } - ], - "message": { - "$ref": "#/components/messages/lightMeasuredEvent" - } - } - }, - "subscribe/light/measured": { - "servers": [ - "mosquitto" - ], - "subscribe": { - "operationId": "PublishLightMeasurement", - "summary": "Subscribe to environmental lighting conditions for a particular streetlight.", - "tags": [ - { - "name": "Light" - } - ], - "message": { - "payload": { - "$ref": "#/components/schemas/lightMeasuredEvent" - } - } - } - } - }, - "components": { - "schemas": { - "lightMeasuredEvent": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int32", - "description": "Id of the streetlight." - }, - "lumens": { - "type": "integer", - "format": "int32", - "description": "Light intensity measured in lumens." - }, - "sentAt": { - "type": "string", - "format": "date-time", - "description": "Light intensity measured in lumens." - } - }, - "additionalProperties": false - } - }, - "messages": { - "lightMeasuredEvent": { - "payload": { - "$ref": "#/components/schemas/lightMeasuredEvent" - }, - "name": "lightMeasuredEvent" - } - } - } - } - """, "json"); + var json = File.ReadAllText(Path.Combine(csprojName, "asyncapi.json")); + json.ShouldBe(ExpectedSpecFiles.Json_v2_6, "json"); } } diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs new file mode 100644 index 00000000..aa6ed033 --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs @@ -0,0 +1,12 @@ +// 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. + +namespace AsyncAPI.Saunter.Generator.Cli.Tests; + +public static class ExpectedSpecFiles +{ + public static string Json_v2_6 => File.ReadAllText("Specs/streetlights_v2.6.json"); + + public static string Yml_v2_6 => File.ReadAllText("Specs/streetlights_v2.6.yml"); +} diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.json b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.json new file mode 100644 index 00000000..8a429cbb --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.json @@ -0,0 +1,94 @@ +{ + "asyncapi": "2.6.0", + "info": { + "title": "Streetlights API", + "version": "1.0.0", + "description": "The Smartylighting Streetlights API allows you to remotely manage the city lights.", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0" + } + }, + "servers": { + "mosquitto": { + "url": "test.mosquitto.org", + "protocol": "mqtt" + }, + "webapi": { + "url": "localhost:5000", + "protocol": "http" + } + }, + "defaultContentType": "application/json", + "channels": { + "publish/light/measured": { + "servers": [ + "webapi" + ], + "publish": { + "operationId": "MeasureLight", + "summary": "Inform about environmental lighting conditions for a particular streetlight.", + "tags": [ + { + "name": "Light" + } + ], + "message": { + "$ref": "#/components/messages/lightMeasuredEvent" + } + } + }, + "subscribe/light/measured": { + "servers": [ + "mosquitto" + ], + "subscribe": { + "operationId": "PublishLightMeasurement", + "summary": "Subscribe to environmental lighting conditions for a particular streetlight.", + "tags": [ + { + "name": "Light" + } + ], + "message": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredEvent" + } + } + } + } + }, + "components": { + "schemas": { + "lightMeasuredEvent": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Id of the streetlight." + }, + "lumens": { + "type": "integer", + "format": "int32", + "description": "Light intensity measured in lumens." + }, + "sentAt": { + "type": "string", + "format": "date-time", + "description": "Light intensity measured in lumens." + } + }, + "additionalProperties": false + } + }, + "messages": { + "lightMeasuredEvent": { + "payload": { + "$ref": "#/components/schemas/lightMeasuredEvent" + }, + "name": "lightMeasuredEvent" + } + } + } +} \ No newline at end of file diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.yml b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.yml new file mode 100644 index 00000000..efccd7ed --- /dev/null +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/streetlights_v2.6.yml @@ -0,0 +1,61 @@ +asyncapi: 2.6.0 +info: + title: Streetlights API + version: 1.0.0 + description: The Smartylighting Streetlights API allows you to remotely manage the city lights. + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0 +servers: + mosquitto: + url: test.mosquitto.org + protocol: mqtt + webapi: + url: localhost:5000 + protocol: http +defaultContentType: application/json +channels: + publish/light/measured: + servers: + - webapi + publish: + operationId: MeasureLight + summary: Inform about environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + $ref: '#/components/messages/lightMeasuredEvent' + subscribe/light/measured: + servers: + - mosquitto + subscribe: + operationId: PublishLightMeasurement + summary: Subscribe to environmental lighting conditions for a particular streetlight. + tags: + - name: Light + message: + payload: + $ref: '#/components/schemas/lightMeasuredEvent' +components: + schemas: + lightMeasuredEvent: + type: object + properties: + id: + type: integer + format: int32 + description: Id of the streetlight. + lumens: + type: integer + format: int32 + description: Light intensity measured in lumens. + sentAt: + type: string + format: date-time + description: Light intensity measured in lumens. + additionalProperties: false + messages: + lightMeasuredEvent: + payload: + $ref: '#/components/schemas/lightMeasuredEvent' + name: lightMeasuredEvent \ No newline at end of file From ba8dd8e5d1e3a1a20e9b3a3f22c329dc5363bcc5 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:23:37 +0200 Subject: [PATCH 26/34] #196 Ignore expected warning (ASP0014 needs #173) --- examples/StreetlightsAPI.TopLevelStatement/Program.cs | 2 +- .../StreetlightsAPI.TopLevelStatement.csproj | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/StreetlightsAPI.TopLevelStatement/Program.cs b/examples/StreetlightsAPI.TopLevelStatement/Program.cs index 79aa9229..164eb0ce 100644 --- a/examples/StreetlightsAPI.TopLevelStatement/Program.cs +++ b/examples/StreetlightsAPI.TopLevelStatement/Program.cs @@ -14,7 +14,7 @@ LogManager.Setup().LoadConfigurationFromAppSettings(); var builder = WebApplication.CreateBuilder(args); -builder.Host.ConfigureLogging(logging => logging.AddSimpleConsole(console => console.SingleLine = true)); +builder.Logging.AddSimpleConsole(console => console.SingleLine = true); builder.Host.UseNLog(); // Add Saunter to the application services. diff --git a/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj index 4e4dfcc0..64639d4f 100644 --- a/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj +++ b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj @@ -5,6 +5,9 @@ the AsyncAPI.Saunter.Generator.Cli tool can generate specs for projects targetting .NET6 and .NET8. --> net8.0 false + + + $(NoWarn);ASP0014 true From 3d6c0ea1b5c323bd928de0c4bbff93b82f60d999 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:37:16 +0200 Subject: [PATCH 27/34] #196 formatting and readme wording --- .../ToFile/ToFileCommand.cs | 12 ++++++------ src/AsyncAPI.Saunter.Generator.Cli/readme.md | 12 ++++++------ .../IntegrationTests.cs | 14 +++++++------- .../Specs/ExpectedSpecFiles.cs | 6 +----- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs index 0a62c43d..91055b47 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/ToFileCommand.cs @@ -13,11 +13,11 @@ internal class ToFileCommand(ILogger logger, IEnvironmentBuilder /// Retrieves AsyncAPI spec from a startup assembly and writes to file. /// /// relative path to the application's startup assembly - /// -o,relative path where the AsyncAPI will be output [defaults to stdout] - /// -d,name(s) of the AsyncAPI documents you want to retrieve, as configured in your startup class [defaults to all documents] - /// exports AsyncAPI in json and/or yml format [defaults to json] - /// defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}\"] - /// define environment variable(s) for the application. Formatted as a comma separated list of key=value pairs or just key for flags + /// -o,relative path where the AsyncAPI documents will be exported to + /// -d,name(s) of the AsyncAPI documents you want to export as configured in your startup class. To export all documents using null. + /// exports AsyncAPI in json and/or yml format + /// defines the file name template, {document} and {extension} template variables can be used + /// define environment variable(s) for the application. Formatted as a comma separated list of _key=value_ pairs [Command("tofile")] public int ToFile([Argument] string startupassembly, string output = "./", string doc = null, string format = "json", string filename = DEFAULT_FILENAME, string env = "") { @@ -68,6 +68,6 @@ public int ToFile([Argument] string startupassembly, string output = "./", strin } } - return 1; + return 0; } } diff --git a/src/AsyncAPI.Saunter.Generator.Cli/readme.md b/src/AsyncAPI.Saunter.Generator.Cli/readme.md index b5ee1273..01030989 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/readme.md +++ b/src/AsyncAPI.Saunter.Generator.Cli/readme.md @@ -1,17 +1,17 @@ # AsyncApi Generator.Cli Tool -A dotnet tool to generate AsyncAPI specification files based of a dotnet DLL (The application itself). +A dotnet tool to generate AsyncAPI specification files based of a dotnet assembly (The application itself). ## Tool usage ``` dotnet asyncapi tofile [startup-assembly] --output [output-path] --format [json,yml,yaml] --doc [asyncapi-document-name] ``` -- _startup-assembly_: the file path to the entrypoint dotnet DLL that hosts AsyncAPI document(s). +- _startup-assembly_: the file path to the dotnet startup assembly (DLL) that hosts AsyncAPI document(s). ## Tool options - _--doc_: The name of the AsyncAPI document as defined in the startup class by the ```.ConfigureNamedAsyncApi()```-method. If only ```.AddAsyncApiSchemaGeneration()``` is used, the document is unnamed and will always be exported. 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. +- _--output_: relative path where the AsyncAPI documents will be exported to (Default: the csproj root "./"). +- _--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 (Default: "json"). - _--env_: 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```. ## Install the Generator.Cli dotnet Tool @@ -24,4 +24,4 @@ Want to learn more about .NET tools? Or want to install it local using a manifes [Check out this Microsoft page on how to manage .NET tools](https://learn.microsoft.com/en-us/dotnet/core/tools/global-tools) ## Internals -How does the tool work internally? It tries to exact an ```IServiceProvider``` from the provided _startup-assembly_ and exports AsyncApiDocument(s) as registered in the services provider. \ No newline at end of file +How does the tool work internally? It tries to exact an ```IServiceProvider``` from the provided _startup-assembly_ and exports AsyncApiDocument(s) as registered with the services provider. \ No newline at end of file diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 52cb62e7..7b5c6fb6 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -5,7 +5,7 @@ namespace AsyncAPI.Saunter.Generator.Cli.Tests; public class IntegrationTests(ITestOutputHelper output) { - private string RunTool(string args, int expectedExitCode = 1) + private string RunTool(string args, int expectedExitCode = 0) { using var outWriter = new StringWriter(); using var errorWriter = new StringWriter(); @@ -31,7 +31,7 @@ private string RunTool(string args, int expectedExitCode = 1) [Fact] public void DefaultCallPrintsCommandInfo() { - var stdOut = RunTool("tofile", 0).Trim(); + var stdOut = RunTool("tofile").Trim(); stdOut.ShouldBe(""" Usage: tofile [arguments...] [options...] [-h|--help] [--version] @@ -42,11 +42,11 @@ Retrieves AsyncAPI spec from a startup assembly and writes to file. [0] relative path to the application's startup assembly Options: - -o|--output relative path where the AsyncAPI will be output [defaults to stdout] (Default: "./") - -d|--doc name(s) of the AsyncAPI documents you want to retrieve as configured in your startup class [defaults to all documents] (Default: null) - --format exports AsyncAPI in json and/or yml format [defaults to json] (Default: "json") - --filename defines the file name template, {document} and {extension} template variables can be used [defaults to "{document}_asyncapi.{extension}\"] (Default: "{document}_asyncapi.{extension}") - --env define environment variable(s) for the application. Formatted as a comma separated list of key=value pairs or just key for flags (Default: "") + -o|--output relative path where the AsyncAPI documents will be exported to (Default: "./") + -d|--doc name(s) of the AsyncAPI documents you want to export as configured in your startup class. To export all documents using null. (Default: null) + --format exports AsyncAPI in json and/or yml format (Default: "json") + --filename defines the file name template, {document} and {extension} template variables can be used (Default: "{document}_asyncapi.{extension}") + --env define environment variable(s) for the application. Formatted as a comma separated list of _key=value_ pairs (Default: "") """, StringCompareShould.IgnoreLineEndings); } diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs index aa6ed033..0d569e3a 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/Specs/ExpectedSpecFiles.cs @@ -1,8 +1,4 @@ -// 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. - -namespace AsyncAPI.Saunter.Generator.Cli.Tests; +namespace AsyncAPI.Saunter.Generator.Cli.Tests; public static class ExpectedSpecFiles { From 8a4f5c6b6c80abc614de6137adf20782d8388d01 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:43:36 +0200 Subject: [PATCH 28/34] #196 ExitCode test problems --- examples/StreetlightsAPI.TopLevelStatement/Program.cs | 3 +++ .../StreetlightsAPI.TopLevelStatement.csproj | 3 --- src/AsyncAPI.Saunter.Generator.Cli/Program.cs | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/StreetlightsAPI.TopLevelStatement/Program.cs b/examples/StreetlightsAPI.TopLevelStatement/Program.cs index 164eb0ce..2e59f1cb 100644 --- a/examples/StreetlightsAPI.TopLevelStatement/Program.cs +++ b/examples/StreetlightsAPI.TopLevelStatement/Program.cs @@ -52,6 +52,8 @@ app.UseRouting(); app.UseCors(configure => configure.AllowAnyOrigin().AllowAnyMethod()); +// to be fixed with issue #173 +#pragma warning disable ASP0014 // Suggest using top level route registrations instead of UseEndpoints app.UseEndpoints(endpoints => { endpoints.MapAsyncApiDocuments(); @@ -59,6 +61,7 @@ endpoints.MapControllers(); }); +#pragma warning restore ASP0014 // Suggest using top level route registrations instead of UseEndpoints await app.StartAsync(); diff --git a/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj index 64639d4f..4e4dfcc0 100644 --- a/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj +++ b/examples/StreetlightsAPI.TopLevelStatement/StreetlightsAPI.TopLevelStatement.csproj @@ -5,9 +5,6 @@ the AsyncAPI.Saunter.Generator.Cli tool can generate specs for projects targetting .NET6 and .NET8. --> net8.0 false - - - $(NoWarn);ASP0014 true diff --git a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs index 3502ea7d..58bfef22 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/Program.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/Program.cs @@ -15,3 +15,5 @@ var app = ConsoleApp.Create(); app.Add(); app.Run(args); + +Environment.ExitCode = 0; From 07b4273b9e1ffaae0fea1208621c5190131a5258 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:48:09 +0200 Subject: [PATCH 29/34] #196 ExitCode test problems -- the issue is a missing reference --- .../AsyncAPI.Saunter.Generator.Cli.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj index ad8e79fc..edd24802 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/AsyncAPI.Saunter.Generator.Cli.Tests.csproj @@ -23,6 +23,7 @@ + From e846dd9ac829ca654b09b2abf4e09afff2f1326e Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 20:57:02 +0200 Subject: [PATCH 30/34] #196 GetStreamFor logging --- .../ToFile/StreamProvider.cs | 8 ++++++-- .../IntegrationTests.cs | 14 +++++++------- .../ToFile/StreamProviderTests.cs | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs index 8af5dbd4..09f56617 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -1,14 +1,18 @@ -namespace AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; + +namespace AsyncAPI.Saunter.Generator.Cli.ToFile; internal interface IStreamProvider { Stream GetStreamFor(string path); } -internal class StreamProvider : IStreamProvider +internal class StreamProvider(ILogger logger) : IStreamProvider { public Stream GetStreamFor(string path) { + logger.LogDebug($"GetStreamFor(path: {path})"); + if (!string.IsNullOrEmpty(path)) { Directory.CreateDirectory(Path.GetDirectoryName(path)); diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 7b5c6fb6..0f352801 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -60,7 +60,7 @@ Retrieves AsyncAPI spec from a startup assembly and writes to file. [InlineData("StreetlightsAPI.TopLevelStatement", "net8.0")] public void Streetlights_ExportSpecTest(string csprojName, string targetFramework) { - var path = Path.Combine(Directory.GetCurrentDirectory(), csprojName); + var path = Path.Combine(Directory.GetCurrentDirectory(), csprojName, "specs"); output.WriteLine($"Output path: {path}"); var stdOut = RunTool($"tofile ../../../../../examples/{csprojName}/bin/Debug/{targetFramework}/{csprojName}.dll --output {path} --format json,yml,yaml"); @@ -69,17 +69,17 @@ public void Streetlights_ExportSpecTest(string csprojName, string targetFramewor stdOut.ShouldContain($"AsyncAPI yml successfully written to {Path.Combine(path, "asyncapi.yml")}"); stdOut.ShouldContain($"AsyncAPI json successfully written to {Path.Combine(path, "asyncapi.json")}"); - File.Exists(Path.Combine(csprojName, "asyncapi.yml")).ShouldBeTrue("asyncapi.yml"); - File.Exists(Path.Combine(csprojName, "asyncapi.yaml")).ShouldBeTrue("asyncapi.yaml"); - File.Exists(Path.Combine(csprojName, "asyncapi.json")).ShouldBeTrue("asyncapi.json"); + File.Exists(Path.Combine(path, "asyncapi.yml")).ShouldBeTrue("asyncapi.yml"); + File.Exists(Path.Combine(path, "asyncapi.yaml")).ShouldBeTrue("asyncapi.yaml"); + File.Exists(Path.Combine(path, "asyncapi.json")).ShouldBeTrue("asyncapi.json"); - var yml = File.ReadAllText(Path.Combine(csprojName, "asyncapi.yml")); + var yml = File.ReadAllText(Path.Combine(path, "asyncapi.yml")); yml.ShouldBe(ExpectedSpecFiles.Yml_v2_6, "yml"); - var yaml = File.ReadAllText(Path.Combine(csprojName, "asyncapi.yaml")); + var yaml = File.ReadAllText(Path.Combine(path, "asyncapi.yaml")); yaml.ShouldBe(yml, "yaml"); - var json = File.ReadAllText(Path.Combine(csprojName, "asyncapi.json")); + var json = File.ReadAllText(Path.Combine(path, "asyncapi.json")); json.ShouldBe(ExpectedSpecFiles.Json_v2_6, "json"); } } diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs index a340b6d4..27051eb1 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/ToFile/StreamProviderTests.cs @@ -1,11 +1,21 @@ using AsyncAPI.Saunter.Generator.Cli.ToFile; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.Community.Logging; using Shouldly; namespace AsyncAPI.Saunter.Generator.Cli.Tests.ToFile; public class StreamProviderTests { - private readonly IStreamProvider _streamProvider = new StreamProvider(); + private readonly IStreamProvider _streamProvider; + private readonly ILogger _logger; + + public StreamProviderTests() + { + this._logger = Substitute.For>(); + this._streamProvider = new StreamProvider(this._logger); + } [Fact] public void NullPathIsStdOut() @@ -14,6 +24,7 @@ public void NullPathIsStdOut() stream.ShouldNotBeNull(); Assert.False(stream is FileStream); + this._logger.Received(1).CallToLog(LogLevel.Debug); } [Fact] @@ -33,5 +44,7 @@ public void StringPathIsFileStream() { File.Delete(path); } + + this._logger.Received(1).CallToLog(LogLevel.Debug); } } From fc7cccd26ad3765a1bb2583d6594b1cb00cd7c8e Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 21:28:48 +0200 Subject: [PATCH 31/34] #196 GetStreamFor try recreate directory? --- .../ToFile/StreamProvider.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs index 09f56617..2e285438 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace AsyncAPI.Saunter.Generator.Cli.ToFile; @@ -15,7 +16,21 @@ public Stream GetStreamFor(string path) if (!string.IsNullOrEmpty(path)) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + var directory = Path.GetDirectoryName(path); + var sw = Stopwatch.StartNew(); + do + { + try + { + Directory.CreateDirectory(directory); + } + catch (Exception e) when (sw.Elapsed < TimeSpan.FromMilliseconds(250)) + { + logger.LogDebug(e, "Retry..."); + Thread.Sleep(100); + } + } + while (!Directory.Exists(directory)); } return path != null ? File.Create(path) : Console.OpenStandardOutput(); From 4f1064e412d07c4206cb2f135c61d4302cb84d6f Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 21:33:08 +0200 Subject: [PATCH 32/34] #196 GetStreamFor try recreate directory?? --- .../ToFile/StreamProvider.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs index 2e285438..19583e6d 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -16,21 +16,21 @@ public Stream GetStreamFor(string path) if (!string.IsNullOrEmpty(path)) { - var directory = Path.GetDirectoryName(path); + var directory = new DirectoryInfo(Path.GetDirectoryName(path)); var sw = Stopwatch.StartNew(); do { try { - Directory.CreateDirectory(directory); + directory.Create(); } catch (Exception e) when (sw.Elapsed < TimeSpan.FromMilliseconds(250)) { - logger.LogDebug(e, "Retry..."); + logger.LogDebug(e, $"Retry... {directory.Parent.Exists}, {directory.Parent.Parent.Exists}, {directory.Parent.Parent.Parent.Exists}"); Thread.Sleep(100); } } - while (!Directory.Exists(directory)); + while (!directory.Exists); } return path != null ? File.Create(path) : Console.OpenStandardOutput(); From 74260a8fb877dcb86dec50ead7afcfb2d5b798f0 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 21:41:02 +0200 Subject: [PATCH 33/34] #196 permission issue? --- .../ToFile/StreamProvider.cs | 16 +------------- .../IntegrationTests.cs | 22 +++++++++---------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs index 19583e6d..7ada6199 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs +++ b/src/AsyncAPI.Saunter.Generator.Cli/ToFile/StreamProvider.cs @@ -16,21 +16,7 @@ public Stream GetStreamFor(string path) if (!string.IsNullOrEmpty(path)) { - var directory = new DirectoryInfo(Path.GetDirectoryName(path)); - var sw = Stopwatch.StartNew(); - do - { - try - { - directory.Create(); - } - catch (Exception e) when (sw.Elapsed < TimeSpan.FromMilliseconds(250)) - { - logger.LogDebug(e, $"Retry... {directory.Parent.Exists}, {directory.Parent.Parent.Exists}, {directory.Parent.Parent.Parent.Exists}"); - Thread.Sleep(100); - } - } - while (!directory.Exists); + Directory.CreateDirectory(Path.GetDirectoryName(path)); } return path != null ? File.Create(path) : Console.OpenStandardOutput(); diff --git a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs index 0f352801..c6079917 100644 --- a/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs +++ b/test/AsyncAPI.Saunter.Generator.Cli.Tests/IntegrationTests.cs @@ -60,26 +60,26 @@ Retrieves AsyncAPI spec from a startup assembly and writes to file. [InlineData("StreetlightsAPI.TopLevelStatement", "net8.0")] public void Streetlights_ExportSpecTest(string csprojName, string targetFramework) { - var path = Path.Combine(Directory.GetCurrentDirectory(), csprojName, "specs"); + var path = Path.Combine(Directory.GetCurrentDirectory()); output.WriteLine($"Output path: {path}"); - var stdOut = RunTool($"tofile ../../../../../examples/{csprojName}/bin/Debug/{targetFramework}/{csprojName}.dll --output {path} --format json,yml,yaml"); + var stdOut = RunTool($"tofile ../../../../../examples/{csprojName}/bin/Debug/{targetFramework}/{csprojName}.dll --output {path} --filename {csprojName}.{{extension}} --format json,yml,yaml"); stdOut.ShouldNotBeEmpty(); - stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, "asyncapi.yaml")}"); - stdOut.ShouldContain($"AsyncAPI yml successfully written to {Path.Combine(path, "asyncapi.yml")}"); - stdOut.ShouldContain($"AsyncAPI json successfully written to {Path.Combine(path, "asyncapi.json")}"); + stdOut.ShouldContain($"AsyncAPI yaml successfully written to {Path.Combine(path, $"{csprojName}.yaml")}"); + stdOut.ShouldContain($"AsyncAPI yml successfully written to {Path.Combine(path, $"{csprojName}.yml")}"); + stdOut.ShouldContain($"AsyncAPI json successfully written to {Path.Combine(path, $"{csprojName}.json")}"); - File.Exists(Path.Combine(path, "asyncapi.yml")).ShouldBeTrue("asyncapi.yml"); - File.Exists(Path.Combine(path, "asyncapi.yaml")).ShouldBeTrue("asyncapi.yaml"); - File.Exists(Path.Combine(path, "asyncapi.json")).ShouldBeTrue("asyncapi.json"); + File.Exists(Path.Combine(path, $"{csprojName}.yml")).ShouldBeTrue(); + File.Exists(Path.Combine(path, $"{csprojName}.yaml")).ShouldBeTrue(); + File.Exists(Path.Combine(path, $"{csprojName}.json")).ShouldBeTrue(); - var yml = File.ReadAllText(Path.Combine(path, "asyncapi.yml")); + var yml = File.ReadAllText(Path.Combine(path, $"{csprojName}.yml")); yml.ShouldBe(ExpectedSpecFiles.Yml_v2_6, "yml"); - var yaml = File.ReadAllText(Path.Combine(path, "asyncapi.yaml")); + var yaml = File.ReadAllText(Path.Combine(path, $"{csprojName}.yaml")); yaml.ShouldBe(yml, "yaml"); - var json = File.ReadAllText(Path.Combine(path, "asyncapi.json")); + var json = File.ReadAllText(Path.Combine(path, $"{csprojName}.json")); json.ShouldBe(ExpectedSpecFiles.Json_v2_6, "json"); } } From 5fe8c6e501f245544cb31510dc9ef091a58857e5 Mon Sep 17 00:00:00 2001 From: Senn Geerts Date: Sat, 13 Jul 2024 22:00:50 +0200 Subject: [PATCH 34/34] #196 cleanup csproj --- .../AsyncAPI.Saunter.Generator.Cli.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 6ab8e1ae..3c902f75 100644 --- a/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj +++ b/src/AsyncAPI.Saunter.Generator.Cli/AsyncAPI.Saunter.Generator.Cli.csproj @@ -8,7 +8,7 @@ AsyncAPI.Saunter.Generator.Cli $(NoWarn);EF1001 - AsyncAPI Command Line Tools: Dotnet tool to generate AsyncAPI spec file from dotnet startup assembly. + AsyncAPI Command Line Tools: Dotnet tool to generate AsyncAPI spec file(s) from dotnet startup assembly. AsyncAPI Initiative true AsyncAPI.Saunter.Generator.Cli @@ -36,7 +36,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -50,7 +50,7 @@ - +