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.
Files changed (72) hide show
  1. package/README.md +30 -163
  2. package/dist/analyzer/analyze.d.ts +18 -0
  3. package/dist/analyzer/analyze.js +534 -0
  4. package/dist/analyzer/naming.js +3 -2
  5. package/dist/analyzer/path-analyzer.d.ts +45 -6
  6. package/dist/analyzer/path-analyzer.js +82 -81
  7. package/dist/analyzer/schema-mapper.d.ts +3 -2
  8. package/dist/analyzer/schema-mapper.js +16 -8
  9. package/dist/analyzer/types.d.ts +119 -0
  10. package/dist/cli/app.d.ts +5 -2
  11. package/dist/cli/app.js +30 -4
  12. package/dist/cli/config-loader.d.ts +20 -0
  13. package/dist/cli/config-loader.js +186 -0
  14. package/dist/cli/config-schema.d.ts +105 -0
  15. package/dist/cli/config-schema.js +183 -0
  16. package/dist/cli/impl.d.ts +9 -1
  17. package/dist/cli/impl.js +190 -31
  18. package/dist/generator/client-generator.d.ts +13 -14
  19. package/dist/generator/client-generator.js +45 -77
  20. package/dist/generator/contracts-generator.d.ts +11 -9
  21. package/dist/generator/contracts-generator.js +45 -482
  22. package/dist/generator/method-generator.d.ts +1 -1
  23. package/dist/generator/method-generator.js +10 -150
  24. package/dist/parser/ref-resolver.d.ts +1 -1
  25. package/dist/parser/ref-resolver.js +1 -2
  26. package/dist/parser/spec-reader.d.ts +3 -2
  27. package/dist/parser/spec-reader.js +5 -4
  28. package/dist/parser/version/index.d.ts +4 -16
  29. package/dist/parser/version/index.js +3 -14
  30. package/dist/parser/version/v3.0/validator.d.ts +1 -1
  31. package/dist/parser/version/v3.1/validator.d.ts +17 -0
  32. package/dist/parser/version/v3.1/validator.js +126 -0
  33. package/dist/parser/version/validate.d.ts +9 -0
  34. package/dist/parser/version/validate.js +12 -0
  35. package/dist/parser/version/version-detector.d.ts +21 -0
  36. package/dist/parser/version/version-detector.js +19 -0
  37. package/dist/pipeline.d.ts +13 -0
  38. package/dist/pipeline.js +24 -0
  39. package/dist/types/client.d.ts +1 -12
  40. package/dist/utils/generator-helpers.d.ts +23 -25
  41. package/dist/utils/generator-helpers.js +144 -76
  42. package/dist/utils/operation-naming.d.ts +84 -0
  43. package/dist/utils/operation-naming.js +178 -0
  44. package/dist/utils/proxy.d.ts +6 -0
  45. package/dist/utils/proxy.js +81 -0
  46. package/package.json +5 -8
  47. package/dist/generator/error-types.d.ts +0 -13
  48. package/dist/generator/error-types.js +0 -67
  49. package/dist/index.d.ts +0 -8
  50. package/dist/index.js +0 -8
  51. package/dist/parser/validators.d.ts +0 -7
  52. package/dist/parser/validators.js +0 -79
  53. package/dist/parser/version/normalized-spec.d.ts +0 -199
  54. package/dist/parser/version/registry.d.ts +0 -28
  55. package/dist/parser/version/registry.js +0 -44
  56. package/dist/parser/version/v3.0/index.d.ts +0 -3
  57. package/dist/parser/version/v3.0/index.js +0 -3
  58. package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
  59. package/dist/parser/version/v3.0/normalizer.js +0 -389
  60. package/dist/parser/version/v3.0/strategy.d.ts +0 -27
  61. package/dist/parser/version/v3.0/strategy.js +0 -96
  62. package/dist/parser/version/v3.1/index.d.ts +0 -1
  63. package/dist/parser/version/v3.1/index.js +0 -1
  64. package/dist/parser/version/v3.1/strategy.d.ts +0 -42
  65. package/dist/parser/version/v3.1/strategy.js +0 -513
  66. package/dist/parser/version/v3.2/index.d.ts +0 -4
  67. package/dist/parser/version/v3.2/index.js +0 -4
  68. package/dist/parser/version/v3.2/strategy.d.ts +0 -39
  69. package/dist/parser/version/v3.2/strategy.js +0 -57
  70. package/dist/parser/version/version-strategy.d.ts +0 -31
  71. package/dist/parser/version/version-strategy.js +0 -1
  72. /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 by converting dot-separated segments into
13
- * PascalCase. Names without dots pass through unchanged.
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
- * "Api.Error" → "ApiError", "Simple" → "Simple"
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 sanitized schema names → renamed names for any that
20
- * collide with reserved type names.
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
- * Reserved type names used by the generated output's built-in classes,
3
- * functions, and types. User-defined schema names that collide with these
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
- export const RESERVED_TYPE_NAMES = new Set([
7
- // Classes in contracts.ts
8
- 'StreamResponse',
9
- 'ErrorResponse',
10
- 'ApiError',
11
- 'UnspecifiedApiError',
12
- 'DefaultApiError',
13
- 'RequesterFailError',
14
- // Types / functions in client.ts
15
- 'Requester',
16
- 'isDefinedError',
17
- 'decorateWithErrors',
18
- 'ApiClient',
19
- // Interface in contracts.ts (when file uploads present)
20
- 'FileInput',
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 by converting dot-separated segments into
26
- * PascalCase. Names without dots pass through unchanged.
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
- * "Api.Error" → "ApiError", "Simple" → "Simple"
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.includes('.'))
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
- return name
34
- .split('.')
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 sanitized schema names → renamed names for any that
40
- * collide with reserved type names.
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 (reserved.has(sanitized)) {
47
- renameMap.set(sanitized, `${sanitized}${suffix}`);
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,6 @@
1
+ export type LoadOptions = {
2
+ proxy?: string;
3
+ };
4
+ export declare function assertValidProxyUrl(url: string): void;
5
+ export declare function hasProxyEnv(): boolean;
6
+ export declare function fetchSpec(url: string, opts?: LoadOptions): Promise<Response>;
@@ -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
+ }