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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { RefResolver } from '../parser/ref-resolver.js';
|
|
2
2
|
import { formatToBrandTypeName } from '../utils/case.js';
|
|
3
3
|
import { buildTypeJsDoc, sanitizeTypeName } from '../utils/generator-helpers.js';
|
|
4
|
-
import { quoteKey } from '../utils/string.js';
|
|
4
|
+
import { escapeStringLiteral, quoteKey } from '../utils/string.js';
|
|
5
|
+
import { parseJsonPointer } from '../utils/url.js';
|
|
5
6
|
/** Indentation contract for multi-line types — 2-space unit pinned from the generated ServerParams interface; binding for downstream generators. */
|
|
6
7
|
const INDENT_UNIT = ' ';
|
|
7
8
|
function indentBy(level) {
|
|
@@ -28,6 +29,70 @@ function isComplexType(tsType) {
|
|
|
28
29
|
function needsParens(tsType) {
|
|
29
30
|
return tsType.includes(' | ') || tsType.includes(' & ');
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether the rendered type has a top-level `|` (depth 0, outside any
|
|
34
|
+
* brackets/braces and outside string literals). Unlike the substring check
|
|
35
|
+
* in `needsParens`, nested unions inside `{...}`, `<...>` or `'...'` do not
|
|
36
|
+
* trigger it — this is the guard for appending `& {...}` onto a type whose
|
|
37
|
+
* tail could otherwise capture the append (`X | null & {...}` loses the
|
|
38
|
+
* literal to `null`).
|
|
39
|
+
*/
|
|
40
|
+
function hasTopLevelUnion(tsType) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let inString = false;
|
|
43
|
+
for (let i = 0; i < tsType.length; i++) {
|
|
44
|
+
const ch = tsType[i];
|
|
45
|
+
if (ch === '\\') {
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === "'") {
|
|
50
|
+
inString = !inString;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (inString)
|
|
54
|
+
continue;
|
|
55
|
+
if (ch === '(' || ch === '{' || ch === '[' || ch === '<')
|
|
56
|
+
depth++;
|
|
57
|
+
else if (ch === ')' || ch === '}' || ch === ']' || ch === '>')
|
|
58
|
+
depth--;
|
|
59
|
+
else if (depth === 0 && ch === '|' && tsType[i - 1] === ' ' && tsType[i + 1] === ' ') {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Decode the last segment of a `$ref` string into its raw component key (`~1`/`~0` unescaped). */
|
|
66
|
+
function decodedLastRefSegment(refStr) {
|
|
67
|
+
const segments = refStr.startsWith('#') ? parseJsonPointer(refStr.slice(1)) : refStr.split('/');
|
|
68
|
+
return segments[segments.length - 1] ?? refStr;
|
|
69
|
+
}
|
|
70
|
+
/** Encode a component key for embedding in a `#/components/schemas/…` ref string. */
|
|
71
|
+
function encodePointerSegment(segment) {
|
|
72
|
+
return segment.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
73
|
+
}
|
|
74
|
+
function spineForEntry(entry) {
|
|
75
|
+
return {
|
|
76
|
+
familyId: entry.familyId,
|
|
77
|
+
propertyName: entry.propertyName,
|
|
78
|
+
literalValue: entry.literalValue,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A variant's own declared discriminant on the discriminator property:
|
|
83
|
+
* `const` first, then a single-value `enum` (D3). Multi-value enums and
|
|
84
|
+
* absent props yield nothing.
|
|
85
|
+
*/
|
|
86
|
+
function ownDeclaredDiscriminant(propSchema) {
|
|
87
|
+
if (!propSchema)
|
|
88
|
+
return undefined;
|
|
89
|
+
if (propSchema.const !== undefined)
|
|
90
|
+
return String(propSchema.const);
|
|
91
|
+
if (propSchema.enum !== undefined && propSchema.enum.length === 1) {
|
|
92
|
+
return String(propSchema.enum[0]);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
31
96
|
/**
|
|
32
97
|
* SchemaMapper converts OpenAPI 3.1 Schema Objects to TypeScript type strings.
|
|
33
98
|
*
|
|
@@ -38,18 +103,33 @@ export class SchemaMapper {
|
|
|
38
103
|
resolver;
|
|
39
104
|
typeNameGenerator;
|
|
40
105
|
discriminatorTargets;
|
|
41
|
-
|
|
106
|
+
emittedNames;
|
|
107
|
+
discriminatorRegistry;
|
|
42
108
|
brandedTypes = new Map();
|
|
43
|
-
|
|
44
|
-
|
|
109
|
+
nullableWarned = false;
|
|
110
|
+
warnSink;
|
|
111
|
+
effectiveVersion;
|
|
112
|
+
constructor(resolver, opts) {
|
|
45
113
|
// NOTE: discriminatorTargets must be keyed by names produced by the SAME
|
|
46
114
|
// (rename-aware) typeNameGenerator that resolves $refs. Passing targets
|
|
47
115
|
// keyed by raw schema names means renamed subtypes silently lose their
|
|
48
116
|
// `& { prop: 'literal' }` intersection in the generated output.
|
|
117
|
+
// analyze() no longer builds or passes a targets map — the discriminator
|
|
118
|
+
// registry (when provided) is the SINGLE discriminator knowledge source
|
|
119
|
+
// (D1–D10 seam). The optional legacy targets param is kept for
|
|
120
|
+
// registry-less constructions; it only drives the legacy ref fallback in
|
|
121
|
+
// `resolveDiscriminatorInfo` below.
|
|
49
122
|
this.resolver = resolver;
|
|
50
|
-
this.typeNameGenerator = typeNameGenerator ?? defaultTypeNameGenerator;
|
|
51
|
-
this.discriminatorTargets = discriminatorTargets ?? new Map();
|
|
52
|
-
this.
|
|
123
|
+
this.typeNameGenerator = opts?.typeNameGenerator ?? defaultTypeNameGenerator;
|
|
124
|
+
this.discriminatorTargets = opts?.discriminatorTargets ?? new Map();
|
|
125
|
+
this.emittedNames = opts?.emittedNames ?? new Set();
|
|
126
|
+
this.discriminatorRegistry = opts?.discriminatorRegistry;
|
|
127
|
+
this.warnSink =
|
|
128
|
+
opts?.warnSink ??
|
|
129
|
+
((message) => {
|
|
130
|
+
process.stderr.write(message);
|
|
131
|
+
});
|
|
132
|
+
this.effectiveVersion = opts?.effectiveVersion;
|
|
53
133
|
}
|
|
54
134
|
getBrandedTypes() {
|
|
55
135
|
return this.brandedTypes;
|
|
@@ -67,12 +147,28 @@ export class SchemaMapper {
|
|
|
67
147
|
return { tsType: schema ? 'unknown' : 'never', imports: [] };
|
|
68
148
|
}
|
|
69
149
|
const visited = new Set();
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
result.
|
|
150
|
+
if (this.discriminatorRegistry && name !== undefined) {
|
|
151
|
+
const entry = this.discriminatorRegistry.byName.get(name);
|
|
152
|
+
const spine = entry ? spineForEntry(entry) : undefined;
|
|
153
|
+
const result = this.mapInternal(schema, name, context, visited, 0, spine);
|
|
154
|
+
if (entry) {
|
|
155
|
+
return this.appendDiscriminatorLiteral(result, entry.propertyName, entry.literalValue);
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
74
158
|
}
|
|
75
|
-
return
|
|
159
|
+
return this.mapInternal(schema, name, context, visited, 0, undefined);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Append the discriminator literal intersection exactly ONCE (site 2),
|
|
163
|
+
* parenthesizing a top-level union so `X | null & {...}` cannot bind the
|
|
164
|
+
* literal to the last union member only (D9).
|
|
165
|
+
*/
|
|
166
|
+
appendDiscriminatorLiteral(result, propertyName, literalValue) {
|
|
167
|
+
const appendage = ` & { ${quoteKey(propertyName)}: '${literalValue}' }`;
|
|
168
|
+
const tsType = hasTopLevelUnion(result.tsType)
|
|
169
|
+
? `(${result.tsType})${appendage}`
|
|
170
|
+
: `${result.tsType}${appendage}`;
|
|
171
|
+
return { tsType, imports: result.imports };
|
|
76
172
|
}
|
|
77
173
|
getBrandTypeName(format, openApiType) {
|
|
78
174
|
if (!format || format.trim() === '')
|
|
@@ -80,14 +176,17 @@ export class SchemaMapper {
|
|
|
80
176
|
if (format === 'binary' || format === 'byte')
|
|
81
177
|
return null;
|
|
82
178
|
const brandName = formatToBrandTypeName(format, openApiType);
|
|
83
|
-
if (this.
|
|
179
|
+
if (this.emittedNames.has(brandName))
|
|
84
180
|
return null;
|
|
85
181
|
return brandName;
|
|
86
182
|
}
|
|
87
|
-
mapInternal(schema, name, context, visited, indent) {
|
|
183
|
+
mapInternal(schema, name, context, visited, indent, spine) {
|
|
88
184
|
if (isRefObject(schema)) {
|
|
89
185
|
const refStr = schema.$ref;
|
|
90
186
|
const refName = this.typeNameGenerator(refStr);
|
|
187
|
+
if (this.discriminatorRegistry) {
|
|
188
|
+
return this.translateRefThroughSeam(refStr, refName, context, visited, indent, spine);
|
|
189
|
+
}
|
|
91
190
|
let resolved;
|
|
92
191
|
try {
|
|
93
192
|
resolved = this.resolver.resolveSchema(schema);
|
|
@@ -98,9 +197,8 @@ export class SchemaMapper {
|
|
|
98
197
|
if (resolved) {
|
|
99
198
|
const discInfo = this.resolveDiscriminatorInfo(resolved, refStr);
|
|
100
199
|
if (discInfo) {
|
|
101
|
-
const expanded = this.mapInternal(resolved, undefined, context, visited, indent);
|
|
102
|
-
expanded
|
|
103
|
-
return expanded;
|
|
200
|
+
const expanded = this.mapInternal(resolved, undefined, context, visited, indent, undefined);
|
|
201
|
+
return this.appendDiscriminatorLiteral(expanded, discInfo.propertyName, escapeStringLiteral(discInfo.literalValue));
|
|
104
202
|
}
|
|
105
203
|
}
|
|
106
204
|
return { tsType: refName, imports: [refName] };
|
|
@@ -116,11 +214,9 @@ export class SchemaMapper {
|
|
|
116
214
|
// TODO: Remove deprecated nullable warning and handling when OpenAPI 3.1 support is complete
|
|
117
215
|
// The 'nullable' property is deprecated in OpenAPI 3.1 in favor of type arrays like ["string", "null"]
|
|
118
216
|
// This warning should be removed once full type array support is implemented
|
|
119
|
-
if (s.nullable === true && !
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
console.warn('Warning: \'nullable\' is deprecated in OpenAPI 3.1. Use \'type: ["string", "null"]\' instead.');
|
|
123
|
-
SchemaMapper.nullableWarned = true;
|
|
217
|
+
if (s.nullable === true && this.effectiveVersion !== '3.0' && !this.nullableWarned) {
|
|
218
|
+
this.warnSink('Warning: \'nullable\' is deprecated in OpenAPI 3.1. Use \'type: ["string", "null"]\' instead.');
|
|
219
|
+
this.nullableWarned = true;
|
|
124
220
|
}
|
|
125
221
|
if (s.enum !== undefined && s.enum.length > 0) {
|
|
126
222
|
let tsType = this.mapEnumValues(s.enum);
|
|
@@ -130,7 +226,7 @@ export class SchemaMapper {
|
|
|
130
226
|
return { tsType, imports: [] };
|
|
131
227
|
}
|
|
132
228
|
if (s.allOf !== undefined && s.allOf.length > 0) {
|
|
133
|
-
const result = this.mapCombinator(s.allOf, '&', context, visited, indent);
|
|
229
|
+
const result = this.mapCombinator(s.allOf, '&', context, visited, indent, spine);
|
|
134
230
|
if (s.nullable === true) {
|
|
135
231
|
return {
|
|
136
232
|
tsType: needsParens(result.tsType)
|
|
@@ -143,8 +239,8 @@ export class SchemaMapper {
|
|
|
143
239
|
}
|
|
144
240
|
if (s.oneOf !== undefined && s.oneOf.length > 0) {
|
|
145
241
|
const result = s.discriminator
|
|
146
|
-
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited, indent)
|
|
147
|
-
: this.mapCombinator(s.oneOf, '|', context, visited, indent);
|
|
242
|
+
? this.mapDiscriminatedUnion(s.oneOf, s.discriminator, context, visited, indent, spine)
|
|
243
|
+
: this.mapCombinator(s.oneOf, '|', context, visited, indent, spine);
|
|
148
244
|
if (s.nullable === true) {
|
|
149
245
|
return {
|
|
150
246
|
tsType: needsParens(result.tsType)
|
|
@@ -157,8 +253,8 @@ export class SchemaMapper {
|
|
|
157
253
|
}
|
|
158
254
|
if (s.anyOf !== undefined && s.anyOf.length > 0) {
|
|
159
255
|
const result = s.discriminator
|
|
160
|
-
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited, indent)
|
|
161
|
-
: this.mapCombinator(s.anyOf, '|', context, visited, indent);
|
|
256
|
+
? this.mapDiscriminatedUnion(s.anyOf, s.discriminator, context, visited, indent, spine)
|
|
257
|
+
: this.mapCombinator(s.anyOf, '|', context, visited, indent, spine);
|
|
162
258
|
if (s.nullable === true) {
|
|
163
259
|
return {
|
|
164
260
|
tsType: needsParens(result.tsType)
|
|
@@ -178,7 +274,7 @@ export class SchemaMapper {
|
|
|
178
274
|
if (nonNull.length === 0) {
|
|
179
275
|
return { tsType: 'null', imports: [] };
|
|
180
276
|
}
|
|
181
|
-
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited), indent);
|
|
277
|
+
const baseResult = this.mapInternal({ ...s, type: nonNull[0] }, name, context, new Set(visited), indent, spine);
|
|
182
278
|
if (hasNull) {
|
|
183
279
|
return {
|
|
184
280
|
tsType: `${baseResult.tsType} | null`,
|
|
@@ -235,11 +331,96 @@ export class SchemaMapper {
|
|
|
235
331
|
case 'array':
|
|
236
332
|
return this.mapArray(s, context, visited, indent);
|
|
237
333
|
case 'object':
|
|
238
|
-
return this.mapObject(s, name, context, visited, indent);
|
|
334
|
+
return this.mapObject(s, name, context, visited, indent, spine);
|
|
239
335
|
default:
|
|
240
336
|
return { tsType: 'unknown', imports: [] };
|
|
241
337
|
}
|
|
242
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* The single ref-translation seam (site 1). When the mapper carries a
|
|
341
|
+
* discriminator registry, EVERY `$ref` resolves through here:
|
|
342
|
+
*
|
|
343
|
+
* - a discriminator BASE becomes `{Base}Variant` at all sites except
|
|
344
|
+
* inside a same-family member's own definition spine, where it stays
|
|
345
|
+
* the bare base name (structural inheritance, no circularity — D1);
|
|
346
|
+
* - a same-family mapping target inside a spine becomes `Omit<M, 'P'>`,
|
|
347
|
+
* falling back to inline expansion WITHOUT the literal when M is a
|
|
348
|
+
* union, nullable, unnamed or non-object (D2/D4);
|
|
349
|
+
* - a mapping target at any other site is its bare name — the literal is
|
|
350
|
+
* injected exactly once, at the target's own named definition.
|
|
351
|
+
*/
|
|
352
|
+
translateRefThroughSeam(refStr, refName, context, visited, indent, spine) {
|
|
353
|
+
const registry = this.discriminatorRegistry;
|
|
354
|
+
const canonical = this.canonicalSchemasRef(refStr);
|
|
355
|
+
const baseFamily = registry.baseRefs.get(refStr) ?? registry.baseRefs.get(canonical);
|
|
356
|
+
if (baseFamily) {
|
|
357
|
+
if (spine && spine.familyId === baseFamily.familyId) {
|
|
358
|
+
return { tsType: baseFamily.baseTypeName, imports: [baseFamily.baseTypeName] };
|
|
359
|
+
}
|
|
360
|
+
return { tsType: baseFamily.variantUnionName, imports: [baseFamily.variantUnionName] };
|
|
361
|
+
}
|
|
362
|
+
const entry = registry.byRef.get(refStr) ?? registry.byRef.get(canonical);
|
|
363
|
+
if (entry) {
|
|
364
|
+
if (spine && spine.familyId === entry.familyId) {
|
|
365
|
+
return this.mapSameFamilySpineRef(entry, context, visited, indent);
|
|
366
|
+
}
|
|
367
|
+
if (entry.isNamed) {
|
|
368
|
+
return { tsType: entry.typeName, imports: [entry.typeName] };
|
|
369
|
+
}
|
|
370
|
+
// Unnamed target at a leaf site: expand inline and append the literal
|
|
371
|
+
// here — the target has no named definition to carry it.
|
|
372
|
+
const resolved = this.tryResolveRef(entry.refStr);
|
|
373
|
+
if (resolved) {
|
|
374
|
+
const expanded = this.mapInternal(resolved, undefined, context, visited, indent, spineForEntry(entry));
|
|
375
|
+
return this.appendDiscriminatorLiteral(expanded, entry.propertyName, entry.literalValue);
|
|
376
|
+
}
|
|
377
|
+
return { tsType: entry.typeName, imports: [entry.typeName] };
|
|
378
|
+
}
|
|
379
|
+
return { tsType: refName, imports: [refName] };
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* D2/D4 spine rule for a same-family `$ref`. `Omit<M, 'P'>` strips the
|
|
383
|
+
* sibling's own literal so the appender can re-add ours; the inline-strip
|
|
384
|
+
* fallback covers the cases where `Omit` would silently drop data
|
|
385
|
+
* (union target: `Omit<A|B,K>` drops variant props; nullable target:
|
|
386
|
+
* `Omit<M|null,K>` collapses to `{}`; unnamed/non-object target).
|
|
387
|
+
*/
|
|
388
|
+
mapSameFamilySpineRef(entry, context, visited, indent) {
|
|
389
|
+
const resolved = this.tryResolveRef(entry.refStr);
|
|
390
|
+
if (entry.isNamed && !entry.isUnion && !entry.isNullable && this.isObjectIshSchema(resolved)) {
|
|
391
|
+
const key = `'${escapeStringLiteral(entry.propertyName)}'`;
|
|
392
|
+
return { tsType: `Omit<${entry.typeName}, ${key}>`, imports: [entry.typeName] };
|
|
393
|
+
}
|
|
394
|
+
if (resolved) {
|
|
395
|
+
return this.mapInternal(resolved, undefined, context, visited, indent, spineForEntry(entry));
|
|
396
|
+
}
|
|
397
|
+
return { tsType: entry.typeName, imports: [entry.typeName] };
|
|
398
|
+
}
|
|
399
|
+
/** Normalize a `#/components/schemas/…` ref to its canonically-encoded form (`a/b` ↔ `a~1b`). */
|
|
400
|
+
canonicalSchemasRef(refStr) {
|
|
401
|
+
if (!refStr.startsWith('#/components/schemas/'))
|
|
402
|
+
return refStr;
|
|
403
|
+
return `#/components/schemas/${encodePointerSegment(decodedLastRefSegment(refStr))}`;
|
|
404
|
+
}
|
|
405
|
+
tryResolveRef(refStr) {
|
|
406
|
+
try {
|
|
407
|
+
return this.resolver.resolveSchema({ $ref: refStr });
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
isObjectIshSchema(s) {
|
|
414
|
+
if (!s)
|
|
415
|
+
return false;
|
|
416
|
+
if (s.type === 'object')
|
|
417
|
+
return true;
|
|
418
|
+
if (Array.isArray(s.type))
|
|
419
|
+
return s.type.includes('object');
|
|
420
|
+
if (s.allOf !== undefined && s.allOf.length > 0)
|
|
421
|
+
return true;
|
|
422
|
+
return s.type === undefined && s.properties !== undefined;
|
|
423
|
+
}
|
|
243
424
|
resolveDiscriminatorInfo(schema, refStr) {
|
|
244
425
|
const schemaName = this.typeNameGenerator(refStr);
|
|
245
426
|
const target = this.discriminatorTargets.get(schemaName);
|
|
@@ -268,7 +449,7 @@ export class SchemaMapper {
|
|
|
268
449
|
return values
|
|
269
450
|
.map((v) => {
|
|
270
451
|
if (typeof v === 'string')
|
|
271
|
-
return `'${v}'`;
|
|
452
|
+
return `'${escapeStringLiteral(v)}'`;
|
|
272
453
|
if (typeof v === 'number')
|
|
273
454
|
return String(v);
|
|
274
455
|
if (typeof v === 'boolean')
|
|
@@ -279,8 +460,8 @@ export class SchemaMapper {
|
|
|
279
460
|
})
|
|
280
461
|
.join(' | ');
|
|
281
462
|
}
|
|
282
|
-
mapCombinator(schemas, kind, context, visited, indent) {
|
|
283
|
-
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited, indent));
|
|
463
|
+
mapCombinator(schemas, kind, context, visited, indent, spine) {
|
|
464
|
+
const results = schemas.map((s) => this.mapInternal(s, undefined, context, visited, indent, spine));
|
|
284
465
|
const allImports = [];
|
|
285
466
|
for (const r of results) {
|
|
286
467
|
allImports.push(...r.imports);
|
|
@@ -294,20 +475,83 @@ export class SchemaMapper {
|
|
|
294
475
|
});
|
|
295
476
|
return { tsType: parts.join(separator), imports: allImports };
|
|
296
477
|
}
|
|
297
|
-
mapDiscriminatedUnion(schemas, discriminator, context, visited, indent) {
|
|
478
|
+
mapDiscriminatedUnion(schemas, discriminator, context, visited, indent, spine) {
|
|
298
479
|
const propertyName = discriminator.propertyName;
|
|
299
480
|
const mapping = discriminator.mapping;
|
|
300
481
|
const allImports = [];
|
|
301
482
|
const parts = [];
|
|
302
483
|
for (const schema of schemas) {
|
|
303
|
-
|
|
304
|
-
|
|
484
|
+
if (this.discriminatorRegistry && isRefObject(schema)) {
|
|
485
|
+
const refStr = schema.$ref;
|
|
486
|
+
const canonical = this.canonicalSchemasRef(refStr);
|
|
487
|
+
const entry = this.discriminatorRegistry.byRef.get(refStr) ??
|
|
488
|
+
this.discriminatorRegistry.byRef.get(canonical);
|
|
489
|
+
// D7: registered family variants are bare names — their named
|
|
490
|
+
// definitions already carry the literal exactly once. An explicit
|
|
491
|
+
// mapping defines the family's membership: when one exists, a
|
|
492
|
+
// oneOf member absent from it is foreign (wrapper + warn below);
|
|
493
|
+
// without a mapping every oneOf ref is an implicit member (D3).
|
|
494
|
+
const mappedRef = mapping !== undefined &&
|
|
495
|
+
(Object.values(mapping).includes(refStr) || Object.values(mapping).includes(canonical));
|
|
496
|
+
if (entry && entry.propertyName === propertyName && (mapping === undefined || mappedRef)) {
|
|
497
|
+
if (entry.isNamed) {
|
|
498
|
+
allImports.push(entry.typeName);
|
|
499
|
+
parts.push(entry.typeName);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const resolved = this.tryResolveRef(entry.refStr);
|
|
503
|
+
if (resolved) {
|
|
504
|
+
const expanded = this.mapInternal(resolved, undefined, context, visited, indent, spineForEntry(entry));
|
|
505
|
+
allImports.push(...expanded.imports);
|
|
506
|
+
parts.push(this.appendDiscriminatorLiteral(expanded, propertyName, entry.literalValue).tsType);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Foreign (non-family) ref: keep the literal wrapper and warn —
|
|
511
|
+
// nothing guarantees the target declares its own discriminant.
|
|
512
|
+
this.warnSink(`Warning: discriminated union member "${refStr}" is not registered in a discriminator family; keeping the inline '{ ${propertyName}: <literal> } &' wrapper.\n`);
|
|
513
|
+
}
|
|
514
|
+
const discriminantValue = this.discriminatorRegistry
|
|
515
|
+
? this.resolveRegistryDiscriminantValue(schema, propertyName, mapping)
|
|
516
|
+
: this.resolveDiscriminantValue(schema, propertyName, mapping);
|
|
517
|
+
const variantResult = this.mapInternal(schema, undefined, context, visited, indent, this.discriminatorRegistry ? undefined : spine);
|
|
305
518
|
allImports.push(...variantResult.imports);
|
|
306
519
|
const quotedProp = quoteKey(propertyName);
|
|
307
520
|
parts.push(`({ ${quotedProp}: '${discriminantValue}' } & ${variantResult.tsType})`);
|
|
308
521
|
}
|
|
309
522
|
return { tsType: parts.join(' | '), imports: allImports };
|
|
310
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* D3 literal precedence for registry-mode wrapper literals (inline variant
|
|
526
|
+
* schemas and foreign refs): mapping key > own `const` on the discriminant
|
|
527
|
+
* property > single-value `enum` > RAW last ref segment (the runtime
|
|
528
|
+
* value, NOT the sanitized type name). Values are escaped for a
|
|
529
|
+
* single-quoted TS string.
|
|
530
|
+
*/
|
|
531
|
+
resolveRegistryDiscriminantValue(schema, propertyName, mapping) {
|
|
532
|
+
if (mapping && isRefObject(schema)) {
|
|
533
|
+
const refStr = schema.$ref;
|
|
534
|
+
for (const [value, ref] of Object.entries(mapping)) {
|
|
535
|
+
if (ref === refStr)
|
|
536
|
+
return escapeStringLiteral(value);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const resolved = isRefObject(schema)
|
|
540
|
+
? this.tryResolveRef(schema.$ref)
|
|
541
|
+
: schema;
|
|
542
|
+
const propSchema = resolved?.properties?.[propertyName];
|
|
543
|
+
if (propSchema && propSchema.const !== undefined) {
|
|
544
|
+
return escapeStringLiteral(String(propSchema.const));
|
|
545
|
+
}
|
|
546
|
+
if (propSchema && propSchema.enum !== undefined && propSchema.enum.length === 1) {
|
|
547
|
+
return escapeStringLiteral(String(propSchema.enum[0]));
|
|
548
|
+
}
|
|
549
|
+
if (isRefObject(schema)) {
|
|
550
|
+
const refStr = schema.$ref;
|
|
551
|
+
return escapeStringLiteral(decodedLastRefSegment(refStr));
|
|
552
|
+
}
|
|
553
|
+
return 'unknown';
|
|
554
|
+
}
|
|
311
555
|
resolveDiscriminantValue(schema, propertyName, mapping) {
|
|
312
556
|
// Try explicit mapping first
|
|
313
557
|
if (mapping && isRefObject(schema)) {
|
|
@@ -339,7 +583,7 @@ export class SchemaMapper {
|
|
|
339
583
|
imports: [],
|
|
340
584
|
};
|
|
341
585
|
}
|
|
342
|
-
const itemResult = this.mapInternal(schema.items, undefined, context, visited, indent);
|
|
586
|
+
const itemResult = this.mapInternal(schema.items, undefined, context, visited, indent, undefined);
|
|
343
587
|
const tsType = isComplexType(itemResult.tsType)
|
|
344
588
|
? `Array<${itemResult.tsType}>`
|
|
345
589
|
: `${itemResult.tsType}[]`;
|
|
@@ -374,7 +618,7 @@ export class SchemaMapper {
|
|
|
374
618
|
const ind = indentBy(indentLevel);
|
|
375
619
|
return block.split('\n').map((line) => `${ind}${line}`);
|
|
376
620
|
}
|
|
377
|
-
mapObject(schema, name, context, visited, indent) {
|
|
621
|
+
mapObject(schema, name, context, visited, indent, spine) {
|
|
378
622
|
const properties = schema.properties ?? {};
|
|
379
623
|
const requiredSet = new Set(schema.required ?? []);
|
|
380
624
|
const propNames = Object.keys(properties);
|
|
@@ -389,6 +633,19 @@ export class SchemaMapper {
|
|
|
389
633
|
return false;
|
|
390
634
|
return true;
|
|
391
635
|
});
|
|
636
|
+
// Inside a mapping target's own definition the discriminator property is
|
|
637
|
+
// supplied by the single literal append — an own redeclaration (const or
|
|
638
|
+
// enum) would intersect a conflicting literal and collapse to `never`.
|
|
639
|
+
if (spine) {
|
|
640
|
+
const ownDiscIndex = filteredPropNames.indexOf(spine.propertyName);
|
|
641
|
+
if (ownDiscIndex !== -1) {
|
|
642
|
+
const ownValue = ownDeclaredDiscriminant(properties[spine.propertyName]);
|
|
643
|
+
if (ownValue !== undefined && escapeStringLiteral(ownValue) !== spine.literalValue) {
|
|
644
|
+
this.warnSink(`Warning: Discriminator mapping key "${spine.literalValue}" for property "${spine.propertyName}" conflicts with the variant's own declared value "${ownValue}"; the mapping key wins.\n`);
|
|
645
|
+
}
|
|
646
|
+
filteredPropNames.splice(ownDiscIndex, 1);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
392
649
|
const ownIndent = indentBy(indent);
|
|
393
650
|
const memberIndent = indentBy(indent + 1);
|
|
394
651
|
const memberLines = [];
|
|
@@ -403,7 +660,7 @@ export class SchemaMapper {
|
|
|
403
660
|
}
|
|
404
661
|
for (const propName of filteredPropNames) {
|
|
405
662
|
const propSchema = properties[propName];
|
|
406
|
-
const propResult = this.mapInternal(propSchema, undefined, context, visited, indent + 1);
|
|
663
|
+
const propResult = this.mapInternal(propSchema, undefined, context, visited, indent + 1, undefined);
|
|
407
664
|
allImports.push(...propResult.imports);
|
|
408
665
|
memberLines.push(...this.renderPropertyJsDoc(propSchema, indent + 1));
|
|
409
666
|
const optional = requiredSet.has(propName) ? '' : '?';
|
|
@@ -419,7 +676,7 @@ export class SchemaMapper {
|
|
|
419
676
|
indexSignature = '[key: string]: unknown';
|
|
420
677
|
}
|
|
421
678
|
else if (additionalProps !== undefined && typeof additionalProps === 'object') {
|
|
422
|
-
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited, indent + 1);
|
|
679
|
+
const addPropResult = this.mapInternal(additionalProps, undefined, context, visited, indent + 1, undefined);
|
|
423
680
|
allImports.push(...addPropResult.imports);
|
|
424
681
|
indexSignature = `[key: string]: ${addPropResult.tsType}`;
|
|
425
682
|
}
|
|
@@ -467,7 +724,9 @@ export class SchemaMapper {
|
|
|
467
724
|
}
|
|
468
725
|
if (schema.nullable === true) {
|
|
469
726
|
return {
|
|
470
|
-
tsType:
|
|
727
|
+
tsType: hasTopLevelUnion(result.tsType)
|
|
728
|
+
? `(${result.tsType}) | null`
|
|
729
|
+
: `${result.tsType} | null`,
|
|
471
730
|
imports: result.imports,
|
|
472
731
|
};
|
|
473
732
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { RefResolver } from '../parser/ref-resolver.js';
|
|
2
|
+
import type { MethodNameStrategy } from '../types/client.js';
|
|
3
|
+
import type { AnalyzedOperation } from './path-analyzer.js';
|
|
4
|
+
/**
|
|
5
|
+
* Finished (render-ready) type declaration for the contracts file: the emitted
|
|
6
|
+
* type name, its complete definition statement, and an optional type-level
|
|
7
|
+
* JSDoc block rendered above it.
|
|
8
|
+
*/
|
|
9
|
+
export interface TypeDeclaration {
|
|
10
|
+
/** Emitted type name (rename-aware). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Complete definition statement, e.g. `export type User = { … };`. */
|
|
13
|
+
definition: string;
|
|
14
|
+
/** Type-level JSDoc block, when the source carries renderable metadata. */
|
|
15
|
+
jsDoc?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Finished security scheme type declaration (contracts Section 1b): the
|
|
19
|
+
* collision-deduped `{Name}Auth` type name, the rendered object type text,
|
|
20
|
+
* and the description-only JSDoc block ('' when absent).
|
|
21
|
+
*/
|
|
22
|
+
export interface SecuritySchemeTypeDeclaration {
|
|
23
|
+
/** Emitted `{Name}Auth` type name (sanitized, numbered on collision). */
|
|
24
|
+
name: string;
|
|
25
|
+
/** Rendered object type text — the `securitySchemeToTsType` output. */
|
|
26
|
+
tsType: string;
|
|
27
|
+
/** Description JSDoc block; '' when the scheme carries no description. */
|
|
28
|
+
jsDoc: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Finished server variable type declaration (contracts Section 1c). Names
|
|
32
|
+
* are index-aware over ALL `doc.servers` — `ServerParams` when the document
|
|
33
|
+
* declares a single server, `Server{i}Params` otherwise — so servers without
|
|
34
|
+
* variables still occupy an index.
|
|
35
|
+
*/
|
|
36
|
+
export interface ServerTypeDeclaration {
|
|
37
|
+
name: string;
|
|
38
|
+
/** Server URL, emitted as the `/** Server: url *\/` comment when present. */
|
|
39
|
+
url?: string;
|
|
40
|
+
/** Finished interface body lines — variable JSDoc comments and property lines, indentation included. */
|
|
41
|
+
propertyLines: string[];
|
|
42
|
+
}
|
|
43
|
+
/** Branded format type discovered by the schema mapper during translation. */
|
|
44
|
+
export interface BrandedTypeDeclaration {
|
|
45
|
+
name: string;
|
|
46
|
+
format: string;
|
|
47
|
+
baseType: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One multipart `FileInput` property fact from the ref-resolving walk of a
|
|
51
|
+
* multipart request body ($refs followed; a resolved `format: binary`
|
|
52
|
+
* property is a `file`/`file-array`, not a `field`).
|
|
53
|
+
*/
|
|
54
|
+
export interface FileUploadPropertyFact {
|
|
55
|
+
/** Property name in the multipart schema. */
|
|
56
|
+
name: string;
|
|
57
|
+
/** Whether the property is listed in the schema's `required` array. */
|
|
58
|
+
required: boolean;
|
|
59
|
+
/** `file` → `FileInput`, `file-array` → `FileInput[]`, `field` → `string`. */
|
|
60
|
+
kind: 'file' | 'file-array' | 'field';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Analyzed operation plus its finished translation results.
|
|
64
|
+
*
|
|
65
|
+
* The structural fields are the plain `AnalyzedOperation`; the added fields
|
|
66
|
+
* carry the real-mapper output the contracts renderer emits verbatim.
|
|
67
|
+
*/
|
|
68
|
+
export interface FinishedOperation extends AnalyzedOperation {
|
|
69
|
+
/**
|
|
70
|
+
* Finished contracts-file emission lines for this operation (Sections 2–4:
|
|
71
|
+
* query/header/body/response/error types, with JSDoc attached), in emission
|
|
72
|
+
* order. Assembled by `analyze()`; `renderContracts` spills them unchanged.
|
|
73
|
+
*/
|
|
74
|
+
contractsLines: string[];
|
|
75
|
+
/**
|
|
76
|
+
* Multipart `FileInput` property facts from the ref-resolving walk of the
|
|
77
|
+
* request body schema (`$ref`s followed; a resolved `format: binary`
|
|
78
|
+
* property is a `file`/`file-array`, not a `field`). Present exactly when
|
|
79
|
+
* the operation has a multipart body WITH a schema — an empty array means a
|
|
80
|
+
* zero-property body; `undefined` means no multipart schema (no FormData
|
|
81
|
+
* emission). Sole fact source for the client generator's FormData appends.
|
|
82
|
+
*/
|
|
83
|
+
fileUploadProperties?: FileUploadPropertyFact[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Single-pass analysis model of an OpenAPI document — the ONLY input every
|
|
87
|
+
* generator renderer consumes. Renderers get the model plus config; they never
|
|
88
|
+
* touch the raw spec or a `RefResolver`.
|
|
89
|
+
*/
|
|
90
|
+
export interface AnalyzedSpec {
|
|
91
|
+
/** `doc.openapi` verbatim (e.g. `"3.1.0"`) — feeds generated file headers. */
|
|
92
|
+
specVersion: string;
|
|
93
|
+
/** Operations with structural facts and finished contracts emission lines. */
|
|
94
|
+
operations: FinishedOperation[];
|
|
95
|
+
/**
|
|
96
|
+
* Section-1 component schema declarations as finished entries, in the
|
|
97
|
+
* pre-refactor emission order: topologically sorted schemas first, then
|
|
98
|
+
* one always-emitted `{Base}Variant` union per discriminator family
|
|
99
|
+
* (members = mapping values ∪ implicit oneOf/anyOf refs, sourced from the
|
|
100
|
+
* discriminator registry; collision-safe names) in family discovery order.
|
|
101
|
+
*/
|
|
102
|
+
schemaTypes: TypeDeclaration[];
|
|
103
|
+
/** Branded format types discovered during mapping, in discovery order. */
|
|
104
|
+
brandedTypes: BrandedTypeDeclaration[];
|
|
105
|
+
/** Any operation has a multipart request body → contracts emits `FileInput`. */
|
|
106
|
+
hasFileUpload: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Finished security scheme type declarations (`{Name}Auth`), in spec key
|
|
109
|
+
* order. The `SecuritySchemes` union is emitted when more than one exists.
|
|
110
|
+
*/
|
|
111
|
+
securitySchemeTypes: SecuritySchemeTypeDeclaration[];
|
|
112
|
+
/** Finished server variable interface declarations, in spec order. */
|
|
113
|
+
serverTypes: ServerTypeDeclaration[];
|
|
114
|
+
}
|
|
115
|
+
/** Options for `analyze()`. */
|
|
116
|
+
export interface AnalyzeOptions {
|
|
117
|
+
/** Resolver for `$ref` pointers; defaults to a fresh `RefResolver(doc)`. */
|
|
118
|
+
resolver?: RefResolver;
|
|
119
|
+
/** Method naming strategy; defaults to `'path-based'`. */
|
|
120
|
+
strategy?: MethodNameStrategy;
|
|
121
|
+
/**
|
|
122
|
+
* Effective OpenAPI dialect, sourced from `VersionProfile.effective`.
|
|
123
|
+
* Omitted is treated as `'3.1'`.
|
|
124
|
+
*/
|
|
125
|
+
effectiveVersion?: '3.0' | '3.1';
|
|
126
|
+
}
|
package/dist/cli/app.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
interface Flags {
|
|
2
|
-
outputDir
|
|
2
|
+
outputDir?: string;
|
|
3
3
|
methodNameStrategy: 'path-based' | 'operationId' | 'operationId-with-fallback';
|
|
4
4
|
specVersion?: string;
|
|
5
|
-
strictVersion
|
|
5
|
+
strictVersion?: boolean;
|
|
6
6
|
runtimeImportPath?: string;
|
|
7
7
|
proxy?: string;
|
|
8
|
+
config?: string;
|
|
9
|
+
project?: string;
|
|
8
10
|
}
|
|
9
11
|
export type AppFlags = Flags;
|
|
10
12
|
export declare const app: import("@stricli/core").Application<import("@stricli/core").CommandContext>;
|