Skip to content

Commit 2fded6c

Browse files
committed
Drive C# codegen from @seamapi/blueprint only
Replace the OpenAPI-spec parsing port with a generator that depends solely on @seamapi/blueprint, removing every TEMPORARY/TODO output-parity workaround. - smith.ts wires the @seamapi/smith `blueprint` plugin and the csharp plugin reads the resolved blueprint from Metalsmith metadata. - build-model.ts normalizes blueprint resources, endpoints, parameters, and properties into the durable class-model, resolving int vs. float, enum members, inline objects, and discriminated unions directly from the blueprint (no schema traversal). - csharp.ts iterates blueprint.resources / routes and emits action_attempt and event as discriminated unions. - Delete the OpenAPI parsing helpers (lib/openapi/*), the ported types.ts, and schema-modifications.ts. - Regenerate the C# output. Model properties are now uniformly optional and nullable (the blueprint does not carry per-property required/nullable metadata); Api class names derive from the route path. Prune generated files for routes/resources no longer present in the blueprint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ThCbfBhJb4Hydvo8Es4qJA
1 parent f460307 commit 2fded6c

109 files changed

Lines changed: 74751 additions & 89039 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

codegen/lib/build-model.ts

Lines changed: 448 additions & 414 deletions
Large diffs are not rendered by default.

codegen/lib/class-model.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Durable data model for the C# SDK codegen.
22
//
33
// These interfaces hold the resolved structure of each generated file, decoupled
4-
// from serialization. build-model.ts produces them from the (raw-OpenAPI)
5-
// schemas; the Handlebars layouts and their context builders turn them into C#.
6-
// String serialization lives entirely in the templates.
4+
// from serialization. build-model.ts produces them from the @seamapi/blueprint;
5+
// the Handlebars layouts turn them into C#. String serialization lives entirely
6+
// in the templates.
77

88
// A single enum member, e.g. `[EnumMember(Value = "setting")] Setting = 1,`.
99
export interface CsEnumMember {

codegen/lib/csharp.ts

Lines changed: 57 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,81 @@
1-
import * as types from '@seamapi/types/connect'
1+
import type { Blueprint, Endpoint } from '@seamapi/blueprint'
22
import { pascalCase } from 'change-case'
33
import type Metalsmith from 'metalsmith'
44

5-
import { buildApiFile, buildModelFile } from './build-model.js'
6-
import { GLOBAL_NAMESPACE } from './constants.js'
7-
import { deepFlattenAllOfSchema } from './openapi/flatten-obj-schema.js'
8-
import { getFilteredRoutes } from './openapi/get-filtered-routes.js'
9-
import { getParameterAndResponseSchema } from './openapi/get-parameter-and-response-schema.js'
10-
import { modifySchemaForSpecialCases } from './schema-modifications.js'
11-
import type { ObjSchema, OpenAPISchema, PropertySchema } from './types.js'
5+
import {
6+
buildActionAttemptFile,
7+
buildApiFile,
8+
buildEventFile,
9+
buildModelFile,
10+
} from './build-model.js'
1211

1312
const outputRoot = 'output/csharp/src/Seam'
1413

15-
interface RouteInfo {
16-
methodName: string
17-
path: string
18-
parameterSchema: ObjSchema
19-
responseObjType: string | undefined
20-
responseArrType: string | undefined
21-
isVoid: boolean
22-
nullable: boolean
23-
returnPath: string
24-
}
14+
// Resource types that are emitted as discriminated unions rather than plain
15+
// model classes.
16+
const UNION_RESOURCE_TYPES = new Set(['event', 'action_attempt'])
17+
18+
// Derives the Api class name from a route path: the path segments in reverse,
19+
// pascal-cased (e.g. /acs/credential_pools -> CredentialPoolsAcs).
20+
const apiClassName = (path: string): string =>
21+
pascalCase(path.split('/').filter(Boolean).reverse().join('_'))
2522

26-
// Metalsmith plugin that generates the schema-derived C# SDK files: the Api
23+
// Metalsmith plugin that generates the blueprint-derived C# SDK files: the Api
2724
// route classes (output/csharp/src/Seam/Api/*.cs) and the resource models
2825
// (output/csharp/src/Seam/Model/*.cs). Static, schema-independent files (the
29-
// Client/* runtime, the two static Model helpers, the .sln, the test project)
30-
// are normal committed package source and are intentionally NOT generated here.
26+
// Client/* runtime, the static Model helpers, the .sln, the test project) are
27+
// normal committed package source and are intentionally NOT generated here.
3128
//
32-
// The iteration reads the raw OpenAPI spec from @seamapi/types rather than
33-
// @seamapi/blueprint so the generated output stays byte-identical to the
34-
// previous generator.
35-
// TODO: Drive iteration and structure from metalsmith.metadata().blueprint once
36-
// the generated output is allowed to change. Blueprint is not wired into the
37-
// pipeline: the port does not use blueprint data, and @seamapi/blueprint does
38-
// not currently parse the pinned @seamapi/types.
39-
export const csharp = (files: Metalsmith.Files): void => {
40-
const openapi = types.openapi as unknown as OpenAPISchema
41-
42-
const classMap: Record<string, RouteInfo[]> = {}
43-
44-
for (const route of getFilteredRoutes(openapi)) {
45-
if (!route.post) continue
46-
if (!route.post['x-fern-sdk-group-name']) continue
47-
48-
// TODO: Use blueprint route/namespace names once the generated output is
49-
// allowed to change. The class name reverses x-fern-sdk-group-name (e.g.
50-
// ['acs', 'credential_pools'] -> CredentialPoolsAcs), a load-bearing quirk
51-
// of the previous generator that must be preserved for output parity.
52-
const groupNames = [...route.post['x-fern-sdk-group-name']]
53-
groupNames.reverse()
54-
const className = pascalCase(groupNames.join('_'))
55-
56-
const {
57-
parameter_schema: parameterSchema,
58-
response_obj_type: responseObjType,
59-
response_arr_type: responseArrType,
60-
nullable,
61-
response_schema: responseSchema,
62-
} = getParameterAndResponseSchema(route)
29+
// The blueprint is placed on the Metalsmith metadata by the @seamapi/smith
30+
// `blueprint` plugin, which must run before this one.
31+
export const csharp = (
32+
files: Metalsmith.Files,
33+
metalsmith: Metalsmith,
34+
): void => {
35+
const { blueprint } = metalsmith.metadata() as { blueprint: Blueprint }
6336

64-
// TODO: Determine void vs. returning endpoints from
65-
// @seamapi/blueprint endpoint.response once the generated output is allowed
66-
// to change. This reproduces the previous generator's filter, including its
67-
// `ok`-property and x-response-key special-casing, from the raw OpenAPI.
68-
let isVoid = false
69-
if (!responseObjType && !responseArrType) {
70-
if (
71-
!responseSchema ||
72-
'oneOf' in responseSchema ||
73-
(Object.keys(responseSchema.properties).filter(
74-
(k) => k.toLowerCase() !== 'ok',
75-
).length > 0 &&
76-
route.post['x-response-key'] !== null)
77-
) {
78-
continue
79-
}
80-
isVoid = true
37+
const writeModel = (name: string, file: unknown): void => {
38+
files[`${outputRoot}/Model/${name}.cs`] = {
39+
contents: Buffer.from('\n'),
40+
layout: 'model.hbs',
41+
...(file as object),
8142
}
43+
}
8244

83-
if (!parameterSchema) continue
45+
for (const resource of blueprint.resources) {
46+
if (UNION_RESOURCE_TYPES.has(resource.resourceType)) continue
47+
const { name, file } = buildModelFile(resource)
48+
writeModel(name, file)
49+
}
8450

85-
;(classMap[className] ??= []).push({
86-
methodName: route.post['x-fern-sdk-method-name'],
87-
path: route.path,
88-
parameterSchema,
89-
responseObjType,
90-
responseArrType,
91-
isVoid,
92-
nullable,
93-
returnPath: route.post['x-fern-sdk-return-value'],
94-
})
51+
if (blueprint.actionAttempts.length > 0) {
52+
const { name, file } = buildActionAttemptFile(blueprint.actionAttempts)
53+
writeModel(name, file)
9554
}
9655

97-
for (const [className, routes] of Object.entries(classMap)) {
98-
const apiFile = buildApiFile(className, routes)
99-
files[`${outputRoot}/Api/${apiFile.className}.cs`] = {
100-
contents: Buffer.from('\n'),
101-
layout: 'api.hbs',
102-
...apiFile,
103-
}
56+
if (blueprint.events.length > 0) {
57+
const { name, file } = buildEventFile(blueprint.events)
58+
writeModel(name, file)
10459
}
10560

106-
for (const [schemaName, rawSchema] of Object.entries(
107-
openapi.components.schemas,
108-
)) {
109-
let schema = modifySchemaForSpecialCases(
110-
schemaName,
111-
rawSchema as PropertySchema,
61+
const endpointsByClass = new Map<string, Endpoint[]>()
62+
for (const route of blueprint.routes) {
63+
if (route.isUndocumented) continue
64+
const endpoints = route.endpoints.filter(
65+
(endpoint) => !endpoint.isUndocumented,
11266
)
67+
if (endpoints.length === 0) continue
68+
const className = apiClassName(route.path)
69+
const existing = endpointsByClass.get(className) ?? []
70+
endpointsByClass.set(className, [...existing, ...endpoints])
71+
}
11372

114-
if ('allOf' in schema) {
115-
const flattened = deepFlattenAllOfSchema(schema)
116-
if (flattened == null) continue
117-
schema = flattened
118-
}
119-
120-
const { name, file } = buildModelFile(schemaName, schema, [
121-
...GLOBAL_NAMESPACE,
122-
'Model',
123-
])
124-
files[`${outputRoot}/Model/${name}.cs`] = {
73+
for (const [className, endpoints] of endpointsByClass) {
74+
const apiFile = buildApiFile(className, endpoints)
75+
files[`${outputRoot}/Api/${apiFile.className}.cs`] = {
12576
contents: Buffer.from('\n'),
126-
layout: 'model.hbs',
127-
...file,
77+
layout: 'api.hbs',
78+
...apiFile,
12879
}
12980
}
13081
}

codegen/lib/openapi/deep-flatten-one-of-and-all-of-schema.ts

Lines changed: 0 additions & 136 deletions
This file was deleted.

0 commit comments

Comments
 (0)