genoc 0.1.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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/analyzer/naming.d.ts +24 -0
- package/dist/analyzer/naming.js +122 -0
- package/dist/analyzer/path-analyzer.d.ts +53 -0
- package/dist/analyzer/path-analyzer.js +222 -0
- package/dist/analyzer/schema-mapper.d.ts +48 -0
- package/dist/analyzer/schema-mapper.js +435 -0
- package/dist/cli/app.d.ts +9 -0
- package/dist/cli/app.js +60 -0
- package/dist/cli/errors.d.ts +3 -0
- package/dist/cli/errors.js +6 -0
- package/dist/cli/impl.d.ts +3 -0
- package/dist/cli/impl.js +45 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/generator/client-generator.d.ts +21 -0
- package/dist/generator/client-generator.js +287 -0
- package/dist/generator/contracts-generator.d.ts +16 -0
- package/dist/generator/contracts-generator.js +525 -0
- package/dist/generator/error-types.d.ts +24 -0
- package/dist/generator/error-types.js +94 -0
- package/dist/generator/method-generator.d.ts +9 -0
- package/dist/generator/method-generator.js +249 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/parser/ref-resolver.d.ts +24 -0
- package/dist/parser/ref-resolver.js +119 -0
- package/dist/parser/spec-reader.d.ts +4 -0
- package/dist/parser/spec-reader.js +116 -0
- package/dist/parser/validators.d.ts +7 -0
- package/dist/parser/validators.js +79 -0
- package/dist/parser/version/index.d.ts +18 -0
- package/dist/parser/version/index.js +16 -0
- package/dist/parser/version/normalized-spec.d.ts +199 -0
- package/dist/parser/version/normalized-spec.js +1 -0
- package/dist/parser/version/registry.d.ts +28 -0
- package/dist/parser/version/registry.js +44 -0
- package/dist/parser/version/v3.0/index.d.ts +3 -0
- package/dist/parser/version/v3.0/index.js +3 -0
- package/dist/parser/version/v3.0/normalizer.d.ts +15 -0
- package/dist/parser/version/v3.0/normalizer.js +389 -0
- package/dist/parser/version/v3.0/strategy.d.ts +27 -0
- package/dist/parser/version/v3.0/strategy.js +96 -0
- package/dist/parser/version/v3.0/validator.d.ts +13 -0
- package/dist/parser/version/v3.0/validator.js +117 -0
- package/dist/parser/version/v3.1/index.d.ts +1 -0
- package/dist/parser/version/v3.1/index.js +1 -0
- package/dist/parser/version/v3.1/strategy.d.ts +42 -0
- package/dist/parser/version/v3.1/strategy.js +513 -0
- package/dist/parser/version/v3.2/index.d.ts +4 -0
- package/dist/parser/version/v3.2/index.js +4 -0
- package/dist/parser/version/v3.2/strategy.d.ts +39 -0
- package/dist/parser/version/v3.2/strategy.js +57 -0
- package/dist/parser/version/version-detector.d.ts +4 -0
- package/dist/parser/version/version-detector.js +34 -0
- package/dist/parser/version/version-strategy.d.ts +31 -0
- package/dist/parser/version/version-strategy.js +1 -0
- package/dist/types/client.d.ts +25 -0
- package/dist/types/client.js +1 -0
- package/dist/types/contracts.d.ts +13 -0
- package/dist/types/contracts.js +1 -0
- package/dist/types/openapi.d.ts +173 -0
- package/dist/types/openapi.js +1 -0
- package/dist/utils/case.d.ts +5 -0
- package/dist/utils/case.js +51 -0
- package/dist/utils/generator-helpers.d.ts +23 -0
- package/dist/utils/generator-helpers.js +66 -0
- package/dist/utils/string.d.ts +34 -0
- package/dist/utils/string.js +182 -0
- package/dist/utils/url.d.ts +10 -0
- package/dist/utils/url.js +40 -0
- package/package.json +60 -0
|
@@ -0,0 +1,287 @@
|
|
|
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 { getOperationTypePrefix, getSuccessType, getErrorType, makeHeader, } from '../utils/generator-helpers.js';
|
|
6
|
+
import { generateContracts } from './contracts-generator.js';
|
|
7
|
+
import { generateMethod } from './method-generator.js';
|
|
8
|
+
function collectImportTypes(operations) {
|
|
9
|
+
const types = new Set();
|
|
10
|
+
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`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// oxlint-disable-next-line unicorn/no-array-sort
|
|
38
|
+
return [...types].sort();
|
|
39
|
+
}
|
|
40
|
+
function buildClientMethodBody(op) {
|
|
41
|
+
const successType = getSuccessType(op);
|
|
42
|
+
let urlTemplate = op.path;
|
|
43
|
+
for (const param of op.pathParams) {
|
|
44
|
+
urlTemplate = urlTemplate.replace(`{${param.name}}`, `\${encodeURIComponent(${param.name})}`);
|
|
45
|
+
}
|
|
46
|
+
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
|
+
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}\`);`);
|
|
54
|
+
}
|
|
55
|
+
if (hasDefaultResponse) {
|
|
56
|
+
errorCheckLines.push(`throw new DefaultApiError(result.status, result.data as ${prefix}DefaultError, result.message ?? \`Request failed with status \${result.status}\`);`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
errorCheckLines.push('throw new UnspecifiedApiError(result.status, result.data, result.message ?? `Request failed with status ${result.status}`);');
|
|
60
|
+
}
|
|
61
|
+
const buildTryCatch = (opts) => {
|
|
62
|
+
const block = [];
|
|
63
|
+
block.push('try {');
|
|
64
|
+
block.push(` const result = await requester<${successType}>("${op.method.toUpperCase()}", ${urlExpr}, ${opts});`);
|
|
65
|
+
block.push(' if (result instanceof ErrorResponse) {');
|
|
66
|
+
for (const check of errorCheckLines) {
|
|
67
|
+
block.push(` ${check}`);
|
|
68
|
+
}
|
|
69
|
+
block.push(' }');
|
|
70
|
+
if (op.responses.some((r) => r.isSuccess && r.isBinary)) {
|
|
71
|
+
block.push(' if (!(result instanceof StreamResponse)) {');
|
|
72
|
+
block.push(' throw new RequesterFailError(new Error("Expected stream response"));');
|
|
73
|
+
block.push(' }');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
block.push(' if (result instanceof StreamResponse) {');
|
|
77
|
+
block.push(' throw new RequesterFailError(new Error("Unexpected stream response"));');
|
|
78
|
+
block.push(' }');
|
|
79
|
+
}
|
|
80
|
+
block.push(' return result;');
|
|
81
|
+
block.push('} catch (error) {');
|
|
82
|
+
block.push(' if (error instanceof UnspecifiedApiError) throw error;');
|
|
83
|
+
block.push(' if (error instanceof ApiError) throw error;');
|
|
84
|
+
block.push(' throw new RequesterFailError(error);');
|
|
85
|
+
block.push('}');
|
|
86
|
+
return block;
|
|
87
|
+
};
|
|
88
|
+
const opts = [];
|
|
89
|
+
if (op.queryParams.length > 0) {
|
|
90
|
+
opts.push('query');
|
|
91
|
+
}
|
|
92
|
+
if (op.headerParams.length > 0) {
|
|
93
|
+
opts.push('headers');
|
|
94
|
+
}
|
|
95
|
+
if (op.requestBody) {
|
|
96
|
+
opts.push('body');
|
|
97
|
+
}
|
|
98
|
+
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);
|
|
104
|
+
const bodyRequired = op.requestBody.required;
|
|
105
|
+
const formDataLines = [];
|
|
106
|
+
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)) {
|
|
112
|
+
formDataLines.push(`formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
formDataLines.push(`if (body.${propName} !== undefined) formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (isArrayBinary) {
|
|
119
|
+
formDataLines.push(`if (body.${propName} !== undefined) { for (const file of body.${propName}) { formData.append("${propName}", file.data, file.filename); } }`);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
formDataLines.push(`if (body.${propName} !== undefined) formData.append("${propName}", body.${propName});`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const bodyIdx = opts.indexOf('body');
|
|
126
|
+
if (bodyIdx !== -1)
|
|
127
|
+
opts[bodyIdx] = 'body: formData';
|
|
128
|
+
if (bodyRequired) {
|
|
129
|
+
lines.push(...formDataLines);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push('if (body) {');
|
|
133
|
+
for (const line of formDataLines) {
|
|
134
|
+
lines.push(` ${line}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (op.responses.some((r) => r.isSuccess && r.isBinary)) {
|
|
139
|
+
opts.push('expectStream: true');
|
|
140
|
+
}
|
|
141
|
+
const optsStr = opts.length > 0 ? `{ ${opts.join(', ')} }` : '{}';
|
|
142
|
+
if (op.requestBody?.isMultipart && !op.requestBody.required) {
|
|
143
|
+
for (const line of buildTryCatch(optsStr)) {
|
|
144
|
+
lines.push(` ${line}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push('}');
|
|
147
|
+
const fallbackOpts = opts.filter((o) => o !== 'body: formData' && o !== 'body');
|
|
148
|
+
const fallbackOptsStr = fallbackOpts.length > 0 ? `{ ${fallbackOpts.join(', ')} }` : '{}';
|
|
149
|
+
lines.push(...buildTryCatch(fallbackOptsStr));
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
const tryCatchLines = buildTryCatch(optsStr);
|
|
153
|
+
if (lines.length > 0) {
|
|
154
|
+
lines.push(...tryCatchLines);
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
return tryCatchLines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
function buildClientFile(operations, version) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
lines.push(makeHeader(version));
|
|
162
|
+
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
|
+
}
|
|
174
|
+
const typeImports = importTypes.filter((t) => !valueImports.includes(t));
|
|
175
|
+
lines.push(`import { ${valueImports.join(', ')} } from './contracts.js';`);
|
|
176
|
+
if (typeImports.length > 0) {
|
|
177
|
+
lines.push(`import type { ${typeImports.join(', ')} } from './contracts.js';`);
|
|
178
|
+
}
|
|
179
|
+
const hasMultipart = operations.some((op) => op.requestBody?.isMultipart);
|
|
180
|
+
if (hasMultipart) {
|
|
181
|
+
lines.push('/* global FormData */');
|
|
182
|
+
}
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push("const errorsSymbol = Symbol('errors');");
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push('function decorateWithErrors<T, E>(');
|
|
187
|
+
lines.push(' item: T,');
|
|
188
|
+
lines.push(' runtimeErrors: unknown,');
|
|
189
|
+
lines.push('): T & { [errorsSymbol]: E } {');
|
|
190
|
+
lines.push(' Object.defineProperty(item, errorsSymbol, {');
|
|
191
|
+
lines.push(' value: runtimeErrors,');
|
|
192
|
+
lines.push(' enumerable: false,');
|
|
193
|
+
lines.push(' configurable: true,');
|
|
194
|
+
lines.push(' writable: false,');
|
|
195
|
+
lines.push(' });');
|
|
196
|
+
lines.push(' return item as T & { [errorsSymbol]: E };');
|
|
197
|
+
lines.push('}');
|
|
198
|
+
lines.push('');
|
|
199
|
+
lines.push('export function isDefinedError<E extends ApiError<number, unknown>>(');
|
|
200
|
+
lines.push(' err: unknown,');
|
|
201
|
+
lines.push(' fn: { [errorsSymbol]: E },');
|
|
202
|
+
lines.push('): err is E {');
|
|
203
|
+
lines.push(' if (err instanceof UnspecifiedApiError) return false;');
|
|
204
|
+
lines.push(' if (!(err instanceof ApiError)) return false;');
|
|
205
|
+
lines.push(' return true;');
|
|
206
|
+
lines.push('}');
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push('/**');
|
|
209
|
+
lines.push(' * Performs an HTTP request and returns the response.');
|
|
210
|
+
lines.push(' *');
|
|
211
|
+
lines.push(' * When `expectStream` is true, the implementation should return');
|
|
212
|
+
lines.push(' * a `StreamResponse` containing the stream data, filename (from');
|
|
213
|
+
lines.push(' * Content-Disposition header), and response headers.');
|
|
214
|
+
lines.push(' */');
|
|
215
|
+
lines.push('export type Requester = <TResponse>(');
|
|
216
|
+
lines.push(' method: string,');
|
|
217
|
+
lines.push(' path: string,');
|
|
218
|
+
lines.push(' options: {');
|
|
219
|
+
lines.push(' query?: Record<string, unknown>;');
|
|
220
|
+
lines.push(' body?: unknown;');
|
|
221
|
+
lines.push(' headers?: Record<string, string>;');
|
|
222
|
+
lines.push(' expectStream?: true;');
|
|
223
|
+
lines.push(' },');
|
|
224
|
+
lines.push(') => Promise<TResponse | StreamResponse | ErrorResponse>;');
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push('export function createClient(requester: Requester) {');
|
|
228
|
+
lines.push(' return {');
|
|
229
|
+
for (let i = 0; i < operations.length; i++) {
|
|
230
|
+
const op = operations[i];
|
|
231
|
+
const method = generateMethod(op);
|
|
232
|
+
if (method.jsDoc) {
|
|
233
|
+
const indentedJsDoc = method.jsDoc
|
|
234
|
+
.split('\n')
|
|
235
|
+
.map((line) => ` ${line}`)
|
|
236
|
+
.join('\n');
|
|
237
|
+
lines.push(indentedJsDoc);
|
|
238
|
+
}
|
|
239
|
+
const body = buildClientMethodBody(op);
|
|
240
|
+
const sig = method.signature;
|
|
241
|
+
const firstParen = sig.indexOf('(');
|
|
242
|
+
const closeParenColon = sig.indexOf('): ');
|
|
243
|
+
const params = sig.substring(firstParen + 1, closeParenColon);
|
|
244
|
+
const returnType = sig.substring(closeParenColon + 3);
|
|
245
|
+
const methodName = method.name;
|
|
246
|
+
const errorType = getErrorType(op);
|
|
247
|
+
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
248
|
+
const errorCodes = errorResponses.map((r) => r.statusCode);
|
|
249
|
+
const errorCodeArray = errorCodes.length > 0 ? `[${errorCodes.join(', ')}] as const` : '[] as const';
|
|
250
|
+
const indentedBody = body
|
|
251
|
+
.split('\n')
|
|
252
|
+
.map((line) => ' ' + line)
|
|
253
|
+
.join('\n');
|
|
254
|
+
lines.push(` ${methodName}: decorateWithErrors<(${params}) => ${returnType}, ${errorType}>(`);
|
|
255
|
+
lines.push(` async (${params}): ${returnType} => {`);
|
|
256
|
+
lines.push(indentedBody);
|
|
257
|
+
lines.push(` },`);
|
|
258
|
+
lines.push(` ${errorCodeArray},`);
|
|
259
|
+
lines.push(` )${i < operations.length - 1 ? ',' : ''}`);
|
|
260
|
+
}
|
|
261
|
+
lines.push(' };');
|
|
262
|
+
lines.push('}');
|
|
263
|
+
lines.push('');
|
|
264
|
+
lines.push('export type ApiClient = ReturnType<typeof createClient>;');
|
|
265
|
+
return lines.join('\n');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Generate both the contracts and client file content from an OpenAPI document.
|
|
269
|
+
*/
|
|
270
|
+
export function generateClient(doc, config, options) {
|
|
271
|
+
const resolver = new RefResolver(doc, undefined, {
|
|
272
|
+
preserveRefSiblings: options?.preserveRefSiblings,
|
|
273
|
+
});
|
|
274
|
+
const contracts = generateContracts(doc, resolver);
|
|
275
|
+
const operations = analyzePaths(doc, resolver, config.methodNameStrategy ?? 'path-based');
|
|
276
|
+
const client = buildClientFile(operations, doc.openapi);
|
|
277
|
+
return { contracts, client };
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Generate and write both output files to disk.
|
|
281
|
+
*/
|
|
282
|
+
export async function generateFullOutput(doc, config, options) {
|
|
283
|
+
const { contracts, client } = generateClient(doc, config, options);
|
|
284
|
+
await mkdir(config.outputDir, { recursive: true });
|
|
285
|
+
await writeFile(join(config.outputDir, 'contracts.ts'), contracts, 'utf-8');
|
|
286
|
+
await writeFile(join(config.outputDir, 'client.ts'), client, 'utf-8');
|
|
287
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
|
+
import type { OpenAPIDocument } from '../types/openapi.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate the complete `*.contracts.ts` file content as a string.
|
|
5
|
+
*
|
|
6
|
+
* Sections produced:
|
|
7
|
+
* 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
|
|
13
|
+
* 7. ApiError class
|
|
14
|
+
* 7b. UnspecifiedApiError class
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateContracts(doc: OpenAPIDocument, resolver: RefResolver): string;
|