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.
- package/README.md +30 -186
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +534 -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 +3 -2
- package/dist/analyzer/schema-mapper.js +11 -7
- package/dist/analyzer/types.d.ts +119 -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 +186 -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 +13 -0
- package/dist/pipeline.js +24 -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/package.json +4 -8
- 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
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sanitizeJsDocText } from '../utils/generator-helpers.js';
|
|
2
|
+
import { getSuccessType, operationEmissions } from '../utils/operation-naming.js';
|
|
2
3
|
function buildParameters(op) {
|
|
3
4
|
const params = [];
|
|
5
|
+
const emissions = operationEmissions(op);
|
|
4
6
|
for (const param of op.pathParams) {
|
|
5
7
|
params.push(`${param.name}: string`);
|
|
6
8
|
}
|
|
7
|
-
if (
|
|
8
|
-
const prefix = getOperationTypePrefix(op);
|
|
9
|
+
if (emissions.query !== undefined) {
|
|
9
10
|
const allOptional = op.queryParams.every((p) => !p.required);
|
|
10
11
|
const hasRequiredAfter = !!op.requestBody?.required || op.headerParams.some((p) => p.required);
|
|
11
12
|
if (allOptional && hasRequiredAfter) {
|
|
12
13
|
// All optional query params + required param after: use explicit undefined to avoid "required param cannot follow optional" error
|
|
13
|
-
params.push(`query: ${
|
|
14
|
+
params.push(`query: ${emissions.query} | undefined`);
|
|
14
15
|
}
|
|
15
16
|
else {
|
|
16
17
|
// Normal case: use optional notation
|
|
17
18
|
const optional = allOptional ? '?' : '';
|
|
18
|
-
params.push(`query${optional}: ${
|
|
19
|
+
params.push(`query${optional}: ${emissions.query}`);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
if (op.requestBody) {
|
|
22
|
-
const prefix = getOperationTypePrefix(op);
|
|
23
23
|
const optional = op.requestBody.required ? '' : '?';
|
|
24
|
-
params.push(`body${optional}: ${
|
|
24
|
+
params.push(`body${optional}: ${emissions.body}`);
|
|
25
25
|
}
|
|
26
|
-
if (
|
|
27
|
-
const prefix = getOperationTypePrefix(op);
|
|
26
|
+
if (emissions.headers !== undefined) {
|
|
28
27
|
const allOptional = op.headerParams.every((p) => !p.required);
|
|
29
28
|
const optional = allOptional ? '?' : '';
|
|
30
|
-
params.push(`headers${optional}: ${
|
|
29
|
+
params.push(`headers${optional}: ${emissions.headers}`);
|
|
31
30
|
}
|
|
32
31
|
return params.join(', ');
|
|
33
32
|
}
|
|
@@ -96,159 +95,20 @@ function buildJsDoc(op) {
|
|
|
96
95
|
}
|
|
97
96
|
return `/**\n${lines.join('\n')}\n */`;
|
|
98
97
|
}
|
|
99
|
-
function buildUrlConstruction(op) {
|
|
100
|
-
let url = op.path;
|
|
101
|
-
for (const param of op.pathParams) {
|
|
102
|
-
url = url.replace(`{${param.name}}`, `\${encodeURIComponent(${param.name})}`);
|
|
103
|
-
}
|
|
104
|
-
return `\`${url}\``;
|
|
105
|
-
}
|
|
106
|
-
function buildImplementation(op) {
|
|
107
|
-
const successType = getSuccessType(op);
|
|
108
|
-
const prefix = getOperationTypePrefix(op);
|
|
109
|
-
const lines = [];
|
|
110
|
-
if (op.pathParams.length > 0) {
|
|
111
|
-
lines.push(`const url = ${buildUrlConstruction(op)};`);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
lines.push(`const url = "${op.path}";`);
|
|
115
|
-
}
|
|
116
|
-
if (op.queryParams.length > 0) {
|
|
117
|
-
lines.push(`const queryString = query ? "?" + new URLSearchParams(query as Record<string, string>).toString() : "";`);
|
|
118
|
-
lines.push('const fullUrl = url + queryString;');
|
|
119
|
-
}
|
|
120
|
-
const finalUrl = op.queryParams.length > 0 ? 'fullUrl' : 'url';
|
|
121
|
-
const requesterOpts = [];
|
|
122
|
-
if (op.queryParams.length > 0) {
|
|
123
|
-
requesterOpts.push('query');
|
|
124
|
-
}
|
|
125
|
-
if (op.requestBody) {
|
|
126
|
-
requesterOpts.push('body');
|
|
127
|
-
}
|
|
128
|
-
if (op.headerParams.length > 0) {
|
|
129
|
-
requesterOpts.push('headers');
|
|
130
|
-
}
|
|
131
|
-
// Error responses for status-specific checks
|
|
132
|
-
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
133
|
-
// Lines for inside `if (result instanceof ErrorResponse) { ... }`
|
|
134
|
-
const hasDefaultResponse = op.responses.some((r) => !r.isSuccess && r.statusCode === 'default');
|
|
135
|
-
const errorCheckLines = [];
|
|
136
|
-
for (const errResp of errorResponses) {
|
|
137
|
-
const status = errResp.statusCode;
|
|
138
|
-
errorCheckLines.push(`if (result.status === ${status}) throw new ApiError(${status}, result.data as ${prefix}Error${status}, result.message ?? \`Request failed with status ${status}\`);`);
|
|
139
|
-
}
|
|
140
|
-
if (hasDefaultResponse) {
|
|
141
|
-
errorCheckLines.push(`throw new DefaultApiError(result.status, result.data as ${prefix}DefaultError, result.message ?? \`Request failed with status \${result.status}\`);`);
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
errorCheckLines.push('throw new UnspecifiedApiError(result.status, result.data, result.message ?? `Request failed with status ${result.status}`);');
|
|
145
|
-
}
|
|
146
|
-
// Build try/catch block around requester call
|
|
147
|
-
const buildTryCatch = (opts) => {
|
|
148
|
-
const block = [];
|
|
149
|
-
block.push('try {');
|
|
150
|
-
block.push(` const result = await requester<${successType}>("${op.method.toUpperCase()}", ${finalUrl}, ${opts});`);
|
|
151
|
-
block.push(' if (result instanceof ErrorResponse) {');
|
|
152
|
-
for (const check of errorCheckLines) {
|
|
153
|
-
block.push(` ${check}`);
|
|
154
|
-
}
|
|
155
|
-
block.push(' }');
|
|
156
|
-
if (op.responses.some((r) => r.isSuccess && r.isBinary)) {
|
|
157
|
-
block.push(' if (!(result instanceof StreamResponse)) {');
|
|
158
|
-
block.push(' throw new RequesterFailError(new Error("Expected stream response"));');
|
|
159
|
-
block.push(' }');
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
block.push(' if (result instanceof StreamResponse) {');
|
|
163
|
-
block.push(' throw new RequesterFailError(new Error("Unexpected stream response"));');
|
|
164
|
-
block.push(' }');
|
|
165
|
-
}
|
|
166
|
-
block.push(' return result;');
|
|
167
|
-
block.push('} catch (error) {');
|
|
168
|
-
block.push(' if (error instanceof UnspecifiedApiError) throw error;');
|
|
169
|
-
block.push(' if (error instanceof ApiError) throw error;');
|
|
170
|
-
block.push(' throw new RequesterFailError(error);');
|
|
171
|
-
block.push('}');
|
|
172
|
-
return block;
|
|
173
|
-
};
|
|
174
|
-
if (op.requestBody?.isMultipart && op.requestBody.schema) {
|
|
175
|
-
const schema = op.requestBody.schema;
|
|
176
|
-
const requiredSet = new Set((schema?.required ?? []));
|
|
177
|
-
const properties = schema.properties ?? {};
|
|
178
|
-
const propNames = Object.keys(properties);
|
|
179
|
-
const bodyRequired = op.requestBody.required;
|
|
180
|
-
const formDataLines = [];
|
|
181
|
-
formDataLines.push('const formData = new FormData();');
|
|
182
|
-
for (const propName of propNames) {
|
|
183
|
-
const propSchema = properties[propName];
|
|
184
|
-
const isArrayBinary = propSchema?.type === 'array' && propSchema?.items?.format === 'binary';
|
|
185
|
-
if (propSchema?.format === 'binary') {
|
|
186
|
-
if (requiredSet.has(propName)) {
|
|
187
|
-
formDataLines.push(`formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
formDataLines.push(`if (body.${propName} !== undefined) formData.append("${propName}", body.${propName}.data, body.${propName}.filename);`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
else if (isArrayBinary) {
|
|
194
|
-
formDataLines.push(`if (body.${propName} !== undefined) { for (const file of body.${propName}) { formData.append("${propName}", file.data, file.filename); } }`);
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
formDataLines.push(`if (body.${propName} !== undefined) formData.append("${propName}", body.${propName});`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const bodyIdx = requesterOpts.indexOf('body');
|
|
201
|
-
if (bodyIdx !== -1)
|
|
202
|
-
requesterOpts[bodyIdx] = 'body: formData';
|
|
203
|
-
if (op.responses.some((r) => r.isSuccess && r.isBinary)) {
|
|
204
|
-
requesterOpts.push('expectStream: true');
|
|
205
|
-
}
|
|
206
|
-
const multipartOpts = requesterOpts.length > 0 ? `{ ${requesterOpts.join(', ')} }` : '{}';
|
|
207
|
-
if (bodyRequired) {
|
|
208
|
-
lines.push(...formDataLines);
|
|
209
|
-
lines.push(...buildTryCatch(multipartOpts));
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
lines.push('if (body) {');
|
|
213
|
-
for (const line of formDataLines) {
|
|
214
|
-
lines.push(` ${line}`);
|
|
215
|
-
}
|
|
216
|
-
for (const line of buildTryCatch(multipartOpts)) {
|
|
217
|
-
lines.push(` ${line}`);
|
|
218
|
-
}
|
|
219
|
-
lines.push('}');
|
|
220
|
-
const fallbackOpts = requesterOpts.filter((o) => o !== 'body: formData' && o !== 'body');
|
|
221
|
-
const fallbackOptsStr = fallbackOpts.length > 0 ? `{ ${fallbackOpts.join(', ')} }` : '{}';
|
|
222
|
-
lines.push(...buildTryCatch(fallbackOptsStr));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
if (op.responses.some((r) => r.isSuccess && r.isBinary)) {
|
|
227
|
-
requesterOpts.push('expectStream: true');
|
|
228
|
-
}
|
|
229
|
-
const optsStr = requesterOpts.length > 0 ? `{ ${requesterOpts.join(', ')} }` : '{}';
|
|
230
|
-
lines.push(...buildTryCatch(optsStr));
|
|
231
|
-
}
|
|
232
|
-
return lines.join('\n ');
|
|
233
|
-
}
|
|
234
98
|
/**
|
|
235
99
|
* Generate a client method from an analyzed OpenAPI operation.
|
|
236
100
|
*
|
|
237
101
|
* @param op - The analyzed operation
|
|
238
|
-
* @returns Generated method with name, JSDoc,
|
|
102
|
+
* @returns Generated method with name, JSDoc, and signature
|
|
239
103
|
*/
|
|
240
104
|
export function generateMethod(op) {
|
|
241
105
|
const params = buildParameters(op);
|
|
242
106
|
const successType = getSuccessType(op);
|
|
243
107
|
const jsDoc = buildJsDoc(op);
|
|
244
108
|
const signature = `${op.methodName}(${params}): Promise<${successType}>`;
|
|
245
|
-
const implementation = `${signature} {
|
|
246
|
-
${buildImplementation(op)}
|
|
247
|
-
}`;
|
|
248
109
|
return {
|
|
249
110
|
name: op.methodName,
|
|
250
111
|
jsDoc,
|
|
251
112
|
signature,
|
|
252
|
-
implementation,
|
|
253
113
|
};
|
|
254
114
|
}
|
|
@@ -2,7 +2,7 @@ import type { OpenAPIDocument, ReferenceObject, SchemaObject } from '../types/op
|
|
|
2
2
|
export declare class RefResolver {
|
|
3
3
|
private doc;
|
|
4
4
|
private preserveRefSiblings;
|
|
5
|
-
constructor(doc: OpenAPIDocument,
|
|
5
|
+
constructor(doc: OpenAPIDocument, options?: {
|
|
6
6
|
preserveRefSiblings?: boolean;
|
|
7
7
|
});
|
|
8
8
|
/**
|
|
@@ -3,10 +3,9 @@ const MAX_DEPTH = 10;
|
|
|
3
3
|
export class RefResolver {
|
|
4
4
|
doc;
|
|
5
5
|
preserveRefSiblings;
|
|
6
|
-
constructor(doc,
|
|
6
|
+
constructor(doc, options) {
|
|
7
7
|
this.doc = doc;
|
|
8
8
|
this.preserveRefSiblings = options?.preserveRefSiblings ?? false;
|
|
9
|
-
void documentUrl;
|
|
10
9
|
}
|
|
11
10
|
/**
|
|
12
11
|
* If input has `$ref`, resolve it; otherwise return as-is.
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version
|
|
2
|
+
* Version detection and validation for multi-version OpenAPI support
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
export { detectSpecVersion } from './version-detector.js';
|
|
9
|
-
export { VersionStrategyRegistry } from './registry.js';
|
|
10
|
-
/**
|
|
11
|
-
* Normalized schema and specification types for consistent multi-version handling
|
|
12
|
-
*/
|
|
13
|
-
export type { NormalizedSchema, NormalizedSpec } from './normalized-spec.js';
|
|
14
|
-
export { V3_0_VersionStrategy } from './v3.0/strategy.js';
|
|
15
|
-
export { V3_1_VersionStrategy } from './v3.1/strategy.js';
|
|
16
|
-
export { V3_2_VersionStrategy } from './v3.2/strategy.js';
|
|
17
|
-
import { VersionStrategyRegistry } from './registry.js';
|
|
18
|
-
export declare const defaultRegistry: VersionStrategyRegistry;
|
|
4
|
+
export { detectSpecVersion, resolveVersion } from './version-detector.js';
|
|
5
|
+
export type { VersionProfile } from './version-detector.js';
|
|
6
|
+
export { validateSpec } from './validate.js';
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Version detection and
|
|
2
|
+
* Version detection and validation for multi-version OpenAPI support
|
|
3
3
|
*/
|
|
4
|
-
export { detectSpecVersion } from './version-detector.js';
|
|
5
|
-
export {
|
|
6
|
-
export { V3_0_VersionStrategy } from './v3.0/strategy.js';
|
|
7
|
-
export { V3_1_VersionStrategy } from './v3.1/strategy.js';
|
|
8
|
-
export { V3_2_VersionStrategy } from './v3.2/strategy.js';
|
|
9
|
-
import { VersionStrategyRegistry } from './registry.js';
|
|
10
|
-
import { V3_0_VersionStrategy } from './v3.0/strategy.js';
|
|
11
|
-
import { V3_1_VersionStrategy } from './v3.1/strategy.js';
|
|
12
|
-
import { V3_2_VersionStrategy } from './v3.2/strategy.js';
|
|
13
|
-
export const defaultRegistry = new VersionStrategyRegistry();
|
|
14
|
-
defaultRegistry.register(new V3_0_VersionStrategy());
|
|
15
|
-
defaultRegistry.register(new V3_1_VersionStrategy());
|
|
16
|
-
defaultRegistry.register(new V3_2_VersionStrategy());
|
|
4
|
+
export { detectSpecVersion, resolveVersion } from './version-detector.js';
|
|
5
|
+
export { validateSpec } from './validate.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validate.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate a raw OpenAPI 3.1.x specification.
|
|
4
|
+
*
|
|
5
|
+
* 3.1-specific validation rules:
|
|
6
|
+
* - `openapi` must start with "3.1"
|
|
7
|
+
* - at least one of `paths`, `components`, or `webhooks` (unlike 3.0 where `paths` is required)
|
|
8
|
+
* - `info` with `title` and `version` required
|
|
9
|
+
* - `paths` must be an object when present
|
|
10
|
+
* - `components.schemas` entries must be objects, validated recursively for 3.0-only features
|
|
11
|
+
* - Warns about 3.0-only features in schemas: `nullable`, boolean `exclusiveMinimum`/`exclusiveMaximum`,
|
|
12
|
+
* array-form `items` (tuples must use `prefixItems` in 3.1)
|
|
13
|
+
* - Accepts `type` as array, `$schema`, `$id`, `$comment`, `const`, `examples`, and `$ref` siblings
|
|
14
|
+
* (all legal in 3.1 via JSON Schema 2020-12)
|
|
15
|
+
* - Accepts schema-level `example` (deprecated in 3.1 but still legal)
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateSpec31(spec: unknown): ValidationResult;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a raw OpenAPI 3.1.x specification.
|
|
3
|
+
*
|
|
4
|
+
* 3.1-specific validation rules:
|
|
5
|
+
* - `openapi` must start with "3.1"
|
|
6
|
+
* - at least one of `paths`, `components`, or `webhooks` (unlike 3.0 where `paths` is required)
|
|
7
|
+
* - `info` with `title` and `version` required
|
|
8
|
+
* - `paths` must be an object when present
|
|
9
|
+
* - `components.schemas` entries must be objects, validated recursively for 3.0-only features
|
|
10
|
+
* - Warns about 3.0-only features in schemas: `nullable`, boolean `exclusiveMinimum`/`exclusiveMaximum`,
|
|
11
|
+
* array-form `items` (tuples must use `prefixItems` in 3.1)
|
|
12
|
+
* - Accepts `type` as array, `$schema`, `$id`, `$comment`, `const`, `examples`, and `$ref` siblings
|
|
13
|
+
* (all legal in 3.1 via JSON Schema 2020-12)
|
|
14
|
+
* - Accepts schema-level `example` (deprecated in 3.1 but still legal)
|
|
15
|
+
*/
|
|
16
|
+
export function validateSpec31(spec) {
|
|
17
|
+
const errors = [];
|
|
18
|
+
if (!spec || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
19
|
+
errors.push('Document must be an object');
|
|
20
|
+
return { valid: false, errors };
|
|
21
|
+
}
|
|
22
|
+
const s = spec;
|
|
23
|
+
if (!s.openapi || typeof s.openapi !== 'string') {
|
|
24
|
+
errors.push("OpenAPI specification must have an 'openapi' field with string value");
|
|
25
|
+
}
|
|
26
|
+
else if (!s.openapi.startsWith('3.1')) {
|
|
27
|
+
errors.push(`OpenAPI version must start with '3.1', got: ${s.openapi}`);
|
|
28
|
+
}
|
|
29
|
+
if (!s.info || typeof s.info !== 'object' || Array.isArray(s.info)) {
|
|
30
|
+
errors.push("OpenAPI specification must have an 'info' field with object value");
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const info = s.info;
|
|
34
|
+
if (!info.title || typeof info.title !== 'string') {
|
|
35
|
+
errors.push("Info object must have a 'title' field with string value");
|
|
36
|
+
}
|
|
37
|
+
if (!info.version || typeof info.version !== 'string') {
|
|
38
|
+
errors.push("Info object must have a 'version' field with string value");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!s.paths && !s.components && !s.webhooks) {
|
|
42
|
+
errors.push("OpenAPI specification must have at least one of 'paths', 'components', or 'webhooks'");
|
|
43
|
+
}
|
|
44
|
+
if (s.paths) {
|
|
45
|
+
if (typeof s.paths !== 'object' || Array.isArray(s.paths)) {
|
|
46
|
+
errors.push("'paths' field must be an object");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (s.components && typeof s.components === 'object' && !Array.isArray(s.components)) {
|
|
50
|
+
const components = s.components;
|
|
51
|
+
if (components.schemas) {
|
|
52
|
+
if (typeof components.schemas !== 'object' || Array.isArray(components.schemas)) {
|
|
53
|
+
errors.push("'components.schemas' must be an object");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
for (const [key, schema] of Object.entries(components.schemas)) {
|
|
57
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
58
|
+
errors.push(`Schema '${key}' must be an object`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
validateSchema31(schema, `components.schemas.${key}`, errors);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
valid: errors.length === 0,
|
|
69
|
+
errors,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Recursively validate a schema object for 3.1-specific rules.
|
|
74
|
+
* Detects 3.0-only features and reports them as errors.
|
|
75
|
+
*/
|
|
76
|
+
function validateSchema31(schema, path, errors) {
|
|
77
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const s = schema;
|
|
81
|
+
if ('nullable' in s) {
|
|
82
|
+
errors.push(`Schema '${path}' uses 'nullable' which is a 3.0-only feature`);
|
|
83
|
+
}
|
|
84
|
+
if (typeof s.exclusiveMinimum === 'boolean') {
|
|
85
|
+
errors.push(`Schema '${path}' uses 'exclusiveMinimum' as a boolean which is a 3.0-only feature`);
|
|
86
|
+
}
|
|
87
|
+
if (typeof s.exclusiveMaximum === 'boolean') {
|
|
88
|
+
errors.push(`Schema '${path}' uses 'exclusiveMaximum' as a boolean which is a 3.0-only feature`);
|
|
89
|
+
}
|
|
90
|
+
if (s.items !== undefined && Array.isArray(s.items)) {
|
|
91
|
+
errors.push(`Schema '${path}' uses 'items' as an array which is not allowed in 3.1 (use 'prefixItems')`);
|
|
92
|
+
}
|
|
93
|
+
if (s.properties && typeof s.properties === 'object' && !Array.isArray(s.properties)) {
|
|
94
|
+
for (const [key, prop] of Object.entries(s.properties)) {
|
|
95
|
+
validateSchema31(prop, `${path}.properties.${key}`, errors);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (s.items && typeof s.items === 'object' && !Array.isArray(s.items)) {
|
|
99
|
+
validateSchema31(s.items, `${path}.items`, errors);
|
|
100
|
+
}
|
|
101
|
+
if (s.additionalProperties &&
|
|
102
|
+
typeof s.additionalProperties === 'object' &&
|
|
103
|
+
!Array.isArray(s.additionalProperties)) {
|
|
104
|
+
validateSchema31(s.additionalProperties, `${path}.additionalProperties`, errors);
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(s.allOf)) {
|
|
107
|
+
s.allOf.forEach((item, i) => {
|
|
108
|
+
validateSchema31(item, `${path}.allOf[${i}]`, errors);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(s.oneOf)) {
|
|
112
|
+
s.oneOf.forEach((item, i) => {
|
|
113
|
+
validateSchema31(item, `${path}.oneOf[${i}]`, errors);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(s.anyOf)) {
|
|
117
|
+
s.anyOf.forEach((item, i) => {
|
|
118
|
+
validateSchema31(item, `${path}.anyOf[${i}]`, errors);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(s.prefixItems)) {
|
|
122
|
+
s.prefixItems.forEach((item, i) => {
|
|
123
|
+
validateSchema31(item, `${path}.prefixItems[${i}]`, errors);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ValidationResult {
|
|
2
|
+
valid: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Validate a raw OpenAPI document against the rules of the given dialect.
|
|
7
|
+
* Throws when the document is invalid; returns void otherwise.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateSpec(doc: unknown, version: '3.0' | '3.1'): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { validateSpec30 } from './v3.0/validator.js';
|
|
2
|
+
import { validateSpec31 } from './v3.1/validator.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validate a raw OpenAPI document against the rules of the given dialect.
|
|
5
|
+
* Throws when the document is invalid; returns void otherwise.
|
|
6
|
+
*/
|
|
7
|
+
export function validateSpec(doc, version) {
|
|
8
|
+
const result = version === '3.0' ? validateSpec30(doc) : validateSpec31(doc);
|
|
9
|
+
if (!result.valid) {
|
|
10
|
+
throw new Error(`Invalid OpenAPI specification:\n${result.errors.map((e) => ` - ${e}`).join('\n')}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Version detector for multi-version OpenAPI support
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* The dialect of a spec and every behavioral consequence derived from it.
|
|
6
|
+
* Produced by `resolveVersion`; the only sanctioned way code outside this
|
|
7
|
+
* module learns about spec dialect.
|
|
8
|
+
*/
|
|
9
|
+
export interface VersionProfile {
|
|
10
|
+
/** Dialect actually found in the document. */
|
|
11
|
+
detected: '3.0' | '3.1';
|
|
12
|
+
/** Dialect in effect: the override when provided, otherwise `detected`. */
|
|
13
|
+
effective: '3.0' | '3.1';
|
|
14
|
+
/** Whether sibling properties alongside `$ref` are preserved (3.1 behavior). */
|
|
15
|
+
preserveRefSiblings: boolean;
|
|
16
|
+
}
|
|
4
17
|
export declare function detectSpecVersion(rawSpec: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the dialect of a document into a `VersionProfile`.
|
|
20
|
+
*
|
|
21
|
+
* Detection always runs, even when an override is provided (so a 3.2 document
|
|
22
|
+
* is rejected regardless). The override, when given, becomes the effective
|
|
23
|
+
* dialect; `preserveRefSiblings` follows the effective dialect.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveVersion(doc: unknown, override?: '3.0' | '3.1'): VersionProfile;
|
|
@@ -32,3 +32,22 @@ export function detectSpecVersion(rawSpec) {
|
|
|
32
32
|
}
|
|
33
33
|
return majorMinorVersion;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the dialect of a document into a `VersionProfile`.
|
|
37
|
+
*
|
|
38
|
+
* Detection always runs, even when an override is provided (so a 3.2 document
|
|
39
|
+
* is rejected regardless). The override, when given, becomes the effective
|
|
40
|
+
* dialect; `preserveRefSiblings` follows the effective dialect.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveVersion(doc, override) {
|
|
43
|
+
const detected = detectSpecVersion(doc);
|
|
44
|
+
if (detected !== '3.0' && detected !== '3.1') {
|
|
45
|
+
throw new Error('OpenAPI 3.2 is not yet supported. Supported versions: 3.0, 3.1');
|
|
46
|
+
}
|
|
47
|
+
const effective = override ?? detected;
|
|
48
|
+
return {
|
|
49
|
+
detected,
|
|
50
|
+
effective,
|
|
51
|
+
preserveRefSiblings: effective === '3.1',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GeneratorConfig } from './types/client.js';
|
|
2
|
+
import type { OpenAPIDocument } from './types/openapi.js';
|
|
3
|
+
/** Options for controlling generation behavior. */
|
|
4
|
+
export interface GenerationOptions {
|
|
5
|
+
/** When true, sibling properties alongside $ref are preserved (OpenAPI 3.1 behavior). */
|
|
6
|
+
preserveRefSiblings?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generate and write all output files to disk: builds the resolver, runs the
|
|
10
|
+
* single `analyze()` pass, feeds both renderers via `generateOutput`, and
|
|
11
|
+
* writes contracts.ts / client.ts / index.ts into the configured directory.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateFullOutput(doc: OpenAPIDocument, config: GeneratorConfig, options?: GenerationOptions): Promise<void>;
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { analyze } from './analyzer/analyze.js';
|
|
4
|
+
import { generateOutput } from './generator/client-generator.js';
|
|
5
|
+
import { RefResolver } from './parser/ref-resolver.js';
|
|
6
|
+
/**
|
|
7
|
+
* Generate and write all output files to disk: builds the resolver, runs the
|
|
8
|
+
* single `analyze()` pass, feeds both renderers via `generateOutput`, and
|
|
9
|
+
* writes contracts.ts / client.ts / index.ts into the configured directory.
|
|
10
|
+
*/
|
|
11
|
+
export async function generateFullOutput(doc, config, options) {
|
|
12
|
+
const resolver = new RefResolver(doc, {
|
|
13
|
+
preserveRefSiblings: options?.preserveRefSiblings,
|
|
14
|
+
});
|
|
15
|
+
const analyzed = analyze(doc, {
|
|
16
|
+
resolver,
|
|
17
|
+
strategy: config.methodNameStrategy ?? 'path-based',
|
|
18
|
+
});
|
|
19
|
+
const { contracts, client, index } = generateOutput(analyzed, config);
|
|
20
|
+
await mkdir(config.outputDir, { recursive: true });
|
|
21
|
+
await writeFile(join(config.outputDir, 'contracts.ts'), contracts, 'utf-8');
|
|
22
|
+
await writeFile(join(config.outputDir, 'client.ts'), client, 'utf-8');
|
|
23
|
+
await writeFile(join(config.outputDir, 'index.ts'), index, 'utf-8');
|
|
24
|
+
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -4,24 +4,12 @@ export type GeneratorConfig = {
|
|
|
4
4
|
outputDir: string;
|
|
5
5
|
methodNameStrategy?: MethodNameStrategy;
|
|
6
6
|
requesterModuleName?: string;
|
|
7
|
-
specVersion?: string;
|
|
8
7
|
strictVersion?: boolean;
|
|
9
8
|
runtimeImportPath?: string;
|
|
10
9
|
proxy?: string;
|
|
11
10
|
};
|
|
12
|
-
export type RequesterFunction = <TResponse>(method: string, path: string, options: {
|
|
13
|
-
query?: Record<string, unknown>;
|
|
14
|
-
body?: unknown;
|
|
15
|
-
headers?: Record<string, string>;
|
|
16
|
-
}) => Promise<TResponse>;
|
|
17
11
|
export type GeneratedMethod = {
|
|
18
12
|
name: string;
|
|
19
13
|
jsDoc: string;
|
|
20
14
|
signature: string;
|
|
21
|
-
implementation: string;
|
|
22
|
-
};
|
|
23
|
-
export type ClientOutput = {
|
|
24
|
-
imports: string[];
|
|
25
|
-
methods: GeneratedMethod[];
|
|
26
|
-
errorTypes: string[];
|
|
27
15
|
};
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
|
|
2
1
|
import type { SchemaObject } from '../types/openapi.js';
|
|
3
|
-
/**
|
|
4
|
-
* Reserved type names used by the generated output's built-in classes,
|
|
5
|
-
* functions, and types. User-defined schema names that collide with these
|
|
6
|
-
* are automatically renamed with a suffix to prevent duplicate identifiers.
|
|
7
|
-
*/
|
|
8
|
-
export declare const RESERVED_TYPE_NAMES: ReadonlySet<string>;
|
|
9
2
|
/** Default module specifier generated code imports shared runtime classes from. */
|
|
10
3
|
export declare const DEFAULT_RUNTIME_IMPORT_PATH = "genoc/runtime";
|
|
11
4
|
/**
|
|
@@ -42,23 +35,6 @@ export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, rese
|
|
|
42
35
|
* and colon-separated segments.
|
|
43
36
|
*/
|
|
44
37
|
export declare function toPascalCase(str: string): string;
|
|
45
|
-
/**
|
|
46
|
-
* Build a PascalCase type-name prefix from an operation's method + path.
|
|
47
|
-
* get + /api/v1/products → "GetApiV1Products"
|
|
48
|
-
*
|
|
49
|
-
* Returns the deduped prefix assigned by analyzePaths when available
|
|
50
|
-
* (distinct routes folding to the same identifier get numbered suffixes),
|
|
51
|
-
* otherwise computes it from the operation directly.
|
|
52
|
-
*/
|
|
53
|
-
export declare function getOperationTypePrefix(op: AnalyzedOperation): string;
|
|
54
|
-
/**
|
|
55
|
-
* Determine the success return type for an operation.
|
|
56
|
-
*/
|
|
57
|
-
export declare function getSuccessType(op: AnalyzedOperation): string;
|
|
58
|
-
/**
|
|
59
|
-
* Determine the error type name for an operation.
|
|
60
|
-
*/
|
|
61
|
-
export declare function getErrorType(op: AnalyzedOperation): string;
|
|
62
38
|
/**
|
|
63
39
|
* Generate the auto-generated header comment.
|
|
64
40
|
*/
|
|
@@ -94,3 +70,4 @@ export declare function buildFieldJsDocLines(schema: SchemaObject): string[];
|
|
|
94
70
|
* ≥2 segments → multiline block with a blank ` *` line between segments.
|
|
95
71
|
*/
|
|
96
72
|
export declare function buildTypeJsDoc(schema: SchemaObject): string;
|
|
73
|
+
export declare function isBinaryContentType(ct: string): boolean;
|