@obsidiane/meridiane 1.0.1 → 2.0.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 +8 -5
- package/cli.js +7 -11
- package/package.json +1 -1
- package/templates/_lib_template/README.md +194 -0
- package/templates/_lib_template/ng-package.json +3 -0
- package/tools/build.js +6 -5
- package/tools/core/generate.js +40 -24
- package/tools/core/paths.js +0 -7
- package/tools/dev.js +6 -5
- package/tools/generator/models/oas-contract.js +159 -0
- package/tools/generator/models/oas-schema-name.js +71 -0
- package/tools/generator/models/openapi-to-models.js +171 -378
- package/tools/generator/models/schema-utils.js +3 -100
- package/tools/generator/models/templates/model.hbs +3 -1
- package/tools/generator/models/type-resolver.js +6 -3
- package/tools/generator/models/naming.js +0 -26
|
@@ -1,193 +1,47 @@
|
|
|
1
1
|
import { sanitizeTypeName, quoteKeyIfNeeded } from './utils.js';
|
|
2
|
-
import {
|
|
3
|
-
import { schemaLabel } from './naming.js';
|
|
2
|
+
import { mergeAllOf, isObject, schemaNameFromRef } from './schema-utils.js';
|
|
4
3
|
import { tsTypeOf, withNullable } from './type-resolver.js';
|
|
4
|
+
import { canonicalizeMediaType, collectUsedSchemasForMediaType, expandWithSchemaDependencies } from './oas-contract.js';
|
|
5
|
+
import { baseTsNameFromBasePath, formatSuffixToken, groupTsNameFromGroupPath, parseOpenApiSchemaName } from './oas-schema-name.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {{
|
|
8
|
-
* requiredMode?: 'all
|
|
9
|
-
*
|
|
9
|
+
* requiredMode?: 'all'|'spec',
|
|
10
|
+
* formats?: string[],
|
|
10
11
|
* includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
11
12
|
* excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
|
|
12
13
|
* }} BuildOptions
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* @typedef {{
|
|
18
|
+
* name: string,
|
|
19
|
+
* tsKey: string,
|
|
20
|
+
* type: string,
|
|
21
|
+
* optional: boolean,
|
|
22
|
+
* }} ModelProp
|
|
18
23
|
*/
|
|
19
|
-
function normalizeNativeBase(original) {
|
|
20
|
-
return String(original)
|
|
21
|
-
.replace(/\.jsonld\b/ig, '')
|
|
22
|
-
.replace(/\.jsonapi\b/ig, '')
|
|
23
|
-
.replace(/\.jsonmergepatch\b/ig, '')
|
|
24
|
-
.split('-')[0];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function buildNameMap(schemaNames, schemas, preset) {
|
|
28
|
-
// Build final map with uniqueness (deterministic order)
|
|
29
|
-
const used = new Set();
|
|
30
|
-
const nameMap = new Map();
|
|
31
|
-
for (const original of schemaNames) {
|
|
32
|
-
const label = schemaLabel(original, {preset});
|
|
33
|
-
nameMap.set(original, sanitizeTypeName(label, used));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// In native mode, alias excluded variants to the selected base type
|
|
37
|
-
// so `$ref` doesn't leak "User-user.write" etc into generated types.
|
|
38
|
-
if (preset === 'native') {
|
|
39
|
-
const baseToSanitized = new Map();
|
|
40
|
-
for (const original of schemaNames) {
|
|
41
|
-
baseToSanitized.set(normalizeNativeBase(original), nameMap.get(original));
|
|
42
|
-
}
|
|
43
|
-
for (const original of Object.keys(schemas)) {
|
|
44
|
-
const base = normalizeNativeBase(original);
|
|
45
|
-
const sanitized = baseToSanitized.get(base);
|
|
46
|
-
if (sanitized) nameMap.set(original, sanitized);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return nameMap;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getContentSchema(content, wantedLower) {
|
|
54
|
-
if (!content || typeof content !== 'object') return undefined;
|
|
55
|
-
for (const [k, v] of Object.entries(content)) {
|
|
56
|
-
if (String(k).toLowerCase() === wantedLower) return v?.schema;
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function is2xxStatus(code) {
|
|
62
|
-
const s = String(code || '');
|
|
63
|
-
return /^[0-9]{3}$/.test(s) && s.startsWith('2');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function collectRefsFromSchema(schema, out) {
|
|
67
|
-
const seen = new Set();
|
|
68
|
-
|
|
69
|
-
function walk(node) {
|
|
70
|
-
if (!node || typeof node !== 'object') return;
|
|
71
|
-
if (seen.has(node)) return;
|
|
72
|
-
seen.add(node);
|
|
73
|
-
|
|
74
|
-
if (node.$ref && typeof node.$ref === 'string') {
|
|
75
|
-
out.add(schemaNameFromRef(node.$ref));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (Array.isArray(node)) {
|
|
80
|
-
for (const it of node) walk(it);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
24
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
walk(schema);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function listNativeRootsFromPaths(spec, { mediaType = 'application/ld+json' } = {}) {
|
|
99
|
-
const wantedLower = String(mediaType).toLowerCase();
|
|
100
|
-
const roots = [];
|
|
101
|
-
|
|
102
|
-
const paths = spec?.paths;
|
|
103
|
-
if (!paths || typeof paths !== 'object') return roots;
|
|
104
|
-
|
|
105
|
-
for (const pathItem of Object.values(paths)) {
|
|
106
|
-
if (!pathItem || typeof pathItem !== 'object') continue;
|
|
107
|
-
for (const op of Object.values(pathItem)) {
|
|
108
|
-
if (!op || typeof op !== 'object') continue;
|
|
109
|
-
|
|
110
|
-
// requestBody (LD+JSON only)
|
|
111
|
-
const rbSchema = getContentSchema(op.requestBody?.content, wantedLower);
|
|
112
|
-
if (rbSchema) roots.push(rbSchema);
|
|
113
|
-
|
|
114
|
-
// responses 2xx (LD+JSON only)
|
|
115
|
-
const responses = op.responses;
|
|
116
|
-
if (!responses || typeof responses !== 'object') continue;
|
|
117
|
-
for (const [code, resp] of Object.entries(responses)) {
|
|
118
|
-
if (!is2xxStatus(code)) continue;
|
|
119
|
-
const schema = getContentSchema(resp?.content, wantedLower);
|
|
120
|
-
if (schema) roots.push(schema);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return roots;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function parseOpenApiSchemaName(originalName) {
|
|
129
|
-
const raw = String(originalName || '');
|
|
130
|
-
const i = raw.indexOf('-');
|
|
131
|
-
let baseRaw = i >= 0 ? raw.slice(0, i) : raw;
|
|
132
|
-
let groupRaw = i >= 0 ? raw.slice(i + 1) : '';
|
|
133
|
-
|
|
134
|
-
let baseVariant = 'none';
|
|
135
|
-
if (/\.jsonld$/i.test(baseRaw)) {
|
|
136
|
-
baseVariant = 'jsonld';
|
|
137
|
-
baseRaw = baseRaw.replace(/\.jsonld$/i, '');
|
|
138
|
-
} else if (/\.multipart$/i.test(baseRaw)) {
|
|
139
|
-
baseVariant = 'multipart';
|
|
140
|
-
baseRaw = baseRaw.replace(/\.multipart$/i, '');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let isMergePatch = false;
|
|
144
|
-
if (/\.jsonmergepatch$/i.test(baseRaw)) {
|
|
145
|
-
isMergePatch = true;
|
|
146
|
-
baseRaw = baseRaw.replace(/\.jsonmergepatch$/i, '');
|
|
147
|
-
}
|
|
148
|
-
if (/\.jsonmergepatch$/i.test(groupRaw)) {
|
|
149
|
-
isMergePatch = true;
|
|
150
|
-
groupRaw = groupRaw.replace(/\.jsonmergepatch$/i, '');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
originalName: raw,
|
|
155
|
-
basePath: baseRaw,
|
|
156
|
-
groupPath: groupRaw,
|
|
157
|
-
baseVariant,
|
|
158
|
-
isMergePatch,
|
|
159
|
-
key: `${baseRaw.toLowerCase()}|${groupRaw.toLowerCase()}`,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function pascalizeTokens(tokens) {
|
|
164
|
-
return tokens
|
|
165
|
-
.filter(Boolean)
|
|
166
|
-
.map((t) => String(t).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
|
|
167
|
-
.filter(Boolean)
|
|
168
|
-
.map((t) => t.charAt(0).toUpperCase() + t.slice(1))
|
|
169
|
-
.join('');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function baseTsNameFromBasePath(basePath) {
|
|
173
|
-
const parts = String(basePath || '').split('.').filter(Boolean);
|
|
174
|
-
if (parts.length === 0) return 'Model';
|
|
175
|
-
const a = pascalizeTokens([parts[0]]);
|
|
176
|
-
if (parts.length === 1) return a;
|
|
177
|
-
const b = pascalizeTokens([parts[1]]);
|
|
178
|
-
if (parts.length === 2 && b.toLowerCase().startsWith(a.toLowerCase())) return b;
|
|
179
|
-
return parts.map((p) => pascalizeTokens([p])).join('');
|
|
180
|
-
}
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{
|
|
27
|
+
* name: string,
|
|
28
|
+
* props: ModelProp[],
|
|
29
|
+
* imports: string[],
|
|
30
|
+
* extendsTypes: string[],
|
|
31
|
+
* extendsItem?: boolean,
|
|
32
|
+
* }} ModelDefinition
|
|
33
|
+
*/
|
|
181
34
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
return pascalizeTokens(tokens);
|
|
35
|
+
function ensureNullable(ts) {
|
|
36
|
+
return /\|\s*null\b/.test(ts) ? ts : `${ts} | null`;
|
|
185
37
|
}
|
|
186
38
|
|
|
187
|
-
function
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
39
|
+
function normalizeRequiredMode(value) {
|
|
40
|
+
const v = String(value ?? '').trim().toLowerCase();
|
|
41
|
+
if (!v) return 'all';
|
|
42
|
+
if (v === 'spec') return 'spec';
|
|
43
|
+
if (v === 'all') return 'all';
|
|
44
|
+
return 'all';
|
|
191
45
|
}
|
|
192
46
|
|
|
193
47
|
/**
|
|
@@ -197,250 +51,189 @@ function stableUnion(types) {
|
|
|
197
51
|
* @returns {{ models: ModelDefinition[] }}
|
|
198
52
|
*/
|
|
199
53
|
export function buildModelsFromOpenAPI(spec, options) {
|
|
200
|
-
const opts = { requiredMode: 'all
|
|
54
|
+
const opts = { requiredMode: 'all', ...(options || {}) };
|
|
201
55
|
const schemas = spec?.components?.schemas || {};
|
|
202
|
-
const
|
|
56
|
+
const formatsOpt = Array.isArray(opts.formats) ? opts.formats : [];
|
|
57
|
+
const formats = formatsOpt.length > 0 ? formatsOpt : ['application/ld+json'];
|
|
58
|
+
const requiredMode = normalizeRequiredMode(opts.requiredMode);
|
|
203
59
|
|
|
204
60
|
/** @type {ModelDefinition[]} */
|
|
205
61
|
const models = [];
|
|
206
62
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!sanitized) continue;
|
|
216
|
-
|
|
217
|
-
/** @type {string[]} */
|
|
218
|
-
const extendsTypes = [];
|
|
219
|
-
|
|
220
|
-
let effective = raw;
|
|
221
|
-
if (raw?.allOf) {
|
|
222
|
-
const merged = mergeAllOf(raw.allOf);
|
|
223
|
-
if (merged) {
|
|
224
|
-
effective = merged;
|
|
225
|
-
} else {
|
|
226
|
-
// Non-flattenable allOf (refs, mixed forms): keep inline object props and model refs as extends.
|
|
227
|
-
const props = {};
|
|
228
|
-
const required = new Set();
|
|
229
|
-
const ext = new Set();
|
|
230
|
-
|
|
231
|
-
for (const part of raw.allOf) {
|
|
232
|
-
if (part?.$ref) {
|
|
233
|
-
const orig = schemaNameFromRef(part.$ref);
|
|
234
|
-
const refName = orig ? nameMap.get(orig) : undefined;
|
|
235
|
-
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
239
|
-
if (!isObj) continue;
|
|
240
|
-
Object.assign(props, part.properties || {});
|
|
241
|
-
for (const r of part.required || []) required.add(r);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
|
|
245
|
-
effective = { type: 'object', properties: props, required: [...required] };
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
|
|
250
|
-
if (!isObjectLike) continue;
|
|
251
|
-
|
|
252
|
-
const required = new Set(effective.required || []);
|
|
253
|
-
const deps = new Set();
|
|
254
|
-
|
|
255
|
-
for (const e of extendsTypes) deps.add(e);
|
|
256
|
-
|
|
257
|
-
const props = Object.entries(effective.properties || {}).map(([propName, prop]) => {
|
|
258
|
-
const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode: opts.requiredMode ?? 'all-optional', forceNullable: false });
|
|
259
|
-
const type = withNullable(baseType, prop);
|
|
260
|
-
return {
|
|
261
|
-
name: propName,
|
|
262
|
-
tsKey: quoteKeyIfNeeded(propName),
|
|
263
|
-
type,
|
|
264
|
-
optional: opts.requiredMode === 'spec' ? !required.has(propName) : true,
|
|
265
|
-
};
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
const imports = [...deps]
|
|
269
|
-
.filter((n) => n !== sanitized)
|
|
270
|
-
.filter((n) => allSanitizedNames.has(n))
|
|
271
|
-
.sort((a, b) => a.localeCompare(b));
|
|
63
|
+
// Contract-driven (formats): keep only schemas used by selected media types in real endpoints.
|
|
64
|
+
const normalizedFormats = [];
|
|
65
|
+
for (const f of formats) {
|
|
66
|
+
const canon = canonicalizeMediaType(f);
|
|
67
|
+
if (!canon) continue;
|
|
68
|
+
if (!normalizedFormats.includes(canon)) normalizedFormats.push(canon);
|
|
69
|
+
}
|
|
70
|
+
if (normalizedFormats.length === 0) normalizedFormats.push('application/ld+json');
|
|
272
71
|
|
|
273
|
-
|
|
274
|
-
|
|
72
|
+
/** @type {Map<string, Set<string>>} */
|
|
73
|
+
const usedByFormat = new Map(); // formatLower -> Set<schemaName>
|
|
275
74
|
|
|
276
|
-
|
|
75
|
+
for (const fmt of normalizedFormats) {
|
|
76
|
+
usedByFormat.set(fmt, collectUsedSchemasForMediaType(spec, { mediaType: fmt }));
|
|
277
77
|
}
|
|
278
78
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!name || usedSchemaNames.has(name)) continue;
|
|
289
|
-
usedSchemaNames.add(name);
|
|
290
|
-
const schema = schemas[name];
|
|
291
|
-
if (!schema) continue;
|
|
292
|
-
const refs = new Set();
|
|
293
|
-
collectRefsFromSchema(schema, refs);
|
|
294
|
-
for (const ref of refs) if (ref && !usedSchemaNames.has(ref)) queue.push(ref);
|
|
79
|
+
/** @type {Map<string, Set<string>>} */
|
|
80
|
+
const schemaToFormats = new Map();
|
|
81
|
+
for (const [fmt, names] of usedByFormat.entries()) {
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
if (!name) continue;
|
|
84
|
+
const set = schemaToFormats.get(name) || new Set();
|
|
85
|
+
set.add(fmt);
|
|
86
|
+
schemaToFormats.set(name, set);
|
|
87
|
+
}
|
|
295
88
|
}
|
|
296
89
|
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
// User include/exclude filters (apply to canonical name and to raw variants)
|
|
307
|
-
const canonical = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
|
|
308
|
-
const fulls = [originalName, canonical];
|
|
309
|
-
const include = Array.isArray(opts.includeSchemaNames) ? opts.includeSchemaNames : [];
|
|
310
|
-
const exclude = Array.isArray(opts.excludeSchemaNames) ? opts.excludeSchemaNames : [];
|
|
311
|
-
|
|
312
|
-
const matches = (val, rule) => {
|
|
313
|
-
if (rule instanceof RegExp) return rule.test(val);
|
|
314
|
-
if (typeof rule === 'function') return !!rule(val);
|
|
315
|
-
if (typeof rule === 'string' && rule.length) return val.includes(rule);
|
|
316
|
-
return false;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const hasInclude = include.length > 0;
|
|
320
|
-
const included = !hasInclude || include.some((r) => fulls.some((v) => matches(v, r)));
|
|
321
|
-
const excluded = exclude.some((r) => fulls.some((v) => matches(v, r)));
|
|
322
|
-
if (!included || excluded) continue;
|
|
323
|
-
|
|
324
|
-
const bucket = buckets.get(parsed.key) || { basePath: parsed.basePath, groupPath: parsed.groupPath, entries: [] };
|
|
325
|
-
bucket.entries.push(parsed);
|
|
326
|
-
buckets.set(parsed.key, bucket);
|
|
327
|
-
}
|
|
90
|
+
const include = Array.isArray(opts.includeSchemaNames) ? opts.includeSchemaNames : [];
|
|
91
|
+
const exclude = Array.isArray(opts.excludeSchemaNames) ? opts.excludeSchemaNames : [];
|
|
92
|
+
const matches = (val, rule) => {
|
|
93
|
+
if (rule instanceof RegExp) return rule.test(val);
|
|
94
|
+
if (typeof rule === 'function') return !!rule(val);
|
|
95
|
+
if (typeof rule === 'string' && rule.length) return val.includes(rule);
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
328
98
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
99
|
+
const allUsed = [...schemaToFormats.keys()]
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.filter((n) => !/^Hydra/i.test(n))
|
|
102
|
+
.sort((a, b) => a.localeCompare(b))
|
|
103
|
+
.filter((originalName) => {
|
|
104
|
+
const parsed = parseOpenApiSchemaName(originalName);
|
|
105
|
+
const canonical = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
|
|
106
|
+
const fulls = [originalName, canonical];
|
|
107
|
+
|
|
108
|
+
const hasInclude = include.length > 0;
|
|
109
|
+
const included = !hasInclude || include.some((r) => fulls.some((v) => matches(v, r)));
|
|
110
|
+
const excluded = exclude.some((r) => fulls.some((v) => matches(v, r)));
|
|
111
|
+
return included && !excluded;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Ensure filters never break compilation: always keep transitive deps of selected schemas.
|
|
115
|
+
const candidatesSet = include.length > 0 || exclude.length > 0 ? expandWithSchemaDependencies(allUsed, schemas) : new Set(allUsed);
|
|
116
|
+
|
|
117
|
+
const candidates = [...candidatesSet]
|
|
118
|
+
.filter((n) => !!n)
|
|
119
|
+
.filter((n) => !/^Hydra/i.test(n))
|
|
120
|
+
.sort((a, b) => a.localeCompare(b));
|
|
121
|
+
|
|
122
|
+
// Assign a preferred format to each schema (used for naming + JSON-LD behavior).
|
|
123
|
+
const formatIndex = new Map(normalizedFormats.map((f, i) => [f, i]));
|
|
124
|
+
const schemaEntries = candidates.map((schemaName) => {
|
|
125
|
+
const fmts = schemaToFormats.get(schemaName) || new Set();
|
|
126
|
+
const best = [...fmts].sort((a, b) => (formatIndex.get(a) ?? 999) - (formatIndex.get(b) ?? 999))[0] || normalizedFormats[0];
|
|
127
|
+
return { schemaName, preferredFormat: best, preferredFormatIndex: formatIndex.get(best) ?? 0 };
|
|
334
128
|
});
|
|
129
|
+
const preferredFormatBySchema = new Map(schemaEntries.map((e) => [e.schemaName, e.preferredFormat]));
|
|
130
|
+
|
|
131
|
+
// Compute desired TS names (format-agnostic), then disambiguate collisions by preferred format.
|
|
132
|
+
const byDesired = new Map(); // desired -> entries[]
|
|
133
|
+
for (const e of schemaEntries) {
|
|
134
|
+
const parsed = parseOpenApiSchemaName(e.schemaName);
|
|
135
|
+
if (parsed.isMergePatch) continue; // never generate patch models
|
|
136
|
+
const baseTs = baseTsNameFromBasePath(parsed.basePath);
|
|
137
|
+
const groupTs = groupTsNameFromGroupPath(parsed.groupPath);
|
|
138
|
+
const desired = `${baseTs}${groupTs}` || 'Model';
|
|
139
|
+
const list = byDesired.get(desired) || [];
|
|
140
|
+
list.push({ ...e, desired });
|
|
141
|
+
byDesired.set(desired, list);
|
|
142
|
+
}
|
|
335
143
|
|
|
336
|
-
//
|
|
144
|
+
// Assign unique TS names and build schemaName -> tsName map.
|
|
337
145
|
const usedTsNames = new Set();
|
|
338
146
|
const nameMap = new Map();
|
|
339
|
-
for (const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
147
|
+
for (const [desired, list] of [...byDesired.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
148
|
+
list.sort((a, b) => {
|
|
149
|
+
const da = a.preferredFormatIndex - b.preferredFormatIndex;
|
|
150
|
+
if (da !== 0) return da;
|
|
151
|
+
return a.schemaName.localeCompare(b.schemaName);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < list.length; i++) {
|
|
155
|
+
const entry = list[i];
|
|
156
|
+
const withFmt = i === 0 ? desired : `${desired}${formatSuffixToken(entry.preferredFormat)}`;
|
|
157
|
+
const tsName = sanitizeTypeName(withFmt, usedTsNames);
|
|
158
|
+
nameMap.set(entry.schemaName, tsName);
|
|
347
159
|
}
|
|
348
160
|
}
|
|
349
|
-
const allSanitizedNames = new Set(
|
|
161
|
+
const allSanitizedNames = new Set([...nameMap.values()]);
|
|
350
162
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const sanitized = b.tsName;
|
|
163
|
+
for (const schemaName of candidates) {
|
|
164
|
+
const sanitized = nameMap.get(schemaName);
|
|
354
165
|
if (!sanitized) continue;
|
|
355
166
|
|
|
167
|
+
const raw = schemas[schemaName];
|
|
168
|
+
if (!raw) continue;
|
|
169
|
+
|
|
170
|
+
const preferredFormat = preferredFormatBySchema.get(schemaName) ?? normalizedFormats[0];
|
|
171
|
+
const isJsonLd = canonicalizeMediaType(preferredFormat) === 'application/ld+json';
|
|
172
|
+
const forceNullable = requiredMode !== 'spec';
|
|
173
|
+
|
|
174
|
+
/** @type {string[]} */
|
|
175
|
+
const extendsTypes = [];
|
|
356
176
|
const deps = new Set();
|
|
357
|
-
const extendsSet = new Set();
|
|
358
|
-
const propTypes = new Map(); // propName -> Set<tsType>
|
|
359
|
-
|
|
360
|
-
// Choose candidates: jsonld + base (no multipart, no merge-patch)
|
|
361
|
-
const candidates = b.entries
|
|
362
|
-
.filter((e) => !e.isMergePatch)
|
|
363
|
-
.filter((e) => e.baseVariant !== 'multipart')
|
|
364
|
-
.sort((a, b2) => {
|
|
365
|
-
const score = (e) => (e.baseVariant === 'jsonld' ? 2 : 1);
|
|
366
|
-
const sa = score(a), sb = score(b2);
|
|
367
|
-
if (sb !== sa) return sb - sa;
|
|
368
|
-
return a.originalName.localeCompare(b2.originalName);
|
|
369
|
-
});
|
|
370
177
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
391
|
-
continue;
|
|
392
|
-
}
|
|
393
|
-
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
394
|
-
if (!isObj) continue;
|
|
395
|
-
Object.assign(props, part.properties || {});
|
|
178
|
+
let effective = raw;
|
|
179
|
+
if (raw?.allOf) {
|
|
180
|
+
const merged = mergeAllOf(raw.allOf);
|
|
181
|
+
if (merged) {
|
|
182
|
+
effective = merged;
|
|
183
|
+
} else {
|
|
184
|
+
// Non-flattenable allOf: keep inline object props + other model refs as extends.
|
|
185
|
+
const props = {};
|
|
186
|
+
const required = new Set();
|
|
187
|
+
const ext = new Set();
|
|
188
|
+
|
|
189
|
+
for (const part of raw.allOf) {
|
|
190
|
+
if (part?.$ref) {
|
|
191
|
+
const orig = schemaNameFromRef(part.$ref);
|
|
192
|
+
if (!orig) continue;
|
|
193
|
+
if (/^Hydra/i.test(orig)) continue;
|
|
194
|
+
const refName = orig ? nameMap.get(orig) || orig : undefined;
|
|
195
|
+
if (refName && refName !== sanitized && allSanitizedNames.has(refName)) ext.add(refName);
|
|
196
|
+
continue;
|
|
396
197
|
}
|
|
397
|
-
|
|
398
|
-
|
|
198
|
+
const isObj = part?.type === 'object' || isObject(part?.properties);
|
|
199
|
+
if (!isObj) continue;
|
|
200
|
+
Object.assign(props, part.properties || {});
|
|
201
|
+
for (const r of part.required || []) required.add(r);
|
|
399
202
|
}
|
|
400
|
-
}
|
|
401
203
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
for (const ex of extendsTypes) extendsSet.add(ex);
|
|
406
|
-
|
|
407
|
-
for (const [propName, prop] of Object.entries(effective.properties || {})) {
|
|
408
|
-
const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode: 'all-optional', forceNullable: true });
|
|
409
|
-
const existing = propTypes.get(propName) || new Set();
|
|
410
|
-
existing.add(baseType);
|
|
411
|
-
propTypes.set(propName, existing);
|
|
204
|
+
extendsTypes.push(...[...ext].sort((a, b) => a.localeCompare(b)));
|
|
205
|
+
effective = { type: 'object', properties: props, required: [...required] };
|
|
412
206
|
}
|
|
413
207
|
}
|
|
414
208
|
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
.filter((n) => allSanitizedNames.has(n))
|
|
418
|
-
.sort((a, b2) => a.localeCompare(b2));
|
|
209
|
+
const isObjectLike = effective?.type === 'object' || isObject(effective?.properties) || extendsTypes.length > 0;
|
|
210
|
+
if (!isObjectLike) continue;
|
|
419
211
|
|
|
420
|
-
// Imports are any referenced model types (+ extends), excluding self.
|
|
421
212
|
for (const e of extendsTypes) deps.add(e);
|
|
422
|
-
const imports = [...deps]
|
|
423
|
-
.filter((n) => n !== sanitized)
|
|
424
|
-
.filter((n) => allSanitizedNames.has(n))
|
|
425
|
-
.sort((a, b2) => a.localeCompare(b2));
|
|
426
213
|
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
.
|
|
430
|
-
|
|
431
|
-
|
|
214
|
+
const required = new Set(effective.required || []);
|
|
215
|
+
const props = Object.entries(effective.properties || {})
|
|
216
|
+
.filter(([propName]) => {
|
|
217
|
+
if (!isJsonLd) return true;
|
|
218
|
+
return propName !== '@id' && propName !== '@type' && propName !== '@context';
|
|
219
|
+
})
|
|
220
|
+
.map(([propName, prop]) => {
|
|
221
|
+
const baseType = tsTypeOf(prop, deps, nameMap, { requiredMode });
|
|
222
|
+
const type = forceNullable ? ensureNullable(baseType) : withNullable(baseType, prop);
|
|
432
223
|
return {
|
|
433
224
|
name: propName,
|
|
434
225
|
tsKey: quoteKeyIfNeeded(propName),
|
|
435
226
|
type,
|
|
436
|
-
optional: true,
|
|
227
|
+
optional: requiredMode === 'spec' ? !required.has(propName) : true,
|
|
437
228
|
};
|
|
438
229
|
});
|
|
439
230
|
|
|
440
|
-
|
|
441
|
-
|
|
231
|
+
const imports = [...deps]
|
|
232
|
+
.filter((n) => n !== sanitized)
|
|
233
|
+
.filter((n) => allSanitizedNames.has(n))
|
|
234
|
+
.sort((a, b) => a.localeCompare(b));
|
|
442
235
|
|
|
443
|
-
models.push({ name: sanitized, props, imports, extendsTypes });
|
|
236
|
+
models.push({ name: sanitized, props, imports, extendsTypes, extendsItem: isJsonLd });
|
|
444
237
|
}
|
|
445
238
|
|
|
446
239
|
return { models };
|