genoc 0.2.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 -163
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +534 -0
- package/dist/analyzer/naming.js +3 -2
- package/dist/analyzer/path-analyzer.d.ts +45 -6
- package/dist/analyzer/path-analyzer.js +82 -81
- package/dist/analyzer/schema-mapper.d.ts +3 -2
- package/dist/analyzer/schema-mapper.js +16 -8
- package/dist/analyzer/types.d.ts +119 -0
- package/dist/cli/app.d.ts +5 -2
- package/dist/cli/app.js +30 -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 +190 -31
- 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 -482
- 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/spec-reader.d.ts +3 -2
- package/dist/parser/spec-reader.js +5 -4
- 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 +1 -12
- package/dist/utils/generator-helpers.d.ts +23 -25
- package/dist/utils/generator-helpers.js +144 -76
- package/dist/utils/operation-naming.d.ts +84 -0
- package/dist/utils/operation-naming.js +178 -0
- package/dist/utils/proxy.d.ts +6 -0
- package/dist/utils/proxy.js +81 -0
- package/package.json +5 -8
- package/dist/generator/error-types.d.ts +0 -13
- package/dist/generator/error-types.js +0 -67
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -8
- 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,23 +1,33 @@
|
|
|
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
|
/**
|
|
12
|
-
* Sanitize a schema type name
|
|
13
|
-
*
|
|
5
|
+
* Sanitize a schema type name into a valid TypeScript identifier.
|
|
6
|
+
*
|
|
7
|
+
* Any run of non-identifier characters (dots, brackets, backticks, hyphens,
|
|
8
|
+
* spaces, slashes, ...) splits the name into segments that are re-joined in
|
|
9
|
+
* PascalCase:
|
|
14
10
|
*
|
|
15
|
-
*
|
|
11
|
+
* "Api.Error" → "ApiError"
|
|
12
|
+
* "User[Dto]" → "UserDto"
|
|
13
|
+
* "my-schema" → "MySchema"
|
|
14
|
+
* "payment id" → "PaymentId"
|
|
15
|
+
*
|
|
16
|
+
* Names that are already valid identifiers pass through unchanged
|
|
17
|
+
* ("User", "my_schema", "mySchema", "$Foo", "Данные"). Results that would
|
|
18
|
+
* start with a digit ("2FA") get an "_" prefix, as do exact reserved words
|
|
19
|
+
* ("class" → "_class"). Names with no usable characters collapse to "_".
|
|
16
20
|
*/
|
|
17
21
|
export declare function sanitizeTypeName(name: string): string;
|
|
18
22
|
/**
|
|
19
|
-
* Build a mapping of
|
|
20
|
-
*
|
|
23
|
+
* Build a mapping of raw schema names → renamed names for any whose
|
|
24
|
+
* sanitized name collides with a reserved type name or with another
|
|
25
|
+
* schema's sanitized name (e.g. "User-Dto" and "User[Dto]" both → "UserDto").
|
|
26
|
+
*
|
|
27
|
+
* The map is keyed by the raw schema name (component keys are unique by
|
|
28
|
+
* construction), so callers resolve raw $ref segments / schema keys first
|
|
29
|
+
* and fall back to sanitizeTypeName for the common no-collision case.
|
|
30
|
+
* Numbered suffixes are used when the first renamed candidate also collides.
|
|
21
31
|
*/
|
|
22
32
|
export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, reserved: ReadonlySet<string>, suffix?: string): Map<string, string>;
|
|
23
33
|
/**
|
|
@@ -25,19 +35,6 @@ export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, rese
|
|
|
25
35
|
* and colon-separated segments.
|
|
26
36
|
*/
|
|
27
37
|
export declare function toPascalCase(str: string): string;
|
|
28
|
-
/**
|
|
29
|
-
* Build a PascalCase type-name prefix from an operation's method + path.
|
|
30
|
-
* get + /api/v1/products → "GetApiV1Products"
|
|
31
|
-
*/
|
|
32
|
-
export declare function getOperationTypePrefix(op: AnalyzedOperation): string;
|
|
33
|
-
/**
|
|
34
|
-
* Determine the success return type for an operation.
|
|
35
|
-
*/
|
|
36
|
-
export declare function getSuccessType(op: AnalyzedOperation): string;
|
|
37
|
-
/**
|
|
38
|
-
* Determine the error type name for an operation.
|
|
39
|
-
*/
|
|
40
|
-
export declare function getErrorType(op: AnalyzedOperation): string;
|
|
41
38
|
/**
|
|
42
39
|
* Generate the auto-generated header comment.
|
|
43
40
|
*/
|
|
@@ -73,3 +70,4 @@ export declare function buildFieldJsDocLines(schema: SchemaObject): string[];
|
|
|
73
70
|
* ≥2 segments → multiline block with a blank ` *` line between segments.
|
|
74
71
|
*/
|
|
75
72
|
export declare function buildTypeJsDoc(schema: SchemaObject): string;
|
|
73
|
+
export declare function isBinaryContentType(ct: string): boolean;
|
|
@@ -1,51 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* are automatically renamed with a suffix to prevent duplicate identifiers.
|
|
2
|
+
* TypeScript/JavaScript reserved words, matched case-sensitively: a type
|
|
3
|
+
* named "Null" or "Class" is legal, only the exact lowercase keywords are.
|
|
5
4
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
'
|
|
5
|
+
const RESERVED_WORDS = new Set([
|
|
6
|
+
'abstract',
|
|
7
|
+
'any',
|
|
8
|
+
'as',
|
|
9
|
+
'asserts',
|
|
10
|
+
'assert',
|
|
11
|
+
'async',
|
|
12
|
+
'await',
|
|
13
|
+
'boolean',
|
|
14
|
+
'break',
|
|
15
|
+
'case',
|
|
16
|
+
'catch',
|
|
17
|
+
'class',
|
|
18
|
+
'continue',
|
|
19
|
+
'const',
|
|
20
|
+
'constructor',
|
|
21
|
+
'debugger',
|
|
22
|
+
'declare',
|
|
23
|
+
'default',
|
|
24
|
+
'delete',
|
|
25
|
+
'do',
|
|
26
|
+
'else',
|
|
27
|
+
'enum',
|
|
28
|
+
'export',
|
|
29
|
+
'extends',
|
|
30
|
+
'false',
|
|
31
|
+
'finally',
|
|
32
|
+
'for',
|
|
33
|
+
'from',
|
|
34
|
+
'function',
|
|
35
|
+
'get',
|
|
36
|
+
'if',
|
|
37
|
+
'implements',
|
|
38
|
+
'import',
|
|
39
|
+
'in',
|
|
40
|
+
'infer',
|
|
41
|
+
'instanceof',
|
|
42
|
+
'interface',
|
|
43
|
+
'is',
|
|
44
|
+
'keyof',
|
|
45
|
+
'let',
|
|
46
|
+
'module',
|
|
47
|
+
'namespace',
|
|
48
|
+
'never',
|
|
49
|
+
'new',
|
|
50
|
+
'null',
|
|
51
|
+
'number',
|
|
52
|
+
'object',
|
|
53
|
+
'package',
|
|
54
|
+
'private',
|
|
55
|
+
'protected',
|
|
56
|
+
'public',
|
|
57
|
+
'readonly',
|
|
58
|
+
'require',
|
|
59
|
+
'return',
|
|
60
|
+
'satisfies',
|
|
61
|
+
'set',
|
|
62
|
+
'static',
|
|
63
|
+
'string',
|
|
64
|
+
'super',
|
|
65
|
+
'switch',
|
|
66
|
+
'symbol',
|
|
67
|
+
'this',
|
|
68
|
+
'throw',
|
|
69
|
+
'true',
|
|
70
|
+
'try',
|
|
71
|
+
'type',
|
|
72
|
+
'typeof',
|
|
73
|
+
'undefined',
|
|
74
|
+
'unique',
|
|
75
|
+
'unknown',
|
|
76
|
+
'var',
|
|
77
|
+
'void',
|
|
78
|
+
'while',
|
|
79
|
+
'with',
|
|
80
|
+
'yield',
|
|
21
81
|
]);
|
|
82
|
+
function isReservedTypeName(name) {
|
|
83
|
+
return RESERVED_WORDS.has(name);
|
|
84
|
+
}
|
|
22
85
|
/** Default module specifier generated code imports shared runtime classes from. */
|
|
23
86
|
export const DEFAULT_RUNTIME_IMPORT_PATH = 'genoc/runtime';
|
|
24
87
|
/**
|
|
25
|
-
* Sanitize a schema type name
|
|
26
|
-
*
|
|
88
|
+
* Sanitize a schema type name into a valid TypeScript identifier.
|
|
89
|
+
*
|
|
90
|
+
* Any run of non-identifier characters (dots, brackets, backticks, hyphens,
|
|
91
|
+
* spaces, slashes, ...) splits the name into segments that are re-joined in
|
|
92
|
+
* PascalCase:
|
|
93
|
+
*
|
|
94
|
+
* "Api.Error" → "ApiError"
|
|
95
|
+
* "User[Dto]" → "UserDto"
|
|
96
|
+
* "my-schema" → "MySchema"
|
|
97
|
+
* "payment id" → "PaymentId"
|
|
27
98
|
*
|
|
28
|
-
*
|
|
99
|
+
* Names that are already valid identifiers pass through unchanged
|
|
100
|
+
* ("User", "my_schema", "mySchema", "$Foo", "Данные"). Results that would
|
|
101
|
+
* start with a digit ("2FA") get an "_" prefix, as do exact reserved words
|
|
102
|
+
* ("class" → "_class"). Names with no usable characters collapse to "_".
|
|
29
103
|
*/
|
|
30
104
|
export function sanitizeTypeName(name) {
|
|
31
|
-
if (!name
|
|
105
|
+
if (!name)
|
|
106
|
+
return '_';
|
|
107
|
+
// Exact reserved words keep their casing and get an "_" prefix
|
|
108
|
+
// ("class" → "_class"), while "Null" / "Class" are legal and pass through.
|
|
109
|
+
if (isReservedTypeName(name))
|
|
110
|
+
return `_${name}`;
|
|
111
|
+
const isValidIdentifier = /^[\p{ID_Start}$_][\p{ID_Continue}$]*$/u.test(name);
|
|
112
|
+
if (isValidIdentifier)
|
|
32
113
|
return name;
|
|
33
|
-
|
|
34
|
-
|
|
114
|
+
const segments = name.match(/[\p{ID_Continue}$]+/gu) ?? [];
|
|
115
|
+
const result = segments
|
|
35
116
|
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
36
117
|
.join('');
|
|
118
|
+
// Covers empty results, ASCII digits, and non-ASCII digits (Nd but not
|
|
119
|
+
// ID_Start, e.g. Arabic-Indic) alike: anything not starting with a valid
|
|
120
|
+
// identifier start character gets the "_" prefix.
|
|
121
|
+
if (!/^[\p{ID_Start}$_]/u.test(result))
|
|
122
|
+
return `_${result}`;
|
|
123
|
+
if (isReservedTypeName(result))
|
|
124
|
+
return `_${result}`;
|
|
125
|
+
return result;
|
|
37
126
|
}
|
|
38
127
|
/**
|
|
39
|
-
* Build a mapping of
|
|
40
|
-
*
|
|
128
|
+
* Build a mapping of raw schema names → renamed names for any whose
|
|
129
|
+
* sanitized name collides with a reserved type name or with another
|
|
130
|
+
* schema's sanitized name (e.g. "User-Dto" and "User[Dto]" both → "UserDto").
|
|
131
|
+
*
|
|
132
|
+
* The map is keyed by the raw schema name (component keys are unique by
|
|
133
|
+
* construction), so callers resolve raw $ref segments / schema keys first
|
|
134
|
+
* and fall back to sanitizeTypeName for the common no-collision case.
|
|
135
|
+
* Numbered suffixes are used when the first renamed candidate also collides.
|
|
41
136
|
*/
|
|
42
137
|
export function buildSchemaRenameMap(schemaNames, reserved, suffix = 'Model') {
|
|
43
138
|
const renameMap = new Map();
|
|
139
|
+
const used = new Set(reserved);
|
|
44
140
|
for (const name of schemaNames) {
|
|
45
141
|
const sanitized = sanitizeTypeName(name);
|
|
46
|
-
if (
|
|
47
|
-
|
|
142
|
+
if (!used.has(sanitized)) {
|
|
143
|
+
used.add(sanitized);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
let candidate = `${sanitized}${suffix}`;
|
|
147
|
+
let counter = 2;
|
|
148
|
+
while (used.has(candidate)) {
|
|
149
|
+
candidate = `${sanitized}${suffix}${counter}`;
|
|
150
|
+
counter += 1;
|
|
48
151
|
}
|
|
152
|
+
used.add(candidate);
|
|
153
|
+
renameMap.set(name, candidate);
|
|
49
154
|
}
|
|
50
155
|
return renameMap;
|
|
51
156
|
}
|
|
@@ -61,54 +166,6 @@ export function toPascalCase(str) {
|
|
|
61
166
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
62
167
|
.join('');
|
|
63
168
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Build a PascalCase type-name prefix from an operation's method + path.
|
|
66
|
-
* get + /api/v1/products → "GetApiV1Products"
|
|
67
|
-
*/
|
|
68
|
-
export function getOperationTypePrefix(op) {
|
|
69
|
-
const methodPascal = op.method.charAt(0).toUpperCase() + op.method.slice(1).toLowerCase();
|
|
70
|
-
const segments = op.path
|
|
71
|
-
.split('/')
|
|
72
|
-
.filter((s) => s.length > 0)
|
|
73
|
-
.map((s) => {
|
|
74
|
-
const cleaned = s.replace(/[{}]/g, '');
|
|
75
|
-
return toPascalCase(cleaned);
|
|
76
|
-
});
|
|
77
|
-
return methodPascal + segments.join('');
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Determine the success return type for an operation.
|
|
81
|
-
*/
|
|
82
|
-
export function getSuccessType(op) {
|
|
83
|
-
const successResponses = op.responses.filter((r) => r.isSuccess);
|
|
84
|
-
if (successResponses.length === 0) {
|
|
85
|
-
return 'unknown';
|
|
86
|
-
}
|
|
87
|
-
const noContent = successResponses.find((r) => r.tsType === 'void');
|
|
88
|
-
const hasOnlyNoContent = noContent && successResponses.every((r) => r.tsType === 'void');
|
|
89
|
-
if (hasOnlyNoContent) {
|
|
90
|
-
return 'void';
|
|
91
|
-
}
|
|
92
|
-
const withSchema = successResponses.filter((r) => r.tsType !== 'void');
|
|
93
|
-
if (withSchema.length === 0) {
|
|
94
|
-
return 'void';
|
|
95
|
-
}
|
|
96
|
-
const prefix = getOperationTypePrefix(op);
|
|
97
|
-
const types = withSchema.map(() => `${prefix}Response`);
|
|
98
|
-
const unique = [...new Set(types)];
|
|
99
|
-
return unique.join(' | ');
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Determine the error type name for an operation.
|
|
103
|
-
*/
|
|
104
|
-
export function getErrorType(op) {
|
|
105
|
-
const prefix = getOperationTypePrefix(op);
|
|
106
|
-
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
107
|
-
if (errorResponses.length === 0) {
|
|
108
|
-
return 'never';
|
|
109
|
-
}
|
|
110
|
-
return `${prefix}Errors`;
|
|
111
|
-
}
|
|
112
169
|
/**
|
|
113
170
|
* Generate the auto-generated header comment.
|
|
114
171
|
*/
|
|
@@ -190,3 +247,14 @@ export function buildTypeJsDoc(schema) {
|
|
|
190
247
|
return `/** ${lines[0]} */`;
|
|
191
248
|
return `/**\n${lines.map((line) => ` * ${line}`).join('\n *\n')}\n */`;
|
|
192
249
|
}
|
|
250
|
+
export function isBinaryContentType(ct) {
|
|
251
|
+
if (ct === 'application/octet-stream')
|
|
252
|
+
return true;
|
|
253
|
+
if (ct.startsWith('image/'))
|
|
254
|
+
return true;
|
|
255
|
+
if (ct.startsWith('video/'))
|
|
256
|
+
return true;
|
|
257
|
+
if (ct.startsWith('audio/'))
|
|
258
|
+
return true;
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical runtime class list in the contracts re-export order. Membership
|
|
4
|
+
* source of truth for every runtime class name the generators emit.
|
|
5
|
+
*/
|
|
6
|
+
export declare const RUNTIME_CLASS_NAMES: readonly ["ApiError", "UnspecifiedApiError", "DefaultApiError", "RequesterFailError", "StreamResponse", "ErrorResponse"];
|
|
7
|
+
/**
|
|
8
|
+
* client.ts value imports from contracts.js in site emission order.
|
|
9
|
+
* `DefaultApiError` is conditional (only when an operation has a default
|
|
10
|
+
* error response), so it is appended by the client generator, not listed here.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CLIENT_BASE_VALUE_IMPORTS: readonly ["ApiError", "UnspecifiedApiError", "ErrorResponse", "StreamResponse", "RequesterFailError"];
|
|
13
|
+
/** Fixed client-file surface names (values and types client.ts declares). */
|
|
14
|
+
export declare const CLIENT_SURFACE_NAMES: readonly ["Requester", "isDefinedError", "decorateWithErrors", "ApiClient", "createClient"];
|
|
15
|
+
/**
|
|
16
|
+
* Reserved type names used by the generated output's built-in classes,
|
|
17
|
+
* functions, and types. User-defined schema names that collide with these
|
|
18
|
+
* are automatically renamed with a suffix to prevent duplicate identifiers.
|
|
19
|
+
*
|
|
20
|
+
* Derived — never hand-edited: add new fixed names to the owning constant
|
|
21
|
+
* (`RUNTIME_CLASS_NAMES` / `CLIENT_SURFACE_NAMES`) or, for contracts-only
|
|
22
|
+
* interfaces like `FileInput`, extend the derivation below.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RESERVED_TYPE_NAMES: ReadonlySet<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Build a PascalCase type-name prefix from an operation's method + path.
|
|
27
|
+
* get + /api/v1/products → "GetApiV1Products"
|
|
28
|
+
*
|
|
29
|
+
* Returns the deduped prefix assigned by analyzePaths when available
|
|
30
|
+
* (distinct routes folding to the same identifier get numbered suffixes),
|
|
31
|
+
* otherwise computes it from the operation directly.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getOperationTypePrefix(op: AnalyzedOperation): string;
|
|
34
|
+
/**
|
|
35
|
+
* Determine the success return type for an operation.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getSuccessType(op: AnalyzedOperation): string;
|
|
38
|
+
/**
|
|
39
|
+
* Determine the error type name for an operation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function getErrorType(op: AnalyzedOperation): string;
|
|
42
|
+
/** Name of one per-status error type (`{prefix}Error{status}`). */
|
|
43
|
+
export interface StatusErrorName {
|
|
44
|
+
status: string;
|
|
45
|
+
name: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Inventory of every operation-derived type name the contracts file emits
|
|
49
|
+
* for one operation. A field is present exactly when the contracts loop
|
|
50
|
+
* emits that type — presence and naming are decided here and nowhere else.
|
|
51
|
+
*
|
|
52
|
+
* The client file imports a subset of this inventory (`clientImportedNames`);
|
|
53
|
+
* the asymmetry is deliberate: e.g. 204-only operations emit
|
|
54
|
+
* `{prefix}Response = void` which the client does not import.
|
|
55
|
+
*/
|
|
56
|
+
export interface OperationEmissions {
|
|
57
|
+
query?: string;
|
|
58
|
+
headers?: string;
|
|
59
|
+
body?: string;
|
|
60
|
+
response?: string;
|
|
61
|
+
statusErrors: StatusErrorName[];
|
|
62
|
+
defaultError?: string;
|
|
63
|
+
errorsUnion?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Derive the emission inventory for an operation. Pure function of the
|
|
67
|
+
* AnalyzedOperation — no mapper or resolver state participates in naming
|
|
68
|
+
* or presence decisions.
|
|
69
|
+
*/
|
|
70
|
+
export declare function operationEmissions(op: AnalyzedOperation): OperationEmissions;
|
|
71
|
+
/**
|
|
72
|
+
* Type names the client file imports from contracts for one operation.
|
|
73
|
+
*
|
|
74
|
+
* Deliberately a subset of the emission inventory: the client names the
|
|
75
|
+
* response type only when the success type is a named type — 204-only
|
|
76
|
+
* operations emit `{prefix}Response = void` in contracts, but the client
|
|
77
|
+
* method returns `void` without ever naming the type.
|
|
78
|
+
*/
|
|
79
|
+
export declare function clientImportedNames(op: AnalyzedOperation): string[];
|
|
80
|
+
/**
|
|
81
|
+
* Value imports for the client file: the fixed base list plus
|
|
82
|
+
* `DefaultApiError` when any operation has a default error response.
|
|
83
|
+
*/
|
|
84
|
+
export declare function clientValueImports(operations: AnalyzedOperation[]): string[];
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { sanitizeTypeName, toPascalCase } from './generator-helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Canonical runtime class list in the contracts re-export order. Membership
|
|
4
|
+
* source of truth for every runtime class name the generators emit.
|
|
5
|
+
*/
|
|
6
|
+
export const RUNTIME_CLASS_NAMES = [
|
|
7
|
+
'ApiError',
|
|
8
|
+
'UnspecifiedApiError',
|
|
9
|
+
'DefaultApiError',
|
|
10
|
+
'RequesterFailError',
|
|
11
|
+
'StreamResponse',
|
|
12
|
+
'ErrorResponse',
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* client.ts value imports from contracts.js in site emission order.
|
|
16
|
+
* `DefaultApiError` is conditional (only when an operation has a default
|
|
17
|
+
* error response), so it is appended by the client generator, not listed here.
|
|
18
|
+
*/
|
|
19
|
+
export const CLIENT_BASE_VALUE_IMPORTS = [
|
|
20
|
+
'ApiError',
|
|
21
|
+
'UnspecifiedApiError',
|
|
22
|
+
'ErrorResponse',
|
|
23
|
+
'StreamResponse',
|
|
24
|
+
'RequesterFailError',
|
|
25
|
+
];
|
|
26
|
+
/** Fixed client-file surface names (values and types client.ts declares). */
|
|
27
|
+
export const CLIENT_SURFACE_NAMES = [
|
|
28
|
+
'Requester',
|
|
29
|
+
'isDefinedError',
|
|
30
|
+
'decorateWithErrors',
|
|
31
|
+
'ApiClient',
|
|
32
|
+
'createClient',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Reserved type names used by the generated output's built-in classes,
|
|
36
|
+
* functions, and types. User-defined schema names that collide with these
|
|
37
|
+
* are automatically renamed with a suffix to prevent duplicate identifiers.
|
|
38
|
+
*
|
|
39
|
+
* Derived — never hand-edited: add new fixed names to the owning constant
|
|
40
|
+
* (`RUNTIME_CLASS_NAMES` / `CLIENT_SURFACE_NAMES`) or, for contracts-only
|
|
41
|
+
* interfaces like `FileInput`, extend the derivation below.
|
|
42
|
+
*/
|
|
43
|
+
export const RESERVED_TYPE_NAMES = new Set([
|
|
44
|
+
...RUNTIME_CLASS_NAMES,
|
|
45
|
+
...CLIENT_SURFACE_NAMES,
|
|
46
|
+
'FileInput',
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* Build a PascalCase type-name prefix from an operation's method + path.
|
|
50
|
+
* get + /api/v1/products → "GetApiV1Products"
|
|
51
|
+
*
|
|
52
|
+
* Returns the deduped prefix assigned by analyzePaths when available
|
|
53
|
+
* (distinct routes folding to the same identifier get numbered suffixes),
|
|
54
|
+
* otherwise computes it from the operation directly.
|
|
55
|
+
*/
|
|
56
|
+
export function getOperationTypePrefix(op) {
|
|
57
|
+
if (op.typePrefix)
|
|
58
|
+
return op.typePrefix;
|
|
59
|
+
return computeOperationTypePrefix(op);
|
|
60
|
+
}
|
|
61
|
+
function computeOperationTypePrefix(op) {
|
|
62
|
+
const methodPascal = op.method.charAt(0).toUpperCase() + op.method.slice(1).toLowerCase();
|
|
63
|
+
const segments = op.path
|
|
64
|
+
.split('/')
|
|
65
|
+
.filter((s) => s.length > 0)
|
|
66
|
+
.map((s) => {
|
|
67
|
+
const cleaned = s.replace(/[{}]/g, '');
|
|
68
|
+
return sanitizeTypeName(toPascalCase(cleaned));
|
|
69
|
+
});
|
|
70
|
+
return methodPascal + segments.join('');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Classify a response as void by its finished type text. The finished type
|
|
74
|
+
* is `'void'` exactly for schema-less, non-binary 2xx responses — the same
|
|
75
|
+
* condition the analyzer records as `isVoid`.
|
|
76
|
+
*/
|
|
77
|
+
function isVoidResponse(r) {
|
|
78
|
+
return r.finishedType === 'void';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Determine the success return type for an operation.
|
|
82
|
+
*/
|
|
83
|
+
export function getSuccessType(op) {
|
|
84
|
+
const successResponses = op.responses.filter((r) => r.isSuccess);
|
|
85
|
+
if (successResponses.length === 0) {
|
|
86
|
+
return 'unknown';
|
|
87
|
+
}
|
|
88
|
+
const noContent = successResponses.find((r) => isVoidResponse(r));
|
|
89
|
+
const hasOnlyNoContent = noContent && successResponses.every((r) => isVoidResponse(r));
|
|
90
|
+
if (hasOnlyNoContent) {
|
|
91
|
+
return 'void';
|
|
92
|
+
}
|
|
93
|
+
const withSchema = successResponses.filter((r) => !isVoidResponse(r));
|
|
94
|
+
if (withSchema.length === 0) {
|
|
95
|
+
return 'void';
|
|
96
|
+
}
|
|
97
|
+
const prefix = getOperationTypePrefix(op);
|
|
98
|
+
const types = withSchema.map(() => `${prefix}Response`);
|
|
99
|
+
const unique = [...new Set(types)];
|
|
100
|
+
return unique.join(' | ');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Determine the error type name for an operation.
|
|
104
|
+
*/
|
|
105
|
+
export function getErrorType(op) {
|
|
106
|
+
const prefix = getOperationTypePrefix(op);
|
|
107
|
+
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
108
|
+
if (errorResponses.length === 0) {
|
|
109
|
+
return 'never';
|
|
110
|
+
}
|
|
111
|
+
return `${prefix}Errors`;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Derive the emission inventory for an operation. Pure function of the
|
|
115
|
+
* AnalyzedOperation — no mapper or resolver state participates in naming
|
|
116
|
+
* or presence decisions.
|
|
117
|
+
*/
|
|
118
|
+
export function operationEmissions(op) {
|
|
119
|
+
const prefix = getOperationTypePrefix(op);
|
|
120
|
+
const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
|
|
121
|
+
const statusErrors = errorResponses.map((r) => ({
|
|
122
|
+
status: r.statusCode,
|
|
123
|
+
name: `${prefix}Error${r.statusCode}`,
|
|
124
|
+
}));
|
|
125
|
+
return {
|
|
126
|
+
query: op.queryParams.length > 0 ? `${prefix}Query` : undefined,
|
|
127
|
+
headers: op.headerParams.length > 0 ? `${prefix}Headers` : undefined,
|
|
128
|
+
body: op.requestBody?.hasSchema ? `${prefix}Body` : undefined,
|
|
129
|
+
response: op.responses.some((r) => r.isSuccess) ? `${prefix}Response` : undefined,
|
|
130
|
+
statusErrors,
|
|
131
|
+
defaultError: op.responses.some((r) => !r.isSuccess && r.statusCode === 'default')
|
|
132
|
+
? `${prefix}DefaultError`
|
|
133
|
+
: undefined,
|
|
134
|
+
errorsUnion: statusErrors.length > 0 ? `${prefix}Errors` : undefined,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Type names the client file imports from contracts for one operation.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately a subset of the emission inventory: the client names the
|
|
141
|
+
* response type only when the success type is a named type — 204-only
|
|
142
|
+
* operations emit `{prefix}Response = void` in contracts, but the client
|
|
143
|
+
* method returns `void` without ever naming the type.
|
|
144
|
+
*/
|
|
145
|
+
export function clientImportedNames(op) {
|
|
146
|
+
const emissions = operationEmissions(op);
|
|
147
|
+
const names = [];
|
|
148
|
+
if (emissions.query !== undefined)
|
|
149
|
+
names.push(emissions.query);
|
|
150
|
+
if (emissions.headers !== undefined)
|
|
151
|
+
names.push(emissions.headers);
|
|
152
|
+
if (emissions.body !== undefined)
|
|
153
|
+
names.push(emissions.body);
|
|
154
|
+
if (emissions.response !== undefined) {
|
|
155
|
+
const successType = getSuccessType(op);
|
|
156
|
+
if (successType !== 'void' && successType !== 'unknown' && /^[A-Z]/.test(successType)) {
|
|
157
|
+
names.push(successType);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const statusError of emissions.statusErrors) {
|
|
161
|
+
names.push(statusError.name);
|
|
162
|
+
}
|
|
163
|
+
if (emissions.errorsUnion !== undefined)
|
|
164
|
+
names.push(emissions.errorsUnion);
|
|
165
|
+
if (emissions.defaultError !== undefined)
|
|
166
|
+
names.push(emissions.defaultError);
|
|
167
|
+
return names;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Value imports for the client file: the fixed base list plus
|
|
171
|
+
* `DefaultApiError` when any operation has a default error response.
|
|
172
|
+
*/
|
|
173
|
+
export function clientValueImports(operations) {
|
|
174
|
+
const needsDefaultApiError = operations.some((op) => operationEmissions(op).defaultError !== undefined);
|
|
175
|
+
return needsDefaultApiError
|
|
176
|
+
? [...CLIENT_BASE_VALUE_IMPORTS, 'DefaultApiError']
|
|
177
|
+
: [...CLIENT_BASE_VALUE_IMPORTS];
|
|
178
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const SUPPORTED_PROXY_PROTOCOLS = new Set(['http:', 'https:']);
|
|
2
|
+
/** Security: never echo credentials — user:pass@ becomes ***@ in any echoed value. */
|
|
3
|
+
function redactCredentials(proxyUrl) {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(proxyUrl);
|
|
6
|
+
if (url.username || url.password) {
|
|
7
|
+
url.username = '***';
|
|
8
|
+
url.password = '';
|
|
9
|
+
}
|
|
10
|
+
return url.toString();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return proxyUrl;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
17
|
+
/**
|
|
18
|
+
* undici's fetch() nests the meaningful error up to 3 levels deep via `cause`
|
|
19
|
+
* (e.g. "fetch failed" → "Request was cancelled." → "Proxy response (502) !== 200...").
|
|
20
|
+
* Flatten the chain so the deepest message (proxy status, ECONNREFUSED, ...) is user-visible.
|
|
21
|
+
*/
|
|
22
|
+
function describeFetchError(err) {
|
|
23
|
+
const messages = [];
|
|
24
|
+
let current = err;
|
|
25
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH && current instanceof Error; depth++) {
|
|
26
|
+
if (!messages.includes(current.message)) {
|
|
27
|
+
messages.push(current.message);
|
|
28
|
+
}
|
|
29
|
+
current = current.cause;
|
|
30
|
+
}
|
|
31
|
+
return messages.join(': ');
|
|
32
|
+
}
|
|
33
|
+
export function assertValidProxyUrl(url) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(url);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
throw new Error(`Invalid proxy URL "${redactCredentials(url)}": only http:// and https:// proxies are supported`);
|
|
40
|
+
}
|
|
41
|
+
if (!SUPPORTED_PROXY_PROTOCOLS.has(parsed.protocol)) {
|
|
42
|
+
throw new Error(`Invalid proxy URL "${redactCredentials(url)}": only http:// and https:// proxies are supported (got scheme "${parsed.protocol.replace(/:$/, '')}:")`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function hasProxyEnv() {
|
|
46
|
+
return Boolean(process.env.HTTP_PROXY ||
|
|
47
|
+
process.env.http_proxy ||
|
|
48
|
+
process.env.HTTPS_PROXY ||
|
|
49
|
+
process.env.https_proxy);
|
|
50
|
+
}
|
|
51
|
+
export async function fetchSpec(url, opts) {
|
|
52
|
+
if (opts?.proxy) {
|
|
53
|
+
assertValidProxyUrl(opts.proxy);
|
|
54
|
+
const redacted = redactCredentials(opts.proxy);
|
|
55
|
+
try {
|
|
56
|
+
const { fetch: undiciFetch, ProxyAgent } = await import('undici');
|
|
57
|
+
return (await undiciFetch(url, {
|
|
58
|
+
dispatcher: new ProxyAgent(opts.proxy),
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new Error(`Failed to fetch spec from URL via proxy ${redacted}: ${describeFetchError(err)}`, {
|
|
63
|
+
cause: err,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (hasProxyEnv()) {
|
|
68
|
+
try {
|
|
69
|
+
const { fetch: undiciFetch, EnvHttpProxyAgent } = await import('undici');
|
|
70
|
+
return (await undiciFetch(url, {
|
|
71
|
+
dispatcher: new EnvHttpProxyAgent(),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
throw new Error(`Failed to fetch spec from URL via proxy env: ${describeFetchError(err)}`, {
|
|
76
|
+
cause: err,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return fetch(url);
|
|
81
|
+
}
|