Skip to content

Commit

Permalink
asyncapi#196 Fixed resolving, added support for multiple asyncAPI doc…
Browse files Browse the repository at this point in the history
…uments, default all. Added support for env vars
  • Loading branch information
Senn Geerts authored and Senn Geerts committed Jul 6, 2024
1 parent cc5d952 commit 569a30a
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 88 deletions.
4 changes: 3 additions & 1 deletion src/AsyncAPI.Saunter.Generator.Cli/Args.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12</LangVersion>
Expand All @@ -11,8 +10,8 @@
<OutputType>Exe</OutputType>
<PackAsTool>true</PackAsTool>
<PackageId>AsyncAPI.Saunter.Generator.Cli</PackageId>
<ToolCommandName>asyncapi</ToolCommandName>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<ToolCommandName>AsyncAPI.NET</ToolCommandName>
<TargetFrameworks>net8.0;net6.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">
Expand Down
18 changes: 5 additions & 13 deletions src/AsyncAPI.Saunter.Generator.Cli/Commands/Tofile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,24 +27,16 @@ internal static Func<IDictionary<string, string>, 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)
Expand Down
118 changes: 59 additions & 59 deletions src/AsyncAPI.Saunter.Generator.Cli/Commands/TofileInternal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,62 +27,79 @@ internal static int Run(IDictionary<string, string> 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<string>();
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
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))
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;
Expand All @@ -99,31 +116,14 @@ private static void WriteFile(string outputPath, Action<Stream> 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)
Expand Down
28 changes: 28 additions & 0 deletions src/AsyncAPI.Saunter.Generator.Cli/Internal/DependencyResolver.cs
Original file line number Diff line number Diff line change
@@ -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;
};
}
}
21 changes: 14 additions & 7 deletions src/AsyncAPI.Saunter.Generator.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
});

Expand Down
12 changes: 7 additions & 5 deletions src/AsyncAPI.Saunter.Generator.Cli/readme.md
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 569a30a

Please sign in to comment.