Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype of the cli utility for AOT/Trim applications #190

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion Saunter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.IntegrationTests.ReverseProxy", "test\Saunter.IntegrationTests.ReverseProxy\Saunter.IntegrationTests.ReverseProxy.csproj", "{7CD09B89-130A-41AF-ADAE-2166C4ED695B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Saunter.Tests.MarkerTypeTests", "test\Saunter.Tests.MarkerTypeTests\Saunter.Tests.MarkerTypeTests.csproj", "{02284473-6DE7-4EE0-8433-2AC295045549}"
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}") = "Saunter.Cli", "src\Saunter.Cli\Saunter.Cli.csproj", "{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -97,6 +99,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
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|x64.Build.0 = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Debug|x86.Build.0 = Debug|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|Any CPU.Build.0 = Release|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|x64.ActiveCfg = Release|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|x64.Build.0 = Release|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|x86.ActiveCfg = Release|Any CPU
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -107,6 +121,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}
{BC3D32A1-539C-4421-88A8-0F1EB27C85A9} = {28D4C365-FDED-49AE-A97D-36202E24A55A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F85D9DA-DBCF-4F13-8C42-5719F1469B2E}
Expand Down
402 changes: 402 additions & 0 deletions src/Saunter.Cli/.editorconfig

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/Saunter.Cli/AsyncApiPrototype.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Saunter.AsyncApiSchema.v2;

namespace Saunter.Cli;

/// <summary>
/// DTO to create specification prototype
/// </summary>
/// <param name="Id">Identifier of the application the AsyncAPI document is defining.</param>
/// <param name="Info">Provides metadata about the API. The metadata can be used by the clients if needed.</param>
/// <param name="Servers">Provides connection details of servers.</param>
/// <param name="DefaultContentType">A string representing the default content type to use when encoding/decoding a message's payload.</param>
/// <param name="ExternalDocs">Additional external documentation.</param>
public record AsyncApiPrototype(
string Id,
Info? Info,
Dictionary<string, Server>? Servers,
string? DefaultContentType,
ExternalDocumentation? ExternalDocs);
12 changes: 12 additions & 0 deletions src/Saunter.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Saunter;
using Saunter.Cli;

ConsoleAppBuilder builder = ConsoleApp.CreateBuilder(args);

builder.ConfigureServices(s => s.AddAsyncApiSchemaGeneration());

ConsoleApp app = builder.Build();

app.AddSubCommands<SpecificationGeneratorCommand>();

app.Run();
15 changes: 15 additions & 0 deletions src/Saunter.Cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Saunter.Cli

A simple utility for generating async api specifications from a set of dlls.

## Idea

For aot and trim assemblies, it is inject at the post-build stage, сreates a json document that is included in the build and used by the running application.

## Usage

So far, just an example:

```bash
saunter-cli specification generate -p saunter/examples/StreetlightsAPI/bin/Release/net6.0/ --prototype '{ "id": "tester" }'
```
20 changes: 20 additions & 0 deletions src/Saunter.Cli/Saunter.Cli.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ConsoleAppFramework" Version="4.2.4" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Saunter\Saunter.csproj" />
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions src/Saunter.Cli/Saunter.Cli.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Saunter.Cli", "Saunter.Cli.csproj", "{CD1C0B2D-99A8-4C86-B90B-ED20A654BCD3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{CD1C0B2D-99A8-4C86-B90B-ED20A654BCD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CD1C0B2D-99A8-4C86-B90B-ED20A654BCD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CD1C0B2D-99A8-4C86-B90B-ED20A654BCD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CD1C0B2D-99A8-4C86-B90B-ED20A654BCD3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B4A02E6B-5357-475B-9CDA-233CFFE79CCB}
EndGlobalSection
EndGlobal
83 changes: 83 additions & 0 deletions src/Saunter.Cli/SpecificationGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Reflection;
using System.Runtime.Loader;

using Saunter.AsyncApiSchema.v2;
using Saunter.Serialization;

namespace Saunter.Cli;

/// <summary>
/// Generate documentation from dll
/// </summary>
[Command("specification", "Generate documentation from dll")]
public class SpecificationGeneratorCommand : ConsoleAppBase
{
private readonly IAsyncApiDocumentProvider _provider;
private readonly IAsyncApiDocumentSerializer _serializer;

/// <summary>
/// Documentation generator from dll
/// </summary>
public SpecificationGeneratorCommand(IAsyncApiDocumentProvider provider, IAsyncApiDocumentSerializer serializer)
{
_provider = provider;
_serializer = serializer;
}

/// <summary>
/// Generate specification from dll directory
/// </summary>
/// <param name="dllDirectory"></param>
/// <param name="prototype"></param>
/// <returns></returns>
[Command("generate", "Generate specification from dll directory")]
public async Task Generate([Option("p", "The path containing the target application's dll")] string dllDirectory, AsyncApiPrototype prototype)
{
AsyncApiDocument documentPrototype = new()
{
Id = prototype.Id,
};

if (!string.IsNullOrEmpty(prototype.DefaultContentType))
{
documentPrototype.DefaultContentType = prototype.DefaultContentType;
}

if (prototype.Info is not null)
{
documentPrototype.Info = prototype.Info;
}

if (prototype.Servers is not null)
{
documentPrototype.Servers = prototype.Servers;
}

if (prototype.ExternalDocs is not null)
{
documentPrototype.ExternalDocs = prototype.ExternalDocs;
}

AsyncApiOptions options = new()
{
AsyncApi = documentPrototype,
};

string[] dllFiles = Directory.GetFiles(dllDirectory, "*.dll");

foreach (string dllFile in dllFiles)
{
Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllFile);

options.Assemblies.Add(assembly);
}

AsyncApiDocument document = _provider.GetDocument(options, options.AsyncApi);

string jsonSpecification = _serializer.Serialize(document);

await using StreamWriter writer = File.CreateText("asyncapi.json");

await writer.WriteAsync(jsonSpecification);
}
}
14 changes: 9 additions & 5 deletions src/Saunter/AsyncApiOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
Expand All @@ -16,10 +17,9 @@ namespace Saunter
{
public class AsyncApiOptions
{
private readonly List<Type> _documentFilters = new List<Type>();
private readonly List<Type> _channelItemFilters = new List<Type>();
private readonly List<Type> _operationFilters = new List<Type>();

private readonly List<Type> _documentFilters = new();
private readonly List<Type> _channelItemFilters = new();
private readonly List<Type> _operationFilters = new();

/// <summary>
/// The base asyncapi schema. This will be augmented with other information auto-discovered
Expand All @@ -32,6 +32,11 @@ public class AsyncApiOptions
/// </summary>
public IList<Type> AssemblyMarkerTypes { get; set; } = new List<Type>();

/// <summary>
/// A list of assemblies to scan for Saunter attributes.
/// </summary>
public IList<Assembly> Assemblies { get; set; } = new List<Assembly>();

/// <summary>
/// A list of filters that will be applied to the generated AsyncAPI document.
/// </summary>
Expand Down Expand Up @@ -73,7 +78,6 @@ public void AddOperationFilter<T>() where T : IOperationFilter
}



/// <summary>
/// Options related to the Saunter middleware.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Saunter/AsyncApiServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static IServiceCollection AddAsyncApiSchemaGeneration(this IServiceCollec
/// Add a named AsyncAPI document to the service collection.
/// </summary>
/// <param name="services">The collection to add the document to.</param>
/// <param name="documentName">The name used to refer to the document. Used in the <see cref="Saunter.Attributes.AsyncApiAttribute"/> and in middleware HTTP paths.</param>
/// <param name="documentName">The name used to refer to the document. Used in the <see cref="Attributes.AsyncApiAttribute"/> and in middleware HTTP paths.</param>
/// <param name="setupAction">An action used to configure the named document.</param>
/// <returns>The service collection so additional calls can be chained.</returns>
public static IServiceCollection ConfigureNamedAsyncApi(this IServiceCollection services, string documentName, Action<AsyncApiDocument> setupAction)
Expand Down
11 changes: 7 additions & 4 deletions src/Saunter/Generation/AsyncApiDocumentProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@
/// </summary>
private static TypeInfo[] GetAsyncApiTypes(AsyncApiOptions options, AsyncApiDocument prototype)
{
var assembliesToScan = options.AssemblyMarkerTypes.Select(t => t.Assembly).Distinct();

var asyncApiTypes = assembliesToScan
.SelectMany(a => a.DefinedTypes.Where(t => t.GetCustomAttribute<AsyncApiAttribute>()?.DocumentName == prototype.DocumentName))
var asyncApiTypes = options
.AssemblyMarkerTypes
.Select(t => t.Assembly)
.Distinct()
.Union(options.Assemblies)
.SelectMany(a => a.DefinedTypes

Check warning on line 44 in src/Saunter/Generation/AsyncApiDocumentProvider.cs

View workflow job for this annotation

GitHub Actions / build

Using member 'System.Reflection.Assembly.DefinedTypes.get' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.

Check warning on line 44 in src/Saunter/Generation/AsyncApiDocumentProvider.cs

View workflow job for this annotation

GitHub Actions / build

Using member 'System.Reflection.Assembly.DefinedTypes.get' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed.
.Where(t => t.GetCustomAttribute<AsyncApiAttribute>()?.DocumentName == prototype.DocumentName))
.ToArray();

return asyncApiTypes;
Expand Down
6 changes: 1 addition & 5 deletions src/Saunter/Generation/DocumentGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ namespace Saunter.Generation
{
public class DocumentGenerator : IDocumentGenerator
{
public DocumentGenerator()
{
}

public AsyncApiSchema.v2.AsyncApiDocument GenerateDocument(TypeInfo[] asyncApiTypes, AsyncApiOptions options, AsyncApiDocument prototype, IServiceProvider serviceProvider)
public AsyncApiDocument GenerateDocument(TypeInfo[] asyncApiTypes, AsyncApiOptions options, AsyncApiDocument prototype, IServiceProvider serviceProvider)
{
var asyncApiSchema = prototype.Clone();

Expand Down
3 changes: 3 additions & 0 deletions src/Saunter/Saunter.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Saunter.IntegrationTests.ReverseProxy": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:65155;http://localhost:65158"
}
}
}
Comment on lines +1 to +12

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be added to the .gitignore?

2 changes: 1 addition & 1 deletion test/Saunter.Tests/ServiceCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public void TestAddAsyncApiSchemaGeneration()
var services = new ServiceCollection() as IServiceCollection;
services.AddAsyncApiSchemaGeneration(options =>
{
options.AsyncApi = new AsyncApiSchema.v2.AsyncApiDocument
options.AsyncApi = new AsyncApiDocument
{
Id = "urn:com:example:example-events",
Info = new Info("Example API", "2019.01.12345")
Expand Down
Loading