chrometools-mcp 3.3.8 → 3.3.9
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/CHANGELOG.md +40 -0
- package/README.md +129 -24
- package/SPEC-pom-integration.md +227 -0
- package/SPEC-swagger-api-tools.md +3101 -0
- package/index.js +503 -198
- package/package.json +2 -1
- package/pom/apom-tree-converter.js +5 -26
- package/recorder/page-object-generator.js +45 -1
- package/server/tool-definitions.js +54 -5
- package/server/tool-schemas.js +29 -0
- package/test-swagger-phase1.mjs +959 -0
- package/utils/api-generators/api-models-python.js +448 -0
- package/utils/api-generators/api-models-typescript.js +375 -0
- package/utils/code-generators/code-generator-base.js +111 -6
- package/utils/code-generators/playwright-python.js +74 -0
- package/utils/code-generators/playwright-typescript.js +69 -0
- package/utils/code-generators/pom-integrator.js +373 -0
- package/utils/code-generators/selenium-java.js +72 -0
- package/utils/code-generators/selenium-python.js +75 -0
- package/utils/hints-generator.js +114 -19
- package/utils/openapi/helpers.js +25 -0
- package/utils/openapi/parser.js +448 -0
- package/utils/openapi/ref-resolver.js +149 -0
- package/utils/openapi/type-mapper.js +174 -0
- package/nul +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/openapi/helpers.js
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for OpenAPI processing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compare two arrays for shallow equality
|
|
9
|
+
*/
|
|
10
|
+
export function arraysEqual(a, b) {
|
|
11
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
12
|
+
return a.every((v, i) => v === b[i]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a signature from object properties (key + type) for schema matching.
|
|
17
|
+
* Used by _findSchemaName to avoid false positives from key-only comparison.
|
|
18
|
+
*/
|
|
19
|
+
export function schemaSignature(properties) {
|
|
20
|
+
if (!properties) return '';
|
|
21
|
+
return Object.entries(properties)
|
|
22
|
+
.map(([k, v]) => `${k}:${v.type || v.$circularRef || '?'}`)
|
|
23
|
+
.sort()
|
|
24
|
+
.join(',');
|
|
25
|
+
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/openapi/parser.js
|
|
3
|
+
*
|
|
4
|
+
* Unified parser for OpenAPI 2.0 (Swagger) and 3.x specs.
|
|
5
|
+
* Normalizes 2.0 to 3.x internally.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import yaml from 'js-yaml';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { resolveAllRefs } from './ref-resolver.js';
|
|
11
|
+
import { schemaSignature } from './helpers.js';
|
|
12
|
+
|
|
13
|
+
export class OpenAPIParser {
|
|
14
|
+
constructor(rawSpec) {
|
|
15
|
+
this.raw = rawSpec;
|
|
16
|
+
this.version = this.detectVersion();
|
|
17
|
+
this.spec = this.normalize();
|
|
18
|
+
this.spec = resolveAllRefs(this.spec);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load spec from URL or file path
|
|
23
|
+
* @param {string} source - URL or file path
|
|
24
|
+
* @param {string} [format='auto'] - 'auto' | 'json' | 'yaml'
|
|
25
|
+
* @returns {OpenAPIParser}
|
|
26
|
+
*/
|
|
27
|
+
static async load(source, format = 'auto') {
|
|
28
|
+
let content;
|
|
29
|
+
|
|
30
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(source, { signal: controller.signal });
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(`Failed to fetch ${source}: ${response.status} ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
content = await response.text();
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timeoutId);
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
content = readFileSync(source, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spec = parseContent(content, format, source);
|
|
47
|
+
return new OpenAPIParser(spec);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
detectVersion() {
|
|
51
|
+
if (this.raw.swagger === '2.0') return '2.0';
|
|
52
|
+
if (this.raw.openapi && this.raw.openapi.startsWith('3.')) return this.raw.openapi;
|
|
53
|
+
throw new Error('Unsupported OpenAPI version. Expected swagger: "2.0" or openapi: "3.x.x"');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
normalize() {
|
|
57
|
+
if (this.version === '2.0') return this.normalize2to3(this.raw);
|
|
58
|
+
return this.raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert OpenAPI 2.0 to 3.x structure
|
|
63
|
+
*/
|
|
64
|
+
normalize2to3(spec) {
|
|
65
|
+
const result = {
|
|
66
|
+
openapi: '3.0.0',
|
|
67
|
+
info: spec.info || {},
|
|
68
|
+
servers: [{
|
|
69
|
+
url: `${spec.schemes?.[0] || 'https'}://${spec.host || 'localhost'}${spec.basePath || ''}`
|
|
70
|
+
}],
|
|
71
|
+
paths: this.normalizePaths2to3(spec.paths || {}, spec),
|
|
72
|
+
components: {
|
|
73
|
+
schemas: spec.definitions || {},
|
|
74
|
+
securitySchemes: this.normalizeSecurityDefs(spec.securityDefinitions || {})
|
|
75
|
+
},
|
|
76
|
+
security: spec.security,
|
|
77
|
+
tags: spec.tags
|
|
78
|
+
};
|
|
79
|
+
// Rewrite $ref from #/definitions/X to #/components/schemas/X
|
|
80
|
+
this._rewriteRefs(result);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rewrite all $ref strings: #/definitions/X → #/components/schemas/X
|
|
86
|
+
*/
|
|
87
|
+
_rewriteRefs(obj) {
|
|
88
|
+
if (!obj || typeof obj !== 'object') return;
|
|
89
|
+
if (Array.isArray(obj)) { obj.forEach(item => this._rewriteRefs(item)); return; }
|
|
90
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
91
|
+
if (key === '$ref' && typeof val === 'string' && val.startsWith('#/definitions/')) {
|
|
92
|
+
obj[key] = val.replace('#/definitions/', '#/components/schemas/');
|
|
93
|
+
} else if (val && typeof val === 'object') {
|
|
94
|
+
this._rewriteRefs(val);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Normalize paths: convert body params to requestBody
|
|
101
|
+
*/
|
|
102
|
+
normalizePaths2to3(paths, spec) {
|
|
103
|
+
const normalized = {};
|
|
104
|
+
for (const [pathStr, methods] of Object.entries(paths)) {
|
|
105
|
+
normalized[pathStr] = {};
|
|
106
|
+
const pathParams = methods.parameters || [];
|
|
107
|
+
|
|
108
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
109
|
+
if (method === 'parameters') continue;
|
|
110
|
+
if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) continue;
|
|
111
|
+
|
|
112
|
+
// Operation params override path params with same name+in (per OpenAPI spec)
|
|
113
|
+
const opParams = operation.parameters || [];
|
|
114
|
+
const opKeys = new Set(opParams.map(p => `${p.name}:${p.in}`));
|
|
115
|
+
const mergedParams = [...opParams, ...pathParams.filter(p => !opKeys.has(`${p.name}:${p.in}`))];
|
|
116
|
+
const bodyParam = mergedParams.find(p => p.in === 'body');
|
|
117
|
+
const nonBodyParams = mergedParams.filter(p => p.in !== 'body' && p.in !== 'formData');
|
|
118
|
+
|
|
119
|
+
const op = { ...operation, parameters: nonBodyParams };
|
|
120
|
+
|
|
121
|
+
// Convert body param to requestBody
|
|
122
|
+
if (bodyParam) {
|
|
123
|
+
op.requestBody = {
|
|
124
|
+
required: bodyParam.required || false,
|
|
125
|
+
content: {
|
|
126
|
+
'application/json': {
|
|
127
|
+
schema: bodyParam.schema || {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Convert formData params to requestBody
|
|
134
|
+
const formParams = mergedParams.filter(p => p.in === 'formData');
|
|
135
|
+
if (formParams.length > 0 && !op.requestBody) {
|
|
136
|
+
const properties = {};
|
|
137
|
+
const required = [];
|
|
138
|
+
for (const fp of formParams) {
|
|
139
|
+
properties[fp.name] = { type: fp.type, description: fp.description };
|
|
140
|
+
if (fp.required) required.push(fp.name);
|
|
141
|
+
}
|
|
142
|
+
const contentType = operation.consumes?.includes('multipart/form-data')
|
|
143
|
+
? 'multipart/form-data'
|
|
144
|
+
: 'application/x-www-form-urlencoded';
|
|
145
|
+
op.requestBody = {
|
|
146
|
+
required: required.length > 0,
|
|
147
|
+
content: {
|
|
148
|
+
[contentType]: {
|
|
149
|
+
schema: { type: 'object', properties, required: required.length > 0 ? required : undefined }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Normalize responses
|
|
156
|
+
if (op.responses) {
|
|
157
|
+
for (const [code, resp] of Object.entries(op.responses)) {
|
|
158
|
+
if (resp.schema) {
|
|
159
|
+
op.responses[code] = {
|
|
160
|
+
description: resp.description || '',
|
|
161
|
+
content: {
|
|
162
|
+
'application/json': { schema: resp.schema }
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
normalized[pathStr][method] = op;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Normalize securityDefinitions (2.0) to securitySchemes (3.x)
|
|
177
|
+
*/
|
|
178
|
+
normalizeSecurityDefs(defs) {
|
|
179
|
+
const schemes = {};
|
|
180
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
181
|
+
switch (def.type) {
|
|
182
|
+
case 'basic':
|
|
183
|
+
schemes[name] = { type: 'http', scheme: 'basic' };
|
|
184
|
+
break;
|
|
185
|
+
case 'apiKey':
|
|
186
|
+
schemes[name] = { type: 'apiKey', in: def.in, name: def.name };
|
|
187
|
+
break;
|
|
188
|
+
case 'oauth2': {
|
|
189
|
+
const flows = {};
|
|
190
|
+
const flowMap = {
|
|
191
|
+
implicit: 'implicit',
|
|
192
|
+
password: 'password',
|
|
193
|
+
application: 'clientCredentials',
|
|
194
|
+
accessCode: 'authorizationCode'
|
|
195
|
+
};
|
|
196
|
+
const flowName = flowMap[def.flow] || def.flow;
|
|
197
|
+
flows[flowName] = {
|
|
198
|
+
...(def.authorizationUrl ? { authorizationUrl: def.authorizationUrl } : {}),
|
|
199
|
+
...(def.tokenUrl ? { tokenUrl: def.tokenUrl } : {}),
|
|
200
|
+
scopes: def.scopes || {}
|
|
201
|
+
};
|
|
202
|
+
schemes[name] = { type: 'oauth2', flows };
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
default:
|
|
206
|
+
schemes[name] = def;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return schemes;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getSchemas() {
|
|
213
|
+
return this.spec.components?.schemas || {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getSecuritySchemes() {
|
|
217
|
+
return this.spec.components?.securitySchemes || {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getBaseUrl() {
|
|
221
|
+
return this.spec.servers?.[0]?.url || '';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get all endpoints as flat list
|
|
226
|
+
* @returns {Array<Object>}
|
|
227
|
+
*/
|
|
228
|
+
getEndpoints() {
|
|
229
|
+
const endpoints = [];
|
|
230
|
+
for (const [pathStr, methods] of Object.entries(this.spec.paths || {})) {
|
|
231
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
232
|
+
if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(method)) continue;
|
|
233
|
+
|
|
234
|
+
endpoints.push({
|
|
235
|
+
method: method.toUpperCase(),
|
|
236
|
+
path: pathStr,
|
|
237
|
+
operationId: operation.operationId || this.generateOperationId(method, pathStr),
|
|
238
|
+
summary: operation.summary || '',
|
|
239
|
+
description: operation.description || '',
|
|
240
|
+
tags: operation.tags || [],
|
|
241
|
+
parameters: this.extractParameters(operation),
|
|
242
|
+
requestBody: this.extractRequestBody(operation),
|
|
243
|
+
responses: this.extractResponses(operation),
|
|
244
|
+
security: operation.security || this.spec.security || [],
|
|
245
|
+
deprecated: operation.deprecated || false
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return endpoints;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Generate operationId from method + path
|
|
254
|
+
* GET /pets/{petId} → getPetsByPetId
|
|
255
|
+
*/
|
|
256
|
+
generateOperationId(method, pathStr) {
|
|
257
|
+
const prefixMap = { get: 'get', post: 'create', put: 'update', patch: 'patch', delete: 'delete', head: 'head', options: 'options' };
|
|
258
|
+
const prefix = prefixMap[method] || method;
|
|
259
|
+
|
|
260
|
+
const segments = pathStr.split('/').filter(Boolean);
|
|
261
|
+
const parts = segments.map(seg => {
|
|
262
|
+
if (seg.startsWith('{') && seg.endsWith('}')) {
|
|
263
|
+
const param = seg.slice(1, -1);
|
|
264
|
+
return 'By' + param.charAt(0).toUpperCase() + param.slice(1);
|
|
265
|
+
}
|
|
266
|
+
return seg.charAt(0).toUpperCase() + seg.slice(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return prefix + parts.join('');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Extract parameters (path, query, header)
|
|
274
|
+
*/
|
|
275
|
+
extractParameters(operation) {
|
|
276
|
+
return (operation.parameters || []).map(p => ({
|
|
277
|
+
name: p.name,
|
|
278
|
+
in: p.in,
|
|
279
|
+
type: p.schema?.type || p.type || 'string',
|
|
280
|
+
format: p.schema?.format || p.format,
|
|
281
|
+
required: p.required || p.in === 'path',
|
|
282
|
+
description: p.description || '',
|
|
283
|
+
enum: p.schema?.enum || p.enum,
|
|
284
|
+
schema: p.schema || { type: p.type || 'string' }
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Extract request body info
|
|
290
|
+
*/
|
|
291
|
+
extractRequestBody(operation) {
|
|
292
|
+
const rb = operation.requestBody;
|
|
293
|
+
if (!rb) return null;
|
|
294
|
+
|
|
295
|
+
const content = rb.content || {};
|
|
296
|
+
const jsonContent = content['application/json'] || content[Object.keys(content)[0]];
|
|
297
|
+
const schema = jsonContent?.schema;
|
|
298
|
+
|
|
299
|
+
// Get schema name from the schema itself
|
|
300
|
+
let schemaName = null;
|
|
301
|
+
if (schema) {
|
|
302
|
+
// If it was a $ref that got resolved, try to get the name
|
|
303
|
+
schemaName = this._getSchemaName(schema);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
required: rb.required || false,
|
|
308
|
+
schema: schemaName,
|
|
309
|
+
schemaObject: schema || null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Extract response info
|
|
315
|
+
*/
|
|
316
|
+
extractResponses(operation) {
|
|
317
|
+
const responses = {};
|
|
318
|
+
for (const [code, resp] of Object.entries(operation.responses || {})) {
|
|
319
|
+
const content = resp.content || {};
|
|
320
|
+
const jsonContent = content['application/json'] || content[Object.keys(content)[0]];
|
|
321
|
+
const schema = jsonContent?.schema;
|
|
322
|
+
let schemaName = schema ? this._getSchemaName(schema) : null;
|
|
323
|
+
|
|
324
|
+
responses[code] = {
|
|
325
|
+
description: resp.description || '',
|
|
326
|
+
schema: schemaName
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return responses;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Try to determine schema name from a resolved schema object
|
|
334
|
+
*/
|
|
335
|
+
_getSchemaName(schema) {
|
|
336
|
+
if (!schema) return null;
|
|
337
|
+
|
|
338
|
+
// Fast path: resolved $ref carries its origin name
|
|
339
|
+
if (schema._refName && this.spec.components?.schemas?.[schema._refName]) {
|
|
340
|
+
return schema._refName;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check if this matches a known schema
|
|
344
|
+
const schemas = this.spec.components?.schemas || {};
|
|
345
|
+
for (const [name, s] of Object.entries(schemas)) {
|
|
346
|
+
if (s === schema) return name;
|
|
347
|
+
// Signature-based match (keys + types to avoid false positives)
|
|
348
|
+
if (s.properties && schema.properties) {
|
|
349
|
+
const sSig = schemaSignature(s.properties);
|
|
350
|
+
const schemaSig = schemaSignature(schema.properties);
|
|
351
|
+
if (sSig === schemaSig && sSig.length > 0) return name;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Array type
|
|
356
|
+
if (schema.type === 'array' && schema.items) {
|
|
357
|
+
const itemName = this._getSchemaName(schema.items);
|
|
358
|
+
if (itemName) return `${itemName}[]`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get auth summary for loadSwagger response
|
|
366
|
+
*/
|
|
367
|
+
getAuthSummary() {
|
|
368
|
+
const schemes = this.getSecuritySchemes();
|
|
369
|
+
return Object.entries(schemes).map(([name, scheme]) => {
|
|
370
|
+
const summary = { name, type: scheme.type };
|
|
371
|
+
if (scheme.scheme) summary.scheme = scheme.scheme;
|
|
372
|
+
if (scheme.bearerFormat) summary.bearerFormat = scheme.bearerFormat;
|
|
373
|
+
if (scheme.in) summary.in = scheme.in;
|
|
374
|
+
if (scheme.name) summary.paramName = scheme.name;
|
|
375
|
+
if (scheme.flows) summary.flows = scheme.flows;
|
|
376
|
+
return summary;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Get schemas summary (names + top-level property names)
|
|
382
|
+
*/
|
|
383
|
+
getSchemasSummary() {
|
|
384
|
+
const schemas = this.getSchemas();
|
|
385
|
+
const summary = {};
|
|
386
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
387
|
+
summary[name] = {
|
|
388
|
+
type: schema.type || 'object',
|
|
389
|
+
required: schema.required || [],
|
|
390
|
+
properties: schema.properties
|
|
391
|
+
? Object.entries(schema.properties).reduce((acc, [k, v]) => {
|
|
392
|
+
acc[k] = {
|
|
393
|
+
type: v.type || (v.enum ? 'enum' : 'object'),
|
|
394
|
+
...(v.format ? { format: v.format } : {}),
|
|
395
|
+
...(v.enum ? { enum: v.enum } : {}),
|
|
396
|
+
...(v.items ? { items: v.items.type || 'object' } : {}),
|
|
397
|
+
...(v.$circularRef ? { $circularRef: v.$circularRef } : {})
|
|
398
|
+
};
|
|
399
|
+
return acc;
|
|
400
|
+
}, {})
|
|
401
|
+
: undefined,
|
|
402
|
+
...(schema.enum ? { enum: schema.enum } : {}),
|
|
403
|
+
...(schema.description ? { description: schema.description } : {}),
|
|
404
|
+
...(schema.allOf ? { allOf: true } : {}),
|
|
405
|
+
...(schema.oneOf ? { oneOf: true } : {}),
|
|
406
|
+
...(schema.anyOf ? { anyOf: true } : {})
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return summary;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Full summary for loadSwagger response
|
|
414
|
+
*/
|
|
415
|
+
getSummary() {
|
|
416
|
+
const endpoints = this.getEndpoints();
|
|
417
|
+
return {
|
|
418
|
+
version: this.version,
|
|
419
|
+
title: this.spec.info?.title || '',
|
|
420
|
+
description: this.spec.info?.description || '',
|
|
421
|
+
baseUrl: this.getBaseUrl(),
|
|
422
|
+
auth: this.getAuthSummary(),
|
|
423
|
+
endpoints,
|
|
424
|
+
schemas: this.getSchemasSummary(),
|
|
425
|
+
endpointCount: endpoints.length,
|
|
426
|
+
schemaCount: Object.keys(this.getSchemas()).length
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Parse content string to object
|
|
433
|
+
*/
|
|
434
|
+
function parseContent(content, format, source) {
|
|
435
|
+
if (format === 'json') return JSON.parse(content);
|
|
436
|
+
if (format === 'yaml') return yaml.load(content);
|
|
437
|
+
|
|
438
|
+
// Auto-detect
|
|
439
|
+
try {
|
|
440
|
+
return JSON.parse(content);
|
|
441
|
+
} catch {
|
|
442
|
+
try {
|
|
443
|
+
return yaml.load(content);
|
|
444
|
+
} catch (yamlErr) {
|
|
445
|
+
throw new Error(`Failed to parse spec from ${source}. Not valid JSON or YAML: ${yamlErr.message}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/openapi/ref-resolver.js
|
|
3
|
+
*
|
|
4
|
+
* Recursive $ref resolution for OpenAPI specs.
|
|
5
|
+
* Handles internal refs, circular refs, allOf/oneOf/anyOf.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve all $ref in a spec object (in-place on a deep clone)
|
|
10
|
+
* @param {Object} spec - full OpenAPI spec
|
|
11
|
+
* @returns {Object} - spec with resolved $ref
|
|
12
|
+
*/
|
|
13
|
+
export function resolveAllRefs(spec) {
|
|
14
|
+
const clone = JSON.parse(JSON.stringify(spec));
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
resolveObject(clone, clone, new Set(), cache);
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Recursively walk an object and resolve $ref
|
|
22
|
+
*/
|
|
23
|
+
function resolveObject(obj, root, visited, cache) {
|
|
24
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(obj)) {
|
|
27
|
+
for (let i = 0; i < obj.length; i++) {
|
|
28
|
+
if (obj[i] && typeof obj[i] === 'object' && obj[i].$ref) {
|
|
29
|
+
obj[i] = resolveRef(obj[i].$ref, root, visited, cache);
|
|
30
|
+
} else {
|
|
31
|
+
resolveObject(obj[i], root, visited, cache);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return obj;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const key of Object.keys(obj)) {
|
|
38
|
+
const val = obj[key];
|
|
39
|
+
if (!val || typeof val !== 'object') continue;
|
|
40
|
+
|
|
41
|
+
if (val.$ref) {
|
|
42
|
+
obj[key] = resolveRef(val.$ref, root, visited, cache);
|
|
43
|
+
} else {
|
|
44
|
+
resolveObject(val, root, visited, cache);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return obj;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a single $ref string
|
|
53
|
+
* @param {string} ref - e.g. "#/components/schemas/Pet"
|
|
54
|
+
* @param {Object} root - root spec object
|
|
55
|
+
* @param {Set} visited - cycle detection
|
|
56
|
+
* @param {Map} cache - resolved ref cache
|
|
57
|
+
* @returns {Object} - resolved object
|
|
58
|
+
*/
|
|
59
|
+
export function resolveRef(ref, root, visited = new Set(), cache = new Map()) {
|
|
60
|
+
if (!ref || typeof ref !== 'string' || !ref.startsWith('#/')) {
|
|
61
|
+
// External $ref - not supported
|
|
62
|
+
return { $externalRef: ref, _warning: 'External $ref not supported' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Circular check MUST come before cache check:
|
|
66
|
+
// During resolution A→B→A, ref A is both in visited and cache.
|
|
67
|
+
// Cache would return the partially-resolved object, creating circular JS refs.
|
|
68
|
+
// Visited correctly returns a $circularRef marker instead.
|
|
69
|
+
if (visited.has(ref)) {
|
|
70
|
+
const name = ref.split('/').pop();
|
|
71
|
+
return { $circularRef: name };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (cache.has(ref)) {
|
|
75
|
+
return cache.get(ref);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
visited.add(ref);
|
|
79
|
+
|
|
80
|
+
const parts = ref.replace('#/', '').split('/');
|
|
81
|
+
let resolved = root;
|
|
82
|
+
for (const part of parts) {
|
|
83
|
+
const decoded = decodeURIComponent(part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
84
|
+
if (resolved == null || typeof resolved !== 'object') {
|
|
85
|
+
visited.delete(ref);
|
|
86
|
+
return { $brokenRef: ref, _warning: `Could not resolve path segment: ${decoded}` };
|
|
87
|
+
}
|
|
88
|
+
resolved = resolved[decoded];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (resolved == null) {
|
|
92
|
+
visited.delete(ref);
|
|
93
|
+
return { $brokenRef: ref };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Deep clone to avoid mutation between different $ref usages
|
|
97
|
+
const result = JSON.parse(JSON.stringify(resolved));
|
|
98
|
+
|
|
99
|
+
// Tag with original ref name for fast identification by generators
|
|
100
|
+
const refName = ref.split('/').pop();
|
|
101
|
+
if (refName) {
|
|
102
|
+
Object.defineProperty(result, '_refName', {
|
|
103
|
+
value: refName, enumerable: false, writable: false
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Cache before recursing to handle self-references
|
|
108
|
+
cache.set(ref, result);
|
|
109
|
+
|
|
110
|
+
// Recursively resolve nested $ref
|
|
111
|
+
resolveObject(result, root, visited, cache);
|
|
112
|
+
|
|
113
|
+
visited.delete(ref);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge allOf schemas into a single schema
|
|
119
|
+
* @param {Array} allOfSchemas - array of schemas
|
|
120
|
+
* @returns {Object} - merged schema
|
|
121
|
+
*/
|
|
122
|
+
export function mergeAllOf(allOfSchemas) {
|
|
123
|
+
const merged = {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {},
|
|
126
|
+
required: [],
|
|
127
|
+
};
|
|
128
|
+
let description = '';
|
|
129
|
+
|
|
130
|
+
for (const schema of allOfSchemas) {
|
|
131
|
+
if (schema.properties) {
|
|
132
|
+
Object.assign(merged.properties, schema.properties);
|
|
133
|
+
}
|
|
134
|
+
if (schema.required) {
|
|
135
|
+
merged.required.push(...schema.required);
|
|
136
|
+
}
|
|
137
|
+
if (schema.description && !description) {
|
|
138
|
+
description = schema.description;
|
|
139
|
+
}
|
|
140
|
+
// Carry over other top-level fields
|
|
141
|
+
if (schema.type) merged.type = schema.type;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (description) merged.description = description;
|
|
145
|
+
merged.required = [...new Set(merged.required)];
|
|
146
|
+
if (merged.required.length === 0) delete merged.required;
|
|
147
|
+
|
|
148
|
+
return merged;
|
|
149
|
+
}
|