Skip to content

Commit

Permalink
create api code gen tool
Browse files Browse the repository at this point in the history
* use .g.cs for generated code files
* convert all records to mutable
  • Loading branch information
0xor1 committed Aug 4, 2024
1 parent 4f07074 commit 447a394
Show file tree
Hide file tree
Showing 21 changed files with 398 additions and 18 deletions.
4 changes: 2 additions & 2 deletions Common.Client/Common.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>0xor1.Common.Client</PackageId>
<Version>4.0.4</Version>
<Version>5.0.0</Version>
<Authors>Daniel Robinson</Authors>
<Company>Personal</Company>
<Product>Personal</Product>
Expand All @@ -18,7 +18,7 @@

<ItemGroup>
<PackageReference Include="DartSassBuilder" Version="1.0.0" />
<PackageReference Include="0xor1.Common.Shared" Version="4.0.4" />
<PackageReference Include="0xor1.Common.Shared" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.0" />
Expand Down
301 changes: 301 additions & 0 deletions Common.Cmds/Api.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
using Cocona;
using Common.Shared;
using Fluid;
using Fluid.Values;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

namespace Common.Cmds;

public class Api
{
private const string ApiFile =
"""
// Generated Code File, Do Not Edit.
// This file is generated with Common.Cmds.
// see https://github.com/0xor1/common/blob/main/Common.Cmds/Api.cs
// executed with arguments: api {{YmlDirPath}}
using Common.Shared;
using Common.Shared.Auth;
{% for sec in Sections %}using {{ApiNameSpace}}.{{sec.Key}};
{% endfor %}
namespace {{ApiNameSpace}};
public interface IApi : Common.Shared.Auth.IApi
{
{% for sec in Sections %}public I{{sec.Key}}Api {{sec.Key}} { get; }
{% endfor %}
}
public class Api : IApi
{
public Api(IRpcClient client)
{
App = new AppApi(client);
Auth = new AuthApi(client);
{% for sec in Sections %}{{sec.Key}} = new {{sec.Key}}Api(client);
{% endfor %}
}
public IAppApi App { get; }
public IAuthApi Auth { get; }
{% for sec in Sections %}public I{{sec.Key}}Api {{sec.Key}} { get; }
{% endfor %}
}
""";

private const string SectionFile =
"""
// Generated Code File, Do Not Edit.
// This file is generated with Common.Cmds.
// see https://github.com/0xor1/common/blob/main/Common.Cmds/Api.cs
// executed with arguments: api {{YmlDirPath}}
#nullable enable
using Common.Shared;
using MessagePack;
{% for import in Imports %}using {{import}};
{% endfor %}
namespace {{ApiNameSpace}}.{{Key}};
public interface I{{Key}}Api
{
{% for ep in Eps %}public {% if ep.FullyQualifyTask %}System.Threading.Tasks.{% endif %}Task{% if ep.Res != "Nothing" %}<{{ep.Res}}>{% endif %} {{ep.Key}}({% if ep.Arg != "Nothing" %}{{ep.Arg}} arg, {% endif %}CancellationToken ctkn = default);{% if ep.GetUrl %}
public string {{ep.Key}}Url({% if ep.Arg != "Nothing" %}{{ep.Arg}} arg{% endif %});{% endif %}
{% endfor %}
}
public class {{Key}}Api : I{{Key}}Api
{
private readonly IRpcClient _client;
public {{Key}}Api(IRpcClient client)
{
_client = client;
}
{% for ep in Eps %}public {% if ep.FullyQualifyTask %}System.Threading.Tasks.{% endif %}Task{% if ep.Res != "Nothing" %}<{{ep.Res}}>{% endif %} {{ep.Key}}({% if ep.Arg != "Nothing" %}{{ep.Arg}} arg, {% endif %}CancellationToken ctkn = default) =>
_client.Do({{Key}}Rpcs.{{ep.Key}}, {% if ep.Arg != "Nothing" %}arg{% else %}Nothing.Inst{% endif %}, ctkn);
{% if ep.GetUrl %}
public string {{ep.Key}}Url({% if ep.Arg != "Nothing" %}{{ep.Arg}} arg{% endif %}) =>
_client.GetUrl({{Key}}Rpcs.{{ep.Key}}, {% if ep.Arg != "Nothing" %}arg{% else %}Nothing.Inst{% endif %});
{% endif %}
{% endfor %}
}
public static class {{Key}}Rpcs
{
{% for ep in Eps %}public static readonly Rpc<{{ep.Arg}}, {{ep.Res}}> {{ep.Key}} = new("/{{Key | lowerfirst }}/{{ep.Key | lowerfirst}}");
{% endfor %}
}
{% for type in Types %}
{% if type.IsInterface %}
public interface {{type.Key}}{% if type.Extends %} : {{type.Extends}}{% endif %}
{
{% for field in type.Fields %}[Key({{forloop.index0}})]
public {{field.Type}} {{field.Key}} { get; set; }{% if field.Default %} = {{field.Default}};{% endif %}
{% endfor %}
}
{% else %}
[MessagePackObject]
public record {{type.Key}}{% if type.Extends %} : {{type.Extends}}{% endif %}
{
public {{type.Key}}(
{% for field in type.Fields %}{{field.Type}} {{field.Key | lowerfirst }}{% if field.Default %} = {{field.Default}}{% endif %}{% unless forloop.last %},{% endunless %}
{% endfor %}
)
{
{% for field in type.Fields %}{{field.Key}} = {{field.Key | lowerfirst }};
{% endfor %}
}
{% for field in type.Fields %}[Key({{forloop.index0}})]
public {{field.Type}} {{field.Key}} { get; set; }{% if field.Default %} = {{field.Default}};{% endif %}
{% endfor %}
}
{% endif %}
{% endfor %}
{% for enum in Enums %}
public enum {{enum.Key}}
{
{% for val in enum.Vals %}{{val}}{% unless forloop.last %},{% endunless %}
{% endfor %}
}
{% endfor %}
""";

private const string ymlFileName = "api.yml";
private const string ApiFileName = "Api.g.cs";

[Command("api")]
public async Task Run([Argument] string ymlDirPath)
{
var ymlPathSegs = ymlDirPath.Split(['/', '\\']);
var @namespace = ymlPathSegs.Last();
var slnDirPath = ymlPathSegs.Take(ymlPathSegs.Length - 1);
var printCsvDirPath = $"<abs_file_path_to>/{@namespace}";
using var reader = new StreamReader(Path.Join(ymlDirPath, ymlFileName));
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance) // see height_in_inches in sample yml
.Build();
var apiDef = deserializer.Deserialize<ApiDef>(reader);
apiDef.Validate();

apiDef.ApiNameSpace = @namespace;
apiDef.YmlDirPath = printCsvDirPath;
apiDef.Sections.ForEach(x =>
{
x.YmlDirPath = printCsvDirPath;
x.ApiNameSpace = @namespace;
});

var options = new TemplateOptions();
options.Filters.AddFilter("lowerfirst", (input, arguments, context) =>
{
var source = input.ToStringValue().ToCharArray();
if (source.Length > 0)
{
source[0] = char.ToLower(source[0]);
}
return new StringValue(new string(source));
});
options.MemberAccessStrategy.Register<ApiDef>();
options.MemberAccessStrategy.Register<Section>();
options.MemberAccessStrategy.Register<Type>();
options.MemberAccessStrategy.Register<Enum>();
options.MemberAccessStrategy.Register<Field>();
options.MemberAccessStrategy.Register<Ep>();

var fParser = new FluidParser();
var apiFileTpl = fParser.Parse(ApiFile).NotNull();
var sectionFileTpl = fParser.Parse(SectionFile).NotNull();

await File.WriteAllTextAsync(Path.Join(ymlDirPath, ApiFileName), apiFileTpl.Render(new TemplateContext(apiDef, options)));
foreach (var s in apiDef.Sections)
{
await File.WriteAllTextAsync(Path.Join(ymlDirPath, Path.Join(s.Key, $"{s.Key}.g.cs")), sectionFileTpl.Render(new TemplateContext(s, options)));
}
}
}

public class ApiDef
{
private readonly List<string> _reservedSectionKeys = new (){ "app", "auth" };

public string YmlDirPath { get; set; }
public string ApiNameSpace { get; set; }
public List<Section> Sections { get; set; } = new();

public void Validate()
{
var errors = new List<string>();
var dupes = Sections.Select(x => x.Key).GetDuplicates();
if (dupes.Any())
{
errors.Add($"duplicate section keys: {dupes.Join()}");
}

if (Sections.Any(x => _reservedSectionKeys.Contains(x.Key)))
{
errors.Add($"reserved section keys: {_reservedSectionKeys.Join()}");
}

Sections.ForEach(x => x.Validate(errors));

if (errors.Any())
{
throw new InvalidDataException(string.Join("\n", errors));
}
}
}
public class Section
{
private readonly List<string> _reservedTypeKeys = new (){"nothing"};

public string YmlDirPath { get; set; }
public string ApiNameSpace { get; set; }
public List<string> Imports { get; set; } = new();
public string Key { get; set; }
public List<Type> Types { get; set; } = new();
public List<Type> Interfaces { get; set; } = new();
public List<Enum> Enums { get; set; } = new();
public List<Ep> Eps { get; set; } = new();

public void Validate(List<string> errors)
{
var dupes = Types.Select(x => x.Key).GetDuplicates();
if (dupes.Any())
{
errors.Add($"section: {Key}, duplicate type keys: {dupes.Join()}");
}
dupes = Enums.Select(x => x.Key).GetDuplicates();
if (dupes.Any())
{
errors.Add($"section: {Key}, duplicate enums keys: {dupes.Join()}");
}
dupes = Eps.Select(x => x.Key).GetDuplicates();
if (dupes.Any())
{
errors.Add($"section: {Key}, duplicate eps keys: {dupes.Join()}");
}
if (Types.Any(x => _reservedTypeKeys.Contains(x.Key)))
{
errors.Add($"section: {Key}, reserved type key used: {Key}, reserved keys: {_reservedTypeKeys.Join()}");
}
}
}

public class Type
{
public string Extends { get; set; }

public bool IsInterface { get; set; }
public string Key { get; set; }
public List<Field> Fields { get; set; } = new();

public void Validate(List<string> errors)
{
var dupes = Fields.Select(x => x.Key).GetDuplicates();
if (dupes.Any())
{
errors.Add($"type: {Key}, duplicate field keys: {dupes.Join()}");
}
}
}

public class Enum
{
public string Key { get; set; }
public List<string> Vals { get; set; } = new();

public void Validate(List<string> errors)
{
var dupes = Vals.GetDuplicates();
if (dupes.Any())
{
errors.Add($"enum: {Key}, duplicate vals: {dupes.Join()}");
}
}
}

public class Field
{
public string Key { get; set; }
public string Type { get; set; }
public string Default { get; set; }
}

public class Ep
{
public string Key { get; set; }
public string Arg { get; set; }
public string Res { get; set; }
public bool GetUrl { get; set; }
public bool FullyQualifyTask { get; set; } = false;
}
5 changes: 3 additions & 2 deletions Common.Cmds/Common.Cmds.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>0xor1.Common.Cmds</PackageId>
<Version>4.0.4</Version>
<Version>5.0.0</Version>
<Authors>Daniel Robinson</Authors>
<Company>Personal</Company>
<Product>Personal</Product>
Expand All @@ -20,10 +20,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="0xor1.Common.Shared" Version="4.0.4" />
<PackageReference Include="0xor1.Common.Shared" Version="5.0.0" />
<PackageReference Include="Cocona" Version="2.2.0" />
<PackageReference Include="CsvHelper" Version="30.0.1" />
<PackageReference Include="Fluid.Core" Version="2.5.0" />
<PackageReference Include="YamlDotNet" Version="16.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
6 changes: 3 additions & 3 deletions Common.Cmds/I18n.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ public static partial class S

private const string CsvFileName = "strings.csv";
private const string Key = "key";
private const string KeysFileName = "Keys.cs";
private const string LibraryFileName = "SZLibrary.cs";
private const string KeysFileName = "Keys.g.cs";
private const string LibraryFileName = "SZLibrary.g.cs";

[Command("i18n")]
public async Task Run([Argument] string csvDirPath, [Argument] string @namespace, [Argument] bool @readonly, [Argument] string prefix = "")
Expand Down Expand Up @@ -108,7 +108,7 @@ public async Task Run([Argument] string csvDirPath, [Argument] string @namespace
await File.WriteAllTextAsync(Path.Join(csvDirPath, LibraryFileName), zlibFileTpl.Render(new TemplateContext(zlfm)));
foreach (var lang in langs)
{
await File.WriteAllTextAsync(Path.Join(csvDirPath, $"S{lang.ToUpper()}.cs"), langFileTpl.Render(new TemplateContext(lfms[lang])));
await File.WriteAllTextAsync(Path.Join(csvDirPath, $"S{lang.ToUpper()}.g.cs"), langFileTpl.Render(new TemplateContext(lfms[lang])));
}
}

Expand Down
1 change: 1 addition & 0 deletions Common.Cmds/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
var builder = CoconaApp.CreateBuilder();
builder.Services.AddLogging();
var app = builder.Build();
app.AddCommands<Api>();
app.AddCommands<I18n>();
app.AddCommands<Dnsk>();
await app.RunAsync();
4 changes: 2 additions & 2 deletions Common.Server/Common.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>0xor1.Common.Server</PackageId>
<Version>4.0.4</Version>
<Version>5.0.0</Version>
<Authors>Daniel Robinson</Authors>
<Company>Personal</Company>
<Product>Personal</Product>
Expand All @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="0xor1.Common.Shared" Version="4.0.4" />
<PackageReference Include="0xor1.Common.Shared" Version="5.0.0" />
<PackageReference Include="AWSSDK.S3" Version="3.7.300.2" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.2" />
<PackageReference Include="CSharpier.MsBuild" Version="0.26.2">
Expand Down
2 changes: 1 addition & 1 deletion Common.Shared/Common.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PackageId>0xor1.Common.Shared</PackageId>
<Version>4.0.4</Version>
<Version>5.0.0</Version>
<Authors>Daniel Robinson</Authors>
<Company>Personal</Company>
<Product>Personal</Product>
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 447a394

Please sign in to comment.