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,3101 @@
|
|
|
1
|
+
# Спецификация: Swagger/OpenAPI -> API-модели и API-клиент
|
|
2
|
+
|
|
3
|
+
## Чеклист прогресса
|
|
4
|
+
|
|
5
|
+
### Фаза 1 (текущая)
|
|
6
|
+
- [ ] **1.1** OpenAPI парсер (`utils/openapi/parser.js`) - единый для 2.0 и 3.x
|
|
7
|
+
- [ ] **1.2** $ref резолвер (`utils/openapi/ref-resolver.js`) - рекурсивное разрешение ссылок
|
|
8
|
+
- [ ] **1.3** Маппинг типов (`utils/openapi/type-mapper.js`) - OpenAPI -> TypeScript/Python типы
|
|
9
|
+
- [ ] **1.4** Тул `loadSwagger` - чтение и парсинг спецификации (URL или файл, JSON или YAML)
|
|
10
|
+
- [ ] **1.5** Генератор TypeScript моделей (`utils/api-generators/api-models-typescript.js`)
|
|
11
|
+
- [ ] **1.6** Генератор Python моделей (`utils/api-generators/api-models-python.js`)
|
|
12
|
+
- [ ] **1.7** Тул `generateApiModels` - генерация моделей из swagger
|
|
13
|
+
- [ ] **1.8** Схемы и описания тулов (`tool-schemas.js`, `tool-definitions.js`)
|
|
14
|
+
- [ ] **1.9** Хендлеры в `index.js`
|
|
15
|
+
- [ ] **1.10** Документация в `README.md`
|
|
16
|
+
|
|
17
|
+
### Фаза 2 (следующая итерация)
|
|
18
|
+
- [ ] **2.1** Генератор методов из endpoints (`utils/openapi/method-generator.js`) — operationId → имя метода, path/query/body параметры
|
|
19
|
+
- [ ] **2.2** Генератор auth-конфигурации (`utils/openapi/auth-generator.js`) — constructor params, header setup для всех типов auth
|
|
20
|
+
- [ ] **2.3** Генератор API-клиента TypeScript (`utils/api-generators/api-client-typescript.js`) — Playwright APIRequestContext
|
|
21
|
+
- [ ] **2.4** Генератор API-клиента Python (`utils/api-generators/api-client-python.js`) — requests.Session
|
|
22
|
+
- [ ] **2.5** Тул `generateApiClient` — Zod schema, описание
|
|
23
|
+
- [ ] **2.6** Схема в `tool-schemas.js` (`GenerateApiClientSchema`)
|
|
24
|
+
- [ ] **2.7** Описание в `tool-definitions.js`
|
|
25
|
+
- [ ] **2.8** Хендлер в `index.js`
|
|
26
|
+
- [ ] **2.9** Документация в `README.md`
|
|
27
|
+
- [ ] **2.10** Верификация — Petstore 2.0/3.0, auth types, path params, query params, request body
|
|
28
|
+
|
|
29
|
+
### Фаза 3 (будущее)
|
|
30
|
+
- [ ] **3.1** Генератор тестовых скаффолдов (`utils/openapi/test-scaffold-generator.js`) — CRUD detection, happy path + error cases
|
|
31
|
+
- [ ] **3.2** Генератор API-тестов TypeScript (`utils/api-generators/api-tests-typescript.js`) — Playwright test
|
|
32
|
+
- [ ] **3.3** Генератор API-тестов Python (`utils/api-generators/api-tests-python.js`) — pytest + requests
|
|
33
|
+
- [ ] **3.4** Тул `generateApiTests` — Zod schema, описание
|
|
34
|
+
- [ ] **3.5** Схема в `tool-schemas.js` (`GenerateApiTestsSchema`)
|
|
35
|
+
- [ ] **3.6** Описание в `tool-definitions.js`
|
|
36
|
+
- [ ] **3.7** Хендлер в `index.js`
|
|
37
|
+
- [ ] **3.8** Документация в `README.md`
|
|
38
|
+
- [ ] **3.9** Интеграция API-клиента с API-тестами (аналог POM-интеграции: опциональное использование клиента вместо raw HTTP)
|
|
39
|
+
- [ ] **3.10** Верификация — Petstore CRUD, auth тесты, error case scaffolds, grouping by tags
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Контекст
|
|
44
|
+
|
|
45
|
+
QA-автоматизаторы хотят быстро создавать API-тесты из Swagger/OpenAPI спецификации. Сейчас процесс ручной: открыть Swagger UI, скопировать эндпоинты, вручную написать типы и клиент.
|
|
46
|
+
|
|
47
|
+
**Цель**: MCP-тулы, которые читают Swagger-спецификацию и генерируют:
|
|
48
|
+
1. Типизированные модели данных (TypeScript interfaces / Python dataclasses)
|
|
49
|
+
2. API-клиент с методами для каждого endpoint (фаза 2)
|
|
50
|
+
3. Тестовые скаффолды (фаза 3)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Фаза 1: loadSwagger + generateApiModels
|
|
55
|
+
|
|
56
|
+
### Тул 1: `loadSwagger`
|
|
57
|
+
|
|
58
|
+
**Назначение**: загрузить и распарсить OpenAPI-спецификацию, вернуть структурированную сводку.
|
|
59
|
+
|
|
60
|
+
**Параметры**:
|
|
61
|
+
```js
|
|
62
|
+
LoadSwaggerSchema = z.object({
|
|
63
|
+
source: z.string().describe("URL (http/https) or local file path to swagger.json / openapi.yaml"),
|
|
64
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
65
|
+
.describe("Spec format. 'auto' (default) detects from extension/content"),
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Алгоритм**:
|
|
70
|
+
1. Определить тип источника (URL vs файл) по наличию `http://` / `https://`
|
|
71
|
+
2. Загрузить контент:
|
|
72
|
+
- URL: HTTP GET через `fetch()` (Node 18+ built-in)
|
|
73
|
+
- Файл: `fs.readFileSync()`
|
|
74
|
+
3. Определить формат (auto): попробовать `JSON.parse()`, если fail — `yaml.load()`
|
|
75
|
+
4. Определить версию: `spec.swagger === '2.0'` → OpenAPI 2.0, `spec.openapi?.startsWith('3.')` → OpenAPI 3.x
|
|
76
|
+
5. Нормализовать структуру (внутренне привести 2.0 к формату 3.x)
|
|
77
|
+
6. Разрезолвить все `$ref` ссылки
|
|
78
|
+
7. Вернуть сводку
|
|
79
|
+
|
|
80
|
+
**Возвращаемое значение**:
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"success": true,
|
|
84
|
+
"version": "3.0.3",
|
|
85
|
+
"title": "Pet Store API",
|
|
86
|
+
"description": "A sample API",
|
|
87
|
+
"baseUrl": "https://petstore.swagger.io/v2",
|
|
88
|
+
"auth": [
|
|
89
|
+
{ "name": "bearerAuth", "type": "http", "scheme": "bearer" },
|
|
90
|
+
{ "name": "apiKey", "type": "apiKey", "in": "header", "paramName": "X-API-Key" }
|
|
91
|
+
],
|
|
92
|
+
"endpoints": [
|
|
93
|
+
{
|
|
94
|
+
"method": "GET",
|
|
95
|
+
"path": "/pets",
|
|
96
|
+
"operationId": "listPets",
|
|
97
|
+
"summary": "List all pets",
|
|
98
|
+
"tags": ["pets"],
|
|
99
|
+
"parameters": [
|
|
100
|
+
{ "name": "limit", "in": "query", "type": "integer", "required": false }
|
|
101
|
+
],
|
|
102
|
+
"requestBody": null,
|
|
103
|
+
"responses": {
|
|
104
|
+
"200": { "description": "OK", "schema": "PetList" },
|
|
105
|
+
"400": { "description": "Bad Request", "schema": "Error" }
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"method": "POST",
|
|
110
|
+
"path": "/pets",
|
|
111
|
+
"operationId": "createPet",
|
|
112
|
+
"summary": "Create a pet",
|
|
113
|
+
"tags": ["pets"],
|
|
114
|
+
"parameters": [],
|
|
115
|
+
"requestBody": { "schema": "CreatePetRequest", "required": true },
|
|
116
|
+
"responses": {
|
|
117
|
+
"201": { "description": "Created", "schema": "Pet" }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"schemas": {
|
|
122
|
+
"Pet": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"required": ["id", "name"],
|
|
125
|
+
"properties": {
|
|
126
|
+
"id": { "type": "integer", "format": "int64" },
|
|
127
|
+
"name": { "type": "string" },
|
|
128
|
+
"status": { "type": "string", "enum": ["available", "pending", "sold"] },
|
|
129
|
+
"tags": { "type": "array", "items": { "$ref": "Tag" } }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"Tag": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"id": { "type": "integer" },
|
|
136
|
+
"name": { "type": "string" }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"endpointCount": 15,
|
|
141
|
+
"schemaCount": 8,
|
|
142
|
+
"instruction": "Use generateApiModels to generate typed models, or generateApiClient to generate API client class."
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Тул 2: `generateApiModels`
|
|
147
|
+
|
|
148
|
+
**Назначение**: сгенерировать типизированные модели данных из OpenAPI schemas.
|
|
149
|
+
|
|
150
|
+
**Параметры**:
|
|
151
|
+
```js
|
|
152
|
+
GenerateApiModelsSchema = z.object({
|
|
153
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
154
|
+
language: z.enum(['typescript', 'python']).describe("Target language for models"),
|
|
155
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
156
|
+
.describe("Spec format (default: auto)"),
|
|
157
|
+
style: z.enum(['interface', 'type']).optional()
|
|
158
|
+
.describe("TypeScript only: 'interface' (default) or 'type' aliases"),
|
|
159
|
+
pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
|
|
160
|
+
.describe("Python only: 'dataclass' (default), 'pydantic' BaseModel, or TypedDict"),
|
|
161
|
+
includeEnums: z.boolean().optional()
|
|
162
|
+
.describe("Generate separate enum types (default: true)"),
|
|
163
|
+
includeValidation: z.boolean().optional()
|
|
164
|
+
.describe("Include validation constraints as comments/decorators (default: false)"),
|
|
165
|
+
schemas: z.array(z.string()).optional()
|
|
166
|
+
.describe("Generate only these schemas (default: all). E.g. ['User', 'Pet']"),
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Алгоритм**:
|
|
171
|
+
1. Загрузить и распарсить спеку (переиспользует парсер из loadSwagger)
|
|
172
|
+
2. Отфильтровать schemas если указан параметр `schemas`
|
|
173
|
+
3. Топологически отсортировать schemas по зависимостям ($ref), чтобы зависимости шли первыми
|
|
174
|
+
4. Для каждой schema сгенерировать код модели
|
|
175
|
+
5. Вернуть единый файл с кодом + suggested filename
|
|
176
|
+
|
|
177
|
+
**TypeScript выход** (interface):
|
|
178
|
+
```typescript
|
|
179
|
+
// Generated from Pet Store API (https://petstore.swagger.io/v2)
|
|
180
|
+
// OpenAPI 3.0.3 | Generated at 2024-01-15T12:00:00Z
|
|
181
|
+
|
|
182
|
+
/** Pet status in the store */
|
|
183
|
+
export enum PetStatus {
|
|
184
|
+
Available = 'available',
|
|
185
|
+
Pending = 'pending',
|
|
186
|
+
Sold = 'sold',
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface Tag {
|
|
190
|
+
id?: number;
|
|
191
|
+
name?: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface Pet {
|
|
195
|
+
/** Pet ID */
|
|
196
|
+
id: number;
|
|
197
|
+
/** Pet name */
|
|
198
|
+
name: string;
|
|
199
|
+
status?: PetStatus;
|
|
200
|
+
tags?: Tag[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface CreatePetRequest {
|
|
204
|
+
name: string;
|
|
205
|
+
status?: PetStatus;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface Error {
|
|
209
|
+
code: number;
|
|
210
|
+
message: string;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Python выход** (dataclass):
|
|
215
|
+
```python
|
|
216
|
+
"""
|
|
217
|
+
Generated from Pet Store API (https://petstore.swagger.io/v2)
|
|
218
|
+
OpenAPI 3.0.3 | Generated at 2024-01-15T12:00:00Z
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
from __future__ import annotations
|
|
222
|
+
from dataclasses import dataclass, field
|
|
223
|
+
from enum import Enum
|
|
224
|
+
from typing import Optional, List
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class PetStatus(str, Enum):
|
|
228
|
+
"""Pet status in the store"""
|
|
229
|
+
AVAILABLE = 'available'
|
|
230
|
+
PENDING = 'pending'
|
|
231
|
+
SOLD = 'sold'
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class Tag:
|
|
236
|
+
id: Optional[int] = None
|
|
237
|
+
name: Optional[str] = None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class Pet:
|
|
242
|
+
"""Pet object"""
|
|
243
|
+
id: int = 0
|
|
244
|
+
name: str = ''
|
|
245
|
+
status: Optional[PetStatus] = None
|
|
246
|
+
tags: List[Tag] = field(default_factory=list)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass
|
|
250
|
+
class CreatePetRequest:
|
|
251
|
+
name: str = ''
|
|
252
|
+
status: Optional[PetStatus] = None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass
|
|
256
|
+
class Error:
|
|
257
|
+
code: int = 0
|
|
258
|
+
message: str = ''
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**Python выход** (pydantic):
|
|
262
|
+
```python
|
|
263
|
+
from pydantic import BaseModel, Field
|
|
264
|
+
from enum import Enum
|
|
265
|
+
from typing import Optional, List
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class PetStatus(str, Enum):
|
|
269
|
+
AVAILABLE = 'available'
|
|
270
|
+
PENDING = 'pending'
|
|
271
|
+
SOLD = 'sold'
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class Tag(BaseModel):
|
|
275
|
+
id: Optional[int] = None
|
|
276
|
+
name: Optional[str] = None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Pet(BaseModel):
|
|
280
|
+
id: int
|
|
281
|
+
name: str
|
|
282
|
+
status: Optional[PetStatus] = None
|
|
283
|
+
tags: List[Tag] = Field(default_factory=list)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Возвращаемое значение**:
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"action": "create_new_file",
|
|
290
|
+
"suggestedFileName": "pet-store-api.models.ts",
|
|
291
|
+
"code": "// ...generated code...",
|
|
292
|
+
"schemaCount": 5,
|
|
293
|
+
"enumCount": 1,
|
|
294
|
+
"language": "typescript",
|
|
295
|
+
"source": "https://petstore.swagger.io/v2/swagger.json",
|
|
296
|
+
"instruction": "Create file 'pet-store-api.models.ts' with the code."
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Изменения по файлам (Фаза 1)
|
|
303
|
+
|
|
304
|
+
### Новые файлы
|
|
305
|
+
|
|
306
|
+
#### 1. `utils/openapi/parser.js`
|
|
307
|
+
|
|
308
|
+
Единый парсер для OpenAPI 2.0 и 3.x.
|
|
309
|
+
|
|
310
|
+
```js
|
|
311
|
+
import yaml from 'js-yaml';
|
|
312
|
+
import { resolveAllRefs } from './ref-resolver.js';
|
|
313
|
+
|
|
314
|
+
export class OpenAPIParser {
|
|
315
|
+
constructor(rawSpec) {
|
|
316
|
+
this.raw = rawSpec;
|
|
317
|
+
this.version = this.detectVersion();
|
|
318
|
+
this.spec = this.normalize(); // внутренне -> формат 3.x
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Загрузить спеку из URL или файла */
|
|
322
|
+
static async load(source, format = 'auto') { ... }
|
|
323
|
+
|
|
324
|
+
/** Определить версию */
|
|
325
|
+
detectVersion() {
|
|
326
|
+
if (this.raw.swagger === '2.0') return '2.0';
|
|
327
|
+
if (this.raw.openapi?.startsWith('3.')) return this.raw.openapi;
|
|
328
|
+
throw new Error('Unsupported OpenAPI version');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Нормализовать 2.0 → 3.x структуру */
|
|
332
|
+
normalize() {
|
|
333
|
+
if (this.version === '2.0') return this.normalize2to3(this.raw);
|
|
334
|
+
return this.raw;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Конвертация 2.0 → 3.x */
|
|
338
|
+
normalize2to3(spec) {
|
|
339
|
+
return {
|
|
340
|
+
openapi: '3.0.0',
|
|
341
|
+
info: spec.info,
|
|
342
|
+
servers: [{
|
|
343
|
+
url: `${spec.schemes?.[0] || 'https'}://${spec.host || 'localhost'}${spec.basePath || ''}`
|
|
344
|
+
}],
|
|
345
|
+
paths: this.normalizePaths2to3(spec.paths),
|
|
346
|
+
components: {
|
|
347
|
+
schemas: spec.definitions || {},
|
|
348
|
+
securitySchemes: this.normalizeSecurityDefs(spec.securityDefinitions || {})
|
|
349
|
+
},
|
|
350
|
+
security: spec.security
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Нормализация путей 2.0 → 3.x (body params → requestBody) */
|
|
355
|
+
normalizePaths2to3(paths) { ... }
|
|
356
|
+
|
|
357
|
+
/** Нормализация securityDefinitions → securitySchemes */
|
|
358
|
+
normalizeSecurityDefs(defs) { ... }
|
|
359
|
+
|
|
360
|
+
/** Получить все schemas */
|
|
361
|
+
getSchemas() {
|
|
362
|
+
return this.spec.components?.schemas || {};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Получить security schemes */
|
|
366
|
+
getSecuritySchemes() {
|
|
367
|
+
return this.spec.components?.securitySchemes || {};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Получить base URL */
|
|
371
|
+
getBaseUrl() {
|
|
372
|
+
return this.spec.servers?.[0]?.url || '';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Получить все endpoints как плоский список */
|
|
376
|
+
getEndpoints() {
|
|
377
|
+
const endpoints = [];
|
|
378
|
+
for (const [path, methods] of Object.entries(this.spec.paths || {})) {
|
|
379
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
380
|
+
if (['get','post','put','patch','delete','head','options'].includes(method)) {
|
|
381
|
+
endpoints.push({
|
|
382
|
+
method: method.toUpperCase(),
|
|
383
|
+
path,
|
|
384
|
+
operationId: operation.operationId || this.generateOperationId(method, path),
|
|
385
|
+
summary: operation.summary || '',
|
|
386
|
+
description: operation.description || '',
|
|
387
|
+
tags: operation.tags || [],
|
|
388
|
+
parameters: this.extractParameters(operation, methods.parameters),
|
|
389
|
+
requestBody: this.extractRequestBody(operation),
|
|
390
|
+
responses: this.extractResponses(operation),
|
|
391
|
+
security: operation.security || this.spec.security || [],
|
|
392
|
+
deprecated: operation.deprecated || false
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return endpoints;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Генерация operationId из метода и пути: GET /users/{id} -> getUser */
|
|
401
|
+
generateOperationId(method, path) { ... }
|
|
402
|
+
|
|
403
|
+
/** Извлечение параметров (path, query, header) */
|
|
404
|
+
extractParameters(operation, pathParams) { ... }
|
|
405
|
+
|
|
406
|
+
/** Извлечение request body */
|
|
407
|
+
extractRequestBody(operation) { ... }
|
|
408
|
+
|
|
409
|
+
/** Извлечение responses с именами схем */
|
|
410
|
+
extractResponses(operation) { ... }
|
|
411
|
+
|
|
412
|
+
/** Полная сводка для loadSwagger */
|
|
413
|
+
getSummary() {
|
|
414
|
+
return {
|
|
415
|
+
version: this.version,
|
|
416
|
+
title: this.spec.info?.title || '',
|
|
417
|
+
description: this.spec.info?.description || '',
|
|
418
|
+
baseUrl: this.getBaseUrl(),
|
|
419
|
+
auth: this.getAuthSummary(),
|
|
420
|
+
endpoints: this.getEndpoints(),
|
|
421
|
+
schemas: this.getSchemasSummary(),
|
|
422
|
+
endpointCount: this.getEndpoints().length,
|
|
423
|
+
schemaCount: Object.keys(this.getSchemas()).length
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Сводка auth для удобного потребления */
|
|
428
|
+
getAuthSummary() { ... }
|
|
429
|
+
|
|
430
|
+
/** Сводка schemas (без полного тела, только имена + поля верхнего уровня) */
|
|
431
|
+
getSchemasSummary() { ... }
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### 2. `utils/openapi/ref-resolver.js`
|
|
436
|
+
|
|
437
|
+
Рекурсивное разрешение `$ref` ссылок.
|
|
438
|
+
|
|
439
|
+
```js
|
|
440
|
+
/**
|
|
441
|
+
* Разрешить все $ref в объекте спецификации
|
|
442
|
+
* @param {Object} spec - полная спецификация
|
|
443
|
+
* @returns {Object} - спецификация с разрешёнными $ref
|
|
444
|
+
*/
|
|
445
|
+
export function resolveAllRefs(spec) { ... }
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Разрешить одну $ref ссылку
|
|
449
|
+
* "#/components/schemas/Pet" -> объект Pet
|
|
450
|
+
* "#/definitions/Pet" -> объект Pet (OpenAPI 2.0)
|
|
451
|
+
*
|
|
452
|
+
* @param {string} ref - $ref строка
|
|
453
|
+
* @param {Object} spec - корневой объект спецификации
|
|
454
|
+
* @param {Set} visited - отслеживание циклических ссылок
|
|
455
|
+
* @returns {Object} - разрешённый объект
|
|
456
|
+
*/
|
|
457
|
+
export function resolveRef(ref, spec, visited = new Set()) { ... }
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Обработка $ref**:
|
|
461
|
+
- Внутренние: `#/components/schemas/Pet` → разрешить по JSON path
|
|
462
|
+
- Циклические: отслеживать через `visited` Set, при обнаружении цикла — остановиться и вернуть `{ $circularRef: 'SchemaName' }`
|
|
463
|
+
- `allOf`: мержить все элементы в один объект (properties + required)
|
|
464
|
+
- `oneOf` / `anyOf`: сохранять как union type
|
|
465
|
+
- Внешние `$ref` (другие файлы): **НЕ поддерживаем в фазе 1**, бросаем warning
|
|
466
|
+
|
|
467
|
+
#### 3. `utils/openapi/type-mapper.js`
|
|
468
|
+
|
|
469
|
+
Маппинг OpenAPI типов в TypeScript и Python.
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
/**
|
|
473
|
+
* Конвертировать OpenAPI тип в целевой язык
|
|
474
|
+
*/
|
|
475
|
+
export class TypeMapper {
|
|
476
|
+
static toTypeScript(schema, schemaName = null) {
|
|
477
|
+
// string -> string
|
|
478
|
+
// string + format:date-time -> string (ISO)
|
|
479
|
+
// string + format:binary -> Blob
|
|
480
|
+
// integer / number -> number
|
|
481
|
+
// boolean -> boolean
|
|
482
|
+
// array + items -> ItemType[]
|
|
483
|
+
// object + properties -> inline { ... } или имя схемы
|
|
484
|
+
// enum -> ссылка на enum type
|
|
485
|
+
// oneOf/anyOf -> Union type (A | B)
|
|
486
|
+
// allOf -> Intersection type (A & B)
|
|
487
|
+
// nullable: true -> Type | null
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
static toPython(schema, schemaName = null) {
|
|
491
|
+
// string -> str
|
|
492
|
+
// string + format:date-time -> datetime
|
|
493
|
+
// string + format:date -> date
|
|
494
|
+
// integer -> int
|
|
495
|
+
// number -> float
|
|
496
|
+
// boolean -> bool
|
|
497
|
+
// array + items -> List[ItemType]
|
|
498
|
+
// object -> Dict[str, Any] или имя схемы
|
|
499
|
+
// enum -> ссылка на Enum class
|
|
500
|
+
// oneOf/anyOf -> Union[A, B]
|
|
501
|
+
// nullable: true -> Optional[Type]
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Таблица маппинга:
|
|
507
|
+
|
|
508
|
+
| OpenAPI type | format | TypeScript | Python |
|
|
509
|
+
|---|---|---|---|
|
|
510
|
+
| `string` | — | `string` | `str` |
|
|
511
|
+
| `string` | `date-time` | `string` | `datetime` |
|
|
512
|
+
| `string` | `date` | `string` | `date` |
|
|
513
|
+
| `string` | `binary` | `Blob` | `bytes` |
|
|
514
|
+
| `string` | `uuid` | `string` | `str` |
|
|
515
|
+
| `string` | enum [...] | `EnumName` | `EnumName` |
|
|
516
|
+
| `integer` | `int32` | `number` | `int` |
|
|
517
|
+
| `integer` | `int64` | `number` | `int` |
|
|
518
|
+
| `number` | `float` | `number` | `float` |
|
|
519
|
+
| `number` | `double` | `number` | `float` |
|
|
520
|
+
| `boolean` | — | `boolean` | `bool` |
|
|
521
|
+
| `array` | items: T | `T[]` | `List[T]` |
|
|
522
|
+
| `object` | properties | `InterfaceName` | `ClassName` |
|
|
523
|
+
| `object` | additionalProperties: T | `Record<string, T>` | `Dict[str, T]` |
|
|
524
|
+
| — | `nullable: true` | `T \| null` | `Optional[T]` |
|
|
525
|
+
| — | `oneOf: [A, B]` | `A \| B` | `Union[A, B]` |
|
|
526
|
+
| — | `allOf: [A, B]` | `A & B` | наследование / merge |
|
|
527
|
+
|
|
528
|
+
#### 4. `utils/api-generators/api-models-typescript.js`
|
|
529
|
+
|
|
530
|
+
Генератор TypeScript моделей.
|
|
531
|
+
|
|
532
|
+
```js
|
|
533
|
+
export class ApiModelsTypeScriptGenerator {
|
|
534
|
+
constructor(schemas, options = {}) {
|
|
535
|
+
this.schemas = schemas; // resolved schemas object
|
|
536
|
+
this.options = {
|
|
537
|
+
style: 'interface', // 'interface' | 'type'
|
|
538
|
+
includeEnums: true,
|
|
539
|
+
includeValidation: false, // validation constraints as JSDoc comments
|
|
540
|
+
...options
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Сгенерировать весь файл */
|
|
545
|
+
generate(metadata = {}) {
|
|
546
|
+
const lines = [];
|
|
547
|
+
lines.push(...this.generateHeader(metadata));
|
|
548
|
+
lines.push('');
|
|
549
|
+
|
|
550
|
+
// 1. Сначала enums
|
|
551
|
+
if (this.options.includeEnums) {
|
|
552
|
+
lines.push(...this.generateAllEnums());
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 2. Затем interfaces/types в порядке зависимостей
|
|
556
|
+
const sorted = this.topologicalSort();
|
|
557
|
+
for (const name of sorted) {
|
|
558
|
+
lines.push(...this.generateModel(name, this.schemas[name]));
|
|
559
|
+
lines.push('');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return lines.join('\n');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Генерировать один interface/type */
|
|
566
|
+
generateModel(name, schema) { ... }
|
|
567
|
+
|
|
568
|
+
/** Генерировать enum из string + enum [...] */
|
|
569
|
+
generateEnum(name, values, description) { ... }
|
|
570
|
+
|
|
571
|
+
/** Обработка allOf — merge properties */
|
|
572
|
+
mergeAllOf(allOfSchemas) { ... }
|
|
573
|
+
|
|
574
|
+
/** Обработка oneOf/anyOf — union type */
|
|
575
|
+
generateUnionType(name, schemas) { ... }
|
|
576
|
+
|
|
577
|
+
/** Топологическая сортировка по зависимостям */
|
|
578
|
+
topologicalSort() { ... }
|
|
579
|
+
|
|
580
|
+
/** Генерировать заголовок файла */
|
|
581
|
+
generateHeader(metadata) { ... }
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### 5. `utils/api-generators/api-models-python.js`
|
|
586
|
+
|
|
587
|
+
Генератор Python моделей.
|
|
588
|
+
|
|
589
|
+
```js
|
|
590
|
+
export class ApiModelsPythonGenerator {
|
|
591
|
+
constructor(schemas, options = {}) {
|
|
592
|
+
this.schemas = schemas;
|
|
593
|
+
this.options = {
|
|
594
|
+
style: 'dataclass', // 'dataclass' | 'pydantic' | 'typeddict'
|
|
595
|
+
includeEnums: true,
|
|
596
|
+
includeValidation: false,
|
|
597
|
+
...options
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** Сгенерировать весь файл */
|
|
602
|
+
generate(metadata = {}) {
|
|
603
|
+
const lines = [];
|
|
604
|
+
lines.push(...this.generateHeader(metadata));
|
|
605
|
+
lines.push(...this.generateImports());
|
|
606
|
+
lines.push('');
|
|
607
|
+
|
|
608
|
+
// 1. Enums
|
|
609
|
+
if (this.options.includeEnums) {
|
|
610
|
+
lines.push(...this.generateAllEnums());
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 2. Models в порядке зависимостей
|
|
614
|
+
const sorted = this.topologicalSort();
|
|
615
|
+
for (const name of sorted) {
|
|
616
|
+
lines.push('');
|
|
617
|
+
lines.push(...this.generateModel(name, this.schemas[name]));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return lines.join('\n');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Генерировать импорты в зависимости от стиля */
|
|
624
|
+
generateImports() {
|
|
625
|
+
if (this.options.style === 'dataclass') {
|
|
626
|
+
return [
|
|
627
|
+
'from __future__ import annotations',
|
|
628
|
+
'from dataclasses import dataclass, field',
|
|
629
|
+
'from enum import Enum',
|
|
630
|
+
'from typing import Optional, List, Dict, Any, Union',
|
|
631
|
+
'from datetime import datetime, date'
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
if (this.options.style === 'pydantic') {
|
|
635
|
+
return [
|
|
636
|
+
'from __future__ import annotations',
|
|
637
|
+
'from pydantic import BaseModel, Field',
|
|
638
|
+
'from enum import Enum',
|
|
639
|
+
'from typing import Optional, List, Dict, Any, Union',
|
|
640
|
+
'from datetime import datetime, date'
|
|
641
|
+
];
|
|
642
|
+
}
|
|
643
|
+
// typeddict
|
|
644
|
+
return [
|
|
645
|
+
'from typing import TypedDict, Optional, List, Dict, Any, Union',
|
|
646
|
+
'from enum import Enum',
|
|
647
|
+
'from datetime import datetime, date'
|
|
648
|
+
];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
generateModel(name, schema) { ... }
|
|
652
|
+
generateEnum(name, values, description) { ... }
|
|
653
|
+
mergeAllOf(allOfSchemas) { ... }
|
|
654
|
+
topologicalSort() { ... }
|
|
655
|
+
generateHeader(metadata) { ... }
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Изменяемые файлы
|
|
660
|
+
|
|
661
|
+
#### 6. `server/tool-schemas.js`
|
|
662
|
+
|
|
663
|
+
```js
|
|
664
|
+
export const LoadSwaggerSchema = z.object({
|
|
665
|
+
source: z.string().describe("URL (http/https) or local file path to swagger.json / openapi.yaml"),
|
|
666
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
667
|
+
.describe("Spec format. 'auto' (default) detects from extension/content"),
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
export const GenerateApiModelsSchema = z.object({
|
|
671
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
672
|
+
language: z.enum(['typescript', 'python']).describe("Target language for models"),
|
|
673
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
674
|
+
.describe("Spec format (default: auto)"),
|
|
675
|
+
style: z.enum(['interface', 'type']).optional()
|
|
676
|
+
.describe("TypeScript only: 'interface' (default) or 'type' aliases"),
|
|
677
|
+
pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
|
|
678
|
+
.describe("Python only: 'dataclass' (default), 'pydantic', or 'typeddict'"),
|
|
679
|
+
includeEnums: z.boolean().optional()
|
|
680
|
+
.describe("Generate enum types (default: true)"),
|
|
681
|
+
includeValidation: z.boolean().optional()
|
|
682
|
+
.describe("Include validation constraints as comments (default: false)"),
|
|
683
|
+
schemas: z.array(z.string()).optional()
|
|
684
|
+
.describe("Generate only these schemas (default: all)"),
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
#### 7. `server/tool-definitions.js`
|
|
689
|
+
|
|
690
|
+
Добавить определения обоих тулов в массив tools с описаниями и inputSchema.
|
|
691
|
+
|
|
692
|
+
#### 8. `index.js`
|
|
693
|
+
|
|
694
|
+
Добавить хендлеры для `loadSwagger` и `generateApiModels`:
|
|
695
|
+
|
|
696
|
+
**loadSwagger**:
|
|
697
|
+
```js
|
|
698
|
+
if (name === "loadSwagger") {
|
|
699
|
+
const parser = await OpenAPIParser.load(args.source, args.format);
|
|
700
|
+
const summary = parser.getSummary();
|
|
701
|
+
return {
|
|
702
|
+
content: [{
|
|
703
|
+
type: 'text',
|
|
704
|
+
text: JSON.stringify({ success: true, ...summary }, null, 2)
|
|
705
|
+
}]
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**generateApiModels**:
|
|
711
|
+
```js
|
|
712
|
+
if (name === "generateApiModels") {
|
|
713
|
+
const parser = await OpenAPIParser.load(args.source, args.format);
|
|
714
|
+
const schemas = parser.getSchemas();
|
|
715
|
+
// Отфильтровать если указан args.schemas
|
|
716
|
+
// Выбрать генератор по языку
|
|
717
|
+
// Сгенерировать код
|
|
718
|
+
// Вернуть JSON с кодом и suggestedFileName
|
|
719
|
+
}
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
#### 9. `README.md`
|
|
723
|
+
|
|
724
|
+
Документировать оба тула в секции Recorder/API Tools.
|
|
725
|
+
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
## Обработка Auth типов
|
|
729
|
+
|
|
730
|
+
Все типы auth из OpenAPI spec парсятся и возвращаются в `loadSwagger`:
|
|
731
|
+
|
|
732
|
+
### HTTP Bearer
|
|
733
|
+
```yaml
|
|
734
|
+
securitySchemes:
|
|
735
|
+
bearerAuth:
|
|
736
|
+
type: http
|
|
737
|
+
scheme: bearer
|
|
738
|
+
bearerFormat: JWT
|
|
739
|
+
```
|
|
740
|
+
→ `{ name: "bearerAuth", type: "http", scheme: "bearer", bearerFormat: "JWT" }`
|
|
741
|
+
|
|
742
|
+
### API Key
|
|
743
|
+
```yaml
|
|
744
|
+
securitySchemes:
|
|
745
|
+
apiKey:
|
|
746
|
+
type: apiKey
|
|
747
|
+
in: header
|
|
748
|
+
name: X-API-Key
|
|
749
|
+
```
|
|
750
|
+
→ `{ name: "apiKey", type: "apiKey", in: "header", paramName: "X-API-Key" }`
|
|
751
|
+
|
|
752
|
+
### Basic Auth
|
|
753
|
+
```yaml
|
|
754
|
+
securitySchemes:
|
|
755
|
+
basicAuth:
|
|
756
|
+
type: http
|
|
757
|
+
scheme: basic
|
|
758
|
+
```
|
|
759
|
+
→ `{ name: "basicAuth", type: "http", scheme: "basic" }`
|
|
760
|
+
|
|
761
|
+
### OAuth2
|
|
762
|
+
```yaml
|
|
763
|
+
securitySchemes:
|
|
764
|
+
oauth2:
|
|
765
|
+
type: oauth2
|
|
766
|
+
flows:
|
|
767
|
+
authorizationCode:
|
|
768
|
+
authorizationUrl: https://example.com/oauth/authorize
|
|
769
|
+
tokenUrl: https://example.com/oauth/token
|
|
770
|
+
scopes:
|
|
771
|
+
read: Read access
|
|
772
|
+
write: Write access
|
|
773
|
+
```
|
|
774
|
+
→ `{ name: "oauth2", type: "oauth2", flows: { authorizationCode: { ... } } }`
|
|
775
|
+
|
|
776
|
+
### OpenAPI 2.0 → 3.x маппинг auth
|
|
777
|
+
|
|
778
|
+
| OpenAPI 2.0 securityDefinitions | OpenAPI 3.x securitySchemes |
|
|
779
|
+
|---|---|
|
|
780
|
+
| `type: "apiKey"` | `type: "apiKey"` (без изменений) |
|
|
781
|
+
| `type: "basic"` | `type: "http", scheme: "basic"` |
|
|
782
|
+
| `type: "oauth2", flow: "implicit"` | `type: "oauth2", flows: { implicit: {...} }` |
|
|
783
|
+
| `type: "oauth2", flow: "password"` | `type: "oauth2", flows: { password: {...} }` |
|
|
784
|
+
| `type: "oauth2", flow: "application"` | `type: "oauth2", flows: { clientCredentials: {...} }` |
|
|
785
|
+
| `type: "oauth2", flow: "accessCode"` | `type: "oauth2", flows: { authorizationCode: {...} }` |
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
## Обработка $ref
|
|
790
|
+
|
|
791
|
+
### Алгоритм разрешения
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
resolveRef("#/components/schemas/Pet", spec):
|
|
795
|
+
1. Разбить путь: ["components", "schemas", "Pet"]
|
|
796
|
+
2. Пройти по spec: spec.components.schemas.Pet
|
|
797
|
+
3. Если результат содержит $ref — рекурсия
|
|
798
|
+
4. Если уже в visited — вернуть { $circularRef: "Pet" }
|
|
799
|
+
5. Кэшировать в resolvedRefs Map
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### allOf обработка
|
|
803
|
+
```yaml
|
|
804
|
+
NewPet:
|
|
805
|
+
allOf:
|
|
806
|
+
- $ref: '#/components/schemas/Pet'
|
|
807
|
+
- type: object
|
|
808
|
+
properties:
|
|
809
|
+
extraField:
|
|
810
|
+
type: string
|
|
811
|
+
```
|
|
812
|
+
→ Мержить properties и required из всех элементов allOf.
|
|
813
|
+
Для TypeScript: `interface NewPet extends Pet { extraField?: string; }`
|
|
814
|
+
Для Python: `class NewPet(Pet): extra_field: Optional[str] = None`
|
|
815
|
+
|
|
816
|
+
### oneOf/anyOf обработка
|
|
817
|
+
```yaml
|
|
818
|
+
Response:
|
|
819
|
+
oneOf:
|
|
820
|
+
- $ref: '#/components/schemas/Cat'
|
|
821
|
+
- $ref: '#/components/schemas/Dog'
|
|
822
|
+
```
|
|
823
|
+
→ TypeScript: `type Response = Cat | Dog;`
|
|
824
|
+
→ Python: `Response = Union[Cat, Dog]`
|
|
825
|
+
|
|
826
|
+
### Циклические ссылки
|
|
827
|
+
```yaml
|
|
828
|
+
TreeNode:
|
|
829
|
+
properties:
|
|
830
|
+
children:
|
|
831
|
+
type: array
|
|
832
|
+
items:
|
|
833
|
+
$ref: '#/components/schemas/TreeNode' # цикл!
|
|
834
|
+
```
|
|
835
|
+
→ TypeScript: `children?: TreeNode[]` (TS нативно поддерживает)
|
|
836
|
+
→ Python: `children: List['TreeNode'] = field(default_factory=list)` (forward reference)
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## Именование файлов
|
|
841
|
+
|
|
842
|
+
| Спека | TypeScript | Python |
|
|
843
|
+
|---|---|---|
|
|
844
|
+
| Pet Store API | `pet-store-api.models.ts` | `pet_store_api_models.py` |
|
|
845
|
+
| My Service | `my-service.models.ts` | `my_service_models.py` |
|
|
846
|
+
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
## Структура файлов проекта (после реализации)
|
|
850
|
+
|
|
851
|
+
```
|
|
852
|
+
utils/
|
|
853
|
+
openapi/
|
|
854
|
+
parser.js # OpenAPIParser class
|
|
855
|
+
ref-resolver.js # $ref resolution
|
|
856
|
+
type-mapper.js # OpenAPI → TS/Python type mapping
|
|
857
|
+
api-generators/
|
|
858
|
+
api-models-typescript.js # TS interface/type generator
|
|
859
|
+
api-models-python.js # Python dataclass/pydantic generator
|
|
860
|
+
code-generators/
|
|
861
|
+
... (existing POM/test generators)
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
---
|
|
865
|
+
|
|
866
|
+
## Верификация
|
|
867
|
+
|
|
868
|
+
### loadSwagger
|
|
869
|
+
1. Загрузить Petstore 2.0: `https://petstore.swagger.io/v2/swagger.json`
|
|
870
|
+
2. Загрузить Petstore 3.0: `https://petstore3.swagger.io/api/v3/openapi.json`
|
|
871
|
+
3. Загрузить из YAML-файла
|
|
872
|
+
4. Проверить количество endpoints и schemas
|
|
873
|
+
5. Проверить auth types
|
|
874
|
+
6. Проверить $ref resolution
|
|
875
|
+
|
|
876
|
+
### generateApiModels
|
|
877
|
+
1. Сгенерировать TypeScript interfaces из Petstore
|
|
878
|
+
2. Сгенерировать Python dataclasses из Petstore
|
|
879
|
+
3. Сгенерировать Python pydantic из Petstore
|
|
880
|
+
4. Проверить enum-ы (PetStatus)
|
|
881
|
+
5. Проверить nested objects ($ref)
|
|
882
|
+
6. Проверить required vs optional поля
|
|
883
|
+
7. Проверить allOf merge
|
|
884
|
+
8. Проверить массивы (tags: Tag[])
|
|
885
|
+
9. Фильтрация по schemas: только ['Pet', 'Tag']
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Фаза 2: generateApiClient
|
|
890
|
+
|
|
891
|
+
### Тул 3: `generateApiClient`
|
|
892
|
+
|
|
893
|
+
**Назначение**: сгенерировать класс API-клиента с типизированными методами для каждого endpoint из OpenAPI спецификации.
|
|
894
|
+
|
|
895
|
+
**Параметры**:
|
|
896
|
+
```js
|
|
897
|
+
GenerateApiClientSchema = z.object({
|
|
898
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
899
|
+
language: z.enum(['typescript', 'python']).describe("Target language"),
|
|
900
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
901
|
+
.describe("Spec format (default: auto)"),
|
|
902
|
+
modelsImportPath: z.string().optional()
|
|
903
|
+
.describe("Import path for models file. E.g. './pet-store-api.models' (default: auto from spec title)"),
|
|
904
|
+
includeAuth: z.boolean().optional()
|
|
905
|
+
.describe("Generate auth configuration in constructor (default: true)"),
|
|
906
|
+
includeComments: z.boolean().optional()
|
|
907
|
+
.describe("Include JSDoc/docstring comments on methods (default: true)"),
|
|
908
|
+
tags: z.array(z.string()).optional()
|
|
909
|
+
.describe("Generate methods only for these tags (default: all). E.g. ['pets', 'users']"),
|
|
910
|
+
groupByTags: z.boolean().optional()
|
|
911
|
+
.describe("Group methods by tag with section comments (default: true)"),
|
|
912
|
+
responseStyle: z.enum(['raw', 'typed', 'both']).optional()
|
|
913
|
+
.describe("Return type: 'raw' returns Response object, 'typed' returns parsed model, 'both' has overloads (default: 'raw')"),
|
|
914
|
+
})
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
**Алгоритм**:
|
|
918
|
+
1. Загрузить и распарсить спеку (переиспользует `OpenAPIParser`)
|
|
919
|
+
2. Извлечь все endpoints через `parser.getEndpoints()`
|
|
920
|
+
3. Отфильтровать endpoints по `tags` если указан
|
|
921
|
+
4. Извлечь auth schemes через `parser.getSecuritySchemes()`
|
|
922
|
+
5. Для каждого endpoint сгенерировать метод:
|
|
923
|
+
a. Определить имя метода из `operationId` (или сгенерировать из method+path)
|
|
924
|
+
b. Извлечь path params → параметры функции
|
|
925
|
+
c. Извлечь query params → опциональный params объект
|
|
926
|
+
d. Извлечь request body → body параметр с типом из моделей
|
|
927
|
+
e. Определить response type из `responses.200` (или 201) schema
|
|
928
|
+
f. Сгенерировать URL-строку с подстановкой path params
|
|
929
|
+
g. Сгенерировать query string building
|
|
930
|
+
6. Сгенерировать constructor с auth configuration
|
|
931
|
+
7. Собрать файл: imports → class → constructor → методы
|
|
932
|
+
8. Вернуть код + suggested filename
|
|
933
|
+
|
|
934
|
+
**Возвращаемое значение**:
|
|
935
|
+
```json
|
|
936
|
+
{
|
|
937
|
+
"action": "create_new_file",
|
|
938
|
+
"suggestedFileName": "PetStoreApi.ts",
|
|
939
|
+
"modelsFileName": "pet-store-api.models.ts",
|
|
940
|
+
"code": "// ...generated code...",
|
|
941
|
+
"methodCount": 12,
|
|
942
|
+
"tagGroups": ["pets", "store", "user"],
|
|
943
|
+
"authTypes": ["bearer", "apiKey"],
|
|
944
|
+
"language": "typescript",
|
|
945
|
+
"source": "https://petstore.swagger.io/v2/swagger.json",
|
|
946
|
+
"instruction": "Create file 'PetStoreApi.ts'. Make sure models file exists (use generateApiModels if not). Set auth credentials via constructor options."
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
### Генерация имён методов (method-generator)
|
|
953
|
+
|
|
954
|
+
#### operationId → имя метода
|
|
955
|
+
|
|
956
|
+
| operationId | TypeScript method | Python method |
|
|
957
|
+
|---|---|---|
|
|
958
|
+
| `listPets` | `listPets` | `list_pets` |
|
|
959
|
+
| `createPet` | `createPet` | `create_pet` |
|
|
960
|
+
| `getPetById` | `getPetById` | `get_pet_by_id` |
|
|
961
|
+
| `updatePetWithForm` | `updatePetWithForm` | `update_pet_with_form` |
|
|
962
|
+
| `deletePet` | `deletePet` | `delete_pet` |
|
|
963
|
+
|
|
964
|
+
**Fallback (нет operationId)**:
|
|
965
|
+
Генерация из HTTP method + path:
|
|
966
|
+
| method | path | TypeScript | Python |
|
|
967
|
+
|---|---|---|---|
|
|
968
|
+
| `GET` | `/pets` | `getPets` | `get_pets` |
|
|
969
|
+
| `POST` | `/pets` | `createPets` | `create_pets` |
|
|
970
|
+
| `GET` | `/pets/{petId}` | `getPetsByPetId` | `get_pets_by_pet_id` |
|
|
971
|
+
| `PUT` | `/pets/{petId}` | `updatePetsByPetId` | `update_pets_by_pet_id` |
|
|
972
|
+
| `DELETE` | `/pets/{petId}` | `deletePetsByPetId` | `delete_pets_by_pet_id` |
|
|
973
|
+
| `GET` | `/users/{id}/orders` | `getUsersByIdOrders` | `get_users_by_id_orders` |
|
|
974
|
+
|
|
975
|
+
**Алгоритм генерации operationId из method+path**:
|
|
976
|
+
```
|
|
977
|
+
1. Маппинг HTTP метода → префикс: GET→get, POST→create, PUT→update, PATCH→patch, DELETE→delete
|
|
978
|
+
2. Разбить path по '/': /pets/{petId}/tags → ['pets', '{petId}', 'tags']
|
|
979
|
+
3. Для каждого сегмента:
|
|
980
|
+
- Обычный сегмент: capitalize ('pets' → 'Pets')
|
|
981
|
+
- Path param {x}: 'By' + capitalize(x) ('By' + 'PetId' → 'ByPetId')
|
|
982
|
+
4. Склеить: 'get' + 'Pets' + 'ByPetId' + 'Tags' → 'getPetsByPetIdTags'
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
#### Параметры метода
|
|
986
|
+
|
|
987
|
+
**Path params** → обязательные параметры функции:
|
|
988
|
+
```typescript
|
|
989
|
+
// GET /pets/{petId}/toys/{toyId}
|
|
990
|
+
async getPetToy(petId: number, toyId: number)
|
|
991
|
+
```
|
|
992
|
+
```python
|
|
993
|
+
# GET /pets/{petId}/toys/{toyId}
|
|
994
|
+
def get_pet_toy(self, pet_id: int, toy_id: int)
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
**Query params** → опциональный params объект:
|
|
998
|
+
```typescript
|
|
999
|
+
// GET /pets?limit=10&status=available&tags=cat,dog
|
|
1000
|
+
async listPets(params?: { limit?: number; status?: PetStatus; tags?: string[] })
|
|
1001
|
+
```
|
|
1002
|
+
```python
|
|
1003
|
+
# GET /pets?limit=10&status=available&tags=cat,dog
|
|
1004
|
+
def list_pets(self, limit: Optional[int] = None, status: Optional[PetStatus] = None, tags: Optional[List[str]] = None)
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
**Request body** → body параметр:
|
|
1008
|
+
```typescript
|
|
1009
|
+
// POST /pets
|
|
1010
|
+
async createPet(body: CreatePetRequest)
|
|
1011
|
+
```
|
|
1012
|
+
```python
|
|
1013
|
+
# POST /pets
|
|
1014
|
+
def create_pet(self, body: CreatePetRequest)
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Комбинированные** (path + query + body):
|
|
1018
|
+
```typescript
|
|
1019
|
+
// PUT /pets/{petId}?notify=true body: UpdatePetRequest
|
|
1020
|
+
async updatePet(petId: number, body: UpdatePetRequest, params?: { notify?: boolean })
|
|
1021
|
+
```
|
|
1022
|
+
```python
|
|
1023
|
+
# PUT /pets/{petId}?notify=true body: UpdatePetRequest
|
|
1024
|
+
def update_pet(self, pet_id: int, body: UpdatePetRequest, notify: Optional[bool] = None)
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
#### Маппинг типов параметров
|
|
1028
|
+
|
|
1029
|
+
| OpenAPI parameter type | TypeScript | Python |
|
|
1030
|
+
|---|---|---|
|
|
1031
|
+
| `integer` (in: path) | `number` | `int` |
|
|
1032
|
+
| `integer` (in: query) | `number` | `int` |
|
|
1033
|
+
| `string` (in: query) | `string` | `str` |
|
|
1034
|
+
| `boolean` (in: query) | `boolean` | `bool` |
|
|
1035
|
+
| `string` + enum (in: query) | `EnumType` | `EnumType` |
|
|
1036
|
+
| `array` (in: query) | `string[]` | `List[str]` |
|
|
1037
|
+
| `$ref` (requestBody) | `ModelName` | `ModelName` |
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
### Генерация Auth конфигурации
|
|
1042
|
+
|
|
1043
|
+
#### Constructor auth params
|
|
1044
|
+
|
|
1045
|
+
Из `securitySchemes` спецификации генерируется конструктор с соответствующими параметрами.
|
|
1046
|
+
|
|
1047
|
+
**HTTP Bearer**:
|
|
1048
|
+
```typescript
|
|
1049
|
+
constructor(request: APIRequestContext, options: {
|
|
1050
|
+
baseUrl?: string;
|
|
1051
|
+
token?: string; // Bearer token
|
|
1052
|
+
} = {}) {
|
|
1053
|
+
this.request = request;
|
|
1054
|
+
this.baseUrl = options.baseUrl || 'https://petstore.swagger.io/v2';
|
|
1055
|
+
this.headers = {};
|
|
1056
|
+
if (options.token) this.headers['Authorization'] = `Bearer ${options.token}`;
|
|
1057
|
+
}
|
|
1058
|
+
```
|
|
1059
|
+
```python
|
|
1060
|
+
def __init__(self, base_url: str = 'https://petstore.swagger.io/v2',
|
|
1061
|
+
token: Optional[str] = None):
|
|
1062
|
+
self.session = requests.Session()
|
|
1063
|
+
self.base_url = base_url
|
|
1064
|
+
if token:
|
|
1065
|
+
self.session.headers['Authorization'] = f'Bearer {token}'
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
**API Key (header)**:
|
|
1069
|
+
```typescript
|
|
1070
|
+
// securitySchemes: { apiKey: { type: apiKey, in: header, name: X-API-Key } }
|
|
1071
|
+
constructor(request: APIRequestContext, options: {
|
|
1072
|
+
baseUrl?: string;
|
|
1073
|
+
apiKey?: string; // X-API-Key header
|
|
1074
|
+
} = {}) {
|
|
1075
|
+
// ...
|
|
1076
|
+
if (options.apiKey) this.headers['X-API-Key'] = options.apiKey;
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
```python
|
|
1080
|
+
def __init__(self, base_url: str = '...', api_key: Optional[str] = None):
|
|
1081
|
+
# ...
|
|
1082
|
+
if api_key:
|
|
1083
|
+
self.session.headers['X-API-Key'] = api_key
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
**API Key (query)**:
|
|
1087
|
+
```typescript
|
|
1088
|
+
// securitySchemes: { apiKey: { type: apiKey, in: query, name: api_key } }
|
|
1089
|
+
// → сохраняется в this.queryAuth и добавляется ко всем запросам
|
|
1090
|
+
constructor(request: APIRequestContext, options: {
|
|
1091
|
+
baseUrl?: string;
|
|
1092
|
+
apiKey?: string; // api_key query parameter
|
|
1093
|
+
} = {}) {
|
|
1094
|
+
// ...
|
|
1095
|
+
this.queryAuth = {};
|
|
1096
|
+
if (options.apiKey) this.queryAuth['api_key'] = options.apiKey;
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
```python
|
|
1100
|
+
def __init__(self, base_url: str = '...', api_key: Optional[str] = None):
|
|
1101
|
+
# ...
|
|
1102
|
+
self.query_auth = {}
|
|
1103
|
+
if api_key:
|
|
1104
|
+
self.query_auth['api_key'] = api_key
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**Basic Auth**:
|
|
1108
|
+
```typescript
|
|
1109
|
+
constructor(request: APIRequestContext, options: {
|
|
1110
|
+
baseUrl?: string;
|
|
1111
|
+
username?: string;
|
|
1112
|
+
password?: string;
|
|
1113
|
+
} = {}) {
|
|
1114
|
+
// ...
|
|
1115
|
+
if (options.username && options.password) {
|
|
1116
|
+
const creds = Buffer.from(`${options.username}:${options.password}`).toString('base64');
|
|
1117
|
+
this.headers['Authorization'] = `Basic ${creds}`;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
```python
|
|
1122
|
+
def __init__(self, base_url: str = '...', username: Optional[str] = None, password: Optional[str] = None):
|
|
1123
|
+
# ...
|
|
1124
|
+
if username and password:
|
|
1125
|
+
self.session.auth = (username, password)
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**OAuth2** (все flows):
|
|
1129
|
+
```typescript
|
|
1130
|
+
// OAuth2 — генерируем приём готового access token (flow настраивается снаружи)
|
|
1131
|
+
constructor(request: APIRequestContext, options: {
|
|
1132
|
+
baseUrl?: string;
|
|
1133
|
+
accessToken?: string; // OAuth2 access token
|
|
1134
|
+
} = {}) {
|
|
1135
|
+
// ...
|
|
1136
|
+
if (options.accessToken) this.headers['Authorization'] = `Bearer ${options.accessToken}`;
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
1139
|
+
```python
|
|
1140
|
+
def __init__(self, base_url: str = '...', access_token: Optional[str] = None):
|
|
1141
|
+
# ...
|
|
1142
|
+
if access_token:
|
|
1143
|
+
self.session.headers['Authorization'] = f'Bearer {access_token}'
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
**Комбинированные auth** (несколько schemes в одной спеке):
|
|
1147
|
+
```typescript
|
|
1148
|
+
constructor(request: APIRequestContext, options: {
|
|
1149
|
+
baseUrl?: string;
|
|
1150
|
+
token?: string; // Bearer auth
|
|
1151
|
+
apiKey?: string; // X-API-Key header
|
|
1152
|
+
username?: string; // Basic auth
|
|
1153
|
+
password?: string; // Basic auth
|
|
1154
|
+
} = {}) {
|
|
1155
|
+
this.request = request;
|
|
1156
|
+
this.baseUrl = options.baseUrl || '...';
|
|
1157
|
+
this.headers = {};
|
|
1158
|
+
if (options.token) this.headers['Authorization'] = `Bearer ${options.token}`;
|
|
1159
|
+
else if (options.username && options.password) {
|
|
1160
|
+
const creds = Buffer.from(`${options.username}:${options.password}`).toString('base64');
|
|
1161
|
+
this.headers['Authorization'] = `Basic ${creds}`;
|
|
1162
|
+
}
|
|
1163
|
+
if (options.apiKey) this.headers['X-API-Key'] = options.apiKey;
|
|
1164
|
+
}
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
#### Per-endpoint security overrides
|
|
1168
|
+
|
|
1169
|
+
Если endpoint имеет собственный `security` блок (отличный от глобального), генерируется комментарий:
|
|
1170
|
+
```typescript
|
|
1171
|
+
/** GET /admin/users - List admin users
|
|
1172
|
+
* Security: bearerAuth (overrides global apiKey) */
|
|
1173
|
+
async listAdminUsers() { ... }
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
### URL Construction
|
|
1179
|
+
|
|
1180
|
+
#### Path params substitution
|
|
1181
|
+
```typescript
|
|
1182
|
+
// Template: /pets/{petId}/toys/{toyId}
|
|
1183
|
+
// Generated: `${this.baseUrl}/pets/${petId}/toys/${toyId}`
|
|
1184
|
+
```
|
|
1185
|
+
```python
|
|
1186
|
+
# Template: /pets/{petId}/toys/{toyId}
|
|
1187
|
+
# Generated: f'{self.base_url}/pets/{pet_id}/toys/{toy_id}'
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
#### Query string building
|
|
1191
|
+
|
|
1192
|
+
**TypeScript**:
|
|
1193
|
+
```typescript
|
|
1194
|
+
async listPets(params?: { limit?: number; status?: PetStatus; tags?: string[] }) {
|
|
1195
|
+
const query = new URLSearchParams();
|
|
1196
|
+
if (params?.limit !== undefined) query.set('limit', String(params.limit));
|
|
1197
|
+
if (params?.status !== undefined) query.set('status', params.status);
|
|
1198
|
+
if (params?.tags !== undefined) params.tags.forEach(v => query.append('tags', v));
|
|
1199
|
+
// Добавить query auth params
|
|
1200
|
+
for (const [k, v] of Object.entries(this.queryAuth)) query.set(k, v);
|
|
1201
|
+
const qs = query.toString();
|
|
1202
|
+
const url = `${this.baseUrl}/pets${qs ? '?' + qs : ''}`;
|
|
1203
|
+
return this.request.get(url, { headers: this.headers });
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
**Python**:
|
|
1208
|
+
```python
|
|
1209
|
+
def list_pets(self, limit: Optional[int] = None, status: Optional[PetStatus] = None,
|
|
1210
|
+
tags: Optional[List[str]] = None):
|
|
1211
|
+
"""GET /pets - List all pets"""
|
|
1212
|
+
params = {**self.query_auth}
|
|
1213
|
+
if limit is not None: params['limit'] = limit
|
|
1214
|
+
if status is not None: params['status'] = status.value
|
|
1215
|
+
if tags is not None: params['tags'] = ','.join(tags)
|
|
1216
|
+
return self.session.get(f'{self.base_url}/pets', params=params)
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
#### Request body serialization
|
|
1220
|
+
|
|
1221
|
+
**TypeScript** (Playwright request context handles JSON automatically):
|
|
1222
|
+
```typescript
|
|
1223
|
+
async createPet(body: CreatePetRequest) {
|
|
1224
|
+
return this.request.post(`${this.baseUrl}/pets`, {
|
|
1225
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
1226
|
+
data: body
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
**Python**:
|
|
1232
|
+
```python
|
|
1233
|
+
def create_pet(self, body: CreatePetRequest):
|
|
1234
|
+
"""POST /pets - Create a pet"""
|
|
1235
|
+
# dataclass
|
|
1236
|
+
from dataclasses import asdict
|
|
1237
|
+
return self.session.post(f'{self.base_url}/pets', json=asdict(body))
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
Для `pydantic`:
|
|
1241
|
+
```python
|
|
1242
|
+
return self.session.post(f'{self.base_url}/pets', json=body.model_dump())
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Для `typeddict`:
|
|
1246
|
+
```python
|
|
1247
|
+
return self.session.post(f'{self.base_url}/pets', json=body)
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
---
|
|
1251
|
+
|
|
1252
|
+
### Изменения по файлам (Фаза 2)
|
|
1253
|
+
|
|
1254
|
+
#### Новые файлы
|
|
1255
|
+
|
|
1256
|
+
##### 1. `utils/openapi/method-generator.js`
|
|
1257
|
+
|
|
1258
|
+
Генерация метаданных методов из endpoints.
|
|
1259
|
+
|
|
1260
|
+
```js
|
|
1261
|
+
/**
|
|
1262
|
+
* Генератор метаданных методов из OpenAPI endpoints
|
|
1263
|
+
*/
|
|
1264
|
+
export class MethodGenerator {
|
|
1265
|
+
/**
|
|
1266
|
+
* Сгенерировать метаданные метода для endpoint
|
|
1267
|
+
* @param {Object} endpoint - endpoint из OpenAPIParser.getEndpoints()
|
|
1268
|
+
* @param {string} language - 'typescript' | 'python'
|
|
1269
|
+
* @returns {Object} - { methodName, pathParams, queryParams, bodyParam, returnType, comment }
|
|
1270
|
+
*/
|
|
1271
|
+
static generateMethod(endpoint, language) {
|
|
1272
|
+
return {
|
|
1273
|
+
methodName: this.getMethodName(endpoint.operationId, endpoint.method, endpoint.path, language),
|
|
1274
|
+
httpMethod: endpoint.method.toLowerCase(),
|
|
1275
|
+
path: endpoint.path,
|
|
1276
|
+
pathParams: this.extractPathParams(endpoint.parameters, language),
|
|
1277
|
+
queryParams: this.extractQueryParams(endpoint.parameters, language),
|
|
1278
|
+
bodyParam: this.extractBodyParam(endpoint.requestBody, language),
|
|
1279
|
+
returnType: this.extractReturnType(endpoint.responses, language),
|
|
1280
|
+
comment: this.buildComment(endpoint),
|
|
1281
|
+
tags: endpoint.tags,
|
|
1282
|
+
deprecated: endpoint.deprecated,
|
|
1283
|
+
security: endpoint.security
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Получить имя метода из operationId или сгенерировать из method+path
|
|
1289
|
+
* @param {string|null} operationId
|
|
1290
|
+
* @param {string} method - HTTP method
|
|
1291
|
+
* @param {string} path - URL path
|
|
1292
|
+
* @param {string} language - target language
|
|
1293
|
+
* @returns {string} - method name in target language convention
|
|
1294
|
+
*/
|
|
1295
|
+
static getMethodName(operationId, method, path, language) {
|
|
1296
|
+
const camelName = operationId || this.generateOperationId(method, path);
|
|
1297
|
+
if (language === 'python') {
|
|
1298
|
+
return this.camelToSnake(camelName);
|
|
1299
|
+
}
|
|
1300
|
+
return camelName;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Генерация operationId из HTTP method + path
|
|
1305
|
+
* GET /pets/{petId} → getPetsByPetId
|
|
1306
|
+
*/
|
|
1307
|
+
static generateOperationId(method, path) { ... }
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* camelCase → snake_case
|
|
1311
|
+
*/
|
|
1312
|
+
static camelToSnake(name) {
|
|
1313
|
+
return name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, '');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* Извлечь path параметры
|
|
1318
|
+
* @returns {Array<{name, type, required}>}
|
|
1319
|
+
*/
|
|
1320
|
+
static extractPathParams(parameters, language) {
|
|
1321
|
+
return (parameters || [])
|
|
1322
|
+
.filter(p => p.in === 'path')
|
|
1323
|
+
.map(p => ({
|
|
1324
|
+
name: language === 'python' ? this.camelToSnake(p.name) : p.name,
|
|
1325
|
+
originalName: p.name,
|
|
1326
|
+
type: TypeMapper.mapParamType(p, language),
|
|
1327
|
+
required: true
|
|
1328
|
+
}));
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Извлечь query параметры
|
|
1333
|
+
* @returns {Array<{name, type, required, isArray, enumType}>}
|
|
1334
|
+
*/
|
|
1335
|
+
static extractQueryParams(parameters, language) {
|
|
1336
|
+
return (parameters || [])
|
|
1337
|
+
.filter(p => p.in === 'query')
|
|
1338
|
+
.map(p => ({
|
|
1339
|
+
name: language === 'python' ? this.camelToSnake(p.name) : p.name,
|
|
1340
|
+
originalName: p.name,
|
|
1341
|
+
type: TypeMapper.mapParamType(p, language),
|
|
1342
|
+
required: p.required || false,
|
|
1343
|
+
isArray: p.type === 'array',
|
|
1344
|
+
enumType: p.enum ? p.name : null
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Извлечь body параметр
|
|
1350
|
+
* @returns {Object|null} - { type, required } или null
|
|
1351
|
+
*/
|
|
1352
|
+
static extractBodyParam(requestBody, language) { ... }
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Определить return type из responses
|
|
1356
|
+
* Приоритет: 200 → 201 → 204(void) → первый 2xx
|
|
1357
|
+
* @returns {string|null} - type name или null
|
|
1358
|
+
*/
|
|
1359
|
+
static extractReturnType(responses, language) { ... }
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Построить JSDoc / docstring комментарий
|
|
1363
|
+
* @returns {string} - "METHOD /path - summary"
|
|
1364
|
+
*/
|
|
1365
|
+
static buildComment(endpoint) {
|
|
1366
|
+
const deprecated = endpoint.deprecated ? ' [DEPRECATED]' : '';
|
|
1367
|
+
return `${endpoint.method} ${endpoint.path} - ${endpoint.summary || 'No description'}${deprecated}`;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
```
|
|
1371
|
+
|
|
1372
|
+
##### 2. `utils/openapi/auth-generator.js`
|
|
1373
|
+
|
|
1374
|
+
Генерация auth-конфигурации для constructor.
|
|
1375
|
+
|
|
1376
|
+
```js
|
|
1377
|
+
/**
|
|
1378
|
+
* Генератор auth-конфигурации для API-клиента
|
|
1379
|
+
*/
|
|
1380
|
+
export class AuthGenerator {
|
|
1381
|
+
/**
|
|
1382
|
+
* Сгенерировать auth metadata из securitySchemes
|
|
1383
|
+
* @param {Object} securitySchemes - из spec.components.securitySchemes
|
|
1384
|
+
* @returns {Object} - { constructorParams, headerSetup, querySetup }
|
|
1385
|
+
*/
|
|
1386
|
+
static generateAuthConfig(securitySchemes) {
|
|
1387
|
+
const constructorParams = [];
|
|
1388
|
+
const headerSetup = [];
|
|
1389
|
+
const querySetup = [];
|
|
1390
|
+
|
|
1391
|
+
for (const [name, scheme] of Object.entries(securitySchemes || {})) {
|
|
1392
|
+
const config = this.processScheme(name, scheme);
|
|
1393
|
+
constructorParams.push(...config.params);
|
|
1394
|
+
headerSetup.push(...config.headers);
|
|
1395
|
+
querySetup.push(...config.query);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return { constructorParams, headerSetup, querySetup };
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Обработать одну security scheme
|
|
1403
|
+
*/
|
|
1404
|
+
static processScheme(name, scheme) {
|
|
1405
|
+
switch (scheme.type) {
|
|
1406
|
+
case 'http':
|
|
1407
|
+
if (scheme.scheme === 'bearer') return this.bearerAuth(name);
|
|
1408
|
+
if (scheme.scheme === 'basic') return this.basicAuth(name);
|
|
1409
|
+
break;
|
|
1410
|
+
case 'apiKey':
|
|
1411
|
+
if (scheme.in === 'header') return this.apiKeyHeader(name, scheme.name);
|
|
1412
|
+
if (scheme.in === 'query') return this.apiKeyQuery(name, scheme.name);
|
|
1413
|
+
break;
|
|
1414
|
+
case 'oauth2':
|
|
1415
|
+
return this.oauth2Auth(name);
|
|
1416
|
+
}
|
|
1417
|
+
return { params: [], headers: [], query: [] };
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
static bearerAuth(name) {
|
|
1421
|
+
return {
|
|
1422
|
+
params: [{ name: 'token', type: 'string', description: 'Bearer token', optional: true }],
|
|
1423
|
+
headers: [{ condition: 'token', header: 'Authorization', value: 'Bearer ${token}' }],
|
|
1424
|
+
query: []
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
static basicAuth(name) {
|
|
1429
|
+
return {
|
|
1430
|
+
params: [
|
|
1431
|
+
{ name: 'username', type: 'string', description: 'Basic auth username', optional: true },
|
|
1432
|
+
{ name: 'password', type: 'string', description: 'Basic auth password', optional: true }
|
|
1433
|
+
],
|
|
1434
|
+
headers: [{ condition: 'username && password', header: 'Authorization', value: 'Basic(username, password)' }],
|
|
1435
|
+
query: []
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
static apiKeyHeader(name, headerName) {
|
|
1440
|
+
return {
|
|
1441
|
+
params: [{ name: 'apiKey', type: 'string', description: `${headerName} header`, optional: true }],
|
|
1442
|
+
headers: [{ condition: 'apiKey', header: headerName, value: '${apiKey}' }],
|
|
1443
|
+
query: []
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
static apiKeyQuery(name, paramName) {
|
|
1448
|
+
return {
|
|
1449
|
+
params: [{ name: 'apiKey', type: 'string', description: `${paramName} query param`, optional: true }],
|
|
1450
|
+
headers: [],
|
|
1451
|
+
query: [{ condition: 'apiKey', param: paramName, value: '${apiKey}' }]
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
static oauth2Auth(name) {
|
|
1456
|
+
return {
|
|
1457
|
+
params: [{ name: 'accessToken', type: 'string', description: 'OAuth2 access token', optional: true }],
|
|
1458
|
+
headers: [{ condition: 'accessToken', header: 'Authorization', value: 'Bearer ${accessToken}' }],
|
|
1459
|
+
query: []
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
##### 3. `utils/api-generators/api-client-typescript.js`
|
|
1466
|
+
|
|
1467
|
+
Генератор TypeScript API-клиента (Playwright APIRequestContext).
|
|
1468
|
+
|
|
1469
|
+
```js
|
|
1470
|
+
import { MethodGenerator } from '../openapi/method-generator.js';
|
|
1471
|
+
import { AuthGenerator } from '../openapi/auth-generator.js';
|
|
1472
|
+
|
|
1473
|
+
export class ApiClientTypeScriptGenerator {
|
|
1474
|
+
constructor(endpoints, securitySchemes, options = {}) {
|
|
1475
|
+
this.endpoints = endpoints; // из parser.getEndpoints()
|
|
1476
|
+
this.securitySchemes = securitySchemes;
|
|
1477
|
+
this.options = {
|
|
1478
|
+
includeAuth: true,
|
|
1479
|
+
includeComments: true,
|
|
1480
|
+
groupByTags: true,
|
|
1481
|
+
responseStyle: 'raw', // 'raw' | 'typed' | 'both'
|
|
1482
|
+
modelsImportPath: null, // auto-generated if null
|
|
1483
|
+
...options
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Сгенерировать весь файл API-клиента
|
|
1489
|
+
* @param {Object} metadata - { title, baseUrl, version }
|
|
1490
|
+
* @returns {string} - полный TypeScript код
|
|
1491
|
+
*/
|
|
1492
|
+
generate(metadata = {}) {
|
|
1493
|
+
const lines = [];
|
|
1494
|
+
|
|
1495
|
+
// 1. Imports
|
|
1496
|
+
lines.push(...this.generateImports(metadata));
|
|
1497
|
+
lines.push('');
|
|
1498
|
+
|
|
1499
|
+
// 2. Class declaration
|
|
1500
|
+
const className = this.getClassName(metadata.title);
|
|
1501
|
+
lines.push(`export class ${className} {`);
|
|
1502
|
+
|
|
1503
|
+
// 3. Fields
|
|
1504
|
+
lines.push(' private request: APIRequestContext;');
|
|
1505
|
+
lines.push(' private baseUrl: string;');
|
|
1506
|
+
lines.push(' private headers: Record<string, string>;');
|
|
1507
|
+
if (this.hasQueryAuth()) {
|
|
1508
|
+
lines.push(' private queryAuth: Record<string, string>;');
|
|
1509
|
+
}
|
|
1510
|
+
lines.push('');
|
|
1511
|
+
|
|
1512
|
+
// 4. Constructor
|
|
1513
|
+
lines.push(...this.generateConstructor(metadata));
|
|
1514
|
+
lines.push('');
|
|
1515
|
+
|
|
1516
|
+
// 5. Methods — grouped by tag or flat
|
|
1517
|
+
const methods = this.endpoints.map(ep => MethodGenerator.generateMethod(ep, 'typescript'));
|
|
1518
|
+
if (this.options.groupByTags) {
|
|
1519
|
+
lines.push(...this.generateGroupedMethods(methods));
|
|
1520
|
+
} else {
|
|
1521
|
+
for (const m of methods) {
|
|
1522
|
+
lines.push(...this.generateMethod(m));
|
|
1523
|
+
lines.push('');
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// 6. Close class
|
|
1528
|
+
lines.push('}');
|
|
1529
|
+
|
|
1530
|
+
return lines.join('\n');
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/** Генерировать import блок */
|
|
1534
|
+
generateImports(metadata) {
|
|
1535
|
+
const lines = [
|
|
1536
|
+
"import { APIRequestContext } from '@playwright/test';",
|
|
1537
|
+
];
|
|
1538
|
+
|
|
1539
|
+
// Собрать все используемые типы моделей
|
|
1540
|
+
const usedTypes = this.collectUsedTypes();
|
|
1541
|
+
if (usedTypes.length > 0) {
|
|
1542
|
+
const importPath = this.options.modelsImportPath || this.getModelsImportPath(metadata.title);
|
|
1543
|
+
lines.push(`import { ${usedTypes.join(', ')} } from '${importPath}';`);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return lines;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/** Собрать все типы используемые в параметрах и return types */
|
|
1550
|
+
collectUsedTypes() { ... }
|
|
1551
|
+
|
|
1552
|
+
/** Сгенерировать constructor */
|
|
1553
|
+
generateConstructor(metadata) { ... }
|
|
1554
|
+
|
|
1555
|
+
/** Сгенерировать один метод */
|
|
1556
|
+
generateMethod(methodMeta) {
|
|
1557
|
+
const lines = [];
|
|
1558
|
+
|
|
1559
|
+
// JSDoc comment
|
|
1560
|
+
if (this.options.includeComments) {
|
|
1561
|
+
lines.push(` /** ${methodMeta.comment} */`);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// Method signature
|
|
1565
|
+
const params = this.buildMethodSignature(methodMeta);
|
|
1566
|
+
lines.push(` async ${methodMeta.methodName}(${params}) {`);
|
|
1567
|
+
|
|
1568
|
+
// Query string building (if has query params)
|
|
1569
|
+
if (methodMeta.queryParams.length > 0 || this.hasQueryAuth()) {
|
|
1570
|
+
lines.push(...this.generateQueryBuilding(methodMeta));
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// URL construction
|
|
1574
|
+
const urlExpr = this.buildUrlExpression(methodMeta);
|
|
1575
|
+
|
|
1576
|
+
// HTTP call
|
|
1577
|
+
lines.push(...this.generateHttpCall(methodMeta, urlExpr));
|
|
1578
|
+
|
|
1579
|
+
lines.push(' }');
|
|
1580
|
+
return lines;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/** Сгенерировать методы сгруппированные по тегам */
|
|
1584
|
+
generateGroupedMethods(methods) {
|
|
1585
|
+
const lines = [];
|
|
1586
|
+
const grouped = this.groupByTag(methods);
|
|
1587
|
+
for (const [tag, tagMethods] of Object.entries(grouped)) {
|
|
1588
|
+
lines.push(` // ======== ${tag} ========`);
|
|
1589
|
+
lines.push('');
|
|
1590
|
+
for (const m of tagMethods) {
|
|
1591
|
+
lines.push(...this.generateMethod(m));
|
|
1592
|
+
lines.push('');
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
return lines;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/** Построить сигнатуру метода */
|
|
1599
|
+
buildMethodSignature(methodMeta) { ... }
|
|
1600
|
+
|
|
1601
|
+
/** Построить URL expression с path params */
|
|
1602
|
+
buildUrlExpression(methodMeta) { ... }
|
|
1603
|
+
|
|
1604
|
+
/** Сгенерировать query string building code */
|
|
1605
|
+
generateQueryBuilding(methodMeta) { ... }
|
|
1606
|
+
|
|
1607
|
+
/** Сгенерировать HTTP call */
|
|
1608
|
+
generateHttpCall(methodMeta, urlExpr) { ... }
|
|
1609
|
+
|
|
1610
|
+
/** Имя класса из title: "Pet Store API" → "PetStoreApi" */
|
|
1611
|
+
getClassName(title) { ... }
|
|
1612
|
+
|
|
1613
|
+
/** Import path из title: "Pet Store API" → "./pet-store-api.models" */
|
|
1614
|
+
getModelsImportPath(title) { ... }
|
|
1615
|
+
|
|
1616
|
+
/** Есть ли apiKey auth в query */
|
|
1617
|
+
hasQueryAuth() { ... }
|
|
1618
|
+
|
|
1619
|
+
/** Группировка методов по первому тегу */
|
|
1620
|
+
groupByTag(methods) { ... }
|
|
1621
|
+
}
|
|
1622
|
+
```
|
|
1623
|
+
|
|
1624
|
+
##### 4. `utils/api-generators/api-client-python.js`
|
|
1625
|
+
|
|
1626
|
+
Генератор Python API-клиента (requests.Session).
|
|
1627
|
+
|
|
1628
|
+
```js
|
|
1629
|
+
import { MethodGenerator } from '../openapi/method-generator.js';
|
|
1630
|
+
import { AuthGenerator } from '../openapi/auth-generator.js';
|
|
1631
|
+
|
|
1632
|
+
export class ApiClientPythonGenerator {
|
|
1633
|
+
constructor(endpoints, securitySchemes, options = {}) {
|
|
1634
|
+
this.endpoints = endpoints;
|
|
1635
|
+
this.securitySchemes = securitySchemes;
|
|
1636
|
+
this.options = {
|
|
1637
|
+
includeAuth: true,
|
|
1638
|
+
includeComments: true,
|
|
1639
|
+
groupByTags: true,
|
|
1640
|
+
responseStyle: 'raw',
|
|
1641
|
+
modelsImportPath: null,
|
|
1642
|
+
pythonStyle: 'dataclass', // для определения метода сериализации body
|
|
1643
|
+
...options
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Сгенерировать весь файл API-клиента
|
|
1649
|
+
*/
|
|
1650
|
+
generate(metadata = {}) {
|
|
1651
|
+
const lines = [];
|
|
1652
|
+
|
|
1653
|
+
// 1. Imports
|
|
1654
|
+
lines.push(...this.generateImports(metadata));
|
|
1655
|
+
lines.push('');
|
|
1656
|
+
lines.push('');
|
|
1657
|
+
|
|
1658
|
+
// 2. Class
|
|
1659
|
+
const className = this.getClassName(metadata.title);
|
|
1660
|
+
lines.push(`class ${className}:`);
|
|
1661
|
+
|
|
1662
|
+
// 3. Constructor (__init__)
|
|
1663
|
+
lines.push(...this.generateInit(metadata));
|
|
1664
|
+
lines.push('');
|
|
1665
|
+
|
|
1666
|
+
// 4. Methods
|
|
1667
|
+
const methods = this.endpoints.map(ep => MethodGenerator.generateMethod(ep, 'python'));
|
|
1668
|
+
if (this.options.groupByTags) {
|
|
1669
|
+
lines.push(...this.generateGroupedMethods(methods));
|
|
1670
|
+
} else {
|
|
1671
|
+
for (const m of methods) {
|
|
1672
|
+
lines.push(...this.generateMethod(m));
|
|
1673
|
+
lines.push('');
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
return lines.join('\n');
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/** Imports */
|
|
1681
|
+
generateImports(metadata) {
|
|
1682
|
+
const lines = [
|
|
1683
|
+
'import requests',
|
|
1684
|
+
'from typing import Optional, List, Dict, Any',
|
|
1685
|
+
];
|
|
1686
|
+
|
|
1687
|
+
const usedTypes = this.collectUsedTypes();
|
|
1688
|
+
if (usedTypes.length > 0) {
|
|
1689
|
+
const importPath = this.options.modelsImportPath || this.getModelsImportPath(metadata.title);
|
|
1690
|
+
lines.push(`from ${importPath} import ${usedTypes.join(', ')}`);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
return lines;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/** __init__ с auth */
|
|
1697
|
+
generateInit(metadata) {
|
|
1698
|
+
// Аналогично TypeScript, но с Python синтаксисом:
|
|
1699
|
+
// def __init__(self, base_url: str = '...', token: Optional[str] = None, ...)
|
|
1700
|
+
// self.session = requests.Session()
|
|
1701
|
+
// self.base_url = base_url
|
|
1702
|
+
// if token: self.session.headers['Authorization'] = f'Bearer {token}'
|
|
1703
|
+
...
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/** Один метод */
|
|
1707
|
+
generateMethod(methodMeta) {
|
|
1708
|
+
const lines = [];
|
|
1709
|
+
|
|
1710
|
+
// Method signature
|
|
1711
|
+
const params = this.buildMethodSignature(methodMeta);
|
|
1712
|
+
lines.push(` def ${methodMeta.methodName}(self, ${params}):`);
|
|
1713
|
+
|
|
1714
|
+
// Docstring
|
|
1715
|
+
if (this.options.includeComments) {
|
|
1716
|
+
lines.push(` """${methodMeta.comment}"""`);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// Query params building
|
|
1720
|
+
if (methodMeta.queryParams.length > 0 || this.hasQueryAuth()) {
|
|
1721
|
+
lines.push(...this.generateQueryBuilding(methodMeta));
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// URL
|
|
1725
|
+
const urlExpr = this.buildUrlExpression(methodMeta);
|
|
1726
|
+
|
|
1727
|
+
// HTTP call
|
|
1728
|
+
lines.push(...this.generateHttpCall(methodMeta, urlExpr));
|
|
1729
|
+
|
|
1730
|
+
return lines;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/** Сериализация body в зависимости от pythonStyle */
|
|
1734
|
+
serializeBody(varName) {
|
|
1735
|
+
switch (this.options.pythonStyle) {
|
|
1736
|
+
case 'dataclass': return `from dataclasses import asdict; json=asdict(${varName})`;
|
|
1737
|
+
case 'pydantic': return `json=${varName}.model_dump()`;
|
|
1738
|
+
case 'typeddict': return `json=${varName}`;
|
|
1739
|
+
default: return `json=${varName}`;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
buildMethodSignature(methodMeta) { ... }
|
|
1744
|
+
buildUrlExpression(methodMeta) { ... }
|
|
1745
|
+
generateQueryBuilding(methodMeta) { ... }
|
|
1746
|
+
generateHttpCall(methodMeta, urlExpr) { ... }
|
|
1747
|
+
getClassName(title) { ... }
|
|
1748
|
+
getModelsImportPath(title) { ... }
|
|
1749
|
+
hasQueryAuth() { ... }
|
|
1750
|
+
generateGroupedMethods(methods) { ... }
|
|
1751
|
+
collectUsedTypes() { ... }
|
|
1752
|
+
}
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
#### Изменяемые файлы
|
|
1756
|
+
|
|
1757
|
+
##### 5. `server/tool-schemas.js`
|
|
1758
|
+
|
|
1759
|
+
Добавить:
|
|
1760
|
+
```js
|
|
1761
|
+
export const GenerateApiClientSchema = z.object({
|
|
1762
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
1763
|
+
language: z.enum(['typescript', 'python']).describe("Target language"),
|
|
1764
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
1765
|
+
.describe("Spec format (default: auto)"),
|
|
1766
|
+
modelsImportPath: z.string().optional()
|
|
1767
|
+
.describe("Import path for models file (default: auto from spec title)"),
|
|
1768
|
+
includeAuth: z.boolean().optional()
|
|
1769
|
+
.describe("Generate auth config in constructor (default: true)"),
|
|
1770
|
+
includeComments: z.boolean().optional()
|
|
1771
|
+
.describe("Include JSDoc/docstring comments (default: true)"),
|
|
1772
|
+
tags: z.array(z.string()).optional()
|
|
1773
|
+
.describe("Generate only for these tags (default: all)"),
|
|
1774
|
+
groupByTags: z.boolean().optional()
|
|
1775
|
+
.describe("Group methods by tag with section comments (default: true)"),
|
|
1776
|
+
responseStyle: z.enum(['raw', 'typed', 'both']).optional()
|
|
1777
|
+
.describe("Response type style (default: 'raw')"),
|
|
1778
|
+
pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
|
|
1779
|
+
.describe("Python only: model style for body serialization (default: 'dataclass')"),
|
|
1780
|
+
});
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
##### 6. `server/tool-definitions.js`
|
|
1784
|
+
|
|
1785
|
+
Добавить в массив tools:
|
|
1786
|
+
```js
|
|
1787
|
+
{
|
|
1788
|
+
name: "generateApiClient",
|
|
1789
|
+
description: "Generate typed API client class from OpenAPI/Swagger spec. Creates a class with methods for each endpoint, typed parameters, and auth configuration. Use after loadSwagger to understand the API. Requires models file from generateApiModels.",
|
|
1790
|
+
inputSchema: GenerateApiClientSchema
|
|
1791
|
+
}
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
##### 7. `index.js`
|
|
1795
|
+
|
|
1796
|
+
Добавить хендлер:
|
|
1797
|
+
```js
|
|
1798
|
+
if (name === "generateApiClient") {
|
|
1799
|
+
const parser = await OpenAPIParser.load(args.source, args.format);
|
|
1800
|
+
const endpoints = parser.getEndpoints();
|
|
1801
|
+
const securitySchemes = parser.getSecuritySchemes();
|
|
1802
|
+
const metadata = {
|
|
1803
|
+
title: parser.spec.info?.title || '',
|
|
1804
|
+
baseUrl: parser.getBaseUrl(),
|
|
1805
|
+
version: parser.version
|
|
1806
|
+
};
|
|
1807
|
+
|
|
1808
|
+
// Фильтрация по тегам
|
|
1809
|
+
let filteredEndpoints = endpoints;
|
|
1810
|
+
if (args.tags) {
|
|
1811
|
+
filteredEndpoints = endpoints.filter(ep =>
|
|
1812
|
+
ep.tags.some(tag => args.tags.includes(tag))
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Выбрать генератор
|
|
1817
|
+
let generator;
|
|
1818
|
+
if (args.language === 'typescript') {
|
|
1819
|
+
generator = new ApiClientTypeScriptGenerator(filteredEndpoints, securitySchemes, {
|
|
1820
|
+
includeAuth: args.includeAuth ?? true,
|
|
1821
|
+
includeComments: args.includeComments ?? true,
|
|
1822
|
+
groupByTags: args.groupByTags ?? true,
|
|
1823
|
+
responseStyle: args.responseStyle || 'raw',
|
|
1824
|
+
modelsImportPath: args.modelsImportPath,
|
|
1825
|
+
});
|
|
1826
|
+
} else {
|
|
1827
|
+
generator = new ApiClientPythonGenerator(filteredEndpoints, securitySchemes, {
|
|
1828
|
+
includeAuth: args.includeAuth ?? true,
|
|
1829
|
+
includeComments: args.includeComments ?? true,
|
|
1830
|
+
groupByTags: args.groupByTags ?? true,
|
|
1831
|
+
responseStyle: args.responseStyle || 'raw',
|
|
1832
|
+
modelsImportPath: args.modelsImportPath,
|
|
1833
|
+
pythonStyle: args.pythonStyle || 'dataclass',
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const code = generator.generate(metadata);
|
|
1838
|
+
const className = generator.getClassName(metadata.title);
|
|
1839
|
+
const suggestedFileName = args.language === 'typescript'
|
|
1840
|
+
? `${className}.ts`
|
|
1841
|
+
: `${generator.getModelsImportPath(metadata.title).replace(/\./g, '_')}_client.py`;
|
|
1842
|
+
|
|
1843
|
+
return {
|
|
1844
|
+
content: [{
|
|
1845
|
+
type: 'text',
|
|
1846
|
+
text: JSON.stringify({
|
|
1847
|
+
action: 'create_new_file',
|
|
1848
|
+
suggestedFileName,
|
|
1849
|
+
modelsFileName: args.language === 'typescript'
|
|
1850
|
+
? `${generator.getModelsImportPath(metadata.title).replace('./', '')}.ts`
|
|
1851
|
+
: `${generator.getModelsImportPath(metadata.title)}.py`,
|
|
1852
|
+
code,
|
|
1853
|
+
methodCount: filteredEndpoints.length,
|
|
1854
|
+
tagGroups: [...new Set(filteredEndpoints.flatMap(ep => ep.tags))],
|
|
1855
|
+
authTypes: Object.values(securitySchemes || {}).map(s => s.type),
|
|
1856
|
+
language: args.language,
|
|
1857
|
+
source: args.source,
|
|
1858
|
+
instruction: `Create file '${suggestedFileName}'. Make sure models file exists (use generateApiModels if not). Set auth credentials via constructor options.`
|
|
1859
|
+
}, null, 2)
|
|
1860
|
+
}]
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
##### 8. `README.md`
|
|
1866
|
+
|
|
1867
|
+
Добавить в секцию API Tools:
|
|
1868
|
+
```markdown
|
|
1869
|
+
### generateApiClient
|
|
1870
|
+
|
|
1871
|
+
Generate typed API client class from OpenAPI/Swagger spec.
|
|
1872
|
+
|
|
1873
|
+
| Parameter | Type | Default | Description |
|
|
1874
|
+
|-----------|------|---------|-------------|
|
|
1875
|
+
| `source` | string | required | URL or file path to OpenAPI spec |
|
|
1876
|
+
| `language` | 'typescript' \| 'python' | required | Target language |
|
|
1877
|
+
| `format` | 'auto' \| 'json' \| 'yaml' | 'auto' | Spec format |
|
|
1878
|
+
| `modelsImportPath` | string | auto | Import path for models file |
|
|
1879
|
+
| `includeAuth` | boolean | true | Generate auth in constructor |
|
|
1880
|
+
| `includeComments` | boolean | true | Include method comments |
|
|
1881
|
+
| `tags` | string[] | all | Filter by endpoint tags |
|
|
1882
|
+
| `groupByTags` | boolean | true | Group methods by tag |
|
|
1883
|
+
| `responseStyle` | 'raw' \| 'typed' \| 'both' | 'raw' | Response type style |
|
|
1884
|
+
| `pythonStyle` | string | 'dataclass' | Python body serialization style |
|
|
1885
|
+
|
|
1886
|
+
**Example — TypeScript API client:**
|
|
1887
|
+
```json
|
|
1888
|
+
{
|
|
1889
|
+
"source": "https://petstore.swagger.io/v2/swagger.json",
|
|
1890
|
+
"language": "typescript",
|
|
1891
|
+
"includeAuth": true
|
|
1892
|
+
}
|
|
1893
|
+
```
|
|
1894
|
+
→ Returns `PetStoreApi.ts` with methods for all endpoints.
|
|
1895
|
+
|
|
1896
|
+
**Example — Python client, filtered by tags:**
|
|
1897
|
+
```json
|
|
1898
|
+
{
|
|
1899
|
+
"source": "./openapi.yaml",
|
|
1900
|
+
"language": "python",
|
|
1901
|
+
"tags": ["pets"],
|
|
1902
|
+
"pythonStyle": "pydantic"
|
|
1903
|
+
}
|
|
1904
|
+
```
|
|
1905
|
+
→ Returns Python client with only pet-related methods, pydantic body serialization.
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
---
|
|
1909
|
+
|
|
1910
|
+
### Именование файлов (Фаза 2)
|
|
1911
|
+
|
|
1912
|
+
| Спека | TypeScript client | Python client |
|
|
1913
|
+
|---|---|---|
|
|
1914
|
+
| Pet Store API | `PetStoreApi.ts` | `pet_store_api_client.py` |
|
|
1915
|
+
| My Service | `MyServiceApi.ts` | `my_service_api_client.py` |
|
|
1916
|
+
|
|
1917
|
+
---
|
|
1918
|
+
|
|
1919
|
+
### Структура файлов проекта (после Фазы 2)
|
|
1920
|
+
|
|
1921
|
+
```
|
|
1922
|
+
utils/
|
|
1923
|
+
openapi/
|
|
1924
|
+
parser.js # OpenAPIParser (Фаза 1)
|
|
1925
|
+
ref-resolver.js # $ref resolution (Фаза 1)
|
|
1926
|
+
type-mapper.js # type mapping (Фаза 1)
|
|
1927
|
+
method-generator.js # endpoint → method metadata (Фаза 2)
|
|
1928
|
+
auth-generator.js # auth config generation (Фаза 2)
|
|
1929
|
+
api-generators/
|
|
1930
|
+
api-models-typescript.js # TS models (Фаза 1)
|
|
1931
|
+
api-models-python.js # Python models (Фаза 1)
|
|
1932
|
+
api-client-typescript.js # TS API client (Фаза 2)
|
|
1933
|
+
api-client-python.js # Python API client (Фаза 2)
|
|
1934
|
+
```
|
|
1935
|
+
|
|
1936
|
+
---
|
|
1937
|
+
|
|
1938
|
+
### Верификация (Фаза 2)
|
|
1939
|
+
|
|
1940
|
+
#### Функциональные тесты
|
|
1941
|
+
|
|
1942
|
+
1. **Petstore 2.0**: загрузить `https://petstore.swagger.io/v2/swagger.json`, сгенерировать TS клиент
|
|
1943
|
+
- Проверить: все endpoints из спеки стали методами
|
|
1944
|
+
- Проверить: path params (`{petId}`) → параметры функции
|
|
1945
|
+
- Проверить: query params (`limit`, `status`) → опциональный params
|
|
1946
|
+
- Проверить: request body (`CreatePetRequest`) → body параметр
|
|
1947
|
+
- Проверить: импорт моделей из models файла
|
|
1948
|
+
|
|
1949
|
+
2. **Petstore 3.0**: загрузить `https://petstore3.swagger.io/api/v3/openapi.json`, сгенерировать Python клиент
|
|
1950
|
+
- Проверить: все endpoints
|
|
1951
|
+
- Проверить: groupByTags разделяет методы по секциям
|
|
1952
|
+
|
|
1953
|
+
3. **Auth types**: использовать спеку с разными auth schemes
|
|
1954
|
+
- Bearer → `token` в конструкторе, `Authorization: Bearer ...`
|
|
1955
|
+
- API Key (header) → `apiKey` в конструкторе, кастомный header
|
|
1956
|
+
- API Key (query) → `apiKey` → добавляется во все запросы
|
|
1957
|
+
- Basic → `username + password` → `Authorization: Basic ...`
|
|
1958
|
+
- OAuth2 → `accessToken` → `Authorization: Bearer ...`
|
|
1959
|
+
- Комбинированные → все параметры в одном конструкторе
|
|
1960
|
+
|
|
1961
|
+
4. **Tag filtering**: `tags: ['pets']` → только методы для тега 'pets'
|
|
1962
|
+
|
|
1963
|
+
5. **operationId fallback**: спека без operationId → имена генерируются из method+path
|
|
1964
|
+
|
|
1965
|
+
6. **Python styles**: `pythonStyle: 'pydantic'` → body сериализуется через `.model_dump()`
|
|
1966
|
+
|
|
1967
|
+
7. **Custom import path**: `modelsImportPath: './models/api-types'` → используется в import
|
|
1968
|
+
|
|
1969
|
+
#### Boundary тесты
|
|
1970
|
+
|
|
1971
|
+
8. **Пустая спека** (нет endpoints): клиент с пустым классом, не падает
|
|
1972
|
+
9. **Endpoint без параметров**: `GET /health` → метод без параметров
|
|
1973
|
+
10. **Endpoint только с path params**: `GET /pets/{id}` → метод с одним обязательным param
|
|
1974
|
+
11. **Deprecated endpoint**: комментарий `[DEPRECATED]`
|
|
1975
|
+
12. **Конфликт имён методов**: два endpoint'а с одинаковым operationId → добавить суффикс
|
|
1976
|
+
|
|
1977
|
+
---
|
|
1978
|
+
|
|
1979
|
+
## Фаза 3: generateApiTests
|
|
1980
|
+
|
|
1981
|
+
### Тул 4: `generateApiTests`
|
|
1982
|
+
|
|
1983
|
+
**Назначение**: сгенерировать тестовые скаффолды для API, использующие API-клиент (или raw HTTP calls). Тесты покрывают: happy path, validation errors, auth, status codes.
|
|
1984
|
+
|
|
1985
|
+
**Параметры**:
|
|
1986
|
+
```js
|
|
1987
|
+
GenerateApiTestsSchema = z.object({
|
|
1988
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
1989
|
+
language: z.enum(['typescript', 'python']).describe("Target language/framework"),
|
|
1990
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
1991
|
+
.describe("Spec format (default: auto)"),
|
|
1992
|
+
useApiClient: z.boolean().optional()
|
|
1993
|
+
.describe("Use generated API client class (default: true). If false, generates raw HTTP calls"),
|
|
1994
|
+
clientImportPath: z.string().optional()
|
|
1995
|
+
.describe("Import path for API client (default: auto from spec title)"),
|
|
1996
|
+
modelsImportPath: z.string().optional()
|
|
1997
|
+
.describe("Import path for models (default: auto). Only used when useApiClient=false"),
|
|
1998
|
+
tags: z.array(z.string()).optional()
|
|
1999
|
+
.describe("Generate tests only for these tags (default: all)"),
|
|
2000
|
+
testStyle: z.enum(['crud', 'per-endpoint', 'smoke']).optional()
|
|
2001
|
+
.describe("Test generation style (default: 'per-endpoint'). 'crud' groups related CRUD endpoints, 'per-endpoint' one test per endpoint, 'smoke' minimal happy-path only"),
|
|
2002
|
+
includeNegative: z.boolean().optional()
|
|
2003
|
+
.describe("Generate negative test cases: 401, 403, 404, 422 (default: true)"),
|
|
2004
|
+
includeAuth: z.boolean().optional()
|
|
2005
|
+
.describe("Generate auth setup in beforeAll/fixtures (default: true)"),
|
|
2006
|
+
authFromEnv: z.boolean().optional()
|
|
2007
|
+
.describe("Read auth from environment variables (default: true)"),
|
|
2008
|
+
envPrefix: z.string().optional()
|
|
2009
|
+
.describe("Prefix for env vars. E.g. 'PETSTORE' → PETSTORE_TOKEN (default: from spec title)"),
|
|
2010
|
+
})
|
|
2011
|
+
```
|
|
2012
|
+
|
|
2013
|
+
**Алгоритм**:
|
|
2014
|
+
1. Загрузить и распарсить спеку
|
|
2015
|
+
2. Извлечь endpoints, отфильтровать по tags
|
|
2016
|
+
3. Определить стиль тестов:
|
|
2017
|
+
- `per-endpoint`: один `test()`/`def test_()` на каждый endpoint
|
|
2018
|
+
- `crud`: определить CRUD-группы (resource → GET list, GET by id, POST, PUT, DELETE), сгенерировать последовательные тесты
|
|
2019
|
+
- `smoke`: только happy path для каждого endpoint (минимальные assertions)
|
|
2020
|
+
4. Для каждого endpoint/группы:
|
|
2021
|
+
a. Сгенерировать happy-path тест (200/201)
|
|
2022
|
+
b. Если `includeNegative`:
|
|
2023
|
+
- 401 Unauthorized (если есть security)
|
|
2024
|
+
- 404 Not Found (для endpoints с path params)
|
|
2025
|
+
- 422 Validation Error (для POST/PUT с request body)
|
|
2026
|
+
5. Сгенерировать setup (beforeAll / fixture): auth configuration
|
|
2027
|
+
6. Сгенерировать файл
|
|
2028
|
+
|
|
2029
|
+
**Возвращаемое значение**:
|
|
2030
|
+
```json
|
|
2031
|
+
{
|
|
2032
|
+
"action": "create_new_file",
|
|
2033
|
+
"suggestedFileName": "pet-store-api.spec.ts",
|
|
2034
|
+
"code": "// ...generated test code...",
|
|
2035
|
+
"testCount": 24,
|
|
2036
|
+
"endpointsCovered": 8,
|
|
2037
|
+
"testBreakdown": {
|
|
2038
|
+
"happyPath": 8,
|
|
2039
|
+
"unauthorized": 6,
|
|
2040
|
+
"notFound": 4,
|
|
2041
|
+
"validationError": 3,
|
|
2042
|
+
"crud": 3
|
|
2043
|
+
},
|
|
2044
|
+
"language": "typescript",
|
|
2045
|
+
"source": "https://petstore.swagger.io/v2/swagger.json",
|
|
2046
|
+
"instruction": "Create file 'pet-store-api.spec.ts'. Make sure API client and models files exist. Set environment variables for auth: PETSTORE_TOKEN, PETSTORE_API_KEY."
|
|
2047
|
+
}
|
|
2048
|
+
```
|
|
2049
|
+
|
|
2050
|
+
---
|
|
2051
|
+
|
|
2052
|
+
### CRUD Detection Algorithm
|
|
2053
|
+
|
|
2054
|
+
#### Определение CRUD-групп
|
|
2055
|
+
|
|
2056
|
+
CRUD-группа — это набор endpoints, работающих с одним ресурсом (одинаковый base path):
|
|
2057
|
+
|
|
2058
|
+
```
|
|
2059
|
+
Ресурс: /pets
|
|
2060
|
+
- GET /pets → list (Read many)
|
|
2061
|
+
- POST /pets → create (Create)
|
|
2062
|
+
- GET /pets/{petId} → getById (Read one)
|
|
2063
|
+
- PUT /pets/{petId} → update (Update)
|
|
2064
|
+
- DELETE /pets/{petId} → delete (Delete)
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
**Алгоритм**:
|
|
2068
|
+
```
|
|
2069
|
+
1. Для каждого endpoint извлечь base resource:
|
|
2070
|
+
/pets → 'pets'
|
|
2071
|
+
/pets/{petId} → 'pets'
|
|
2072
|
+
/pets/{petId}/tags → 'pets_tags' (sub-resource)
|
|
2073
|
+
/users/{id}/orders → 'users_orders'
|
|
2074
|
+
|
|
2075
|
+
2. Группировать endpoints по base resource
|
|
2076
|
+
|
|
2077
|
+
3. Для каждой группы определить CRUD-операции:
|
|
2078
|
+
- Есть GET без path params → list
|
|
2079
|
+
- Есть POST без path params → create
|
|
2080
|
+
- Есть GET с path params → getById
|
|
2081
|
+
- Есть PUT/PATCH с path params → update
|
|
2082
|
+
- Есть DELETE с path params → delete
|
|
2083
|
+
|
|
2084
|
+
4. Если группа содержит ≥2 CRUD операции → CRUD-группа
|
|
2085
|
+
Иначе → per-endpoint тесты
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
#### CRUD test sequence
|
|
2089
|
+
|
|
2090
|
+
Для CRUD-группы тесты идут в логическом порядке:
|
|
2091
|
+
|
|
2092
|
+
```typescript
|
|
2093
|
+
test.describe('Pets CRUD', () => {
|
|
2094
|
+
let createdId: number;
|
|
2095
|
+
|
|
2096
|
+
test('POST /pets - create pet', async () => {
|
|
2097
|
+
const response = await api.createPet({ name: 'Buddy', status: 'available' });
|
|
2098
|
+
expect(response.status()).toBe(201);
|
|
2099
|
+
const body = await response.json();
|
|
2100
|
+
createdId = body.id;
|
|
2101
|
+
expect(body.name).toBe('Buddy');
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
test('GET /pets/{id} - get created pet', async () => {
|
|
2105
|
+
const response = await api.getPet(createdId);
|
|
2106
|
+
expect(response.status()).toBe(200);
|
|
2107
|
+
const body = await response.json();
|
|
2108
|
+
expect(body.id).toBe(createdId);
|
|
2109
|
+
expect(body.name).toBe('Buddy');
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
test('PUT /pets/{id} - update pet', async () => {
|
|
2113
|
+
const response = await api.updatePet(createdId, { name: 'Max', status: 'sold' });
|
|
2114
|
+
expect(response.status()).toBe(200);
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
test('GET /pets - list pets contains updated', async () => {
|
|
2118
|
+
const response = await api.listPets();
|
|
2119
|
+
expect(response.status()).toBe(200);
|
|
2120
|
+
const body = await response.json();
|
|
2121
|
+
expect(body.some(p => p.id === createdId)).toBe(true);
|
|
2122
|
+
});
|
|
2123
|
+
|
|
2124
|
+
test('DELETE /pets/{id} - delete pet', async () => {
|
|
2125
|
+
const response = await api.deletePet(createdId);
|
|
2126
|
+
expect(response.ok()).toBe(true);
|
|
2127
|
+
});
|
|
2128
|
+
|
|
2129
|
+
test('GET /pets/{id} - deleted pet returns 404', async () => {
|
|
2130
|
+
const response = await api.getPet(createdId);
|
|
2131
|
+
expect(response.status()).toBe(404);
|
|
2132
|
+
});
|
|
2133
|
+
});
|
|
2134
|
+
```
|
|
2135
|
+
|
|
2136
|
+
```python
|
|
2137
|
+
class TestPetsCrud:
|
|
2138
|
+
created_id = None
|
|
2139
|
+
|
|
2140
|
+
def test_create_pet(self, api):
|
|
2141
|
+
response = api.create_pet(CreatePetRequest(name='Buddy', status=PetStatus.AVAILABLE))
|
|
2142
|
+
assert response.status_code == 201
|
|
2143
|
+
body = response.json()
|
|
2144
|
+
TestPetsCrud.created_id = body['id']
|
|
2145
|
+
assert body['name'] == 'Buddy'
|
|
2146
|
+
|
|
2147
|
+
def test_get_created_pet(self, api):
|
|
2148
|
+
response = api.get_pet(TestPetsCrud.created_id)
|
|
2149
|
+
assert response.status_code == 200
|
|
2150
|
+
body = response.json()
|
|
2151
|
+
assert body['id'] == TestPetsCrud.created_id
|
|
2152
|
+
|
|
2153
|
+
def test_update_pet(self, api):
|
|
2154
|
+
response = api.update_pet(TestPetsCrud.created_id,
|
|
2155
|
+
UpdatePetRequest(name='Max', status=PetStatus.SOLD))
|
|
2156
|
+
assert response.status_code == 200
|
|
2157
|
+
|
|
2158
|
+
def test_list_pets_contains_updated(self, api):
|
|
2159
|
+
response = api.list_pets()
|
|
2160
|
+
assert response.status_code == 200
|
|
2161
|
+
ids = [p['id'] for p in response.json()]
|
|
2162
|
+
assert TestPetsCrud.created_id in ids
|
|
2163
|
+
|
|
2164
|
+
def test_delete_pet(self, api):
|
|
2165
|
+
response = api.delete_pet(TestPetsCrud.created_id)
|
|
2166
|
+
assert response.ok
|
|
2167
|
+
|
|
2168
|
+
def test_deleted_pet_returns_404(self, api):
|
|
2169
|
+
response = api.get_pet(TestPetsCrud.created_id)
|
|
2170
|
+
assert response.status_code == 404
|
|
2171
|
+
```
|
|
2172
|
+
|
|
2173
|
+
---
|
|
2174
|
+
|
|
2175
|
+
### Negative Test Generation
|
|
2176
|
+
|
|
2177
|
+
#### 401 Unauthorized
|
|
2178
|
+
|
|
2179
|
+
Генерируется для endpoints с security requirements:
|
|
2180
|
+
```typescript
|
|
2181
|
+
test('GET /pets - 401 without auth', async ({ request }) => {
|
|
2182
|
+
const noAuthApi = new PetStoreApi(request, { baseUrl: BASE_URL });
|
|
2183
|
+
const response = await noAuthApi.listPets();
|
|
2184
|
+
expect(response.status()).toBe(401);
|
|
2185
|
+
});
|
|
2186
|
+
```
|
|
2187
|
+
|
|
2188
|
+
```python
|
|
2189
|
+
def test_list_pets_unauthorized(self):
|
|
2190
|
+
no_auth_api = PetStoreApi(base_url=BASE_URL)
|
|
2191
|
+
response = no_auth_api.list_pets()
|
|
2192
|
+
assert response.status_code == 401
|
|
2193
|
+
```
|
|
2194
|
+
|
|
2195
|
+
#### 404 Not Found
|
|
2196
|
+
|
|
2197
|
+
Генерируется для endpoints с path params:
|
|
2198
|
+
```typescript
|
|
2199
|
+
test('GET /pets/{id} - 404 non-existent', async () => {
|
|
2200
|
+
const response = await api.getPet(999999);
|
|
2201
|
+
expect(response.status()).toBe(404);
|
|
2202
|
+
});
|
|
2203
|
+
```
|
|
2204
|
+
|
|
2205
|
+
```python
|
|
2206
|
+
def test_get_pet_not_found(self, api):
|
|
2207
|
+
response = api.get_pet(999999)
|
|
2208
|
+
assert response.status_code == 404
|
|
2209
|
+
```
|
|
2210
|
+
|
|
2211
|
+
#### 422 Validation Error
|
|
2212
|
+
|
|
2213
|
+
Генерируется для POST/PUT endpoints с required body fields:
|
|
2214
|
+
```typescript
|
|
2215
|
+
test('POST /pets - 422 missing required fields', async () => {
|
|
2216
|
+
const response = await api.createPet({} as any);
|
|
2217
|
+
expect([400, 422]).toContain(response.status());
|
|
2218
|
+
});
|
|
2219
|
+
```
|
|
2220
|
+
|
|
2221
|
+
```python
|
|
2222
|
+
def test_create_pet_validation_error(self, api):
|
|
2223
|
+
response = api.create_pet(CreatePetRequest()) # пустой объект
|
|
2224
|
+
assert response.status_code in (400, 422)
|
|
2225
|
+
```
|
|
2226
|
+
|
|
2227
|
+
---
|
|
2228
|
+
|
|
2229
|
+
### Auth Setup Generation
|
|
2230
|
+
|
|
2231
|
+
#### TypeScript (Playwright)
|
|
2232
|
+
|
|
2233
|
+
```typescript
|
|
2234
|
+
import { test, expect } from '@playwright/test';
|
|
2235
|
+
import { PetStoreApi } from './PetStoreApi';
|
|
2236
|
+
|
|
2237
|
+
const BASE_URL = process.env.PETSTORE_BASE_URL || 'https://petstore.swagger.io/v2';
|
|
2238
|
+
|
|
2239
|
+
test.describe('Pet Store API', () => {
|
|
2240
|
+
let api: PetStoreApi;
|
|
2241
|
+
|
|
2242
|
+
test.beforeAll(async ({ request }) => {
|
|
2243
|
+
api = new PetStoreApi(request, {
|
|
2244
|
+
baseUrl: BASE_URL,
|
|
2245
|
+
token: process.env.PETSTORE_TOKEN,
|
|
2246
|
+
apiKey: process.env.PETSTORE_API_KEY,
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
// ... tests ...
|
|
2251
|
+
});
|
|
2252
|
+
```
|
|
2253
|
+
|
|
2254
|
+
#### Python (pytest)
|
|
2255
|
+
|
|
2256
|
+
```python
|
|
2257
|
+
import pytest
|
|
2258
|
+
import os
|
|
2259
|
+
from pet_store_api_client import PetStoreApi
|
|
2260
|
+
|
|
2261
|
+
BASE_URL = os.environ.get('PETSTORE_BASE_URL', 'https://petstore.swagger.io/v2')
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
@pytest.fixture(scope='session')
|
|
2265
|
+
def api():
|
|
2266
|
+
return PetStoreApi(
|
|
2267
|
+
base_url=BASE_URL,
|
|
2268
|
+
token=os.environ.get('PETSTORE_TOKEN'),
|
|
2269
|
+
api_key=os.environ.get('PETSTORE_API_KEY'),
|
|
2270
|
+
)
|
|
2271
|
+
|
|
2272
|
+
|
|
2273
|
+
class TestPetsApi:
|
|
2274
|
+
# ... tests using 'api' fixture ...
|
|
2275
|
+
```
|
|
2276
|
+
|
|
2277
|
+
#### Environment variable naming
|
|
2278
|
+
|
|
2279
|
+
```
|
|
2280
|
+
envPrefix (из spec title или параметра) + '_' + AUTH_PARAM
|
|
2281
|
+
|
|
2282
|
+
Pet Store API (envPrefix='PETSTORE'):
|
|
2283
|
+
bearer → PETSTORE_TOKEN
|
|
2284
|
+
apiKey → PETSTORE_API_KEY
|
|
2285
|
+
basic → PETSTORE_USERNAME, PETSTORE_PASSWORD
|
|
2286
|
+
oauth2 → PETSTORE_ACCESS_TOKEN
|
|
2287
|
+
baseUrl → PETSTORE_BASE_URL
|
|
2288
|
+
```
|
|
2289
|
+
|
|
2290
|
+
---
|
|
2291
|
+
|
|
2292
|
+
### Интеграция с API-клиентом (аналог POM)
|
|
2293
|
+
|
|
2294
|
+
#### `useApiClient: true` (default)
|
|
2295
|
+
|
|
2296
|
+
Тесты используют API-клиент:
|
|
2297
|
+
```typescript
|
|
2298
|
+
const response = await api.listPets({ limit: 10 });
|
|
2299
|
+
expect(response.status()).toBe(200);
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
#### `useApiClient: false`
|
|
2303
|
+
|
|
2304
|
+
Тесты используют raw HTTP calls (Playwright `request` / Python `requests`):
|
|
2305
|
+
```typescript
|
|
2306
|
+
const response = await request.get(`${BASE_URL}/pets?limit=10`, {
|
|
2307
|
+
headers: { 'Authorization': `Bearer ${process.env.PETSTORE_TOKEN}` }
|
|
2308
|
+
});
|
|
2309
|
+
expect(response.status()).toBe(200);
|
|
2310
|
+
```
|
|
2311
|
+
|
|
2312
|
+
```python
|
|
2313
|
+
response = requests.get(f'{BASE_URL}/pets', params={'limit': 10},
|
|
2314
|
+
headers={'Authorization': f'Bearer {os.environ["PETSTORE_TOKEN"]}'})
|
|
2315
|
+
assert response.status_code == 200
|
|
2316
|
+
```
|
|
2317
|
+
|
|
2318
|
+
Переключение контролируется параметром `useApiClient`.
|
|
2319
|
+
|
|
2320
|
+
---
|
|
2321
|
+
|
|
2322
|
+
### Изменения по файлам (Фаза 3)
|
|
2323
|
+
|
|
2324
|
+
#### Новые файлы
|
|
2325
|
+
|
|
2326
|
+
##### 1. `utils/openapi/test-scaffold-generator.js`
|
|
2327
|
+
|
|
2328
|
+
Генератор тестовых скаффолдов — определяет какие тесты генерировать.
|
|
2329
|
+
|
|
2330
|
+
```js
|
|
2331
|
+
/**
|
|
2332
|
+
* Генератор тестовых скаффолдов из OpenAPI endpoints
|
|
2333
|
+
*/
|
|
2334
|
+
export class TestScaffoldGenerator {
|
|
2335
|
+
/**
|
|
2336
|
+
* Сгенерировать план тестов для endpoint
|
|
2337
|
+
* @param {Object} endpoint - endpoint из parser
|
|
2338
|
+
* @param {Object} options - { includeNegative, testStyle }
|
|
2339
|
+
* @returns {Array<Object>} - массив тестовых кейсов
|
|
2340
|
+
*/
|
|
2341
|
+
static generateTestCases(endpoint, options = {}) {
|
|
2342
|
+
const cases = [];
|
|
2343
|
+
|
|
2344
|
+
// 1. Happy path
|
|
2345
|
+
cases.push(this.happyPathCase(endpoint));
|
|
2346
|
+
|
|
2347
|
+
if (options.includeNegative) {
|
|
2348
|
+
// 2. 401 если есть security
|
|
2349
|
+
if (endpoint.security && endpoint.security.length > 0) {
|
|
2350
|
+
cases.push(this.unauthorizedCase(endpoint));
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// 3. 404 если есть path params
|
|
2354
|
+
if (endpoint.parameters?.some(p => p.in === 'path')) {
|
|
2355
|
+
cases.push(this.notFoundCase(endpoint));
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// 4. 422 если POST/PUT/PATCH с required body
|
|
2359
|
+
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.requestBody?.required) {
|
|
2360
|
+
cases.push(this.validationErrorCase(endpoint));
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
return cases;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* Определить CRUD-группы из списка endpoints
|
|
2369
|
+
* @param {Array} endpoints
|
|
2370
|
+
* @returns {Map<string, Object>} - resourceName → { list, create, getById, update, delete }
|
|
2371
|
+
*/
|
|
2372
|
+
static detectCrudGroups(endpoints) {
|
|
2373
|
+
const groups = new Map();
|
|
2374
|
+
|
|
2375
|
+
for (const ep of endpoints) {
|
|
2376
|
+
const resource = this.extractResource(ep.path);
|
|
2377
|
+
if (!groups.has(resource)) {
|
|
2378
|
+
groups.set(resource, { resource, endpoints: {}, path: '' });
|
|
2379
|
+
}
|
|
2380
|
+
const group = groups.get(resource);
|
|
2381
|
+
const hasPathParam = ep.parameters?.some(p => p.in === 'path');
|
|
2382
|
+
|
|
2383
|
+
switch (ep.method) {
|
|
2384
|
+
case 'GET':
|
|
2385
|
+
if (hasPathParam) group.endpoints.getById = ep;
|
|
2386
|
+
else group.endpoints.list = ep;
|
|
2387
|
+
break;
|
|
2388
|
+
case 'POST':
|
|
2389
|
+
if (!hasPathParam) group.endpoints.create = ep;
|
|
2390
|
+
break;
|
|
2391
|
+
case 'PUT':
|
|
2392
|
+
case 'PATCH':
|
|
2393
|
+
if (hasPathParam) group.endpoints.update = ep;
|
|
2394
|
+
break;
|
|
2395
|
+
case 'DELETE':
|
|
2396
|
+
if (hasPathParam) group.endpoints.delete = ep;
|
|
2397
|
+
break;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// Фильтровать: только группы с ≥2 CRUD операциями
|
|
2402
|
+
const crudGroups = new Map();
|
|
2403
|
+
for (const [name, group] of groups) {
|
|
2404
|
+
if (Object.keys(group.endpoints).length >= 2) {
|
|
2405
|
+
crudGroups.set(name, group);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return crudGroups;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Извлечь имя ресурса из path
|
|
2413
|
+
* /pets → 'pets'
|
|
2414
|
+
* /pets/{petId} → 'pets'
|
|
2415
|
+
* /users/{id}/orders → 'users_orders'
|
|
2416
|
+
*/
|
|
2417
|
+
static extractResource(path) { ... }
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* Сгенерировать happy-path test case
|
|
2421
|
+
*/
|
|
2422
|
+
static happyPathCase(endpoint) {
|
|
2423
|
+
const successCode = this.getSuccessCode(endpoint);
|
|
2424
|
+
return {
|
|
2425
|
+
type: 'happy_path',
|
|
2426
|
+
name: `${endpoint.method} ${endpoint.path} - ${successCode} success`,
|
|
2427
|
+
endpoint,
|
|
2428
|
+
expectedStatus: successCode,
|
|
2429
|
+
sampleParams: this.generateSampleParams(endpoint),
|
|
2430
|
+
sampleBody: this.generateSampleBody(endpoint),
|
|
2431
|
+
assertions: this.generateAssertions(endpoint, successCode)
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** Определить success status code из responses */
|
|
2436
|
+
static getSuccessCode(endpoint) {
|
|
2437
|
+
if (endpoint.responses['200']) return 200;
|
|
2438
|
+
if (endpoint.responses['201']) return 201;
|
|
2439
|
+
if (endpoint.responses['204']) return 204;
|
|
2440
|
+
const first2xx = Object.keys(endpoint.responses).find(s => s.startsWith('2'));
|
|
2441
|
+
return first2xx ? parseInt(first2xx) : 200;
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
/** Сгенерировать sample параметры для тестов */
|
|
2445
|
+
static generateSampleParams(endpoint) {
|
|
2446
|
+
const params = {};
|
|
2447
|
+
for (const p of endpoint.parameters || []) {
|
|
2448
|
+
if (p.in === 'path') {
|
|
2449
|
+
params[p.name] = p.type === 'integer' ? 1 : 'test';
|
|
2450
|
+
} else if (p.in === 'query') {
|
|
2451
|
+
if (p.required) {
|
|
2452
|
+
params[p.name] = this.getSampleValue(p);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return params;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/** Сгенерировать sample body для POST/PUT */
|
|
2460
|
+
static generateSampleBody(endpoint) { ... }
|
|
2461
|
+
|
|
2462
|
+
/** Сгенерировать assertions */
|
|
2463
|
+
static generateAssertions(endpoint, status) { ... }
|
|
2464
|
+
|
|
2465
|
+
/** Получить sample value для типа */
|
|
2466
|
+
static getSampleValue(param) {
|
|
2467
|
+
if (param.enum) return param.enum[0];
|
|
2468
|
+
switch (param.type) {
|
|
2469
|
+
case 'integer': return 10;
|
|
2470
|
+
case 'number': return 10.5;
|
|
2471
|
+
case 'boolean': return true;
|
|
2472
|
+
case 'string': return 'test';
|
|
2473
|
+
default: return 'test';
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
static unauthorizedCase(endpoint) { ... }
|
|
2478
|
+
static notFoundCase(endpoint) { ... }
|
|
2479
|
+
static validationErrorCase(endpoint) { ... }
|
|
2480
|
+
}
|
|
2481
|
+
```
|
|
2482
|
+
|
|
2483
|
+
##### 2. `utils/api-generators/api-tests-typescript.js`
|
|
2484
|
+
|
|
2485
|
+
Генератор TypeScript API-тестов (Playwright test).
|
|
2486
|
+
|
|
2487
|
+
```js
|
|
2488
|
+
import { TestScaffoldGenerator } from '../openapi/test-scaffold-generator.js';
|
|
2489
|
+
import { MethodGenerator } from '../openapi/method-generator.js';
|
|
2490
|
+
|
|
2491
|
+
export class ApiTestsTypeScriptGenerator {
|
|
2492
|
+
constructor(endpoints, securitySchemes, options = {}) {
|
|
2493
|
+
this.endpoints = endpoints;
|
|
2494
|
+
this.securitySchemes = securitySchemes;
|
|
2495
|
+
this.options = {
|
|
2496
|
+
useApiClient: true,
|
|
2497
|
+
clientImportPath: null,
|
|
2498
|
+
modelsImportPath: null,
|
|
2499
|
+
includeNegative: true,
|
|
2500
|
+
includeAuth: true,
|
|
2501
|
+
authFromEnv: true,
|
|
2502
|
+
envPrefix: null,
|
|
2503
|
+
testStyle: 'per-endpoint', // 'crud' | 'per-endpoint' | 'smoke'
|
|
2504
|
+
includeComments: true,
|
|
2505
|
+
...options
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
/**
|
|
2510
|
+
* Сгенерировать весь тестовый файл
|
|
2511
|
+
*/
|
|
2512
|
+
generate(metadata = {}) {
|
|
2513
|
+
const lines = [];
|
|
2514
|
+
const envPrefix = this.options.envPrefix || this.titleToEnvPrefix(metadata.title);
|
|
2515
|
+
|
|
2516
|
+
// 1. Imports
|
|
2517
|
+
lines.push(...this.generateImports(metadata));
|
|
2518
|
+
lines.push('');
|
|
2519
|
+
|
|
2520
|
+
// 2. Constants
|
|
2521
|
+
lines.push(`const BASE_URL = process.env.${envPrefix}_BASE_URL || '${metadata.baseUrl}';`);
|
|
2522
|
+
lines.push('');
|
|
2523
|
+
|
|
2524
|
+
// 3. Test describe block
|
|
2525
|
+
lines.push(`test.describe('${metadata.title || 'API'} Tests', () => {`);
|
|
2526
|
+
|
|
2527
|
+
// 4. Setup
|
|
2528
|
+
if (this.options.useApiClient) {
|
|
2529
|
+
lines.push(...this.generateApiClientSetup(metadata, envPrefix));
|
|
2530
|
+
}
|
|
2531
|
+
lines.push('');
|
|
2532
|
+
|
|
2533
|
+
// 5. Tests based on style
|
|
2534
|
+
if (this.options.testStyle === 'crud') {
|
|
2535
|
+
lines.push(...this.generateCrudTests(metadata));
|
|
2536
|
+
} else if (this.options.testStyle === 'smoke') {
|
|
2537
|
+
lines.push(...this.generateSmokeTests());
|
|
2538
|
+
} else {
|
|
2539
|
+
lines.push(...this.generatePerEndpointTests());
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// 6. Close describe
|
|
2543
|
+
lines.push('});');
|
|
2544
|
+
|
|
2545
|
+
return lines.join('\n');
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/** Imports */
|
|
2549
|
+
generateImports(metadata) {
|
|
2550
|
+
const lines = ["import { test, expect } from '@playwright/test';"];
|
|
2551
|
+
if (this.options.useApiClient) {
|
|
2552
|
+
const clientPath = this.options.clientImportPath || this.getClientImportPath(metadata.title);
|
|
2553
|
+
const className = this.getClientClassName(metadata.title);
|
|
2554
|
+
lines.push(`import { ${className} } from '${clientPath}';`);
|
|
2555
|
+
}
|
|
2556
|
+
return lines;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
/** beforeAll с API client setup */
|
|
2560
|
+
generateApiClientSetup(metadata, envPrefix) {
|
|
2561
|
+
const className = this.getClientClassName(metadata.title);
|
|
2562
|
+
const varName = className.charAt(0).toLowerCase() + className.slice(1);
|
|
2563
|
+
const lines = [
|
|
2564
|
+
` let ${varName}: ${className};`,
|
|
2565
|
+
'',
|
|
2566
|
+
' test.beforeAll(async ({ request }) => {',
|
|
2567
|
+
` ${varName} = new ${className}(request, {`,
|
|
2568
|
+
` baseUrl: BASE_URL,`,
|
|
2569
|
+
];
|
|
2570
|
+
|
|
2571
|
+
// Auth env vars
|
|
2572
|
+
for (const scheme of Object.values(this.securitySchemes || {})) {
|
|
2573
|
+
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
|
|
2574
|
+
lines.push(` token: process.env.${envPrefix}_TOKEN,`);
|
|
2575
|
+
} else if (scheme.type === 'apiKey') {
|
|
2576
|
+
lines.push(` apiKey: process.env.${envPrefix}_API_KEY,`);
|
|
2577
|
+
} else if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
|
2578
|
+
lines.push(` username: process.env.${envPrefix}_USERNAME,`);
|
|
2579
|
+
lines.push(` password: process.env.${envPrefix}_PASSWORD,`);
|
|
2580
|
+
} else if (scheme.type === 'oauth2') {
|
|
2581
|
+
lines.push(` accessToken: process.env.${envPrefix}_ACCESS_TOKEN,`);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
lines.push(' });');
|
|
2586
|
+
lines.push(' });');
|
|
2587
|
+
return lines;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/** Тесты per-endpoint */
|
|
2591
|
+
generatePerEndpointTests() {
|
|
2592
|
+
const lines = [];
|
|
2593
|
+
for (const ep of this.endpoints) {
|
|
2594
|
+
const cases = TestScaffoldGenerator.generateTestCases(ep, {
|
|
2595
|
+
includeNegative: this.options.includeNegative
|
|
2596
|
+
});
|
|
2597
|
+
for (const tc of cases) {
|
|
2598
|
+
lines.push(...this.generateTest(tc));
|
|
2599
|
+
lines.push('');
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return lines;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
/** CRUD тесты */
|
|
2606
|
+
generateCrudTests(metadata) {
|
|
2607
|
+
const lines = [];
|
|
2608
|
+
|
|
2609
|
+
// Определить CRUD-группы
|
|
2610
|
+
const crudGroups = TestScaffoldGenerator.detectCrudGroups(this.endpoints);
|
|
2611
|
+
|
|
2612
|
+
// Для каждой CRUD-группы
|
|
2613
|
+
for (const [resource, group] of crudGroups) {
|
|
2614
|
+
lines.push(` test.describe('${this.capitalize(resource)} CRUD', () => {`);
|
|
2615
|
+
lines.push(` let createdId: number | string;`);
|
|
2616
|
+
lines.push('');
|
|
2617
|
+
|
|
2618
|
+
// Create → Read → Update → List → Delete → Verify Deleted
|
|
2619
|
+
if (group.endpoints.create) {
|
|
2620
|
+
lines.push(...this.generateCrudCreate(group.endpoints.create));
|
|
2621
|
+
}
|
|
2622
|
+
if (group.endpoints.getById) {
|
|
2623
|
+
lines.push(...this.generateCrudGetById(group.endpoints.getById));
|
|
2624
|
+
}
|
|
2625
|
+
if (group.endpoints.update) {
|
|
2626
|
+
lines.push(...this.generateCrudUpdate(group.endpoints.update));
|
|
2627
|
+
}
|
|
2628
|
+
if (group.endpoints.list) {
|
|
2629
|
+
lines.push(...this.generateCrudList(group.endpoints.list));
|
|
2630
|
+
}
|
|
2631
|
+
if (group.endpoints.delete) {
|
|
2632
|
+
lines.push(...this.generateCrudDelete(group.endpoints.delete));
|
|
2633
|
+
// Verify deleted
|
|
2634
|
+
if (group.endpoints.getById) {
|
|
2635
|
+
lines.push(...this.generateCrudVerifyDeleted(group.endpoints.getById));
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
lines.push(' });');
|
|
2640
|
+
lines.push('');
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// Endpoints вне CRUD-групп — per-endpoint
|
|
2644
|
+
const crudEndpoints = new Set();
|
|
2645
|
+
for (const group of crudGroups.values()) {
|
|
2646
|
+
for (const ep of Object.values(group.endpoints)) {
|
|
2647
|
+
crudEndpoints.add(`${ep.method} ${ep.path}`);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
const remaining = this.endpoints.filter(ep => !crudEndpoints.has(`${ep.method} ${ep.path}`));
|
|
2651
|
+
if (remaining.length > 0) {
|
|
2652
|
+
for (const ep of remaining) {
|
|
2653
|
+
const cases = TestScaffoldGenerator.generateTestCases(ep, { includeNegative: this.options.includeNegative });
|
|
2654
|
+
for (const tc of cases) {
|
|
2655
|
+
lines.push(...this.generateTest(tc));
|
|
2656
|
+
lines.push('');
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
return lines;
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
/** Smoke тесты (только happy path) */
|
|
2665
|
+
generateSmokeTests() {
|
|
2666
|
+
const lines = [];
|
|
2667
|
+
for (const ep of this.endpoints) {
|
|
2668
|
+
const tc = TestScaffoldGenerator.happyPathCase(ep);
|
|
2669
|
+
lines.push(...this.generateTest(tc));
|
|
2670
|
+
lines.push('');
|
|
2671
|
+
}
|
|
2672
|
+
return lines;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
/** Генерировать один test() */
|
|
2676
|
+
generateTest(testCase) { ... }
|
|
2677
|
+
|
|
2678
|
+
/** CRUD helpers */
|
|
2679
|
+
generateCrudCreate(endpoint) { ... }
|
|
2680
|
+
generateCrudGetById(endpoint) { ... }
|
|
2681
|
+
generateCrudUpdate(endpoint) { ... }
|
|
2682
|
+
generateCrudList(endpoint) { ... }
|
|
2683
|
+
generateCrudDelete(endpoint) { ... }
|
|
2684
|
+
generateCrudVerifyDeleted(endpoint) { ... }
|
|
2685
|
+
|
|
2686
|
+
/** Utilities */
|
|
2687
|
+
getClientClassName(title) { ... }
|
|
2688
|
+
getClientImportPath(title) { ... }
|
|
2689
|
+
titleToEnvPrefix(title) { ... }
|
|
2690
|
+
capitalize(str) { ... }
|
|
2691
|
+
}
|
|
2692
|
+
```
|
|
2693
|
+
|
|
2694
|
+
##### 3. `utils/api-generators/api-tests-python.js`
|
|
2695
|
+
|
|
2696
|
+
Генератор Python API-тестов (pytest + requests).
|
|
2697
|
+
|
|
2698
|
+
```js
|
|
2699
|
+
import { TestScaffoldGenerator } from '../openapi/test-scaffold-generator.js';
|
|
2700
|
+
import { MethodGenerator } from '../openapi/method-generator.js';
|
|
2701
|
+
|
|
2702
|
+
export class ApiTestsPythonGenerator {
|
|
2703
|
+
constructor(endpoints, securitySchemes, options = {}) {
|
|
2704
|
+
this.endpoints = endpoints;
|
|
2705
|
+
this.securitySchemes = securitySchemes;
|
|
2706
|
+
this.options = {
|
|
2707
|
+
useApiClient: true,
|
|
2708
|
+
clientImportPath: null,
|
|
2709
|
+
modelsImportPath: null,
|
|
2710
|
+
includeNegative: true,
|
|
2711
|
+
includeAuth: true,
|
|
2712
|
+
authFromEnv: true,
|
|
2713
|
+
envPrefix: null,
|
|
2714
|
+
testStyle: 'per-endpoint',
|
|
2715
|
+
includeComments: true,
|
|
2716
|
+
pythonStyle: 'dataclass',
|
|
2717
|
+
...options
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
/**
|
|
2722
|
+
* Сгенерировать весь тестовый файл
|
|
2723
|
+
*/
|
|
2724
|
+
generate(metadata = {}) {
|
|
2725
|
+
const lines = [];
|
|
2726
|
+
const envPrefix = this.options.envPrefix || this.titleToEnvPrefix(metadata.title);
|
|
2727
|
+
|
|
2728
|
+
// 1. Imports
|
|
2729
|
+
lines.push(...this.generateImports(metadata));
|
|
2730
|
+
lines.push('');
|
|
2731
|
+
|
|
2732
|
+
// 2. Constants
|
|
2733
|
+
lines.push(`BASE_URL = os.environ.get('${envPrefix}_BASE_URL', '${metadata.baseUrl}')`);
|
|
2734
|
+
lines.push('');
|
|
2735
|
+
lines.push('');
|
|
2736
|
+
|
|
2737
|
+
// 3. Fixture
|
|
2738
|
+
if (this.options.useApiClient) {
|
|
2739
|
+
lines.push(...this.generateFixture(metadata, envPrefix));
|
|
2740
|
+
lines.push('');
|
|
2741
|
+
lines.push('');
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// 4. Tests based on style
|
|
2745
|
+
if (this.options.testStyle === 'crud') {
|
|
2746
|
+
lines.push(...this.generateCrudTests(metadata));
|
|
2747
|
+
} else if (this.options.testStyle === 'smoke') {
|
|
2748
|
+
lines.push(...this.generateSmokeTests());
|
|
2749
|
+
} else {
|
|
2750
|
+
lines.push(...this.generatePerEndpointTests());
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
return lines.join('\n');
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
/** Imports */
|
|
2757
|
+
generateImports(metadata) {
|
|
2758
|
+
const lines = [
|
|
2759
|
+
'import pytest',
|
|
2760
|
+
'import os',
|
|
2761
|
+
];
|
|
2762
|
+
if (this.options.useApiClient) {
|
|
2763
|
+
const importPath = this.options.clientImportPath || this.getClientImportPath(metadata.title);
|
|
2764
|
+
const className = this.getClientClassName(metadata.title);
|
|
2765
|
+
lines.push(`from ${importPath} import ${className}`);
|
|
2766
|
+
} else {
|
|
2767
|
+
lines.push('import requests');
|
|
2768
|
+
}
|
|
2769
|
+
return lines;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
/** pytest fixture */
|
|
2773
|
+
generateFixture(metadata, envPrefix) {
|
|
2774
|
+
const className = this.getClientClassName(metadata.title);
|
|
2775
|
+
const lines = [
|
|
2776
|
+
"@pytest.fixture(scope='session')",
|
|
2777
|
+
'def api():',
|
|
2778
|
+
` return ${className}(`,
|
|
2779
|
+
` base_url=BASE_URL,`,
|
|
2780
|
+
];
|
|
2781
|
+
|
|
2782
|
+
for (const scheme of Object.values(this.securitySchemes || {})) {
|
|
2783
|
+
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
|
|
2784
|
+
lines.push(` token=os.environ.get('${envPrefix}_TOKEN'),`);
|
|
2785
|
+
} else if (scheme.type === 'apiKey') {
|
|
2786
|
+
lines.push(` api_key=os.environ.get('${envPrefix}_API_KEY'),`);
|
|
2787
|
+
} else if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
|
2788
|
+
lines.push(` username=os.environ.get('${envPrefix}_USERNAME'),`);
|
|
2789
|
+
lines.push(` password=os.environ.get('${envPrefix}_PASSWORD'),`);
|
|
2790
|
+
} else if (scheme.type === 'oauth2') {
|
|
2791
|
+
lines.push(` access_token=os.environ.get('${envPrefix}_ACCESS_TOKEN'),`);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
lines.push(' )');
|
|
2796
|
+
return lines;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
/** Per-endpoint тесты (класс на тег или flat) */
|
|
2800
|
+
generatePerEndpointTests() {
|
|
2801
|
+
const lines = [];
|
|
2802
|
+
// Группировка по тегам → class TestPetsApi / class TestUsersApi
|
|
2803
|
+
const grouped = this.groupByTag(this.endpoints);
|
|
2804
|
+
|
|
2805
|
+
for (const [tag, endpoints] of Object.entries(grouped)) {
|
|
2806
|
+
const className = `Test${this.capitalize(tag)}Api`;
|
|
2807
|
+
lines.push(`class ${className}:`);
|
|
2808
|
+
|
|
2809
|
+
for (const ep of endpoints) {
|
|
2810
|
+
const cases = TestScaffoldGenerator.generateTestCases(ep, {
|
|
2811
|
+
includeNegative: this.options.includeNegative
|
|
2812
|
+
});
|
|
2813
|
+
for (const tc of cases) {
|
|
2814
|
+
lines.push(...this.generateTest(tc));
|
|
2815
|
+
lines.push('');
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
lines.push('');
|
|
2819
|
+
}
|
|
2820
|
+
return lines;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
/** CRUD тесты */
|
|
2824
|
+
generateCrudTests(metadata) {
|
|
2825
|
+
// Аналогично TypeScript, но с Python синтаксисом
|
|
2826
|
+
// Используется class TestResourceCrud: с class-level переменными
|
|
2827
|
+
...
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/** Smoke тесты */
|
|
2831
|
+
generateSmokeTests() { ... }
|
|
2832
|
+
|
|
2833
|
+
/** Генерировать один def test_() */
|
|
2834
|
+
generateTest(testCase) { ... }
|
|
2835
|
+
|
|
2836
|
+
/** Utilities */
|
|
2837
|
+
getClientClassName(title) { ... }
|
|
2838
|
+
getClientImportPath(title) { ... }
|
|
2839
|
+
titleToEnvPrefix(title) { ... }
|
|
2840
|
+
capitalize(str) { ... }
|
|
2841
|
+
groupByTag(endpoints) { ... }
|
|
2842
|
+
}
|
|
2843
|
+
```
|
|
2844
|
+
|
|
2845
|
+
#### Изменяемые файлы
|
|
2846
|
+
|
|
2847
|
+
##### 4. `server/tool-schemas.js`
|
|
2848
|
+
|
|
2849
|
+
Добавить:
|
|
2850
|
+
```js
|
|
2851
|
+
export const GenerateApiTestsSchema = z.object({
|
|
2852
|
+
source: z.string().describe("URL or file path to OpenAPI spec"),
|
|
2853
|
+
language: z.enum(['typescript', 'python']).describe("Target language/framework"),
|
|
2854
|
+
format: z.enum(['auto', 'json', 'yaml']).optional()
|
|
2855
|
+
.describe("Spec format (default: auto)"),
|
|
2856
|
+
useApiClient: z.boolean().optional()
|
|
2857
|
+
.describe("Use generated API client (default: true). False = raw HTTP calls"),
|
|
2858
|
+
clientImportPath: z.string().optional()
|
|
2859
|
+
.describe("Import path for API client (default: auto)"),
|
|
2860
|
+
modelsImportPath: z.string().optional()
|
|
2861
|
+
.describe("Import path for models (default: auto). Used when useApiClient=false"),
|
|
2862
|
+
tags: z.array(z.string()).optional()
|
|
2863
|
+
.describe("Test only these tags (default: all)"),
|
|
2864
|
+
testStyle: z.enum(['crud', 'per-endpoint', 'smoke']).optional()
|
|
2865
|
+
.describe("'crud' groups CRUD ops, 'per-endpoint' one test each, 'smoke' happy-path only (default: 'per-endpoint')"),
|
|
2866
|
+
includeNegative: z.boolean().optional()
|
|
2867
|
+
.describe("Generate 401/404/422 tests (default: true)"),
|
|
2868
|
+
includeAuth: z.boolean().optional()
|
|
2869
|
+
.describe("Generate auth setup (default: true)"),
|
|
2870
|
+
authFromEnv: z.boolean().optional()
|
|
2871
|
+
.describe("Auth from env vars (default: true)"),
|
|
2872
|
+
envPrefix: z.string().optional()
|
|
2873
|
+
.describe("Env var prefix, e.g. 'PETSTORE' → PETSTORE_TOKEN (default: from title)"),
|
|
2874
|
+
pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
|
|
2875
|
+
.describe("Python only: model style for test data (default: 'dataclass')"),
|
|
2876
|
+
});
|
|
2877
|
+
```
|
|
2878
|
+
|
|
2879
|
+
##### 5. `server/tool-definitions.js`
|
|
2880
|
+
|
|
2881
|
+
Добавить:
|
|
2882
|
+
```js
|
|
2883
|
+
{
|
|
2884
|
+
name: "generateApiTests",
|
|
2885
|
+
description: "Generate API test scaffolds from OpenAPI/Swagger spec. Creates test files with happy-path and negative tests for each endpoint. Supports CRUD grouping, auth setup, and both API-client and raw HTTP styles. Use after generateApiClient for best results.",
|
|
2886
|
+
inputSchema: GenerateApiTestsSchema
|
|
2887
|
+
}
|
|
2888
|
+
```
|
|
2889
|
+
|
|
2890
|
+
##### 6. `index.js`
|
|
2891
|
+
|
|
2892
|
+
Добавить хендлер:
|
|
2893
|
+
```js
|
|
2894
|
+
if (name === "generateApiTests") {
|
|
2895
|
+
const parser = await OpenAPIParser.load(args.source, args.format);
|
|
2896
|
+
const endpoints = parser.getEndpoints();
|
|
2897
|
+
const securitySchemes = parser.getSecuritySchemes();
|
|
2898
|
+
const metadata = {
|
|
2899
|
+
title: parser.spec.info?.title || '',
|
|
2900
|
+
baseUrl: parser.getBaseUrl(),
|
|
2901
|
+
version: parser.version
|
|
2902
|
+
};
|
|
2903
|
+
|
|
2904
|
+
// Фильтрация по тегам
|
|
2905
|
+
let filteredEndpoints = endpoints;
|
|
2906
|
+
if (args.tags) {
|
|
2907
|
+
filteredEndpoints = endpoints.filter(ep =>
|
|
2908
|
+
ep.tags.some(tag => args.tags.includes(tag))
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// Выбрать генератор
|
|
2913
|
+
let generator;
|
|
2914
|
+
const genOptions = {
|
|
2915
|
+
useApiClient: args.useApiClient ?? true,
|
|
2916
|
+
clientImportPath: args.clientImportPath,
|
|
2917
|
+
modelsImportPath: args.modelsImportPath,
|
|
2918
|
+
includeNegative: args.includeNegative ?? true,
|
|
2919
|
+
includeAuth: args.includeAuth ?? true,
|
|
2920
|
+
authFromEnv: args.authFromEnv ?? true,
|
|
2921
|
+
envPrefix: args.envPrefix,
|
|
2922
|
+
testStyle: args.testStyle || 'per-endpoint',
|
|
2923
|
+
};
|
|
2924
|
+
|
|
2925
|
+
if (args.language === 'typescript') {
|
|
2926
|
+
generator = new ApiTestsTypeScriptGenerator(filteredEndpoints, securitySchemes, genOptions);
|
|
2927
|
+
} else {
|
|
2928
|
+
generator = new ApiTestsPythonGenerator(filteredEndpoints, securitySchemes, {
|
|
2929
|
+
...genOptions,
|
|
2930
|
+
pythonStyle: args.pythonStyle || 'dataclass',
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
const code = generator.generate(metadata);
|
|
2935
|
+
const envPrefix = genOptions.envPrefix || generator.titleToEnvPrefix(metadata.title);
|
|
2936
|
+
|
|
2937
|
+
// Подсчитать тесты
|
|
2938
|
+
let testCount = 0;
|
|
2939
|
+
const breakdown = { happyPath: 0, unauthorized: 0, notFound: 0, validationError: 0 };
|
|
2940
|
+
for (const ep of filteredEndpoints) {
|
|
2941
|
+
const cases = TestScaffoldGenerator.generateTestCases(ep, { includeNegative: genOptions.includeNegative });
|
|
2942
|
+
testCount += cases.length;
|
|
2943
|
+
for (const c of cases) {
|
|
2944
|
+
if (c.type === 'happy_path') breakdown.happyPath++;
|
|
2945
|
+
else if (c.type === 'unauthorized') breakdown.unauthorized++;
|
|
2946
|
+
else if (c.type === 'not_found') breakdown.notFound++;
|
|
2947
|
+
else if (c.type === 'validation_error') breakdown.validationError++;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
const suggestedFileName = args.language === 'typescript'
|
|
2952
|
+
? `${generator.titleToKebab(metadata.title)}.spec.ts`
|
|
2953
|
+
: `test_${generator.titleToSnake(metadata.title)}.py`;
|
|
2954
|
+
|
|
2955
|
+
// Собрать env vars для instruction
|
|
2956
|
+
const envVars = [`${envPrefix}_BASE_URL`];
|
|
2957
|
+
for (const scheme of Object.values(securitySchemes || {})) {
|
|
2958
|
+
if (scheme.type === 'http' && scheme.scheme === 'bearer') envVars.push(`${envPrefix}_TOKEN`);
|
|
2959
|
+
else if (scheme.type === 'apiKey') envVars.push(`${envPrefix}_API_KEY`);
|
|
2960
|
+
else if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
|
2961
|
+
envVars.push(`${envPrefix}_USERNAME`, `${envPrefix}_PASSWORD`);
|
|
2962
|
+
}
|
|
2963
|
+
else if (scheme.type === 'oauth2') envVars.push(`${envPrefix}_ACCESS_TOKEN`);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
return {
|
|
2967
|
+
content: [{
|
|
2968
|
+
type: 'text',
|
|
2969
|
+
text: JSON.stringify({
|
|
2970
|
+
action: 'create_new_file',
|
|
2971
|
+
suggestedFileName,
|
|
2972
|
+
code,
|
|
2973
|
+
testCount,
|
|
2974
|
+
endpointsCovered: filteredEndpoints.length,
|
|
2975
|
+
testBreakdown: breakdown,
|
|
2976
|
+
language: args.language,
|
|
2977
|
+
source: args.source,
|
|
2978
|
+
instruction: `Create file '${suggestedFileName}'. ${genOptions.useApiClient ? 'Make sure API client and models files exist. ' : ''}Set environment variables: ${envVars.join(', ')}.`
|
|
2979
|
+
}, null, 2)
|
|
2980
|
+
}]
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
```
|
|
2984
|
+
|
|
2985
|
+
##### 7. `README.md`
|
|
2986
|
+
|
|
2987
|
+
Добавить в секцию API Tools:
|
|
2988
|
+
```markdown
|
|
2989
|
+
### generateApiTests
|
|
2990
|
+
|
|
2991
|
+
Generate API test scaffolds from OpenAPI/Swagger spec.
|
|
2992
|
+
|
|
2993
|
+
| Parameter | Type | Default | Description |
|
|
2994
|
+
|-----------|------|---------|-------------|
|
|
2995
|
+
| `source` | string | required | URL or file path to OpenAPI spec |
|
|
2996
|
+
| `language` | 'typescript' \| 'python' | required | Target language |
|
|
2997
|
+
| `format` | 'auto' \| 'json' \| 'yaml' | 'auto' | Spec format |
|
|
2998
|
+
| `useApiClient` | boolean | true | Use API client class vs raw HTTP |
|
|
2999
|
+
| `clientImportPath` | string | auto | Import path for API client |
|
|
3000
|
+
| `tags` | string[] | all | Filter by endpoint tags |
|
|
3001
|
+
| `testStyle` | 'crud' \| 'per-endpoint' \| 'smoke' | 'per-endpoint' | Test grouping style |
|
|
3002
|
+
| `includeNegative` | boolean | true | Generate 401/404/422 tests |
|
|
3003
|
+
| `includeAuth` | boolean | true | Generate auth setup |
|
|
3004
|
+
| `authFromEnv` | boolean | true | Auth from env vars |
|
|
3005
|
+
| `envPrefix` | string | from title | Env var prefix |
|
|
3006
|
+
|
|
3007
|
+
**Example — Full TypeScript test suite:**
|
|
3008
|
+
```json
|
|
3009
|
+
{
|
|
3010
|
+
"source": "https://petstore.swagger.io/v2/swagger.json",
|
|
3011
|
+
"language": "typescript",
|
|
3012
|
+
"testStyle": "crud",
|
|
3013
|
+
"includeNegative": true
|
|
3014
|
+
}
|
|
3015
|
+
```
|
|
3016
|
+
→ Returns `pet-store-api.spec.ts` with CRUD test sequences and negative cases.
|
|
3017
|
+
|
|
3018
|
+
**Example — Python smoke tests:**
|
|
3019
|
+
```json
|
|
3020
|
+
{
|
|
3021
|
+
"source": "./openapi.yaml",
|
|
3022
|
+
"language": "python",
|
|
3023
|
+
"testStyle": "smoke",
|
|
3024
|
+
"includeNegative": false,
|
|
3025
|
+
"envPrefix": "MYAPI"
|
|
3026
|
+
}
|
|
3027
|
+
```
|
|
3028
|
+
→ Returns `test_my_api.py` with minimal happy-path tests.
|
|
3029
|
+
|
|
3030
|
+
**Example — Raw HTTP tests (no API client):**
|
|
3031
|
+
```json
|
|
3032
|
+
{
|
|
3033
|
+
"source": "./openapi.yaml",
|
|
3034
|
+
"language": "typescript",
|
|
3035
|
+
"useApiClient": false,
|
|
3036
|
+
"tags": ["pets"]
|
|
3037
|
+
}
|
|
3038
|
+
```
|
|
3039
|
+
→ Returns tests using raw `request.get()`/`request.post()` instead of API client methods.
|
|
3040
|
+
```
|
|
3041
|
+
|
|
3042
|
+
---
|
|
3043
|
+
|
|
3044
|
+
### Именование файлов (Фаза 3)
|
|
3045
|
+
|
|
3046
|
+
| Спека | TypeScript tests | Python tests |
|
|
3047
|
+
|---|---|---|
|
|
3048
|
+
| Pet Store API | `pet-store-api.spec.ts` | `test_pet_store_api.py` |
|
|
3049
|
+
| My Service | `my-service.spec.ts` | `test_my_service.py` |
|
|
3050
|
+
|
|
3051
|
+
---
|
|
3052
|
+
|
|
3053
|
+
### Структура файлов проекта (после всех 3 фаз)
|
|
3054
|
+
|
|
3055
|
+
```
|
|
3056
|
+
utils/
|
|
3057
|
+
openapi/
|
|
3058
|
+
parser.js # OpenAPIParser (Фаза 1)
|
|
3059
|
+
ref-resolver.js # $ref resolution (Фаза 1)
|
|
3060
|
+
type-mapper.js # type mapping (Фаза 1)
|
|
3061
|
+
method-generator.js # endpoint → method metadata (Фаза 2)
|
|
3062
|
+
auth-generator.js # auth config generation (Фаза 2)
|
|
3063
|
+
test-scaffold-generator.js # test case planning (Фаза 3)
|
|
3064
|
+
api-generators/
|
|
3065
|
+
api-models-typescript.js # TS models (Фаза 1)
|
|
3066
|
+
api-models-python.js # Python models (Фаза 1)
|
|
3067
|
+
api-client-typescript.js # TS API client (Фаза 2)
|
|
3068
|
+
api-client-python.js # Python API client (Фаза 2)
|
|
3069
|
+
api-tests-typescript.js # TS API tests (Фаза 3)
|
|
3070
|
+
api-tests-python.js # Python API tests (Фаза 3)
|
|
3071
|
+
```
|
|
3072
|
+
|
|
3073
|
+
---
|
|
3074
|
+
|
|
3075
|
+
### Верификация (Фаза 3)
|
|
3076
|
+
|
|
3077
|
+
#### Функциональные тесты
|
|
3078
|
+
|
|
3079
|
+
1. **Per-endpoint TypeScript**: Petstore → тест на каждый endpoint, проверить happy path + negative cases
|
|
3080
|
+
2. **Per-endpoint Python**: Petstore → pytest с fixture, class-based group by tags
|
|
3081
|
+
3. **CRUD TypeScript**: Petstore → CRUD-группа для `/pets`, последовательные create→read→update→list→delete→verify
|
|
3082
|
+
4. **CRUD Python**: аналогично, class-level переменные, pytest
|
|
3083
|
+
5. **Smoke**: только happy path, минимальные assertions
|
|
3084
|
+
6. **Auth setup**: проверить env var names для всех auth types
|
|
3085
|
+
7. **Tag filtering**: `tags: ['pets']` → тесты только для pet endpoints
|
|
3086
|
+
8. **useApiClient=false**: raw HTTP calls, без import клиента
|
|
3087
|
+
|
|
3088
|
+
#### Boundary тесты
|
|
3089
|
+
|
|
3090
|
+
9. **Спека без security**: нет auth setup, нет 401 тестов
|
|
3091
|
+
10. **Endpoint без path params**: нет 404 теста
|
|
3092
|
+
11. **GET endpoint**: нет validation error теста (нет body)
|
|
3093
|
+
12. **Пустая спека**: файл с пустым describe/class
|
|
3094
|
+
13. **Custom envPrefix**: `envPrefix: 'MYAPP'` → `MYAPP_TOKEN`, `MYAPP_BASE_URL`
|
|
3095
|
+
14. **Custom import paths**: `clientImportPath`, `modelsImportPath` используются в import
|
|
3096
|
+
|
|
3097
|
+
#### Интеграционные тесты
|
|
3098
|
+
|
|
3099
|
+
15. **Полный пайплайн**: loadSwagger → generateApiModels → generateApiClient → generateApiTests → все файлы совместимы по imports
|
|
3100
|
+
16. **Python pydantic**: `pythonStyle: 'pydantic'` → body в тестах использует `.model_dump()`
|
|
3101
|
+
17. **Python typeddict**: body в тестах передаётся как dict
|