forked from asyncapi/saunter
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
asyncapi#196 Publish a global dotnet tool
- Loading branch information
Senn Geerts
authored and
Senn Geerts
committed
Jul 6, 2024
1 parent
24b915d
commit 6318f80
Showing
13 changed files
with
936 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* text eol=lf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"; | ||
} |
31 changes: 31 additions & 0 deletions
31
src/AsyncApi.Saunter.Generator.Cli/AsyncApi.Saunter.Generator.Cli.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<OutputType>Exe</OutputType> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<LangVersion>12</LangVersion> | ||
<RootNamespace>AsyncAPI.Saunter.Generator.Cli</RootNamespace> | ||
|
||
<Description>AsyncAPI Command Line Tools</Description> | ||
<OutputType>Exe</OutputType> | ||
<PackAsTool>true</PackAsTool> | ||
<PackageId>AsyncAPI.Saunter.Generator.Cli</PackageId> | ||
<!--<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>--> | ||
<ToolCommandName>asyncapi</ToolCommandName> | ||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> | ||
</PropertyGroup> | ||
|
||
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' "> | ||
<FrameworkReference Include="Microsoft.AspNetCore.App" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="AsyncAPI.NET.Readers" Version="5.2.1" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Saunter\Saunter.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IDictionary<string, string>, 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; | ||
} | ||
} |
183 changes: 183 additions & 0 deletions
183
src/AsyncApi.Saunter.Generator.Cli/Commands/TofileInternal.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<string, string> 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<IAsyncApiDocumentProvider>(); | ||
var asyncapiOptions = serviceProvider.GetService<IOptions<AsyncApiOptions>>(); | ||
var documentSerializer = serviceProvider.GetRequiredService<IAsyncApiDocumentSerializer>(); | ||
|
||
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<Stream> 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<THost>(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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |
Oops, something went wrong.