Skip to content

Commit 27b0967

Browse files
committed
refactor: Rework C# codegen into Handlebars templates
Replace the verbatim port of the nextlove C# AST serializer with the Handlebars-template + context architecture used by the python and javascript-http codegen. Serialization now lives in .hbs templates driven by a durable data model and model builder; TEMPORARY banners are reserved for the raw-OpenAPI parsing helpers only, matching the python migration. - Delete the verbatim AST framework (codegen.ts), the serializer (dataclass.ts), and the main loop (generate-csharp-sdk.ts). - Add durable class-model.ts (data interfaces) and build-model.ts (the traversal that produces the data model, preserving every output quirk: group-name reversal, is_void logic, DataContract naming, member order, inline enums, oneOf/JsonSubtypes unions, DeviceProperties forceNullable, errors/warnings message override, reserved keywords, Object vs object). - Add .hbs templates: model.hbs, api.hbs and partials (model-class, enum-def, data-member, oneof-union, route-methods, tostring); load partials via getHandlebarsPartials; add csParams/csNamedArgs/eq helpers. - Stop vendoring static .cs into codegen: the Client/* runtime, the two static Model helpers, and the .sln are normal committed package source under output/csharp/, not codegen assets (remove templates/fs/** and populate-fs.ts). The generator now emits only schema-derived Api/*.cs and Model/*.cs, mirroring how the python codegen only writes seam/routes/. - Remove dead endpoint-rules.ts. Generated output verified byte-identical after csharpier 0.29.2; npm run lint, typecheck, and format pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018oCdmrhyAEX8e9sAFRXZAF
1 parent 79bb7ad commit 27b0967

35 files changed

Lines changed: 956 additions & 5136 deletions

codegen/layouts/api.hbs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Runtime.Serialization;
2+
using System.Text;
3+
using JsonSubTypes;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Converters;
6+
using Newtonsoft.Json.Linq;
7+
using Seam.Client;
8+
using Seam.Model;
9+
10+
namespace Seam.Api
11+
{
12+
public class {{className}}
13+
{
14+
private ISeamClient _seam;
15+
16+
public {{className}}(ISeamClient seam)
17+
{
18+
_seam = seam;
19+
}
20+
{{#each routes}}
21+
22+
{{> model-class request}}
23+
{{#each requestSiblings}}
24+
25+
{{> model-class this}}
26+
{{/each}}
27+
{{#if response}}
28+
29+
{{> model-class response}}
30+
{{/if}}
31+
{{#each responseSiblings}}
32+
33+
{{> model-class this}}
34+
{{/each}}
35+
36+
{{> route-methods this}}
37+
{{/each}}
38+
}
39+
}
40+
41+
namespace Seam.Client
42+
{
43+
public partial class SeamClient
44+
{
45+
public Api.{{className}} {{className}} => new(this);
46+
}
47+
48+
public partial interface ISeamClient
49+
{
50+
public Api.{{className}} {{className}} { get; }
51+
}
52+
}

codegen/layouts/model.hbs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Runtime.Serialization;
2+
using System.Text;
3+
using JsonSubTypes;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Converters;
6+
using Newtonsoft.Json.Linq;
7+
using Seam.Model;
8+
9+
namespace Seam.Model
10+
{
11+
{{#each decls}}
12+
{{#if (eq kind "union")}}
13+
{{> oneof-union this}}
14+
{{else}}
15+
{{> model-class this}}
16+
{{/if}}
17+
18+
{{/each}}
19+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[DataMember(Name = "{{snakeName}}", IsRequired = {{isRequired}}, EmitDefaultValue = false)]
2+
public {{#if isOverride}}override {{/if}}{{type}} {{pascalName}} {{#if getOnly}}{ get; }{{else}}{ get; set; }{{/if}}{{#if initializer}} = {{initializer}};{{/if}}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{#if isString}}
2+
[JsonConverter(typeof(SafeStringEnumConverter))]
3+
{{/if}}
4+
public enum {{name}}
5+
{
6+
{{#each members}}
7+
[EnumMember(Value = {{#if ../isString}}"{{value}}"{{else}}{{value}}{{/if}})]
8+
{{identifier}} = {{assign}},
9+
{{#unless @last}}
10+
11+
{{/unless}}
12+
{{/each}}
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[DataContract(Name = "{{dataContractName}}")]
2+
public class {{className}}{{#if baseClass}} : {{baseClass}}{{/if}}
3+
{
4+
[JsonConstructorAttribute]
5+
{{#if properties.length}}protected{{else}}public{{/if}} {{className}}() { }
6+
{{#if properties.length}}
7+
8+
public {{className}}({{csParams properties}})
9+
{
10+
{{#each properties}}
11+
{{pascalName}} = {{camelName}};
12+
{{/each}}
13+
}
14+
{{/if}}
15+
{{#each nested}}
16+
17+
{{#if enum}}
18+
{{> enum-def enum}}
19+
{{else}}
20+
{{> oneof-union union}}
21+
{{/if}}
22+
{{/each}}
23+
{{#each properties}}
24+
25+
{{> data-member this}}
26+
{{/each}}
27+
28+
{{> tostring}}
29+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[JsonConverter(typeof(JsonSubtypes), "{{discriminatorSnake}}")]
2+
[JsonSubtypes.FallBackSubType(typeof({{unrecognizedTypeName}}))]
3+
{{#each knownSubTypes}}
4+
[JsonSubtypes.KnownSubType(typeof({{typeName}}), "{{value}}")]
5+
{{/each}}
6+
public abstract class {{className}}
7+
{
8+
{{#each abstractProps}}
9+
public abstract {{type}} {{pascalName}} {{#if getOnly}}{ get; }{{else}}{ get; set; }{{/if}}
10+
11+
{{/each}}
12+
public abstract override string ToString();
13+
}
14+
{{#each subclasses}}
15+
16+
{{> model-class this}}
17+
{{/each}}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
public {{#if isVoid}}void{{else}}{{returnType}}{{/if}} {{methodName}}({{methodName}}Request request)
2+
{
3+
var requestOptions = new RequestOptions();
4+
requestOptions.Data = request;
5+
{{#if isVoid}}
6+
_seam.Post<object>("{{path}}", requestOptions);
7+
{{else}}
8+
return _seam.Post<{{responseTypeArg}}>("{{path}}", requestOptions).Data.{{returnProp}};
9+
{{/if}}
10+
}
11+
12+
public {{#if isVoid}}void{{else}}{{returnType}}{{/if}} {{methodName}}({{csParams params}})
13+
{
14+
{{#if isVoid}}
15+
{{methodName}}(new {{methodName}}Request({{csNamedArgs params}}));
16+
{{else}}
17+
return {{methodName}}(new {{methodName}}Request({{csNamedArgs params}}));
18+
{{/if}}
19+
}
20+
21+
public async {{#if isVoid}}Task{{else}}Task<{{returnType}}>{{/if}} {{methodName}}Async({{methodName}}Request request)
22+
{
23+
var requestOptions = new RequestOptions();
24+
requestOptions.Data = request;
25+
{{#if isVoid}}
26+
await _seam.PostAsync<object>("{{path}}", requestOptions);
27+
{{else}}
28+
return (await _seam.PostAsync<{{responseTypeArg}}>("{{path}}", requestOptions)).Data.{{returnProp}};
29+
{{/if}}
30+
}
31+
32+
public async {{#if isVoid}}Task{{else}}Task<{{returnType}}>{{/if}} {{methodName}}Async({{csParams params}})
33+
{
34+
{{#if isVoid}}
35+
await {{methodName}}Async(new {{methodName}}Request({{csNamedArgs params}}));
36+
{{else}}
37+
return (await {{methodName}}Async(new {{methodName}}Request({{csNamedArgs params}})));
38+
{{/if}}
39+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
public override string ToString()
2+
{
3+
JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(null);
4+
5+
StringWriter stringWriter = new StringWriter(
6+
new StringBuilder(256),
7+
System.Globalization.CultureInfo.InvariantCulture
8+
);
9+
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(stringWriter))
10+
{
11+
jsonTextWriter.IndentChar = ' ';
12+
jsonTextWriter.Indentation = 2;
13+
jsonTextWriter.Formatting = Formatting.Indented;
14+
jsonSerializer.Serialize(jsonTextWriter, this, null);
15+
}
16+
17+
return stringWriter.ToString();
18+
}

0 commit comments

Comments
 (0)