genoc 0.3.0 → 0.4.1
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.
- package/README.md +31 -187
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +493 -0
- package/dist/analyzer/discriminator-registry.d.ts +108 -0
- package/dist/analyzer/discriminator-registry.js +209 -0
- package/dist/analyzer/path-analyzer.d.ts +43 -6
- package/dist/analyzer/path-analyzer.js +57 -84
- package/dist/analyzer/schema-mapper.d.ts +68 -6
- package/dist/analyzer/schema-mapper.js +299 -40
- package/dist/analyzer/types.d.ts +126 -0
- package/dist/cli/app.d.ts +4 -2
- package/dist/cli/app.js +23 -4
- package/dist/cli/config-loader.d.ts +20 -0
- package/dist/cli/config-loader.js +186 -0
- package/dist/cli/config-schema.d.ts +105 -0
- package/dist/cli/config-schema.js +183 -0
- package/dist/cli/impl.d.ts +9 -1
- package/dist/cli/impl.js +189 -36
- package/dist/generator/client-generator.d.ts +13 -14
- package/dist/generator/client-generator.js +45 -77
- package/dist/generator/contracts-generator.d.ts +11 -9
- package/dist/generator/contracts-generator.js +45 -491
- package/dist/generator/method-generator.d.ts +1 -1
- package/dist/generator/method-generator.js +10 -150
- package/dist/parser/ref-resolver.d.ts +1 -1
- package/dist/parser/ref-resolver.js +1 -2
- package/dist/parser/version/index.d.ts +4 -16
- package/dist/parser/version/index.js +3 -14
- package/dist/parser/version/v3.0/validator.d.ts +1 -1
- package/dist/parser/version/v3.1/validator.d.ts +17 -0
- package/dist/parser/version/v3.1/validator.js +126 -0
- package/dist/parser/version/validate.d.ts +9 -0
- package/dist/parser/version/validate.js +12 -0
- package/dist/parser/version/version-detector.d.ts +21 -0
- package/dist/parser/version/version-detector.js +19 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.js +25 -0
- package/dist/types/client.d.ts +0 -12
- package/dist/utils/generator-helpers.d.ts +1 -24
- package/dist/utils/generator-helpers.js +11 -78
- package/dist/utils/operation-naming.d.ts +84 -0
- package/dist/utils/operation-naming.js +178 -0
- package/dist/utils/proxy.d.ts +2 -0
- package/dist/utils/proxy.js +16 -0
- package/dist/utils/string.d.ts +7 -0
- package/dist/utils/string.js +9 -0
- package/package.json +19 -15
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -11
- package/dist/parser/validators.d.ts +0 -7
- package/dist/parser/validators.js +0 -79
- package/dist/parser/version/normalized-spec.d.ts +0 -199
- package/dist/parser/version/registry.d.ts +0 -28
- package/dist/parser/version/registry.js +0 -44
- package/dist/parser/version/v3.0/index.d.ts +0 -3
- package/dist/parser/version/v3.0/index.js +0 -3
- package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
- package/dist/parser/version/v3.0/normalizer.js +0 -389
- package/dist/parser/version/v3.0/strategy.d.ts +0 -27
- package/dist/parser/version/v3.0/strategy.js +0 -96
- package/dist/parser/version/v3.1/index.d.ts +0 -1
- package/dist/parser/version/v3.1/index.js +0 -1
- package/dist/parser/version/v3.1/strategy.d.ts +0 -42
- package/dist/parser/version/v3.1/strategy.js +0 -513
- package/dist/parser/version/v3.2/index.d.ts +0 -4
- package/dist/parser/version/v3.2/index.js +0 -4
- package/dist/parser/version/v3.2/strategy.d.ts +0 -39
- package/dist/parser/version/v3.2/strategy.js +0 -57
- package/dist/parser/version/version-strategy.d.ts +0 -31
- package/dist/parser/version/version-strategy.js +0 -1
- /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
package/dist/cli/impl.js
CHANGED
|
@@ -1,49 +1,202 @@
|
|
|
1
|
-
import { generateFullOutput } from '../generator/client-generator.js';
|
|
2
1
|
import { load } from '../parser/spec-reader.js';
|
|
3
|
-
import { validateSpec } from '../parser/
|
|
4
|
-
import {
|
|
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, {
|
|
169
|
+
preserveRefSiblings,
|
|
170
|
+
effectiveVersion: profile.effective,
|
|
171
|
+
});
|
|
172
|
+
context.process.stdout.write(`✅ Success! Generated client files:\n`);
|
|
173
|
+
context.process.stdout.write(` - ${target.outputDir}/contracts.ts\n`);
|
|
174
|
+
context.process.stdout.write(` - ${target.outputDir}/client.ts\n`);
|
|
175
|
+
context.process.stdout.write(` - ${target.outputDir}/index.ts\n`);
|
|
176
|
+
}
|
|
7
177
|
export default async function (flags, spec) {
|
|
8
178
|
try {
|
|
9
|
-
|
|
179
|
+
const { targets, configDriven } = await resolveTargets(flags, spec, this);
|
|
180
|
+
const failures = [];
|
|
181
|
+
for (const target of targets) {
|
|
10
182
|
try {
|
|
11
|
-
|
|
183
|
+
await runOneTarget(target, this);
|
|
12
184
|
}
|
|
13
|
-
catch (
|
|
14
|
-
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (!configDriven) {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
failures.push({ name: target.name, message });
|
|
15
191
|
}
|
|
16
192
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
}
|
|
193
|
+
if (failures.length > 0) {
|
|
194
|
+
const lines = failures.map((failure) => `× client "${failure.name}": ${failure.message}`);
|
|
195
|
+
throw new UserError(`${lines.join('\n')}\n${failures.length}/${targets.length} targets failed`);
|
|
196
|
+
}
|
|
197
|
+
if (configDriven) {
|
|
198
|
+
this.process.stdout.write(`Generated ${targets.length}/${targets.length} clients\n`);
|
|
28
199
|
}
|
|
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
200
|
}
|
|
48
201
|
catch (error) {
|
|
49
202
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
12
|
-
|
|
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
|
|
52
|
-
|
|
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 (
|
|
56
|
-
errorCheckLines.push(`throw new DefaultApiError(result.status, result.data as ${
|
|
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.
|
|
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
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
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 (
|
|
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
|
|
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
|
|
210
|
-
const errorCodes =
|
|
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
|
|
187
|
+
* Generate the `index.ts` barrel file content: re-exports everything from the
|
|
188
|
+
* generated contracts and client files.
|
|
231
189
|
*/
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
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
|
|
238
|
-
const
|
|
239
|
-
|
|
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
|
|
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
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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 {
|
|
2
|
-
import type { OpenAPIDocument } from '../types/openapi.js';
|
|
1
|
+
import type { AnalyzedSpec } from '../analyzer/types.js';
|
|
3
2
|
/**
|
|
4
|
-
*
|
|
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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
17
|
+
export declare function renderContracts(analyzed: AnalyzedSpec, runtimeImportPath?: string): string;
|