genoc 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -187
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +493 -0
- package/dist/analyzer/discriminator-registry.d.ts +108 -0
- package/dist/analyzer/discriminator-registry.js +209 -0
- package/dist/analyzer/path-analyzer.d.ts +43 -6
- package/dist/analyzer/path-analyzer.js +57 -84
- package/dist/analyzer/schema-mapper.d.ts +68 -6
- package/dist/analyzer/schema-mapper.js +299 -40
- package/dist/analyzer/types.d.ts +126 -0
- package/dist/cli/app.d.ts +4 -2
- package/dist/cli/app.js +23 -4
- package/dist/cli/config-loader.d.ts +20 -0
- package/dist/cli/config-loader.js +186 -0
- package/dist/cli/config-schema.d.ts +105 -0
- package/dist/cli/config-schema.js +183 -0
- package/dist/cli/impl.d.ts +9 -1
- package/dist/cli/impl.js +189 -36
- package/dist/generator/client-generator.d.ts +13 -14
- package/dist/generator/client-generator.js +45 -77
- package/dist/generator/contracts-generator.d.ts +11 -9
- package/dist/generator/contracts-generator.js +45 -491
- package/dist/generator/method-generator.d.ts +1 -1
- package/dist/generator/method-generator.js +10 -150
- package/dist/parser/ref-resolver.d.ts +1 -1
- package/dist/parser/ref-resolver.js +1 -2
- package/dist/parser/version/index.d.ts +4 -16
- package/dist/parser/version/index.js +3 -14
- package/dist/parser/version/v3.0/validator.d.ts +1 -1
- package/dist/parser/version/v3.1/validator.d.ts +17 -0
- package/dist/parser/version/v3.1/validator.js +126 -0
- package/dist/parser/version/validate.d.ts +9 -0
- package/dist/parser/version/validate.js +12 -0
- package/dist/parser/version/version-detector.d.ts +21 -0
- package/dist/parser/version/version-detector.js +19 -0
- package/dist/pipeline.d.ts +15 -0
- package/dist/pipeline.js +25 -0
- package/dist/types/client.d.ts +0 -12
- package/dist/utils/generator-helpers.d.ts +1 -24
- package/dist/utils/generator-helpers.js +11 -78
- package/dist/utils/operation-naming.d.ts +84 -0
- package/dist/utils/operation-naming.js +178 -0
- package/dist/utils/proxy.d.ts +2 -0
- package/dist/utils/proxy.js +16 -0
- package/dist/utils/string.d.ts +7 -0
- package/dist/utils/string.js +9 -0
- package/package.json +19 -15
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -11
- package/dist/parser/validators.d.ts +0 -7
- package/dist/parser/validators.js +0 -79
- package/dist/parser/version/normalized-spec.d.ts +0 -199
- package/dist/parser/version/registry.d.ts +0 -28
- package/dist/parser/version/registry.js +0 -44
- package/dist/parser/version/v3.0/index.d.ts +0 -3
- package/dist/parser/version/v3.0/index.js +0 -3
- package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
- package/dist/parser/version/v3.0/normalizer.js +0 -389
- package/dist/parser/version/v3.0/strategy.d.ts +0 -27
- package/dist/parser/version/v3.0/strategy.js +0 -96
- package/dist/parser/version/v3.1/index.d.ts +0 -1
- package/dist/parser/version/v3.1/index.js +0 -1
- package/dist/parser/version/v3.1/strategy.d.ts +0 -42
- package/dist/parser/version/v3.1/strategy.js +0 -513
- package/dist/parser/version/v3.2/index.d.ts +0 -4
- package/dist/parser/version/v3.2/index.js +0 -4
- package/dist/parser/version/v3.2/strategy.d.ts +0 -39
- package/dist/parser/version/v3.2/strategy.js +0 -57
- package/dist/parser/version/version-strategy.d.ts +0 -31
- package/dist/parser/version/version-strategy.js +0 -1
- /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { buildSchemaRenameMap, sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
2
|
+
import { RESERVED_TYPE_NAMES } from '../utils/operation-naming.js';
|
|
3
|
+
import { escapeStringLiteral } from '../utils/string.js';
|
|
4
|
+
import { parseJsonPointer } from '../utils/url.js';
|
|
5
|
+
/**
|
|
6
|
+
* Family-aware discriminator registry (plan D5) — the single knowledge source
|
|
7
|
+
* for discriminator families the mapper seam will translate through:
|
|
8
|
+
*
|
|
9
|
+
* - One record per discriminator family (a component schema declaring
|
|
10
|
+
* `discriminator`), keyed by `familyId` (the base's raw component name).
|
|
11
|
+
* - Variant entries keyed by the FULL `$ref` string (mapping values are exact
|
|
12
|
+
* ref strings) plus a derived rename-aware name index.
|
|
13
|
+
* - Literals are stored PRE-ESCAPED (D8: `'` and `\` only) so the mapper never
|
|
14
|
+
* interpolates raw values into string literals.
|
|
15
|
+
* - Implicit variants (oneOf/anyOf `$ref` members absent from `mapping`) carry
|
|
16
|
+
* the RAW last ref segment as their literal (D3 precedence: mapping key >
|
|
17
|
+
* own `const` > single-value `enum` > raw segment).
|
|
18
|
+
*
|
|
19
|
+
* Field budget (D5): audited — every remaining field has a live consumer in
|
|
20
|
+
* production (seam / Variant-union emission) or in the registry unit tests.
|
|
21
|
+
* Rename-aware keying is mandatory: the name index goes through the same
|
|
22
|
+
* `buildSchemaRenameMap` machinery `analyze()` uses (see the past-bug note at
|
|
23
|
+
* schema-mapper.ts:75–78 — raw-name keying silently drops literal injection
|
|
24
|
+
* for renamed subtypes).
|
|
25
|
+
*/
|
|
26
|
+
/** Rename-aware type-name derivation — the exact expression `analyze()` wires
|
|
27
|
+
* into the mapper's `typeNameGenerator` (`renameMap.get(raw) ?? sanitized`).
|
|
28
|
+
* Single source of truth so registry name keys can never diverge from the
|
|
29
|
+
* names the mapper emits for the same `$ref`.
|
|
30
|
+
*/
|
|
31
|
+
export function renameAwareTypeName(renameMap, rawName) {
|
|
32
|
+
return renameMap.get(rawName) ?? sanitizeTypeName(rawName);
|
|
33
|
+
}
|
|
34
|
+
function isRefObject(obj) {
|
|
35
|
+
return (obj !== null &&
|
|
36
|
+
typeof obj === 'object' &&
|
|
37
|
+
!Array.isArray(obj) &&
|
|
38
|
+
'$ref' in obj);
|
|
39
|
+
}
|
|
40
|
+
/** Decode the last segment of a `$ref` string into its raw component key (`~1`/`~0` unescaped). */
|
|
41
|
+
function rawRefSegment(refStr) {
|
|
42
|
+
const segments = refStr.startsWith('#') ? parseJsonPointer(refStr.slice(1)) : refStr.split('/');
|
|
43
|
+
return segments[segments.length - 1] ?? refStr;
|
|
44
|
+
}
|
|
45
|
+
/** Encode a component key for embedding in a `#/components/schemas/…` ref string. */
|
|
46
|
+
function encodePointerSegment(segment) {
|
|
47
|
+
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A discriminator variant's own declared literal on the discriminator
|
|
51
|
+
* property: `const` first, then a single-value `enum` (D3 steps 2–3).
|
|
52
|
+
* Returns the string form; multi-value enums and absent props yield nothing.
|
|
53
|
+
*/
|
|
54
|
+
function resolveOwnDiscriminatorLiteral(resolved, propertyName, resolver) {
|
|
55
|
+
if (!resolved)
|
|
56
|
+
return undefined;
|
|
57
|
+
let prop = resolved.properties?.[propertyName];
|
|
58
|
+
if (prop === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (isRefObject(prop)) {
|
|
61
|
+
try {
|
|
62
|
+
prop = resolver.resolveSchema(prop);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (prop.const !== undefined)
|
|
69
|
+
return { value: String(prop.const), source: 'const' };
|
|
70
|
+
if (prop.enum !== undefined && prop.enum.length === 1) {
|
|
71
|
+
return { value: String(prop.enum[0]), source: 'enum' };
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Resolve a mapping/oneOf ref target for flag facts; unresolvable refs degrade to undefined (mapper-style try/catch). */
|
|
76
|
+
function resolveTargetSchema(resolver, refStr) {
|
|
77
|
+
try {
|
|
78
|
+
return resolver.resolveSchema({ $ref: refStr });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the family-aware discriminator registry from the component schemas.
|
|
86
|
+
*
|
|
87
|
+
* Inputs must be the SAME `renameMap` / `allSchemaNames` `analyze()` derives
|
|
88
|
+
* (the rename-aware keying discipline) — pass `buildSchemaRenameMap`'s output
|
|
89
|
+
* and the emitted-name set, never a parallel sanitization. Pure with respect
|
|
90
|
+
* to the document: no mutation of the schemas; warnings (D3 mapping-key
|
|
91
|
+
* conflicts) go to `warnSink`.
|
|
92
|
+
*/
|
|
93
|
+
export function buildDiscriminatorRegistry(schemas, resolver, renameMap, allSchemaNames, warnSink) {
|
|
94
|
+
const warn = warnSink ??
|
|
95
|
+
((message) => {
|
|
96
|
+
process.stderr.write(message);
|
|
97
|
+
});
|
|
98
|
+
const registry = {
|
|
99
|
+
families: new Map(),
|
|
100
|
+
byRef: new Map(),
|
|
101
|
+
byName: new Map(),
|
|
102
|
+
baseRefs: new Map(),
|
|
103
|
+
};
|
|
104
|
+
if (!schemas)
|
|
105
|
+
return registry;
|
|
106
|
+
// Pass 1 — discover families; per family, register mapping entries first
|
|
107
|
+
// (D3: mapping key wins), then implicit oneOf/anyOf variants absent from
|
|
108
|
+
// the mapping (literal = own const > single-value enum > RAW ref segment).
|
|
109
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
110
|
+
const resolved = resolver.resolve(schema);
|
|
111
|
+
const disc = resolved.discriminator;
|
|
112
|
+
if (!disc)
|
|
113
|
+
continue;
|
|
114
|
+
const familyId = name;
|
|
115
|
+
const family = {
|
|
116
|
+
familyId,
|
|
117
|
+
baseTypeName: renameAwareTypeName(renameMap, name),
|
|
118
|
+
baseRefStr: `#/components/schemas/${encodePointerSegment(name)}`,
|
|
119
|
+
propertyName: disc.propertyName,
|
|
120
|
+
variantUnionName: '',
|
|
121
|
+
variantUnionMembers: [],
|
|
122
|
+
entriesByRef: new Map(),
|
|
123
|
+
};
|
|
124
|
+
const mappedRefStrs = new Set();
|
|
125
|
+
if (disc.mapping) {
|
|
126
|
+
for (const [mappingKey, refStr] of Object.entries(disc.mapping)) {
|
|
127
|
+
if (mappedRefStrs.has(refStr))
|
|
128
|
+
continue; // two keys → same target: first key wins
|
|
129
|
+
mappedRefStrs.add(refStr);
|
|
130
|
+
const target = resolveTargetSchema(resolver, refStr);
|
|
131
|
+
const own = resolveOwnDiscriminatorLiteral(target, disc.propertyName, resolver);
|
|
132
|
+
if (own !== undefined && own.value !== mappingKey) {
|
|
133
|
+
warn(`Warning: Discriminator mapping key "${mappingKey}" for "${refStr}" conflicts with its own declared ${own.source} literal "${own.value}"; the mapping key wins.\n`);
|
|
134
|
+
}
|
|
135
|
+
const rawSegment = rawRefSegment(refStr);
|
|
136
|
+
const entry = {
|
|
137
|
+
propertyName: disc.propertyName,
|
|
138
|
+
literalValue: escapeStringLiteral(mappingKey),
|
|
139
|
+
isNamed: allSchemaNames.has(renameAwareTypeName(renameMap, rawSegment)),
|
|
140
|
+
isUnion: (target?.oneOf?.length ?? 0) > 0 || (target?.anyOf?.length ?? 0) > 0,
|
|
141
|
+
isNullable: target?.nullable === true ||
|
|
142
|
+
(Array.isArray(target?.type) ? target.type.includes('null') : false),
|
|
143
|
+
familyId,
|
|
144
|
+
refStr,
|
|
145
|
+
typeName: renameAwareTypeName(renameMap, rawSegment),
|
|
146
|
+
};
|
|
147
|
+
family.entriesByRef.set(refStr, entry);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const implicitRefs = [];
|
|
151
|
+
for (const member of [...(resolved.oneOf ?? []), ...(resolved.anyOf ?? [])]) {
|
|
152
|
+
if (isRefObject(member))
|
|
153
|
+
implicitRefs.push(member.$ref);
|
|
154
|
+
}
|
|
155
|
+
for (const refStr of implicitRefs) {
|
|
156
|
+
if (mappedRefStrs.has(refStr))
|
|
157
|
+
continue; // already registered via mapping
|
|
158
|
+
const target = resolveTargetSchema(resolver, refStr);
|
|
159
|
+
const own = resolveOwnDiscriminatorLiteral(target, disc.propertyName, resolver);
|
|
160
|
+
const rawSegment = rawRefSegment(refStr);
|
|
161
|
+
const entry = {
|
|
162
|
+
propertyName: disc.propertyName,
|
|
163
|
+
// D3 fallback chain for unmapped variants: own const > single-value
|
|
164
|
+
// enum > RAW last ref segment (the runtime value, NOT the sanitized
|
|
165
|
+
// type name).
|
|
166
|
+
literalValue: escapeStringLiteral(own?.value ?? rawSegment),
|
|
167
|
+
isNamed: allSchemaNames.has(renameAwareTypeName(renameMap, rawSegment)),
|
|
168
|
+
isUnion: (target?.oneOf?.length ?? 0) > 0 || (target?.anyOf?.length ?? 0) > 0,
|
|
169
|
+
isNullable: target?.nullable === true ||
|
|
170
|
+
(Array.isArray(target?.type) ? target.type.includes('null') : false),
|
|
171
|
+
familyId,
|
|
172
|
+
refStr,
|
|
173
|
+
typeName: renameAwareTypeName(renameMap, rawSegment),
|
|
174
|
+
};
|
|
175
|
+
family.entriesByRef.set(refStr, entry);
|
|
176
|
+
}
|
|
177
|
+
registry.families.set(familyId, family);
|
|
178
|
+
}
|
|
179
|
+
// Pass 2 — flat lookups + per-family name index + union members
|
|
180
|
+
// (mapping order, then unmapped oneOf/anyOf refs; deduped by refStr and
|
|
181
|
+
// by typeName — alias refs to one emitted name collapse).
|
|
182
|
+
for (const family of registry.families.values()) {
|
|
183
|
+
const memberNames = [];
|
|
184
|
+
const seenNames = new Set();
|
|
185
|
+
for (const [refStr, entry] of family.entriesByRef) {
|
|
186
|
+
if (!registry.byRef.has(refStr))
|
|
187
|
+
registry.byRef.set(refStr, entry);
|
|
188
|
+
if (!registry.byName.has(entry.typeName))
|
|
189
|
+
registry.byName.set(entry.typeName, entry);
|
|
190
|
+
if (!seenNames.has(entry.typeName)) {
|
|
191
|
+
seenNames.add(entry.typeName);
|
|
192
|
+
memberNames.push(entry.typeName);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
family.variantUnionMembers = memberNames;
|
|
196
|
+
registry.baseRefs.set(family.baseRefStr, family);
|
|
197
|
+
}
|
|
198
|
+
// Pass 3 — collision-safe `{Base}Variant` union names (D6): candidates go
|
|
199
|
+
// through the SAME reserved-name machinery as schemas, seeded with every
|
|
200
|
+
// emitted schema name, so a user schema literally named `ThingVariant`
|
|
201
|
+
// forces the generated union to `ThingVariantModel` (no duplicate export).
|
|
202
|
+
const unionCandidates = [...registry.families.values()].map((f) => `${f.baseTypeName}Variant`);
|
|
203
|
+
const unionRenames = buildSchemaRenameMap(unionCandidates, new Set([...RESERVED_TYPE_NAMES, ...allSchemaNames]));
|
|
204
|
+
for (const family of registry.families.values()) {
|
|
205
|
+
const candidate = `${family.baseTypeName}Variant`;
|
|
206
|
+
family.variantUnionName = unionRenames.get(candidate) ?? candidate;
|
|
207
|
+
}
|
|
208
|
+
return registry;
|
|
209
|
+
}
|
|
@@ -5,8 +5,8 @@ export interface AnalyzedParameter {
|
|
|
5
5
|
name: string;
|
|
6
6
|
in: 'path' | 'query' | 'header' | 'cookie';
|
|
7
7
|
required: boolean;
|
|
8
|
+
/** Resolved param schema — analyzer-internal translation input for `analyze()`; renderers must not read it. */
|
|
8
9
|
schema: SchemaObject | undefined;
|
|
9
|
-
tsType: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
deprecated?: boolean;
|
|
12
12
|
example?: unknown;
|
|
@@ -14,18 +14,35 @@ export interface AnalyzedParameter {
|
|
|
14
14
|
export interface AnalyzedRequestBody {
|
|
15
15
|
required: boolean;
|
|
16
16
|
contentTypes: string[];
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Whether the first content entry carries a schema — drives the
|
|
19
|
+
* `{Prefix}Body` emission-presence decision (both the emission inventory in
|
|
20
|
+
* operation-naming and the Section-3 emission in analyze() read this fact,
|
|
21
|
+
* so they can never desync). The raw schema itself travels beside the
|
|
22
|
+
* operations array in `AnalyzedPathsResult`.
|
|
23
|
+
*/
|
|
24
|
+
hasSchema: boolean;
|
|
19
25
|
isMultipart: boolean;
|
|
26
|
+
isBinary: boolean;
|
|
20
27
|
description?: string;
|
|
21
28
|
}
|
|
22
29
|
export interface AnalyzedResponse {
|
|
23
30
|
statusCode: string;
|
|
24
31
|
description?: string;
|
|
32
|
+
/** Raw (possibly `$ref`) first-content schema — analyzer-internal translation input for `analyze()`; renderers must not read it. */
|
|
25
33
|
schema: SchemaObject | ReferenceObject | undefined;
|
|
26
|
-
tsType: string;
|
|
27
34
|
isSuccess: boolean;
|
|
28
35
|
isBinary: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* True exactly for schema-less, non-binary 2xx responses with no content
|
|
38
|
+
* (or empty content) — the "empty body → void" classification.
|
|
39
|
+
*/
|
|
40
|
+
isVoid: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Finished (real-mapper) TS type text — StreamResponse substitution and
|
|
43
|
+
* discriminator variant rewrites applied. Set by `analyze()`.
|
|
44
|
+
*/
|
|
45
|
+
finishedType?: string;
|
|
29
46
|
}
|
|
30
47
|
export interface AnalyzedOperation {
|
|
31
48
|
method: string;
|
|
@@ -46,8 +63,28 @@ export interface AnalyzedOperation {
|
|
|
46
63
|
responses: AnalyzedResponse[];
|
|
47
64
|
}
|
|
48
65
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
66
|
+
* Analyzer-internal analysis result: the analyzed operations plus the raw
|
|
67
|
+
* request body schemas in a PARALLEL ARRAY (same length and order as
|
|
68
|
+
* `operations`). The raw schemas are translation input for `analyze()` and
|
|
69
|
+
* deliberately do not travel on the `AnalyzedRequestBody` seam.
|
|
70
|
+
*/
|
|
71
|
+
export interface AnalyzedPathsResult {
|
|
72
|
+
operations: AnalyzedOperation[];
|
|
73
|
+
requestBodySchemas: (SchemaObject | ReferenceObject | undefined)[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Analyze all paths and operations from an OpenAPI document into structured
|
|
77
|
+
* data for code generation, carrying the raw request body schemas alongside.
|
|
78
|
+
*
|
|
79
|
+
* @param doc - The parsed and validated OpenAPI document
|
|
80
|
+
* @param resolver - A RefResolver for resolving $ref pointers
|
|
81
|
+
* @param strategy - Method naming strategy (defaults to 'path-based')
|
|
82
|
+
* @returns Operations with parallel raw request body schemas
|
|
83
|
+
*/
|
|
84
|
+
export declare function analyzePathsDetailed(doc: OpenAPIDocument, resolver: RefResolver, strategy?: MethodNameStrategy): AnalyzedPathsResult;
|
|
85
|
+
/**
|
|
86
|
+
* Analyze all paths and operations from an OpenAPI document into structured
|
|
87
|
+
* data for code generation.
|
|
51
88
|
*
|
|
52
89
|
* @param doc - The parsed and validated OpenAPI document
|
|
53
90
|
* @param resolver - A RefResolver for resolving $ref pointers
|
|
@@ -1,63 +1,16 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
|
-
import {
|
|
2
|
+
import { isBinaryContentType } from '../utils/generator-helpers.js';
|
|
3
|
+
import { getOperationTypePrefix } from '../utils/operation-naming.js';
|
|
3
4
|
import { getMethodName } from './naming.js';
|
|
4
5
|
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return true;
|
|
11
|
-
if (ct.startsWith('image/'))
|
|
12
|
-
return true;
|
|
13
|
-
if (ct.startsWith('video/'))
|
|
14
|
-
return true;
|
|
15
|
-
if (ct.startsWith('audio/'))
|
|
16
|
-
return true;
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
function schemaToTsType(schema, resolver) {
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a possibly-$ref-ed schema and check for an exact top-level `format: binary`
|
|
8
|
+
* match. Top-level only — intentionally no deep-walk into items/allOf/oneOf/anyOf.
|
|
9
|
+
*/
|
|
10
|
+
function isBinarySchema(schema, resolver) {
|
|
20
11
|
if (!schema)
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
const resolved = resolver.resolve(schema);
|
|
24
|
-
const refStr = schema.$ref;
|
|
25
|
-
const lastSegment = sanitizeTypeName(refStr.split('/').pop());
|
|
26
|
-
if (lastSegment && resolved.type) {
|
|
27
|
-
return lastSegment;
|
|
28
|
-
}
|
|
29
|
-
return lastSegment ?? 'unknown';
|
|
30
|
-
}
|
|
31
|
-
const s = schema;
|
|
32
|
-
if (s.type === undefined)
|
|
33
|
-
return 'unknown';
|
|
34
|
-
if (Array.isArray(s.type)) {
|
|
35
|
-
const nonNull = s.type.filter((t) => t !== 'null');
|
|
36
|
-
if (nonNull.length === 0)
|
|
37
|
-
return 'null';
|
|
38
|
-
return schemaToTsType({ ...s, type: nonNull[0] }, resolver);
|
|
39
|
-
}
|
|
40
|
-
switch (s.type) {
|
|
41
|
-
case 'string':
|
|
42
|
-
return 'string';
|
|
43
|
-
case 'integer':
|
|
44
|
-
case 'number':
|
|
45
|
-
return 'number';
|
|
46
|
-
case 'boolean':
|
|
47
|
-
return 'boolean';
|
|
48
|
-
case 'array':
|
|
49
|
-
if (s.items) {
|
|
50
|
-
const itemType = schemaToTsType(s.items, resolver);
|
|
51
|
-
return `${itemType}[]`;
|
|
52
|
-
}
|
|
53
|
-
return 'unknown[]';
|
|
54
|
-
case 'object':
|
|
55
|
-
return 'object';
|
|
56
|
-
case 'null':
|
|
57
|
-
return 'null';
|
|
58
|
-
default:
|
|
59
|
-
return 'unknown';
|
|
60
|
-
}
|
|
12
|
+
return false;
|
|
13
|
+
return resolver.resolveSchema(schema).format === 'binary';
|
|
61
14
|
}
|
|
62
15
|
function resolveParameter(param, resolver) {
|
|
63
16
|
return resolver.resolve(param);
|
|
@@ -75,7 +28,6 @@ function analyzeParameter(param, resolver) {
|
|
|
75
28
|
in: param.in,
|
|
76
29
|
required: param.required ?? param.in === 'path',
|
|
77
30
|
schema,
|
|
78
|
-
tsType: schemaToTsType(param.schema, resolver),
|
|
79
31
|
description: param.description,
|
|
80
32
|
deprecated: param.deprecated,
|
|
81
33
|
example: param.example,
|
|
@@ -100,28 +52,40 @@ function mergeParameters(pathItemParams, operationParams, resolver) {
|
|
|
100
52
|
}
|
|
101
53
|
return merged;
|
|
102
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Analyze one request body. Returns the structural facts plus the RAW
|
|
57
|
+
* (possibly `$ref`) first-content schema as a separate value — the schema is
|
|
58
|
+
* analyzer-internal translation input for `analyze()` and does not travel on
|
|
59
|
+
* the `AnalyzedRequestBody` seam.
|
|
60
|
+
*/
|
|
103
61
|
function analyzeRequestBody(body, resolver) {
|
|
104
62
|
if (!body)
|
|
105
63
|
return undefined;
|
|
106
64
|
const resolved = resolveRequestBody(body, resolver);
|
|
107
65
|
const contentTypes = Object.keys(resolved.content);
|
|
108
66
|
let schema;
|
|
109
|
-
let tsType = 'unknown';
|
|
110
67
|
if (contentTypes.length > 0) {
|
|
111
68
|
const firstContent = resolved.content[contentTypes[0]];
|
|
112
69
|
if (firstContent?.schema) {
|
|
113
70
|
schema = firstContent.schema;
|
|
114
|
-
tsType = schemaToTsType(schema, resolver);
|
|
115
71
|
}
|
|
116
72
|
}
|
|
117
73
|
const isMultipart = contentTypes.length > 0 && contentTypes[0] === 'multipart/form-data';
|
|
74
|
+
// Contract with the generators: multipart bodies keep the FileInput shape
|
|
75
|
+
// and must never be Blob-ified, so the binary signals are bypassed.
|
|
76
|
+
const isBinary = isMultipart
|
|
77
|
+
? false
|
|
78
|
+
: contentTypes.some(isBinaryContentType) || isBinarySchema(schema, resolver);
|
|
118
79
|
return {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
80
|
+
requestBody: {
|
|
81
|
+
required: resolved.required ?? false,
|
|
82
|
+
contentTypes,
|
|
83
|
+
hasSchema: schema !== undefined,
|
|
84
|
+
isMultipart,
|
|
85
|
+
isBinary,
|
|
86
|
+
description: resolved.description,
|
|
87
|
+
},
|
|
88
|
+
rawSchema: schema,
|
|
125
89
|
};
|
|
126
90
|
}
|
|
127
91
|
function analyzeResponses(responses, resolver) {
|
|
@@ -129,7 +93,6 @@ function analyzeResponses(responses, resolver) {
|
|
|
129
93
|
for (const [statusCode, response] of Object.entries(responses)) {
|
|
130
94
|
const resolved = resolveResponse(response, resolver);
|
|
131
95
|
let schema;
|
|
132
|
-
let tsType = 'unknown';
|
|
133
96
|
let contentTypes = [];
|
|
134
97
|
if (resolved.content) {
|
|
135
98
|
contentTypes = Object.keys(resolved.content);
|
|
@@ -137,24 +100,22 @@ function analyzeResponses(responses, resolver) {
|
|
|
137
100
|
const firstContent = resolved.content[contentTypes[0]];
|
|
138
101
|
if (firstContent?.schema) {
|
|
139
102
|
schema = firstContent.schema;
|
|
140
|
-
tsType = schemaToTsType(schema, resolver);
|
|
141
103
|
}
|
|
142
104
|
}
|
|
143
105
|
}
|
|
144
106
|
// Empty-body success responses → void
|
|
145
|
-
|
|
107
|
+
const isVoid = schema === undefined &&
|
|
146
108
|
statusCode.startsWith('2') &&
|
|
147
|
-
(!resolved.content || contentTypes.length === 0)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const isBinary = contentTypes.length > 0 && isBinaryContentType(contentTypes[0]);
|
|
109
|
+
(!resolved.content || contentTypes.length === 0);
|
|
110
|
+
const isBinary = contentTypes.length > 0 &&
|
|
111
|
+
(isBinaryContentType(contentTypes[0]) || isBinarySchema(schema, resolver));
|
|
151
112
|
result.push({
|
|
152
113
|
statusCode,
|
|
153
114
|
description: resolved.description,
|
|
154
115
|
schema,
|
|
155
|
-
tsType,
|
|
156
116
|
isSuccess: statusCode.startsWith('2'),
|
|
157
117
|
isBinary,
|
|
118
|
+
isVoid,
|
|
158
119
|
});
|
|
159
120
|
}
|
|
160
121
|
return result;
|
|
@@ -183,18 +144,19 @@ function categorizeParameters(params) {
|
|
|
183
144
|
return { pathParams, queryParams, headerParams, cookieParams };
|
|
184
145
|
}
|
|
185
146
|
/**
|
|
186
|
-
* Analyze all paths and operations from an OpenAPI document into structured
|
|
187
|
-
* for code generation.
|
|
147
|
+
* Analyze all paths and operations from an OpenAPI document into structured
|
|
148
|
+
* data for code generation, carrying the raw request body schemas alongside.
|
|
188
149
|
*
|
|
189
150
|
* @param doc - The parsed and validated OpenAPI document
|
|
190
151
|
* @param resolver - A RefResolver for resolving $ref pointers
|
|
191
152
|
* @param strategy - Method naming strategy (defaults to 'path-based')
|
|
192
|
-
* @returns
|
|
153
|
+
* @returns Operations with parallel raw request body schemas
|
|
193
154
|
*/
|
|
194
|
-
export function
|
|
155
|
+
export function analyzePathsDetailed(doc, resolver, strategy = 'path-based') {
|
|
195
156
|
const operations = [];
|
|
157
|
+
const requestBodySchemas = [];
|
|
196
158
|
if (!doc.paths)
|
|
197
|
-
return operations;
|
|
159
|
+
return { operations, requestBodySchemas };
|
|
198
160
|
const usedTypePrefixes = new Set();
|
|
199
161
|
const usedMethodNames = new Set();
|
|
200
162
|
for (const [urlPath, pathItem] of Object.entries(doc.paths)) {
|
|
@@ -205,7 +167,7 @@ export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
|
205
167
|
const mergedParams = mergeParameters(pathItem.parameters, operation.parameters, resolver);
|
|
206
168
|
const analyzedParams = mergedParams.map((p) => analyzeParameter(p, resolver));
|
|
207
169
|
const categorized = categorizeParameters(analyzedParams);
|
|
208
|
-
const
|
|
170
|
+
const requestBodyResult = analyzeRequestBody(operation.requestBody, resolver);
|
|
209
171
|
const responses = analyzeResponses(operation.responses, resolver);
|
|
210
172
|
const methodName = getMethodName(method, urlPath, operation.operationId, strategy);
|
|
211
173
|
operations.push({
|
|
@@ -218,17 +180,16 @@ export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
|
218
180
|
deprecated: operation.deprecated ?? false,
|
|
219
181
|
tags: operation.tags ?? [],
|
|
220
182
|
...categorized,
|
|
221
|
-
requestBody,
|
|
183
|
+
requestBody: requestBodyResult?.requestBody,
|
|
222
184
|
responses,
|
|
223
185
|
});
|
|
186
|
+
requestBodySchemas.push(requestBodyResult?.rawSchema);
|
|
224
187
|
}
|
|
225
188
|
}
|
|
226
189
|
// Dedupe type prefixes and method names so that distinct routes folding to
|
|
227
190
|
// the same identifier (e.g. "/weird" and "/weird-", or "/Weird" and
|
|
228
191
|
// "/weird") cannot produce duplicate exported types or client methods.
|
|
229
|
-
//
|
|
230
|
-
// pass); this assignment must stay deterministic - it relies on insertion
|
|
231
|
-
// order and the fixed HTTP_METHODS loop - so both passes agree.
|
|
192
|
+
// Assignment relies on insertion order and the fixed HTTP_METHODS loop.
|
|
232
193
|
for (const op of operations) {
|
|
233
194
|
const baseTypePrefix = getOperationTypePrefix(op);
|
|
234
195
|
let typePrefix = baseTypePrefix;
|
|
@@ -249,5 +210,17 @@ export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
|
249
210
|
usedMethodNames.add(methodName);
|
|
250
211
|
op.methodName = methodName;
|
|
251
212
|
}
|
|
252
|
-
return operations;
|
|
213
|
+
return { operations, requestBodySchemas };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Analyze all paths and operations from an OpenAPI document into structured
|
|
217
|
+
* data for code generation.
|
|
218
|
+
*
|
|
219
|
+
* @param doc - The parsed and validated OpenAPI document
|
|
220
|
+
* @param resolver - A RefResolver for resolving $ref pointers
|
|
221
|
+
* @param strategy - Method naming strategy (defaults to 'path-based')
|
|
222
|
+
* @returns Array of AnalyzedOperation objects
|
|
223
|
+
*/
|
|
224
|
+
export function analyzePaths(doc, resolver, strategy = 'path-based') {
|
|
225
|
+
return analyzePathsDetailed(doc, resolver, strategy).operations;
|
|
253
226
|
}
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import type { TypeMappingResult } from '../types/contracts.js';
|
|
3
3
|
import type { SchemaObject, ReferenceObject } from '../types/openapi.js';
|
|
4
|
+
import type { DiscriminatorRegistry } from './discriminator-registry.js';
|
|
4
5
|
/**
|
|
5
6
|
* Callback to customize how $ref strings are converted to TypeScript type names.
|
|
6
7
|
*/
|
|
7
8
|
export type TypeNameGenerator = (refString: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional construction dependencies for {@link SchemaMapper}. Every field is
|
|
11
|
+
* optional and defaults exactly as the previous positional constructor
|
|
12
|
+
* parameters did.
|
|
13
|
+
*/
|
|
14
|
+
export interface SchemaMapperOptions {
|
|
15
|
+
typeNameGenerator?: TypeNameGenerator;
|
|
16
|
+
discriminatorTargets?: Map<string, {
|
|
17
|
+
propertyName: string;
|
|
18
|
+
literalValue: string;
|
|
19
|
+
}>;
|
|
20
|
+
emittedNames?: Set<string>;
|
|
21
|
+
warnSink?: (message: string) => void;
|
|
22
|
+
discriminatorRegistry?: DiscriminatorRegistry;
|
|
23
|
+
/**
|
|
24
|
+
* Effective OpenAPI dialect. Omitted is treated as '3.1' (the nullable
|
|
25
|
+
* deprecation warning fires). Pass '3.0' to silence it — 'nullable' is the
|
|
26
|
+
* correct mechanism in 3.0.
|
|
27
|
+
*/
|
|
28
|
+
effectiveVersion?: '3.0' | '3.1';
|
|
29
|
+
}
|
|
8
30
|
/**
|
|
9
31
|
* SchemaMapper converts OpenAPI 3.1 Schema Objects to TypeScript type strings.
|
|
10
32
|
*
|
|
@@ -15,13 +37,13 @@ export declare class SchemaMapper {
|
|
|
15
37
|
private readonly resolver;
|
|
16
38
|
private readonly typeNameGenerator;
|
|
17
39
|
private readonly discriminatorTargets;
|
|
18
|
-
private readonly
|
|
40
|
+
private readonly emittedNames;
|
|
41
|
+
private readonly discriminatorRegistry;
|
|
19
42
|
private readonly brandedTypes;
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}>, reservedNames?: Set<string>);
|
|
43
|
+
private nullableWarned;
|
|
44
|
+
private readonly warnSink;
|
|
45
|
+
private readonly effectiveVersion;
|
|
46
|
+
constructor(resolver: RefResolver, opts?: SchemaMapperOptions);
|
|
25
47
|
getBrandedTypes(): Map<string, {
|
|
26
48
|
name: string;
|
|
27
49
|
format: string;
|
|
@@ -36,12 +58,52 @@ export declare class SchemaMapper {
|
|
|
36
58
|
* @returns TypeMappingResult with tsType string and imports array
|
|
37
59
|
*/
|
|
38
60
|
mapSchema(schema: SchemaObject | ReferenceObject, name?: string, context?: 'request' | 'response'): TypeMappingResult;
|
|
61
|
+
/**
|
|
62
|
+
* Append the discriminator literal intersection exactly ONCE (site 2),
|
|
63
|
+
* parenthesizing a top-level union so `X | null & {...}` cannot bind the
|
|
64
|
+
* literal to the last union member only (D9).
|
|
65
|
+
*/
|
|
66
|
+
private appendDiscriminatorLiteral;
|
|
39
67
|
private getBrandTypeName;
|
|
40
68
|
private mapInternal;
|
|
69
|
+
/**
|
|
70
|
+
* The single ref-translation seam (site 1). When the mapper carries a
|
|
71
|
+
* discriminator registry, EVERY `$ref` resolves through here:
|
|
72
|
+
*
|
|
73
|
+
* - a discriminator BASE becomes `{Base}Variant` at all sites except
|
|
74
|
+
* inside a same-family member's own definition spine, where it stays
|
|
75
|
+
* the bare base name (structural inheritance, no circularity — D1);
|
|
76
|
+
* - a same-family mapping target inside a spine becomes `Omit<M, 'P'>`,
|
|
77
|
+
* falling back to inline expansion WITHOUT the literal when M is a
|
|
78
|
+
* union, nullable, unnamed or non-object (D2/D4);
|
|
79
|
+
* - a mapping target at any other site is its bare name — the literal is
|
|
80
|
+
* injected exactly once, at the target's own named definition.
|
|
81
|
+
*/
|
|
82
|
+
private translateRefThroughSeam;
|
|
83
|
+
/**
|
|
84
|
+
* D2/D4 spine rule for a same-family `$ref`. `Omit<M, 'P'>` strips the
|
|
85
|
+
* sibling's own literal so the appender can re-add ours; the inline-strip
|
|
86
|
+
* fallback covers the cases where `Omit` would silently drop data
|
|
87
|
+
* (union target: `Omit<A|B,K>` drops variant props; nullable target:
|
|
88
|
+
* `Omit<M|null,K>` collapses to `{}`; unnamed/non-object target).
|
|
89
|
+
*/
|
|
90
|
+
private mapSameFamilySpineRef;
|
|
91
|
+
/** Normalize a `#/components/schemas/…` ref to its canonically-encoded form (`a/b` ↔ `a~1b`). */
|
|
92
|
+
private canonicalSchemasRef;
|
|
93
|
+
private tryResolveRef;
|
|
94
|
+
private isObjectIshSchema;
|
|
41
95
|
private resolveDiscriminatorInfo;
|
|
42
96
|
private mapEnumValues;
|
|
43
97
|
private mapCombinator;
|
|
44
98
|
private mapDiscriminatedUnion;
|
|
99
|
+
/**
|
|
100
|
+
* D3 literal precedence for registry-mode wrapper literals (inline variant
|
|
101
|
+
* schemas and foreign refs): mapping key > own `const` on the discriminant
|
|
102
|
+
* property > single-value `enum` > RAW last ref segment (the runtime
|
|
103
|
+
* value, NOT the sanitized type name). Values are escaped for a
|
|
104
|
+
* single-quoted TS string.
|
|
105
|
+
*/
|
|
106
|
+
private resolveRegistryDiscriminantValue;
|
|
45
107
|
private resolveDiscriminantValue;
|
|
46
108
|
private mapArray;
|
|
47
109
|
/**
|