genoc 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +30 -186
  2. package/dist/analyzer/analyze.d.ts +18 -0
  3. package/dist/analyzer/analyze.js +534 -0
  4. package/dist/analyzer/path-analyzer.d.ts +43 -6
  5. package/dist/analyzer/path-analyzer.js +57 -84
  6. package/dist/analyzer/schema-mapper.d.ts +3 -2
  7. package/dist/analyzer/schema-mapper.js +11 -7
  8. package/dist/analyzer/types.d.ts +119 -0
  9. package/dist/cli/app.d.ts +4 -2
  10. package/dist/cli/app.js +23 -4
  11. package/dist/cli/config-loader.d.ts +20 -0
  12. package/dist/cli/config-loader.js +186 -0
  13. package/dist/cli/config-schema.d.ts +105 -0
  14. package/dist/cli/config-schema.js +183 -0
  15. package/dist/cli/impl.d.ts +9 -1
  16. package/dist/cli/impl.js +186 -36
  17. package/dist/generator/client-generator.d.ts +13 -14
  18. package/dist/generator/client-generator.js +45 -77
  19. package/dist/generator/contracts-generator.d.ts +11 -9
  20. package/dist/generator/contracts-generator.js +45 -491
  21. package/dist/generator/method-generator.d.ts +1 -1
  22. package/dist/generator/method-generator.js +10 -150
  23. package/dist/parser/ref-resolver.d.ts +1 -1
  24. package/dist/parser/ref-resolver.js +1 -2
  25. package/dist/parser/version/index.d.ts +4 -16
  26. package/dist/parser/version/index.js +3 -14
  27. package/dist/parser/version/v3.0/validator.d.ts +1 -1
  28. package/dist/parser/version/v3.1/validator.d.ts +17 -0
  29. package/dist/parser/version/v3.1/validator.js +126 -0
  30. package/dist/parser/version/validate.d.ts +9 -0
  31. package/dist/parser/version/validate.js +12 -0
  32. package/dist/parser/version/version-detector.d.ts +21 -0
  33. package/dist/parser/version/version-detector.js +19 -0
  34. package/dist/pipeline.d.ts +13 -0
  35. package/dist/pipeline.js +24 -0
  36. package/dist/types/client.d.ts +0 -12
  37. package/dist/utils/generator-helpers.d.ts +1 -24
  38. package/dist/utils/generator-helpers.js +11 -78
  39. package/dist/utils/operation-naming.d.ts +84 -0
  40. package/dist/utils/operation-naming.js +178 -0
  41. package/package.json +4 -8
  42. package/dist/index.d.ts +0 -9
  43. package/dist/index.js +0 -11
  44. package/dist/parser/validators.d.ts +0 -7
  45. package/dist/parser/validators.js +0 -79
  46. package/dist/parser/version/normalized-spec.d.ts +0 -199
  47. package/dist/parser/version/registry.d.ts +0 -28
  48. package/dist/parser/version/registry.js +0 -44
  49. package/dist/parser/version/v3.0/index.d.ts +0 -3
  50. package/dist/parser/version/v3.0/index.js +0 -3
  51. package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
  52. package/dist/parser/version/v3.0/normalizer.js +0 -389
  53. package/dist/parser/version/v3.0/strategy.d.ts +0 -27
  54. package/dist/parser/version/v3.0/strategy.js +0 -96
  55. package/dist/parser/version/v3.1/index.d.ts +0 -1
  56. package/dist/parser/version/v3.1/index.js +0 -1
  57. package/dist/parser/version/v3.1/strategy.d.ts +0 -42
  58. package/dist/parser/version/v3.1/strategy.js +0 -513
  59. package/dist/parser/version/v3.2/index.d.ts +0 -4
  60. package/dist/parser/version/v3.2/index.js +0 -4
  61. package/dist/parser/version/v3.2/strategy.d.ts +0 -39
  62. package/dist/parser/version/v3.2/strategy.js +0 -57
  63. package/dist/parser/version/version-strategy.d.ts +0 -31
  64. package/dist/parser/version/version-strategy.js +0 -1
  65. /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
package/dist/cli/impl.js CHANGED
@@ -1,49 +1,199 @@
1
- import { generateFullOutput } from '../generator/client-generator.js';
2
1
  import { load } from '../parser/spec-reader.js';
3
- import { validateSpec } from '../parser/validators.js';
4
- import { defaultRegistry } from '../parser/version/index.js';
2
+ import { resolveVersion, validateSpec } from '../parser/version/index.js';
3
+ import { generateFullOutput } from '../pipeline.js';
5
4
  import { assertValidProxyUrl } from '../utils/proxy.js';
5
+ import { loadConfigFile } from './config-loader.js';
6
6
  import { UserError } from './errors.js';
7
+ /**
8
+ * Compare-to-default table for flags that declare a default in app.ts.
9
+ *
10
+ * A defaulted flag counts as "explicitly passed" only when its value differs
11
+ * from the declared default. KNOWN LIMITATION: an explicitly-passed flag whose
12
+ * value equals the declared default (e.g. `--method-name-strategy path-based`)
13
+ * is indistinguishable from an omitted flag, so a config-file value wins in
14
+ * that case. This is pinned by a characterization test.
15
+ *
16
+ * `strictVersion` avoids this limitation entirely: it is declared WITHOUT a
17
+ * default (`strictVersion?: boolean`), so `undefined` reliably means "not
18
+ * passed" and any explicit value — including `true` — beats the file value.
19
+ */
20
+ const FLAG_DEFAULTS = {
21
+ methodNameStrategy: 'path-based',
22
+ strictVersion: true,
23
+ };
24
+ function mergeExplicitFlags(flags) {
25
+ return {
26
+ outputDir: flags.outputDir,
27
+ methodNameStrategy: flags.methodNameStrategy === FLAG_DEFAULTS.methodNameStrategy
28
+ ? undefined
29
+ : flags.methodNameStrategy,
30
+ specVersion: flags.specVersion,
31
+ strictVersion: flags.strictVersion,
32
+ runtimeImportPath: flags.runtimeImportPath,
33
+ proxy: flags.proxy,
34
+ };
35
+ }
36
+ function buildTarget(name, input, cli, file) {
37
+ const outputDir = cli.outputDir ?? file?.outputDir;
38
+ if (outputDir === undefined) {
39
+ throw new UserError('No output directory provided. Pass --output-dir, or set "outputDir" in a config file ' +
40
+ '(.genocrc.yml / .genocrc.json).');
41
+ }
42
+ return {
43
+ name,
44
+ input,
45
+ outputDir,
46
+ methodNameStrategy: cli.methodNameStrategy ?? file?.methodNameStrategy ?? FLAG_DEFAULTS.methodNameStrategy,
47
+ specVersion: cli.specVersion ?? file?.specVersion,
48
+ strictVersion: cli.strictVersion ?? file?.strictVersion ?? FLAG_DEFAULTS.strictVersion,
49
+ runtimeImportPath: cli.runtimeImportPath ?? file?.runtimeImportPath,
50
+ proxy: cli.proxy ?? file?.proxy,
51
+ };
52
+ }
53
+ /**
54
+ * Display name for a target that does not come from a `clients` map entry:
55
+ * the spec filename base (`petstore.yaml` -> `petstore`, URL last segment for
56
+ * remote specs). Chosen over the outputDir basename because several targets
57
+ * can share an input spec, but the input is what identifies the client being
58
+ * generated in the failure/aggregation output.
59
+ */
60
+ export function targetNameFromInput(input) {
61
+ const withoutQuery = input.split('?')[0] ?? input;
62
+ const base = withoutQuery.split(/[\\/]/).pop() ?? withoutQuery;
63
+ const dot = base.lastIndexOf('.');
64
+ return dot > 0 ? base.slice(0, dot) : base;
65
+ }
66
+ function processCwd(proc) {
67
+ const cwd = proc.cwd;
68
+ return cwd === undefined ? process.cwd() : cwd.call(proc);
69
+ }
70
+ async function resolveTargets(flags, spec, context) {
71
+ const loaded = await loadConfigFile({
72
+ explicitPath: flags.config,
73
+ searchDir: processCwd(context.process),
74
+ });
75
+ const cli = mergeExplicitFlags(flags);
76
+ if (loaded !== null) {
77
+ const { config, filepath } = loaded;
78
+ if ('clients' in config) {
79
+ if (spec !== undefined) {
80
+ throw new UserError(`Config file ${filepath} defines named clients; a spec argument cannot be combined ` +
81
+ 'with it. Use --project <name> to select one client, or omit the spec argument to run all clients.');
82
+ }
83
+ const names = Object.keys(config.clients);
84
+ if (flags.project !== undefined) {
85
+ const entry = config.clients[flags.project];
86
+ if (entry === undefined) {
87
+ throw new UserError(`Unknown client name "${flags.project}" for --project. Available clients: ${names.join(', ')}`);
88
+ }
89
+ return {
90
+ targets: [buildTarget(flags.project, entry.input, cli, entry)],
91
+ configDriven: true,
92
+ };
93
+ }
94
+ if (cli.outputDir !== undefined) {
95
+ throw new UserError(`--output-dir cannot be combined with running all clients from config file ${filepath}: ` +
96
+ 'every client would be written to the same directory. Use --project <name> to select a ' +
97
+ 'single client, or remove --output-dir.');
98
+ }
99
+ const targets = names.map((name) => buildTarget(name, config.clients[name].input, cli, config.clients[name]));
100
+ return { targets, configDriven: true };
101
+ }
102
+ if (flags.project !== undefined) {
103
+ throw new UserError(`Config file ${filepath} defines no clients; --project requires a multi-client config ` +
104
+ '(a "clients" map).');
105
+ }
106
+ return resolveFlatOrSingleTarget(spec, cli, config, true);
107
+ }
108
+ if (flags.project !== undefined) {
109
+ throw new UserError('No config file found; --project requires a config file (.genocrc.yml / .genocrc.json) ' +
110
+ 'that defines clients.');
111
+ }
112
+ return resolveFlatOrSingleTarget(spec, cli, undefined, false);
113
+ }
114
+ function resolveFlatOrSingleTarget(spec, cli, flat, configDriven) {
115
+ const input = spec ?? flat?.input;
116
+ if (input === undefined) {
117
+ if (cli.outputDir === undefined && flat?.outputDir === undefined) {
118
+ throw new UserError('No spec input and no output directory were provided. Pass a spec path as the positional ' +
119
+ 'argument and --output-dir, or use a config file (.genocrc.yml / .genocrc.json) that ' +
120
+ 'provides "input" and "outputDir".');
121
+ }
122
+ throw new UserError('No spec input provided. Pass a spec path as the positional argument, or set "input" in a ' +
123
+ 'config file (.genocrc.yml / .genocrc.json).');
124
+ }
125
+ return {
126
+ targets: [buildTarget(targetNameFromInput(input), input, cli, flat)],
127
+ configDriven,
128
+ };
129
+ }
130
+ async function runOneTarget(target, context) {
131
+ if (target.proxy) {
132
+ try {
133
+ assertValidProxyUrl(target.proxy);
134
+ }
135
+ catch (err) {
136
+ throw new UserError(err.message);
137
+ }
138
+ }
139
+ context.process.stdout.write(`Loading spec from ${target.input}...\n`);
140
+ const doc = await load(target.input, { proxy: target.proxy });
141
+ context.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
142
+ const override = target.specVersion;
143
+ if (override !== undefined && override !== '3.0' && override !== '3.1') {
144
+ throw new Error(`No strategy registered for version: ${override}`);
145
+ }
146
+ const profile = resolveVersion(doc, override);
147
+ if (override !== undefined &&
148
+ target.strictVersion !== false &&
149
+ profile.detected !== profile.effective) {
150
+ context.process.stderr.write(`Warning: Specified version ${override} does not match detected version ${profile.detected}\n`);
151
+ }
152
+ try {
153
+ validateSpec(doc, profile.effective);
154
+ }
155
+ catch (error) {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ throw new UserError(message);
158
+ }
159
+ const config = {
160
+ input: target.input,
161
+ outputDir: target.outputDir,
162
+ methodNameStrategy: target.methodNameStrategy,
163
+ strictVersion: target.strictVersion,
164
+ runtimeImportPath: target.runtimeImportPath,
165
+ };
166
+ const { preserveRefSiblings } = profile;
167
+ context.process.stdout.write('Generating client...\n');
168
+ await generateFullOutput(doc, config, { preserveRefSiblings });
169
+ context.process.stdout.write(`✅ Success! Generated client files:\n`);
170
+ context.process.stdout.write(` - ${target.outputDir}/contracts.ts\n`);
171
+ context.process.stdout.write(` - ${target.outputDir}/client.ts\n`);
172
+ context.process.stdout.write(` - ${target.outputDir}/index.ts\n`);
173
+ }
7
174
  export default async function (flags, spec) {
8
175
  try {
9
- if (flags.proxy) {
176
+ const { targets, configDriven } = await resolveTargets(flags, spec, this);
177
+ const failures = [];
178
+ for (const target of targets) {
10
179
  try {
11
- assertValidProxyUrl(flags.proxy);
180
+ await runOneTarget(target, this);
12
181
  }
13
- catch (err) {
14
- throw new UserError(err.message);
182
+ catch (error) {
183
+ if (!configDriven) {
184
+ throw error;
185
+ }
186
+ const message = error instanceof Error ? error.message : String(error);
187
+ failures.push({ name: target.name, message });
15
188
  }
16
189
  }
17
- this.process.stdout.write(`Loading spec from ${spec}...\n`);
18
- const doc = await load(spec, { proxy: flags.proxy });
19
- this.process.stdout.write(`Loaded OpenAPI ${doc.openapi} spec\n`);
20
- const strategy = flags.specVersion
21
- ? defaultRegistry.get(flags.specVersion)
22
- : defaultRegistry.detectAndResolve(doc);
23
- if (flags.specVersion && flags.strictVersion !== false) {
24
- const detected = defaultRegistry.detectAndResolve(doc);
25
- if (detected.version() !== flags.specVersion) {
26
- this.process.stderr.write(`Warning: Specified version ${flags.specVersion} does not match detected version ${detected.version()}\n`);
27
- }
190
+ if (failures.length > 0) {
191
+ const lines = failures.map((failure) => `× client "${failure.name}": ${failure.message}`);
192
+ throw new UserError(`${lines.join('\n')}\n${failures.length}/${targets.length} targets failed`);
193
+ }
194
+ if (configDriven) {
195
+ this.process.stdout.write(`Generated ${targets.length}/${targets.length} clients\n`);
28
196
  }
29
- const validation = validateSpec(doc, strategy);
30
- if (!validation.valid) {
31
- throw new UserError(`Invalid OpenAPI specification:\n${validation.errors.map((e) => ` - ${e}`).join('\n')}`);
32
- }
33
- const config = {
34
- input: spec,
35
- outputDir: flags.outputDir,
36
- methodNameStrategy: flags.methodNameStrategy || 'path-based',
37
- specVersion: flags.specVersion,
38
- strictVersion: flags.strictVersion,
39
- runtimeImportPath: flags.runtimeImportPath,
40
- };
41
- const preserveRefSiblings = strategy.version() === '3.1';
42
- this.process.stdout.write('Generating client...\n');
43
- await generateFullOutput(doc, config, { preserveRefSiblings });
44
- this.process.stdout.write(`✅ Success! Generated client files:\n`);
45
- this.process.stdout.write(` - ${flags.outputDir}/contracts.ts\n`);
46
- this.process.stdout.write(` - ${flags.outputDir}/client.ts\n`);
47
197
  }
48
198
  catch (error) {
49
199
  if (error instanceof UserError) {
@@ -1,21 +1,20 @@
1
+ import type { AnalyzedSpec } from '../analyzer/types.js';
1
2
  import type { GeneratorConfig } from '../types/client.js';
2
- import type { OpenAPIDocument } from '../types/openapi.js';
3
- export type ApiClient = {
4
- [key: string]: (...args: any[]) => Promise<any>;
5
- };
6
- /** Options for controlling generation behavior. */
7
- export interface GenerationOptions {
8
- /** When true, sibling properties alongside $ref are preserved (OpenAPI 3.1 behavior). */
9
- preserveRefSiblings?: boolean;
10
- }
11
3
  /**
12
- * Generate both the contracts and client file content from an OpenAPI document.
4
+ * Render the client and index barrel files from the analyzed model.
5
+ * Pure string assembly — reads ONLY the AnalyzedSpec plus config, never the
6
+ * raw spec or a resolver.
13
7
  */
14
- export declare function generateClient(doc: OpenAPIDocument, config: GeneratorConfig, options?: GenerationOptions): {
15
- contracts: string;
8
+ export declare function renderClient(analyzed: AnalyzedSpec, config: GeneratorConfig): {
16
9
  client: string;
10
+ index: string;
17
11
  };
18
12
  /**
19
- * Generate and write both output files to disk.
13
+ * Generate the contracts, client, and index barrel file content from the
14
+ * analyzed model. Thin orchestration over the two renderers.
20
15
  */
21
- export declare function generateFullOutput(doc: OpenAPIDocument, config: GeneratorConfig, options?: GenerationOptions): Promise<void>;
16
+ export declare function generateOutput(analyzed: AnalyzedSpec, config: GeneratorConfig): {
17
+ contracts: string;
18
+ client: string;
19
+ index: string;
20
+ };
@@ -1,37 +1,12 @@
1
- import { mkdir, writeFile } from 'fs/promises';
2
- import { join } from 'path';
3
- import { analyzePaths } from '../analyzer/path-analyzer.js';
4
- import { RefResolver } from '../parser/ref-resolver.js';
5
- import { DEFAULT_RUNTIME_IMPORT_PATH, getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
6
- import { generateContracts } from './contracts-generator.js';
1
+ import { DEFAULT_RUNTIME_IMPORT_PATH, makeHeader } from '../utils/generator-helpers.js';
2
+ import { clientImportedNames, clientValueImports, getErrorType, getSuccessType, operationEmissions, } from '../utils/operation-naming.js';
3
+ import { renderContracts } from './contracts-generator.js';
7
4
  import { generateMethod } from './method-generator.js';
8
5
  function collectImportTypes(operations) {
9
6
  const types = new Set();
10
7
  for (const op of operations) {
11
- const prefix = getOperationTypePrefix(op);
12
- if (op.queryParams.length > 0) {
13
- types.add(`${prefix}Query`);
14
- }
15
- if (op.headerParams.length > 0) {
16
- types.add(`${prefix}Headers`);
17
- }
18
- if (op.requestBody?.schema) {
19
- types.add(`${prefix}Body`);
20
- }
21
- const successType = getSuccessType(op);
22
- if (successType !== 'void' && successType !== 'unknown' && /^[A-Z]/.test(successType)) {
23
- types.add(successType);
24
- }
25
- const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
26
- for (const errResp of errorResponses) {
27
- types.add(`${prefix}Error${errResp.statusCode}`);
28
- }
29
- const errorType = getErrorType(op);
30
- if (errorType !== 'never') {
31
- types.add(errorType);
32
- }
33
- if (op.responses.some((r) => !r.isSuccess && r.statusCode === 'default')) {
34
- types.add(`${prefix}DefaultError`);
8
+ for (const name of clientImportedNames(op)) {
9
+ types.add(name);
35
10
  }
36
11
  }
37
12
  // oxlint-disable-next-line unicorn/no-array-sort
@@ -39,21 +14,18 @@ function collectImportTypes(operations) {
39
14
  }
40
15
  function buildClientMethodBody(op) {
41
16
  const successType = getSuccessType(op);
17
+ const emissions = operationEmissions(op);
42
18
  let urlTemplate = op.path;
43
19
  for (const param of op.pathParams) {
44
20
  urlTemplate = urlTemplate.replace(`{${param.name}}`, `\${encodeURIComponent(${param.name})}`);
45
21
  }
46
22
  const urlExpr = `\`${urlTemplate}\``;
47
- const prefix = getOperationTypePrefix(op);
48
- const hasDefaultResponse = op.responses.some((r) => !r.isSuccess && r.statusCode === 'default');
49
- const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
50
23
  const errorCheckLines = [];
51
- for (const errResp of errorResponses) {
52
- const status = errResp.statusCode;
53
- errorCheckLines.push(`if (result.status === ${status}) throw new ApiError(${status}, result.data as ${prefix}Error${status}, result.message ?? \`Request failed with status ${status}\`);`);
24
+ for (const statusError of emissions.statusErrors) {
25
+ errorCheckLines.push(`if (result.status === ${statusError.status}) throw new ApiError(${statusError.status}, result.data as ${statusError.name}, result.message ?? \`Request failed with status ${statusError.status}\`);`);
54
26
  }
55
- if (hasDefaultResponse) {
56
- errorCheckLines.push(`throw new DefaultApiError(result.status, result.data as ${prefix}DefaultError, result.message ?? \`Request failed with status \${result.status}\`);`);
27
+ if (emissions.defaultError !== undefined) {
28
+ errorCheckLines.push(`throw new DefaultApiError(result.status, result.data as ${emissions.defaultError}, result.message ?? \`Request failed with status \${result.status}\`);`);
57
29
  }
58
30
  else {
59
31
  errorCheckLines.push('throw new UnspecifiedApiError(result.status, result.data, result.message ?? `Request failed with status ${result.status}`);');
@@ -96,26 +68,21 @@ function buildClientMethodBody(op) {
96
68
  opts.push('headers');
97
69
  }
98
70
  const lines = [];
99
- if (op.requestBody?.isMultipart && op.requestBody.schema) {
100
- const schema = op.requestBody.schema;
101
- const requiredSet = new Set((schema?.required ?? []));
102
- const properties = schema.properties ?? {};
103
- const propNames = Object.keys(properties);
71
+ if (op.requestBody?.isMultipart && op.fileUploadProperties !== undefined) {
104
72
  const bodyRequired = op.requestBody.required;
105
73
  const formDataLines = [];
106
74
  formDataLines.push('const formData = new FormData();');
107
- for (const propName of propNames) {
108
- const propSchema = properties[propName];
109
- const isArrayBinary = propSchema?.type === 'array' && propSchema?.items?.format === 'binary';
110
- if (propSchema?.format === 'binary') {
111
- if (requiredSet.has(propName)) {
75
+ for (const prop of op.fileUploadProperties) {
76
+ const propName = prop.name;
77
+ if (prop.kind === 'file') {
78
+ if (prop.required) {
112
79
  formDataLines.push(`formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
113
80
  }
114
81
  else {
115
82
  formDataLines.push(`if (body.${propName} !== undefined) formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
116
83
  }
117
84
  }
118
- else if (isArrayBinary) {
85
+ else if (prop.kind === 'file-array') {
119
86
  formDataLines.push(`if (body.${propName} !== undefined) { for (const file of body.${propName}) { formData.append("${propName}", file.data, file.filename); } }`);
120
87
  }
121
88
  else {
@@ -160,17 +127,7 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
160
127
  const lines = [];
161
128
  lines.push(makeHeader(version));
162
129
  const importTypes = collectImportTypes(operations);
163
- const needsDefaultApiError = operations.some((op) => op.responses.some((r) => !r.isSuccess && r.statusCode === 'default'));
164
- const valueImports = [
165
- 'ApiError',
166
- 'UnspecifiedApiError',
167
- 'ErrorResponse',
168
- 'StreamResponse',
169
- 'RequesterFailError',
170
- ];
171
- if (needsDefaultApiError) {
172
- valueImports.push('DefaultApiError');
173
- }
130
+ const valueImports = clientValueImports(operations);
174
131
  const typeImports = importTypes.filter((t) => !valueImports.includes(t));
175
132
  lines.push(`import { ${valueImports.join(', ')} } from './contracts.js';`);
176
133
  if (typeImports.length > 0) {
@@ -206,8 +163,8 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
206
163
  const returnType = sig.substring(closeParenColon + 3);
207
164
  const methodName = method.name;
208
165
  const errorType = getErrorType(op);
209
- const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
210
- const errorCodes = errorResponses.map((r) => r.statusCode);
166
+ const emissions = operationEmissions(op);
167
+ const errorCodes = emissions.statusErrors.map((statusError) => statusError.status);
211
168
  const errorCodeArray = errorCodes.length > 0 ? `[${errorCodes.join(', ')}] as const` : '[] as const';
212
169
  const indentedBody = body
213
170
  .split('\n')
@@ -227,24 +184,35 @@ function buildClientFile(operations, version, runtimeImportPath = DEFAULT_RUNTIM
227
184
  return lines.join('\n');
228
185
  }
229
186
  /**
230
- * Generate both the contracts and client file content from an OpenAPI document.
187
+ * Generate the `index.ts` barrel file content: re-exports everything from the
188
+ * generated contracts and client files.
231
189
  */
232
- export function generateClient(doc, config, options) {
233
- const resolver = new RefResolver(doc, undefined, {
234
- preserveRefSiblings: options?.preserveRefSiblings,
235
- });
190
+ function buildIndexFile(version) {
191
+ const lines = [];
192
+ lines.push(makeHeader(version));
193
+ lines.push('');
194
+ lines.push(`export * from './contracts.js';`);
195
+ lines.push(`export * from './client.js';`);
196
+ return lines.join('\n') + '\n';
197
+ }
198
+ /**
199
+ * Render the client and index barrel files from the analyzed model.
200
+ * Pure string assembly — reads ONLY the AnalyzedSpec plus config, never the
201
+ * raw spec or a resolver.
202
+ */
203
+ export function renderClient(analyzed, config) {
236
204
  const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
237
- const contracts = generateContracts(doc, resolver, runtimeImportPath);
238
- const operations = analyzePaths(doc, resolver, config.methodNameStrategy ?? 'path-based');
239
- const client = buildClientFile(operations, doc.openapi, runtimeImportPath);
240
- return { contracts, client };
205
+ const client = buildClientFile(analyzed.operations, analyzed.specVersion, runtimeImportPath);
206
+ const index = buildIndexFile(analyzed.specVersion);
207
+ return { client, index };
241
208
  }
242
209
  /**
243
- * Generate and write both output files to disk.
210
+ * Generate the contracts, client, and index barrel file content from the
211
+ * analyzed model. Thin orchestration over the two renderers.
244
212
  */
245
- export async function generateFullOutput(doc, config, options) {
246
- const { contracts, client } = generateClient(doc, config, options);
247
- await mkdir(config.outputDir, { recursive: true });
248
- await writeFile(join(config.outputDir, 'contracts.ts'), contracts, 'utf-8');
249
- await writeFile(join(config.outputDir, 'client.ts'), client, 'utf-8');
213
+ export function generateOutput(analyzed, config) {
214
+ const runtimeImportPath = config.runtimeImportPath ?? DEFAULT_RUNTIME_IMPORT_PATH;
215
+ const contracts = renderContracts(analyzed, runtimeImportPath);
216
+ const { client, index } = renderClient(analyzed, config);
217
+ return { contracts, client, index };
250
218
  }
@@ -1,15 +1,17 @@
1
- import { RefResolver } from '../parser/ref-resolver.js';
2
- import type { OpenAPIDocument } from '../types/openapi.js';
1
+ import type { AnalyzedSpec } from '../analyzer/types.js';
3
2
  /**
4
- * Generate the complete `*.contracts.ts` file content as a string.
3
+ * Render the complete `*.contracts.ts` file content from the analyzed model.
4
+ * Pure string assembly — reads ONLY the AnalyzedSpec, never the raw spec or
5
+ * a resolver.
5
6
  *
6
7
  * Sections produced:
7
8
  * 1. Header comment
8
- * 2. Schema types from `components/schemas`
9
- * 3. Query parameter types per operation
10
- * 4. Header parameter types per operation
11
- * 5. Request body types per operation
12
- * 6. Response / error types per operation
9
+ * 2. Schema types from `components/schemas` (finished model entries)
10
+ * 3. Security scheme types (finished model entries; `SecuritySchemes` union
11
+ * when more than one)
12
+ * 4. Server variable types (finished model entries)
13
+ * 5. `FileInput` when any operation uploads files
14
+ * 6. Operation-derived types (finished emission lines from the model)
13
15
  * 7. Runtime re-export block (shared classes from the genoc runtime package)
14
16
  */
15
- export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver, runtimeImportPath?: string): string;
17
+ export declare function renderContracts(analyzed: AnalyzedSpec, runtimeImportPath?: string): string;