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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/api-generators/api-models-python.js
|
|
3
|
+
*
|
|
4
|
+
* Generates Python dataclass/pydantic/typeddict models from OpenAPI schemas.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { TypeMapper } from '../openapi/type-mapper.js';
|
|
8
|
+
import { mergeAllOf } from '../openapi/ref-resolver.js';
|
|
9
|
+
import { arraysEqual, schemaSignature } from '../openapi/helpers.js';
|
|
10
|
+
|
|
11
|
+
export class ApiModelsPythonGenerator {
|
|
12
|
+
constructor(schemas, options = {}) {
|
|
13
|
+
this.schemas = schemas;
|
|
14
|
+
this.options = {
|
|
15
|
+
style: 'dataclass', // 'dataclass' | 'pydantic' | 'typeddict'
|
|
16
|
+
includeEnums: true,
|
|
17
|
+
includeValidation: false,
|
|
18
|
+
...options
|
|
19
|
+
};
|
|
20
|
+
this.generatedEnums = new Set();
|
|
21
|
+
this.usesDatetime = false;
|
|
22
|
+
this.usesDate = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate full file content
|
|
27
|
+
* @param {Object} metadata - { title, source, version }
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
generate(metadata = {}) {
|
|
31
|
+
// Pre-scan to detect datetime usage
|
|
32
|
+
this._scanForDateTypes();
|
|
33
|
+
|
|
34
|
+
const lines = [];
|
|
35
|
+
|
|
36
|
+
// Header
|
|
37
|
+
lines.push(...this.generateHeader(metadata));
|
|
38
|
+
|
|
39
|
+
// Imports
|
|
40
|
+
lines.push(...this.generateImports());
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
// Enums first
|
|
44
|
+
if (this.options.includeEnums) {
|
|
45
|
+
const enumLines = this.generateAllEnums();
|
|
46
|
+
if (enumLines.length > 0) {
|
|
47
|
+
lines.push(...enumLines);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Models in dependency order
|
|
52
|
+
const sorted = this.topologicalSort();
|
|
53
|
+
for (const name of sorted) {
|
|
54
|
+
const schema = this.schemas[name];
|
|
55
|
+
if (!schema) continue;
|
|
56
|
+
|
|
57
|
+
if (schema.enum && schema.type === 'string' && this.generatedEnums.has(name)) continue;
|
|
58
|
+
|
|
59
|
+
const modelLines = this.generateModel(name, schema);
|
|
60
|
+
if (modelLines.length > 0) {
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(...modelLines);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return lines.join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
generateHeader(metadata) {
|
|
70
|
+
return [
|
|
71
|
+
'"""',
|
|
72
|
+
`Generated from ${metadata.title || 'OpenAPI'} (${metadata.source || 'unknown source'})`,
|
|
73
|
+
`OpenAPI ${metadata.version || 'unknown'} | Generated at ${new Date().toISOString()}`,
|
|
74
|
+
'"""',
|
|
75
|
+
''
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
generateImports() {
|
|
80
|
+
const lines = ['from __future__ import annotations'];
|
|
81
|
+
|
|
82
|
+
if (this.options.style === 'dataclass') {
|
|
83
|
+
lines.push('from dataclasses import dataclass, field');
|
|
84
|
+
lines.push('from enum import Enum');
|
|
85
|
+
lines.push('from typing import Optional, List, Dict, Any, Union');
|
|
86
|
+
} else if (this.options.style === 'pydantic') {
|
|
87
|
+
lines.push('from pydantic import BaseModel, Field');
|
|
88
|
+
lines.push('from enum import Enum');
|
|
89
|
+
lines.push('from typing import Optional, List, Dict, Any, Union');
|
|
90
|
+
} else {
|
|
91
|
+
// typeddict
|
|
92
|
+
lines.push('from typing import TypedDict, Optional, List, Dict, Any, Union');
|
|
93
|
+
lines.push('from enum import Enum');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.usesDatetime || this.usesDate) {
|
|
97
|
+
const imports = [];
|
|
98
|
+
if (this.usesDatetime) imports.push('datetime');
|
|
99
|
+
if (this.usesDate) imports.push('date');
|
|
100
|
+
lines.push(`from datetime import ${imports.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_scanForDateTypes() {
|
|
107
|
+
const scan = (obj) => {
|
|
108
|
+
if (!obj || typeof obj !== 'object') return;
|
|
109
|
+
if (obj.format === 'date-time') this.usesDatetime = true;
|
|
110
|
+
if (obj.format === 'date') this.usesDate = true;
|
|
111
|
+
for (const val of Object.values(obj)) {
|
|
112
|
+
if (val && typeof val === 'object') scan(val);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
scan(this.schemas);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
generateAllEnums() {
|
|
119
|
+
const lines = [];
|
|
120
|
+
for (const [name, schema] of Object.entries(this.schemas)) {
|
|
121
|
+
if (schema.enum && schema.type === 'string') {
|
|
122
|
+
lines.push('');
|
|
123
|
+
lines.push(...this.generateEnum(name, schema.enum, schema.description));
|
|
124
|
+
this.generatedEnums.add(name);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (schema.properties) {
|
|
128
|
+
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
129
|
+
if (propSchema.enum && propSchema.type === 'string') {
|
|
130
|
+
// Skip if values match a top-level enum
|
|
131
|
+
const matchingEnum = this._findMatchingEnum(propSchema.enum);
|
|
132
|
+
if (matchingEnum) continue;
|
|
133
|
+
|
|
134
|
+
const enumName = name + propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
135
|
+
if (!this.generatedEnums.has(enumName)) {
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(...this.generateEnum(enumName, propSchema.enum, propSchema.description));
|
|
138
|
+
this.generatedEnums.add(enumName);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
generateEnum(name, values, description) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push(`class ${name}(str, Enum):`);
|
|
150
|
+
if (description) {
|
|
151
|
+
lines.push(` """${description}"""`);
|
|
152
|
+
}
|
|
153
|
+
for (const value of values) {
|
|
154
|
+
let key = value.toUpperCase().replace(/[^A-Z0-9]/g, '_');
|
|
155
|
+
if (/^\d/.test(key)) key = `_${key}`;
|
|
156
|
+
lines.push(` ${key} = '${value}'`);
|
|
157
|
+
}
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
generateModel(name, schema) {
|
|
162
|
+
if (schema.allOf) {
|
|
163
|
+
return this.generateAllOfModel(name, schema);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (schema.oneOf || schema.anyOf) {
|
|
167
|
+
return this.generateUnionType(name, schema.oneOf || schema.anyOf, schema.description);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (schema.type === 'object' || schema.properties) {
|
|
171
|
+
return this.generateObjectModel(name, schema);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Simple type alias
|
|
175
|
+
if (schema.type && schema.type !== 'object') {
|
|
176
|
+
const pyType = TypeMapper.toPython(schema);
|
|
177
|
+
return [`${name} = ${pyType}`];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return this.generateObjectModel(name, { type: 'object', properties: {} });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
generateObjectModel(name, schema) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
const required = new Set(schema.required || []);
|
|
186
|
+
|
|
187
|
+
if (this.options.style === 'dataclass') {
|
|
188
|
+
lines.push('@dataclass');
|
|
189
|
+
lines.push(`class ${name}:`);
|
|
190
|
+
} else if (this.options.style === 'pydantic') {
|
|
191
|
+
lines.push(`class ${name}(BaseModel):`);
|
|
192
|
+
} else {
|
|
193
|
+
lines.push(`class ${name}(TypedDict, total=False):`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (schema.description) {
|
|
197
|
+
lines.push(` """${schema.description}"""`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const props = Object.entries(schema.properties || {});
|
|
201
|
+
|
|
202
|
+
if (props.length === 0) {
|
|
203
|
+
lines.push(' pass');
|
|
204
|
+
return lines;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sort: required first, then optional
|
|
208
|
+
const sortedProps = props.sort(([a], [b]) => {
|
|
209
|
+
const aReq = required.has(a);
|
|
210
|
+
const bReq = required.has(b);
|
|
211
|
+
if (aReq && !bReq) return -1;
|
|
212
|
+
if (!aReq && bReq) return 1;
|
|
213
|
+
return 0;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (const [propName, propSchema] of sortedProps) {
|
|
217
|
+
const isRequired = required.has(propName);
|
|
218
|
+
const pyName = this.toSnakeCase(propName);
|
|
219
|
+
const type = this.resolvePropertyType(name, propName, propSchema);
|
|
220
|
+
|
|
221
|
+
if (this.options.style === 'typeddict') {
|
|
222
|
+
if (isRequired) {
|
|
223
|
+
lines.push(` ${pyName}: ${type}`);
|
|
224
|
+
} else {
|
|
225
|
+
lines.push(` ${pyName}: Optional[${type}]`);
|
|
226
|
+
}
|
|
227
|
+
} else if (this.options.style === 'pydantic') {
|
|
228
|
+
if (isRequired) {
|
|
229
|
+
lines.push(` ${pyName}: ${type}`);
|
|
230
|
+
} else {
|
|
231
|
+
lines.push(` ${pyName}: Optional[${type}] = None`);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// dataclass: required fields have no default (enforced at construction)
|
|
235
|
+
if (isRequired) {
|
|
236
|
+
lines.push(` ${pyName}: ${type}`);
|
|
237
|
+
} else {
|
|
238
|
+
lines.push(` ${pyName}: Optional[${type}] = None`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return lines;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
generateAllOfModel(name, schema) {
|
|
247
|
+
const lines = [];
|
|
248
|
+
const namedRefs = [];
|
|
249
|
+
const inlineSchemas = [];
|
|
250
|
+
|
|
251
|
+
for (const sub of schema.allOf) {
|
|
252
|
+
const refName = this._findSchemaName(sub);
|
|
253
|
+
if (refName) {
|
|
254
|
+
namedRefs.push(refName);
|
|
255
|
+
} else {
|
|
256
|
+
inlineSchemas.push(sub);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const merged = mergeAllOf(inlineSchemas.length > 0 ? inlineSchemas : schema.allOf);
|
|
261
|
+
const baseClass = namedRefs.length > 0 ? namedRefs[0] : null;
|
|
262
|
+
const required = new Set(merged.required || []);
|
|
263
|
+
|
|
264
|
+
if (this.options.style === 'dataclass') {
|
|
265
|
+
lines.push('@dataclass');
|
|
266
|
+
if (baseClass) {
|
|
267
|
+
lines.push(`class ${name}(${baseClass}):`);
|
|
268
|
+
} else {
|
|
269
|
+
lines.push(`class ${name}:`);
|
|
270
|
+
}
|
|
271
|
+
} else if (this.options.style === 'pydantic') {
|
|
272
|
+
if (baseClass) {
|
|
273
|
+
lines.push(`class ${name}(${baseClass}):`);
|
|
274
|
+
} else {
|
|
275
|
+
lines.push(`class ${name}(BaseModel):`);
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
if (baseClass) {
|
|
279
|
+
lines.push(`class ${name}(${baseClass}, total=False):`);
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(`class ${name}(TypedDict, total=False):`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (schema.description) {
|
|
286
|
+
lines.push(` """${schema.description}"""`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const props = Object.entries(merged.properties || {});
|
|
290
|
+
if (props.length === 0 && !baseClass) {
|
|
291
|
+
lines.push(' pass');
|
|
292
|
+
return lines;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (props.length === 0) {
|
|
296
|
+
lines.push(' pass');
|
|
297
|
+
return lines;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (const [propName, propSchema] of props) {
|
|
301
|
+
const isRequired = required.has(propName);
|
|
302
|
+
const pyName = this.toSnakeCase(propName);
|
|
303
|
+
const type = this.resolvePropertyType(name, propName, propSchema);
|
|
304
|
+
|
|
305
|
+
if (this.options.style === 'pydantic') {
|
|
306
|
+
lines.push(isRequired
|
|
307
|
+
? ` ${pyName}: ${type}`
|
|
308
|
+
: ` ${pyName}: Optional[${type}] = None`);
|
|
309
|
+
} else if (this.options.style === 'dataclass') {
|
|
310
|
+
lines.push(isRequired
|
|
311
|
+
? ` ${pyName}: ${type}`
|
|
312
|
+
: ` ${pyName}: Optional[${type}] = None`);
|
|
313
|
+
} else {
|
|
314
|
+
lines.push(isRequired
|
|
315
|
+
? ` ${pyName}: ${type}`
|
|
316
|
+
: ` ${pyName}: Optional[${type}]`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
generateUnionType(name, schemas, description) {
|
|
324
|
+
const lines = [];
|
|
325
|
+
if (description) {
|
|
326
|
+
lines.push(`# ${description}`);
|
|
327
|
+
}
|
|
328
|
+
const types = schemas.map(s => {
|
|
329
|
+
const refName = this._findSchemaName(s);
|
|
330
|
+
if (refName) return refName;
|
|
331
|
+
return TypeMapper.toPython(s);
|
|
332
|
+
});
|
|
333
|
+
lines.push(`${name} = Union[${types.join(', ')}]`);
|
|
334
|
+
return lines;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
resolvePropertyType(parentName, propName, propSchema) {
|
|
338
|
+
// Enum property
|
|
339
|
+
if (propSchema.enum && propSchema.type === 'string' && this.options.includeEnums) {
|
|
340
|
+
for (const [schemaName, schema] of Object.entries(this.schemas)) {
|
|
341
|
+
if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, propSchema.enum)) {
|
|
342
|
+
return schemaName;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return parentName + propName.charAt(0).toUpperCase() + propName.slice(1);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Array with known items
|
|
349
|
+
if (propSchema.type === 'array' && propSchema.items) {
|
|
350
|
+
const itemName = this._findSchemaName(propSchema.items);
|
|
351
|
+
if (itemName) return `List[${itemName}]`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Object reference
|
|
355
|
+
const refName = this._findSchemaName(propSchema);
|
|
356
|
+
if (refName) return refName;
|
|
357
|
+
|
|
358
|
+
return TypeMapper.toPython(propSchema);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
getDefaultValue(type, schema) {
|
|
362
|
+
if (type === 'str') return "''";
|
|
363
|
+
if (type === 'int') return '0';
|
|
364
|
+
if (type === 'float') return '0.0';
|
|
365
|
+
if (type === 'bool') return 'False';
|
|
366
|
+
if (type.startsWith('List[')) return 'field(default_factory=list)';
|
|
367
|
+
if (type.startsWith('Dict[')) return 'field(default_factory=dict)';
|
|
368
|
+
return 'None';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
toSnakeCase(name) {
|
|
372
|
+
return name.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '').replace(/-/g, '_');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_findMatchingEnum(enumValues) {
|
|
376
|
+
for (const [name, schema] of Object.entries(this.schemas)) {
|
|
377
|
+
if (schema.enum && schema.type === 'string' && arraysEqual(schema.enum, enumValues)) {
|
|
378
|
+
return name;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_findSchemaName(schema) {
|
|
385
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
386
|
+
if (schema.$circularRef) return `'${schema.$circularRef}'`;
|
|
387
|
+
if (schema._refName && this.schemas[schema._refName]) return schema._refName;
|
|
388
|
+
|
|
389
|
+
for (const [name, s] of Object.entries(this.schemas)) {
|
|
390
|
+
if (s === schema) return name;
|
|
391
|
+
if (s.properties && schema.properties) {
|
|
392
|
+
const sSig = schemaSignature(s.properties);
|
|
393
|
+
const schemaSig = schemaSignature(schema.properties);
|
|
394
|
+
if (sSig === schemaSig && sSig.length > 0) return name;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
topologicalSort() {
|
|
401
|
+
const names = Object.keys(this.schemas);
|
|
402
|
+
const visited = new Set();
|
|
403
|
+
const result = [];
|
|
404
|
+
|
|
405
|
+
const visit = (name) => {
|
|
406
|
+
if (visited.has(name)) return;
|
|
407
|
+
visited.add(name);
|
|
408
|
+
const deps = this.getDependencies(name);
|
|
409
|
+
for (const dep of deps) {
|
|
410
|
+
if (this.schemas[dep]) visit(dep);
|
|
411
|
+
}
|
|
412
|
+
result.push(name);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
for (const name of names) {
|
|
416
|
+
visit(name);
|
|
417
|
+
}
|
|
418
|
+
return result;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
getDependencies(name) {
|
|
422
|
+
const schema = this.schemas[name];
|
|
423
|
+
if (!schema) return [];
|
|
424
|
+
const deps = new Set();
|
|
425
|
+
const seen = new WeakSet();
|
|
426
|
+
|
|
427
|
+
const collectDeps = (obj) => {
|
|
428
|
+
if (!obj || typeof obj !== 'object') return;
|
|
429
|
+
if (obj.$circularRef) { deps.add(obj.$circularRef); return; }
|
|
430
|
+
if (seen.has(obj)) return;
|
|
431
|
+
seen.add(obj);
|
|
432
|
+
if (Array.isArray(obj)) { obj.forEach(collectDeps); return; }
|
|
433
|
+
const refName = this._findSchemaName(obj);
|
|
434
|
+
if (refName && refName !== name && !refName.startsWith("'")) deps.add(refName);
|
|
435
|
+
for (const val of Object.values(obj)) {
|
|
436
|
+
if (val && typeof val === 'object') collectDeps(val);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (schema.properties) collectDeps(schema.properties);
|
|
441
|
+
if (schema.allOf) collectDeps(schema.allOf);
|
|
442
|
+
if (schema.oneOf) collectDeps(schema.oneOf);
|
|
443
|
+
if (schema.anyOf) collectDeps(schema.anyOf);
|
|
444
|
+
if (schema.items) collectDeps(schema.items);
|
|
445
|
+
|
|
446
|
+
return [...deps];
|
|
447
|
+
}
|
|
448
|
+
}
|