Skip to content

Commit

Permalink
asyncapi#207 generate code from spec: json only code generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Senn Geerts authored and Senn Geerts committed Jul 20, 2024
1 parent 4af2a99 commit 23b8e4c
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

<ItemGroup>
<!-- Instruct "AsyncAPI.Saunter.Generator.Build" to generate classes for this AsyncAPI specifiction -->
<AsyncAPISpecs Include="specs/streetlights.yml" OutputPath="generated" Namespace="Saunter" />
<AsyncAPISpecs Include="specs/test.yml" OutputPath="generated2" Namespace="SaunterX" />
<AsyncAPISpecs Include="specs/streetlights.json" OutputPath="generated" Namespace="Saunter" />
<!-- <AsyncAPISpecs Include="specs/streetlights.json" OutputPath="generated2" Namespace="SaunterX" />-->
</ItemGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand Down Expand Up @@ -46,7 +46,6 @@
</ItemGroup>

<ItemGroup>

<Compile Include="../StreetlightsAPI/API.cs" />
<Compile Include="../StreetlightsAPI/Messaging.cs" />

Expand All @@ -61,7 +60,10 @@
</ItemGroup>

<ItemGroup>
<Folder Include="generated\" />
<Folder Include="generated/" />
<Folder Include="generated2/" />
<Compile Remove="generated2/*" />
<None Include="generated2/*" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

<Target Name="AsyncAPIInitial">
<Message Text="===== AsyncAPI 999.$([System.DateTime]::Now.ToString(&quot;yy&quot;))$([System.DateTime]::Now.DayOfYear).$([System.DateTime]::Now.ToString(&quot;HHmm.ss&quot;)) ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI ===== AsyncAPI =====" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPISpecs: @(AsyncAPISpecs->'%(DefiningProjectDirectory)%(Identity)')" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPISpecs.Outputs: @(AsyncAPISpecs->'%(DefiningProjectDirectory)%(OutputPath)/%(Filename).g.cs')" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPISpecs : @(AsyncAPISpecs->'%(DefiningProjectDirectory)%(Identity)')" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPISpecs.Outputs : @(AsyncAPISpecs->'%(DefiningProjectDirectory)%(OutputPath)/%(Filename).g.cs')" Importance="High" />
</Target>

<Target Name="AsyncAPICommonBuildProperties">
Expand All @@ -15,13 +15,18 @@
<AsyncAPICliToolPath>$([System.IO.Path]::Combine($(AsyncAPIBuildToolRoot), tools, net8.0, AsyncAPI.Saunter.Generator.Cli.dll))</AsyncAPICliToolPath>
</PropertyGroup>

<Message Text="AsyncAPI.Generator.Build; AsyncAPICliToolPath: $(AsyncAPICliToolPath)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIBuildToolBuildDir: $(AsyncAPIBuildToolBuildDir)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIBuildToolRoot: $(AsyncAPIBuildToolRoot)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPICliToolPath : $(AsyncAPICliToolPath)" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIBuildToolBuildDir : $(AsyncAPIBuildToolBuildDir)" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIBuildToolRoot : $(AsyncAPIBuildToolRoot)" Importance="High" />

<Message Text="AsyncAPI.Generator.Build; MSBuildThisFile : $(MSBuildThisFile)" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; MSBuildThisFileDirectory : $(MSBuildThisFileDirectory)" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; MSBuildProjectFullPath : $(MSBuildProjectFullPath)" Importance="High" />
<Message Text="AsyncAPI.Generator.Build; MSBuildProjectDirectory : $(MSBuildProjectDirectory)" Importance="High" />
</Target>

<!-- AsyncAPIGenerateDocumentsOnBuild: Generate .json/.yml spec at build time from code-first attributes -->
<Target Name="PostBuild" AfterTargets="PostBuildEvent" DependsOnTargets="AsyncAPICommonBuildProperties"
<Target Name="PostBuild" AfterTargets="PostBuildEvent" DependsOnTargets="AsyncAPICommonBuildProperties"
Condition=" '$(AsyncAPIGenerateDocumentsOnBuild)' == 'true' ">
<PropertyGroup>
<AsyncAPICliToolStartupAssembly>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), $(OutputPath), $(AssemblyTitle).dll))</AsyncAPICliToolStartupAssembly>
Expand All @@ -30,28 +35,23 @@

<!-- Debug output: print some paths, set -v flag (verbosity) at least to [n]ormal to show in build output -->
<Message Text="AsyncAPI.Generator.Build; AsyncAPICliToolStartupAssembly: $(AsyncAPICliToolStartupAssembly)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPICliToolOutputPath: $(AsyncAPICliToolOutputPath)" />

<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentFormats: $(AsyncAPIDocumentFormats)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentOutputPath: $(AsyncAPIDocumentOutputPath)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentNames: $(AsyncAPIDocumentNames)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentFilename: $(AsyncAPIDocumentFilename)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentEnvVars: $(AsyncAPIDocumentEnvVars)" />
<Message Text="AsyncAPI.Generator.Build; MSBuildThisFile: $(MSBuildThisFile)" />
<Message Text="AsyncAPI.Generator.Build; MSBuildThisFileDirectory: $(MSBuildThisFileDirectory)" />
<Message Text="AsyncAPI.Generator.Build; MSBuildProjectFullPath: $(MSBuildProjectFullPath)" />
<Message Text="AsyncAPI.Generator.Build; MSBuildProjectDirectory: $(MSBuildProjectDirectory)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPICliToolOutputPath : $(AsyncAPICliToolOutputPath)" />

<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentFormats : $(AsyncAPIDocumentFormats)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentOutputPath : $(AsyncAPIDocumentOutputPath)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentNames : $(AsyncAPIDocumentNames)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentFilename : $(AsyncAPIDocumentFilename)" />
<Message Text="AsyncAPI.Generator.Build; AsyncAPIDocumentEnvVars : $(AsyncAPIDocumentEnvVars)" />

<Exec Command="dotnet &quot;$(AsyncAPICliToolPath)&quot; tofile &quot;$(AsyncAPICliToolStartupAssembly)&quot; --output &quot;$(AsyncAPICliToolOutputPath)&quot; --format &quot;$(AsyncAPIDocumentFormats)&quot; --doc &quot;$(AsyncAPIDocumentNames)&quot; --filename &quot;$(AsyncAPIDocumentFilename)&quot; --env &quot;$(AsyncAPIDocumentEnvVars)&quot;"
WorkingDirectory="$(AsyncAPIBuildToolRoot)" />
</Target>

<!-- AsyncAPISpecs: Generate dataclasses from .json/.yml AsyncAPI spec files -->
<Target Name="GenerateCodeForAsyncAPI" BeforeTargets="BeforeBuild" DependsOnTargets="AsyncAPICommonBuildProperties"
Condition=" '@(AsyncAPISpecs)' != '' "
Inputs="@(AsyncAPISpecs)" Outputs="@(AsyncAPISpecs->'%(DefiningProjectDirectory)%(OutputPath)/%(Filename).g.cs')">
<!-- https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata -->
<Message Text="Generate: --specs @(AsyncAPISpecs->'&quot;%(Namespace),%(OutputPath),%(Identity)&quot;', ' ')" Importance="High" />

<Exec Command="dotnet &quot;$(AsyncAPICliToolPath)&quot; fromspec --specs @(AsyncAPISpecs->'&quot;%(Namespace),%(DefiningProjectDirectory)%(OutputPath),%(DefiningProjectDirectory)%(Identity)&quot;', ' ')"
WorkingDirectory="$(AsyncAPIBuildToolRoot)" />
</Target>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@

<ItemGroup>
<PackageReference Include="AsyncAPI.NET.Readers" Version="5.2.1" />
<PackageReference Include="CaseConverter" Version="2.0.1" />
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.0.7" />
<PackageReference Include="ConsoleAppFramework" Version="5.2.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7" />

<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Reflection;
using System.Text;
using CaseConverter;
using LEGO.AsyncAPI.Models;
using LEGO.AsyncAPI.Readers;

namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;

internal interface IAsyncApiGenerator
{
string GenerateAsyncApiInterfaces(GeneratorOptions options, string text, AsyncApiState state);

string GenerateAsyncApiInterfaces(GeneratorOptions options, AsyncApiDocument asyncApi, AsyncApiState state);
}

internal class AsyncApiGenerator : IAsyncApiGenerator
{
private readonly string _version;
private readonly string _name;

public AsyncApiGenerator()
{
var assembly = Assembly.GetExecutingAssembly().GetName();

this._version = assembly.Version!.ToString();
this._name = assembly.Name;
}

private static string MakeGlobalDocumentTopic(string name) => $"TOPIC_{name.ToSnakeCase().ToUpperInvariant()}";

public string GenerateAsyncApiInterfaces(GeneratorOptions options, string text, AsyncApiState state)
{
var asyncApi = new AsyncApiStringReader().Read(text, out var diagnostic);
return this.GenerateAsyncApiInterfaces(options, asyncApi, state);
}

public string GenerateAsyncApiInterfaces(GeneratorOptions options, AsyncApiDocument asyncApi, AsyncApiState state)
{
var sb = new StringBuilder(
$$"""
//----------------------
// <auto-generated>
// Generated using {{this._name}} v{{this._version}}
// At: {{DateTime.Now:U}}
// </auto-generated>
//----------------------

using Saunter.Attributes;
using System.CodeDom.Compiler;

namespace {{options.Namespace}}
{
{{this.GetGeneratedCodeAttributeLine()}}
public static partial class {{options.TopicsClassName}}
{
""");

sb.AppendLine();
sb.AppendLine($" public const string {MakeGlobalDocumentTopic(options.ClassName)} = \"{options.ClassName.ToPascalCase()}\";");
sb.AppendLine(" }");
sb.AppendLine();

// Commands
this.AddInterface(sb, options, options.ClassName, $"{options.ClassName}Commands", new("SubscribeOperation", "command", x => x.Subscribe), asyncApi.Channels.Where(x => x.Value.Subscribe != null));
sb.AppendLine();

// Events
this.AddInterface(sb, options, options.ClassName, $"{options.ClassName}Events", new("PublishOperation", "evt", x => x.Publish), asyncApi.Channels.Where(x => x.Value.Publish != null));

sb.AppendLine("}"); // close namespace
sb.AppendLine();

state.Documents.Add(options.ClassName);
var contents = sb.ToString();
return contents;
}

private string GetGeneratedCodeAttributeLine() => $" [GeneratedCodeAttribute(\"{this._name}\", \"{this._version}\")]";

private void AddInterface(StringBuilder sb, GeneratorOptions genOptions, string specName, string className, OperationOptions options, IEnumerable<KeyValuePair<string, AsyncApiChannel>> channels)
{
sb.AppendLine($" [AsyncApi({genOptions.TopicsClassName}.{MakeGlobalDocumentTopic(specName)})]");
sb.AppendLine(this.GetGeneratedCodeAttributeLine());
sb.AppendLine($" public interface I{className}");
sb.Append(" {");

foreach (var channel in channels)
{
AddChannelOperation(sb, options, channel);
}

sb.AppendLine(" }");
}

private record OperationOptions(string OperationAttribute, string VarName, Func<AsyncApiChannel, AsyncApiOperation> OperationProvider);

private static void AddChannelOperation(StringBuilder sb, OperationOptions options, KeyValuePair<string, AsyncApiChannel> channel)
{
var operation = options.OperationProvider(channel.Value);

sb.AppendLine();
sb.AppendLine($" [Channel(\"{channel.Key}\")]");
var channelParametersSb = new StringBuilder();
foreach (var channelParameter in channel.Value.Parameters)
{
var refName = FromReference(channelParameter.Value.Reference.Reference);
var typeName = refName.ToPascalCase();
if (channelParameter.Value.Schema.Enum.Any())
{
typeName += "s";
}
var varName = refName.ToCamelCase();
sb.AppendLine($" [ChannelParameter(\"{channelParameter.Key}\", typeof({typeName}), Description = \"{channelParameter.Value.Description}\")]");

channelParametersSb.Append(", ");
channelParametersSb.Append(typeName);
channelParametersSb.Append(" ");
channelParametersSb.Append(varName);
}

var msg = operation.Message.Single();
var msgTypeName = (msg.Name ?? msg.Payload.Reference.Id).ToPascalCase();
sb.AppendLine($" [{options.OperationAttribute}(typeof({msgTypeName}), Summary = \"{operation.Summary}\", Description = \"{operation.Description}\")]");
sb.Append($" void {operation.OperationId}({msgTypeName} {options.VarName}");
sb.Append(channelParametersSb);
sb.AppendLine(");");
}

private static string FromReference(string reference)
{
var prefix = "#/components/parameters/";
if (reference.IndexOf(prefix) == 0)
{
return reference[prefix.Length..];
}
throw new ArgumentException($"Invalid reference: {reference}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.AsyncApiInterface;

internal class AsyncApiState
{
public List<string> Documents { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using NJsonSchema;
using NSwag.CodeGeneration.CSharp;
using NSwag;
using NJsonSchema.CodeGeneration.CSharp;


namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;

internal interface IDataTypesGenerator
{
Task<string> GenerateDataTypesAsync(GeneratorOptions options, string spec, DataTypesGeneratorState state);
}

internal class NSwagGenerator : IDataTypesGenerator
{
public async Task<string> GenerateDataTypesAsync(GeneratorOptions options, string spec, DataTypesGeneratorState state)
{
spec = OpenApiCompatibility.PrepareSpecFile(spec);

var document = await OpenApiDocument.FromJsonAsync(spec).ConfigureAwait(false);
var settings = new CSharpClientGeneratorSettings
{
CSharpGeneratorSettings =
{
Namespace = options.Namespace,
SchemaType = SchemaType.OpenApi3,
ClassStyle = CSharpClassStyle.Record,
ExcludedTypeNames = state.AlreadyGeneratedDataTypes.ToArray(),
},
GenerateClientClasses = false,
AdditionalNamespaceUsages = state.AlreadyGeneratedNamespaces.ToArray()
};

var generator = new CSharpClientGenerator(document, settings);
var contents = generator.GenerateFile();
state.AlreadyGeneratedDataTypes.AddRange(document.Definitions.Keys);
state.AlreadyGeneratedNamespaces.Add(options.Namespace);

return contents;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;

internal class DataTypesGeneratorState
{
public List<string> AlreadyGeneratedDataTypes { get; } = new();

public List<string> AlreadyGeneratedNamespaces { get; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;

namespace AsyncAPI.Saunter.Generator.Cli.FromSpec.DataTypes;

internal static class OpenApiCompatibility
{
internal static string PrepareSpecFile(string spec)
{
var json = (JObject)JsonConvert.DeserializeObject(spec);
// the type is important for NSwag
if (!json.ContainsKey("openapi"))
{
json.Add("openapi", "3.0.1");
}
// NSwag doesn't understand the servers format of AsyncApi, and it is not needed anyway.
if (json.ContainsKey("servers"))
{
json.Remove("servers");
}
return JsonConvert.SerializeObject(json);
}
}
Loading

0 comments on commit 23b8e4c

Please sign in to comment.