truss-api-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/code-generator.d.ts +6 -0
- package/dist/lib/code-generator.d.ts.map +1 -0
- package/dist/lib/code-generator.js +890 -0
- package/dist/lib/code-generator.js.map +1 -0
- package/dist/lib/http-client.d.ts +6 -0
- package/dist/lib/http-client.d.ts.map +1 -0
- package/dist/lib/http-client.js +76 -0
- package/dist/lib/http-client.js.map +1 -0
- package/dist/lib/license.d.ts +4 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +97 -0
- package/dist/lib/license.js.map +1 -0
- package/dist/lib/openapi-parser.d.ts +11 -0
- package/dist/lib/openapi-parser.d.ts.map +1 -0
- package/dist/lib/openapi-parser.js +390 -0
- package/dist/lib/openapi-parser.js.map +1 -0
- package/dist/lib/schema-validator.d.ts +15 -0
- package/dist/lib/schema-validator.d.ts.map +1 -0
- package/dist/lib/schema-validator.js +206 -0
- package/dist/lib/schema-validator.js.map +1 -0
- package/dist/tools/compare-specs.d.ts +3 -0
- package/dist/tools/compare-specs.d.ts.map +1 -0
- package/dist/tools/compare-specs.js +59 -0
- package/dist/tools/compare-specs.js.map +1 -0
- package/dist/tools/generate-client.d.ts +3 -0
- package/dist/tools/generate-client.d.ts.map +1 -0
- package/dist/tools/generate-client.js +65 -0
- package/dist/tools/generate-client.js.map +1 -0
- package/dist/tools/generate-openapi.d.ts +3 -0
- package/dist/tools/generate-openapi.d.ts.map +1 -0
- package/dist/tools/generate-openapi.js +57 -0
- package/dist/tools/generate-openapi.js.map +1 -0
- package/dist/tools/generate-tests.d.ts +3 -0
- package/dist/tools/generate-tests.d.ts.map +1 -0
- package/dist/tools/generate-tests.js +59 -0
- package/dist/tools/generate-tests.js.map +1 -0
- package/dist/tools/mock-server.d.ts +3 -0
- package/dist/tools/mock-server.d.ts.map +1 -0
- package/dist/tools/mock-server.js +60 -0
- package/dist/tools/mock-server.js.map +1 -0
- package/dist/tools/parse-openapi.d.ts +3 -0
- package/dist/tools/parse-openapi.d.ts.map +1 -0
- package/dist/tools/parse-openapi.js +48 -0
- package/dist/tools/parse-openapi.js.map +1 -0
- package/dist/tools/test-endpoint.d.ts +3 -0
- package/dist/tools/test-endpoint.d.ts.map +1 -0
- package/dist/tools/test-endpoint.js +66 -0
- package/dist/tools/test-endpoint.js.map +1 -0
- package/dist/tools/validate-response.d.ts +3 -0
- package/dist/tools/validate-response.d.ts.map +1 -0
- package/dist/tools/validate-response.js +44 -0
- package/dist/tools/validate-response.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/evals/eval-http.ts +163 -0
- package/evals/eval-openapi.ts +506 -0
- package/evals/run-evals.ts +29 -0
- package/glama.json +4 -0
- package/package.json +37 -0
- package/smithery.yaml +9 -0
- package/src/index.ts +110 -0
- package/src/lib/code-generator.ts +1045 -0
- package/src/lib/http-client.ts +87 -0
- package/src/lib/license.ts +121 -0
- package/src/lib/openapi-parser.ts +456 -0
- package/src/lib/schema-validator.ts +234 -0
- package/src/tools/compare-specs.ts +67 -0
- package/src/tools/generate-client.ts +75 -0
- package/src/tools/generate-openapi.ts +67 -0
- package/src/tools/generate-tests.ts +69 -0
- package/src/tools/mock-server.ts +68 -0
- package/src/tools/parse-openapi.ts +54 -0
- package/src/tools/test-endpoint.ts +71 -0
- package/src/tools/validate-response.ts +54 -0
- package/src/types.ts +156 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
// ── Client Code Generation ──────────────────────────────────────────
|
|
2
|
+
export function generateClient(spec, language, style = 'fetch') {
|
|
3
|
+
switch (language) {
|
|
4
|
+
case 'typescript':
|
|
5
|
+
return generateTypeScriptClient(spec, style);
|
|
6
|
+
case 'python':
|
|
7
|
+
return generatePythonClient(spec);
|
|
8
|
+
case 'go':
|
|
9
|
+
return generateGoClient(spec);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// ── TypeScript Client ───────────────────────────────────────────────
|
|
13
|
+
function generateTypeScriptClient(spec, style) {
|
|
14
|
+
const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
|
|
15
|
+
const lines = [];
|
|
16
|
+
lines.push(`// Auto-generated API client for ${spec.info.title} v${spec.info.version}`);
|
|
17
|
+
lines.push(`// Generated by TRUSS API MCP`);
|
|
18
|
+
lines.push('');
|
|
19
|
+
if (style === 'axios') {
|
|
20
|
+
lines.push(`import axios, { type AxiosInstance, type AxiosResponse } from 'axios';`);
|
|
21
|
+
lines.push('');
|
|
22
|
+
}
|
|
23
|
+
// Generate interfaces from endpoints
|
|
24
|
+
const interfaces = generateTypeScriptInterfaces(spec);
|
|
25
|
+
if (interfaces) {
|
|
26
|
+
lines.push(interfaces);
|
|
27
|
+
lines.push('');
|
|
28
|
+
}
|
|
29
|
+
// Client class
|
|
30
|
+
lines.push(`export class ApiClient {`);
|
|
31
|
+
lines.push(` private baseUrl: string;`);
|
|
32
|
+
if (style === 'axios') {
|
|
33
|
+
lines.push(` private client: AxiosInstance;`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
lines.push(` private headers: Record<string, string>;`);
|
|
37
|
+
}
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(` constructor(baseUrl = '${baseUrl}', headers: Record<string, string> = {}) {`);
|
|
40
|
+
lines.push(` this.baseUrl = baseUrl.replace(/\\/$/, '');`);
|
|
41
|
+
if (style === 'axios') {
|
|
42
|
+
lines.push(` this.client = axios.create({`);
|
|
43
|
+
lines.push(` baseURL: this.baseUrl,`);
|
|
44
|
+
lines.push(` headers: { 'Content-Type': 'application/json', ...headers },`);
|
|
45
|
+
lines.push(` });`);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
lines.push(` this.headers = { 'Content-Type': 'application/json', ...headers };`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(` }`);
|
|
51
|
+
lines.push('');
|
|
52
|
+
// Generate methods
|
|
53
|
+
for (const endpoint of spec.endpoints) {
|
|
54
|
+
const methodName = endpointToMethodName(endpoint);
|
|
55
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
|
56
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
|
|
57
|
+
const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
|
|
58
|
+
// Build parameter list
|
|
59
|
+
const params = [];
|
|
60
|
+
for (const pp of pathParams) {
|
|
61
|
+
params.push(`${pp.name}: ${tsType(pp.schema)}`);
|
|
62
|
+
}
|
|
63
|
+
if (hasBody) {
|
|
64
|
+
params.push(`body: ${tsBodyType(endpoint.requestBody)}`);
|
|
65
|
+
}
|
|
66
|
+
if (queryParams.length > 0) {
|
|
67
|
+
const queryType = queryParams
|
|
68
|
+
.map((q) => `${q.name}${q.required ? '' : '?'}: ${tsType(q.schema)}`)
|
|
69
|
+
.join('; ');
|
|
70
|
+
params.push(`query?: { ${queryType} }`);
|
|
71
|
+
}
|
|
72
|
+
// JSDoc
|
|
73
|
+
if (endpoint.summary) {
|
|
74
|
+
lines.push(` /** ${endpoint.summary} */`);
|
|
75
|
+
}
|
|
76
|
+
const returnType = tsResponseType(endpoint);
|
|
77
|
+
if (style === 'axios') {
|
|
78
|
+
lines.push(` async ${methodName}(${params.join(', ')}): Promise<AxiosResponse<${returnType}>> {`);
|
|
79
|
+
const urlExpr = buildUrlExpr(endpoint, 'this.baseUrl');
|
|
80
|
+
const queryExpr = queryParams.length > 0 ? ', params: query' : '';
|
|
81
|
+
if (hasBody) {
|
|
82
|
+
lines.push(` return this.client.${endpoint.method.toLowerCase()}(${urlExpr}, body${queryExpr ? `, { ${queryExpr.slice(2)} }` : ''});`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
lines.push(` return this.client.${endpoint.method.toLowerCase()}(${urlExpr}${queryExpr ? `, { ${queryExpr.slice(2)} }` : ''});`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
lines.push(` async ${methodName}(${params.join(', ')}): Promise<${returnType}> {`);
|
|
90
|
+
const urlExpr = buildUrlExpr(endpoint, 'this.baseUrl');
|
|
91
|
+
if (queryParams.length > 0) {
|
|
92
|
+
lines.push(` const url = new URL(${urlExpr});`);
|
|
93
|
+
lines.push(` if (query) {`);
|
|
94
|
+
lines.push(` for (const [k, v] of Object.entries(query)) {`);
|
|
95
|
+
lines.push(` if (v !== undefined) url.searchParams.set(k, String(v));`);
|
|
96
|
+
lines.push(` }`);
|
|
97
|
+
lines.push(` }`);
|
|
98
|
+
lines.push(` const response = await fetch(url.toString(), {`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push(` const response = await fetch(${urlExpr}, {`);
|
|
102
|
+
}
|
|
103
|
+
lines.push(` method: '${endpoint.method}',`);
|
|
104
|
+
lines.push(` headers: this.headers,`);
|
|
105
|
+
if (hasBody) {
|
|
106
|
+
lines.push(` body: JSON.stringify(body),`);
|
|
107
|
+
}
|
|
108
|
+
lines.push(` });`);
|
|
109
|
+
lines.push(` if (!response.ok) {`);
|
|
110
|
+
lines.push(` throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);`);
|
|
111
|
+
lines.push(` }`);
|
|
112
|
+
lines.push(` return response.json() as Promise<${returnType}>;`);
|
|
113
|
+
}
|
|
114
|
+
lines.push(` }`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
}
|
|
117
|
+
lines.push(`}`);
|
|
118
|
+
return lines.join('\n');
|
|
119
|
+
}
|
|
120
|
+
function generateTypeScriptInterfaces(spec) {
|
|
121
|
+
const lines = [];
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const endpoint of spec.endpoints) {
|
|
124
|
+
// Generate request body interface
|
|
125
|
+
if (endpoint.requestBody?.properties) {
|
|
126
|
+
const name = pascalCase(endpointToMethodName(endpoint)) + 'Request';
|
|
127
|
+
if (!seen.has(name)) {
|
|
128
|
+
seen.add(name);
|
|
129
|
+
lines.push(schemaToTsInterface(name, endpoint.requestBody));
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Generate response interface
|
|
134
|
+
const okResponse = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
|
|
135
|
+
if (okResponse?.schema?.properties) {
|
|
136
|
+
const name = pascalCase(endpointToMethodName(endpoint)) + 'Response';
|
|
137
|
+
if (!seen.has(name)) {
|
|
138
|
+
seen.add(name);
|
|
139
|
+
lines.push(schemaToTsInterface(name, okResponse.schema));
|
|
140
|
+
lines.push('');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
function schemaToTsInterface(name, schema) {
|
|
147
|
+
const lines = [`export interface ${name} {`];
|
|
148
|
+
if (schema.properties) {
|
|
149
|
+
const required = new Set(schema.required ?? []);
|
|
150
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
151
|
+
const optional = required.has(key) ? '' : '?';
|
|
152
|
+
lines.push(` ${key}${optional}: ${tsType(prop)};`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
lines.push('}');
|
|
156
|
+
return lines.join('\n');
|
|
157
|
+
}
|
|
158
|
+
function tsType(schema) {
|
|
159
|
+
if (!schema)
|
|
160
|
+
return 'unknown';
|
|
161
|
+
if (schema.enum) {
|
|
162
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(' | ');
|
|
163
|
+
}
|
|
164
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
165
|
+
switch (type) {
|
|
166
|
+
case 'string':
|
|
167
|
+
return 'string';
|
|
168
|
+
case 'integer':
|
|
169
|
+
case 'number':
|
|
170
|
+
return 'number';
|
|
171
|
+
case 'boolean':
|
|
172
|
+
return 'boolean';
|
|
173
|
+
case 'array':
|
|
174
|
+
return `${tsType(schema.items)}[]`;
|
|
175
|
+
case 'object':
|
|
176
|
+
if (schema.properties) {
|
|
177
|
+
const props = Object.entries(schema.properties)
|
|
178
|
+
.map(([k, v]) => `${k}: ${tsType(v)}`)
|
|
179
|
+
.join('; ');
|
|
180
|
+
return `{ ${props} }`;
|
|
181
|
+
}
|
|
182
|
+
return 'Record<string, unknown>';
|
|
183
|
+
default:
|
|
184
|
+
return 'unknown';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function tsBodyType(schema) {
|
|
188
|
+
if (!schema)
|
|
189
|
+
return 'Record<string, unknown>';
|
|
190
|
+
return tsType(schema);
|
|
191
|
+
}
|
|
192
|
+
function tsResponseType(endpoint) {
|
|
193
|
+
const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
|
|
194
|
+
if (okResp?.schema)
|
|
195
|
+
return tsType(okResp.schema);
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
// ── Python Client ───────────────────────────────────────────────────
|
|
199
|
+
function generatePythonClient(spec) {
|
|
200
|
+
const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
|
|
201
|
+
const lines = [];
|
|
202
|
+
lines.push(`"""Auto-generated API client for ${spec.info.title} v${spec.info.version}"""`);
|
|
203
|
+
lines.push(`# Generated by TRUSS API MCP`);
|
|
204
|
+
lines.push('');
|
|
205
|
+
lines.push('from __future__ import annotations');
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push('from dataclasses import dataclass');
|
|
208
|
+
lines.push('from typing import Any, Optional');
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push('import requests');
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push('');
|
|
213
|
+
// Dataclasses for request/response types
|
|
214
|
+
for (const endpoint of spec.endpoints) {
|
|
215
|
+
if (endpoint.requestBody?.properties) {
|
|
216
|
+
const className = snakeToPascal(endpointToMethodName(endpoint)) + 'Request';
|
|
217
|
+
lines.push(`@dataclass`);
|
|
218
|
+
lines.push(`class ${className}:`);
|
|
219
|
+
for (const [key, prop] of Object.entries(endpoint.requestBody.properties)) {
|
|
220
|
+
lines.push(` ${key}: ${pyType(prop)}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push('');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
lines.push(`class ApiClient:`);
|
|
227
|
+
lines.push(` """API client for ${spec.info.title}"""`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push(` def __init__(self, base_url: str = "${baseUrl}", headers: Optional[dict[str, str]] = None):`);
|
|
230
|
+
lines.push(` self.base_url = base_url.rstrip("/")`);
|
|
231
|
+
lines.push(` self.session = requests.Session()`);
|
|
232
|
+
lines.push(` self.session.headers.update({"Content-Type": "application/json"})`);
|
|
233
|
+
lines.push(` if headers:`);
|
|
234
|
+
lines.push(` self.session.headers.update(headers)`);
|
|
235
|
+
lines.push('');
|
|
236
|
+
for (const endpoint of spec.endpoints) {
|
|
237
|
+
const methodName = camelToSnake(endpointToMethodName(endpoint));
|
|
238
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
|
239
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
|
|
240
|
+
const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
|
|
241
|
+
const params = ['self'];
|
|
242
|
+
for (const pp of pathParams) {
|
|
243
|
+
params.push(`${pp.name}: ${pyType(pp.schema)}`);
|
|
244
|
+
}
|
|
245
|
+
if (hasBody) {
|
|
246
|
+
params.push(`body: dict[str, Any]`);
|
|
247
|
+
}
|
|
248
|
+
if (queryParams.length > 0) {
|
|
249
|
+
params.push(`params: Optional[dict[str, Any]] = None`);
|
|
250
|
+
}
|
|
251
|
+
const docstring = endpoint.summary ? ` """${endpoint.summary}"""` : '';
|
|
252
|
+
lines.push(` def ${methodName}(${params.join(', ')}) -> requests.Response:`);
|
|
253
|
+
if (docstring)
|
|
254
|
+
lines.push(docstring);
|
|
255
|
+
// Build URL
|
|
256
|
+
let urlPath = endpoint.path;
|
|
257
|
+
for (const pp of pathParams) {
|
|
258
|
+
urlPath = urlPath.replace(`{${pp.name}}`, `{${pp.name}}`);
|
|
259
|
+
}
|
|
260
|
+
lines.push(` url = f"{self.base_url}${urlPath}"`);
|
|
261
|
+
const kwargs = [];
|
|
262
|
+
if (hasBody)
|
|
263
|
+
kwargs.push('json=body');
|
|
264
|
+
if (queryParams.length > 0)
|
|
265
|
+
kwargs.push('params=params');
|
|
266
|
+
lines.push(` response = self.session.${endpoint.method.toLowerCase()}(url${kwargs.length ? ', ' + kwargs.join(', ') : ''})`);
|
|
267
|
+
lines.push(` response.raise_for_status()`);
|
|
268
|
+
lines.push(` return response`);
|
|
269
|
+
lines.push('');
|
|
270
|
+
}
|
|
271
|
+
return lines.join('\n');
|
|
272
|
+
}
|
|
273
|
+
function pyType(schema) {
|
|
274
|
+
if (!schema)
|
|
275
|
+
return 'Any';
|
|
276
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
277
|
+
switch (type) {
|
|
278
|
+
case 'string':
|
|
279
|
+
return 'str';
|
|
280
|
+
case 'integer':
|
|
281
|
+
return 'int';
|
|
282
|
+
case 'number':
|
|
283
|
+
return 'float';
|
|
284
|
+
case 'boolean':
|
|
285
|
+
return 'bool';
|
|
286
|
+
case 'array':
|
|
287
|
+
return `list[${pyType(schema.items)}]`;
|
|
288
|
+
case 'object':
|
|
289
|
+
return 'dict[str, Any]';
|
|
290
|
+
default:
|
|
291
|
+
return 'Any';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ── Go Client ───────────────────────────────────────────────────────
|
|
295
|
+
function generateGoClient(spec) {
|
|
296
|
+
const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
|
|
297
|
+
const lines = [];
|
|
298
|
+
lines.push(`// Auto-generated API client for ${spec.info.title} v${spec.info.version}`);
|
|
299
|
+
lines.push(`// Generated by TRUSS API MCP`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
lines.push('package apiclient');
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push('import (');
|
|
304
|
+
lines.push('\t"bytes"');
|
|
305
|
+
lines.push('\t"encoding/json"');
|
|
306
|
+
lines.push('\t"fmt"');
|
|
307
|
+
lines.push('\t"io"');
|
|
308
|
+
lines.push('\t"net/http"');
|
|
309
|
+
lines.push('\t"net/url"');
|
|
310
|
+
lines.push(')');
|
|
311
|
+
lines.push('');
|
|
312
|
+
// Generate structs for request/response types
|
|
313
|
+
for (const endpoint of spec.endpoints) {
|
|
314
|
+
if (endpoint.requestBody?.properties) {
|
|
315
|
+
const structName = pascalCase(endpointToMethodName(endpoint)) + 'Request';
|
|
316
|
+
lines.push(schemaToGoStruct(structName, endpoint.requestBody));
|
|
317
|
+
lines.push('');
|
|
318
|
+
}
|
|
319
|
+
const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
|
|
320
|
+
if (okResp?.schema?.properties) {
|
|
321
|
+
const structName = pascalCase(endpointToMethodName(endpoint)) + 'Response';
|
|
322
|
+
lines.push(schemaToGoStruct(structName, okResp.schema));
|
|
323
|
+
lines.push('');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Client struct
|
|
327
|
+
lines.push('// Client is the API client.');
|
|
328
|
+
lines.push('type Client struct {');
|
|
329
|
+
lines.push('\tBaseURL string');
|
|
330
|
+
lines.push('\tHTTPClient *http.Client');
|
|
331
|
+
lines.push('\tHeaders map[string]string');
|
|
332
|
+
lines.push('}');
|
|
333
|
+
lines.push('');
|
|
334
|
+
// Constructor
|
|
335
|
+
lines.push('// NewClient creates a new API client.');
|
|
336
|
+
lines.push(`func NewClient(baseURL string) *Client {`);
|
|
337
|
+
lines.push('\tif baseURL == "" {');
|
|
338
|
+
lines.push(`\t\tbaseURL = "${baseUrl}"`);
|
|
339
|
+
lines.push('\t}');
|
|
340
|
+
lines.push('\treturn &Client{');
|
|
341
|
+
lines.push('\t\tBaseURL: baseURL,');
|
|
342
|
+
lines.push('\t\tHTTPClient: &http.Client{},');
|
|
343
|
+
lines.push('\t\tHeaders: map[string]string{');
|
|
344
|
+
lines.push('\t\t\t"Content-Type": "application/json",');
|
|
345
|
+
lines.push('\t\t},');
|
|
346
|
+
lines.push('\t}');
|
|
347
|
+
lines.push('}');
|
|
348
|
+
lines.push('');
|
|
349
|
+
// Helper method
|
|
350
|
+
lines.push('func (c *Client) do(method, path string, body interface{}, result interface{}) error {');
|
|
351
|
+
lines.push('\tvar bodyReader io.Reader');
|
|
352
|
+
lines.push('\tif body != nil {');
|
|
353
|
+
lines.push('\t\tdata, err := json.Marshal(body)');
|
|
354
|
+
lines.push('\t\tif err != nil {');
|
|
355
|
+
lines.push('\t\t\treturn fmt.Errorf("marshal request: %w", err)');
|
|
356
|
+
lines.push('\t\t}');
|
|
357
|
+
lines.push('\t\tbodyReader = bytes.NewReader(data)');
|
|
358
|
+
lines.push('\t}');
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push('\treq, err := http.NewRequest(method, c.BaseURL+path, bodyReader)');
|
|
361
|
+
lines.push('\tif err != nil {');
|
|
362
|
+
lines.push('\t\treturn fmt.Errorf("create request: %w", err)');
|
|
363
|
+
lines.push('\t}');
|
|
364
|
+
lines.push('\tfor k, v := range c.Headers {');
|
|
365
|
+
lines.push('\t\treq.Header.Set(k, v)');
|
|
366
|
+
lines.push('\t}');
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push('\tresp, err := c.HTTPClient.Do(req)');
|
|
369
|
+
lines.push('\tif err != nil {');
|
|
370
|
+
lines.push('\t\treturn fmt.Errorf("execute request: %w", err)');
|
|
371
|
+
lines.push('\t}');
|
|
372
|
+
lines.push('\tdefer resp.Body.Close()');
|
|
373
|
+
lines.push('');
|
|
374
|
+
lines.push('\tif resp.StatusCode >= 400 {');
|
|
375
|
+
lines.push('\t\treturn fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)');
|
|
376
|
+
lines.push('\t}');
|
|
377
|
+
lines.push('');
|
|
378
|
+
lines.push('\tif result != nil {');
|
|
379
|
+
lines.push('\t\tif err := json.NewDecoder(resp.Body).Decode(result); err != nil {');
|
|
380
|
+
lines.push('\t\t\treturn fmt.Errorf("decode response: %w", err)');
|
|
381
|
+
lines.push('\t\t}');
|
|
382
|
+
lines.push('\t}');
|
|
383
|
+
lines.push('\treturn nil');
|
|
384
|
+
lines.push('}');
|
|
385
|
+
lines.push('');
|
|
386
|
+
// Generate methods
|
|
387
|
+
for (const endpoint of spec.endpoints) {
|
|
388
|
+
const methodName = pascalCase(endpointToMethodName(endpoint));
|
|
389
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
|
390
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
|
|
391
|
+
const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
|
|
392
|
+
// Build parameters
|
|
393
|
+
const goParams = [];
|
|
394
|
+
for (const pp of pathParams) {
|
|
395
|
+
goParams.push(`${pp.name} ${goType(pp.schema)}`);
|
|
396
|
+
}
|
|
397
|
+
if (hasBody) {
|
|
398
|
+
const bodyStruct = pascalCase(endpointToMethodName(endpoint)) + 'Request';
|
|
399
|
+
const hasStruct = endpoint.requestBody?.properties;
|
|
400
|
+
goParams.push(`body ${hasStruct ? '*' + bodyStruct : 'interface{}'}`);
|
|
401
|
+
}
|
|
402
|
+
if (queryParams.length > 0) {
|
|
403
|
+
goParams.push(`params map[string]string`);
|
|
404
|
+
}
|
|
405
|
+
const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
|
|
406
|
+
const hasResponseStruct = okResp?.schema?.properties;
|
|
407
|
+
const respType = hasResponseStruct
|
|
408
|
+
? '*' + pascalCase(endpointToMethodName(endpoint)) + 'Response'
|
|
409
|
+
: 'error';
|
|
410
|
+
const returnType = hasResponseStruct ? `(${respType}, error)` : 'error';
|
|
411
|
+
if (endpoint.summary) {
|
|
412
|
+
lines.push(`// ${methodName} ${endpoint.summary}`);
|
|
413
|
+
}
|
|
414
|
+
lines.push(`func (c *Client) ${methodName}(${goParams.join(', ')}) ${returnType} {`);
|
|
415
|
+
// Build URL path with substitutions
|
|
416
|
+
let urlPath = endpoint.path;
|
|
417
|
+
for (const pp of pathParams) {
|
|
418
|
+
urlPath = urlPath.replace(`{${pp.name}}`, `" + fmt.Sprintf("%v", ${pp.name}) + "`);
|
|
419
|
+
}
|
|
420
|
+
// Clean up empty concatenations
|
|
421
|
+
urlPath = urlPath.replace(/\+ ""$/, '').replace(/"" \+ /, '');
|
|
422
|
+
if (queryParams.length > 0) {
|
|
423
|
+
lines.push(`\tpath := "${urlPath}"`);
|
|
424
|
+
lines.push(`\tif len(params) > 0 {`);
|
|
425
|
+
lines.push(`\t\tv := url.Values{}`);
|
|
426
|
+
lines.push(`\t\tfor k, val := range params {`);
|
|
427
|
+
lines.push(`\t\t\tv.Set(k, val)`);
|
|
428
|
+
lines.push(`\t\t}`);
|
|
429
|
+
lines.push(`\t\tpath += "?" + v.Encode()`);
|
|
430
|
+
lines.push(`\t}`);
|
|
431
|
+
if (hasResponseStruct) {
|
|
432
|
+
lines.push(`\tvar result ${pascalCase(endpointToMethodName(endpoint))}Response`);
|
|
433
|
+
lines.push(`\terr := c.do("${endpoint.method}", path, ${hasBody ? 'body' : 'nil'}, &result)`);
|
|
434
|
+
lines.push(`\treturn &result, err`);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
lines.push(`\treturn c.do("${endpoint.method}", path, ${hasBody ? 'body' : 'nil'}, nil)`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
if (hasResponseStruct) {
|
|
442
|
+
lines.push(`\tvar result ${pascalCase(endpointToMethodName(endpoint))}Response`);
|
|
443
|
+
lines.push(`\terr := c.do("${endpoint.method}", "${urlPath}", ${hasBody ? 'body' : 'nil'}, &result)`);
|
|
444
|
+
lines.push(`\treturn &result, err`);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
lines.push(`\treturn c.do("${endpoint.method}", "${urlPath}", ${hasBody ? 'body' : 'nil'}, nil)`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
lines.push('}');
|
|
451
|
+
lines.push('');
|
|
452
|
+
}
|
|
453
|
+
return lines.join('\n');
|
|
454
|
+
}
|
|
455
|
+
function schemaToGoStruct(name, schema) {
|
|
456
|
+
const lines = [`type ${name} struct {`];
|
|
457
|
+
if (schema.properties) {
|
|
458
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
459
|
+
const fieldName = pascalCase(key);
|
|
460
|
+
const tag = `\`json:"${key}"\``;
|
|
461
|
+
lines.push(`\t${fieldName} ${goType(prop)} ${tag}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
lines.push('}');
|
|
465
|
+
return lines.join('\n');
|
|
466
|
+
}
|
|
467
|
+
function goType(schema) {
|
|
468
|
+
if (!schema)
|
|
469
|
+
return 'interface{}';
|
|
470
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
471
|
+
switch (type) {
|
|
472
|
+
case 'string':
|
|
473
|
+
return 'string';
|
|
474
|
+
case 'integer':
|
|
475
|
+
return 'int64';
|
|
476
|
+
case 'number':
|
|
477
|
+
return 'float64';
|
|
478
|
+
case 'boolean':
|
|
479
|
+
return 'bool';
|
|
480
|
+
case 'array':
|
|
481
|
+
return `[]${goType(schema.items)}`;
|
|
482
|
+
case 'object':
|
|
483
|
+
return 'map[string]interface{}';
|
|
484
|
+
default:
|
|
485
|
+
return 'interface{}';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// ── Test Generation ─────────────────────────────────────────────────
|
|
489
|
+
export function generateTests(spec, baseUrl, framework = 'vitest') {
|
|
490
|
+
switch (framework) {
|
|
491
|
+
case 'jest':
|
|
492
|
+
case 'vitest':
|
|
493
|
+
return generateJsTests(spec, baseUrl, framework);
|
|
494
|
+
case 'pytest':
|
|
495
|
+
return generatePytestTests(spec, baseUrl);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function generateJsTests(spec, baseUrl, framework) {
|
|
499
|
+
const lines = [];
|
|
500
|
+
lines.push(`// Auto-generated API tests for ${spec.info.title}`);
|
|
501
|
+
lines.push(`// Generated by TRUSS API MCP`);
|
|
502
|
+
lines.push('');
|
|
503
|
+
if (framework === 'vitest') {
|
|
504
|
+
lines.push(`import { describe, it, expect } from 'vitest';`);
|
|
505
|
+
}
|
|
506
|
+
lines.push('');
|
|
507
|
+
lines.push(`const BASE_URL = '${baseUrl}';`);
|
|
508
|
+
lines.push('');
|
|
509
|
+
// Group by tags or paths
|
|
510
|
+
const groups = groupEndpoints(spec.endpoints);
|
|
511
|
+
for (const [groupName, endpoints] of groups) {
|
|
512
|
+
lines.push(`describe('${groupName}', () => {`);
|
|
513
|
+
for (const endpoint of endpoints) {
|
|
514
|
+
const testName = `${endpoint.method} ${endpoint.path}`;
|
|
515
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
|
516
|
+
// Happy path test
|
|
517
|
+
lines.push(` it('${testName} — returns success', async () => {`);
|
|
518
|
+
// Build URL with sample path params
|
|
519
|
+
let testUrl = endpoint.path;
|
|
520
|
+
for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
|
|
521
|
+
testUrl = testUrl.replace(`{${param.name}}`, sampleValue(param.schema, param.name));
|
|
522
|
+
}
|
|
523
|
+
// Add query params
|
|
524
|
+
const queryParams = endpoint.parameters.filter((p) => p.in === 'query' && p.required);
|
|
525
|
+
if (queryParams.length > 0) {
|
|
526
|
+
const qs = queryParams
|
|
527
|
+
.map((q) => `${q.name}=${encodeURIComponent(sampleValue(q.schema, q.name))}`)
|
|
528
|
+
.join('&');
|
|
529
|
+
testUrl += `?${qs}`;
|
|
530
|
+
}
|
|
531
|
+
lines.push(` const response = await fetch(\`\${BASE_URL}${testUrl}\`, {`);
|
|
532
|
+
lines.push(` method: '${endpoint.method}',`);
|
|
533
|
+
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
534
|
+
if (hasBody && endpoint.requestBody) {
|
|
535
|
+
const sampleBody = generateSampleBody(endpoint.requestBody);
|
|
536
|
+
lines.push(` body: JSON.stringify(${JSON.stringify(sampleBody, null, 6).replace(/\n/g, '\n ')}),`);
|
|
537
|
+
}
|
|
538
|
+
lines.push(` });`);
|
|
539
|
+
lines.push('');
|
|
540
|
+
// Assertions
|
|
541
|
+
const okStatus = endpoint.responses.find((r) => r.status === '200' || r.status === '201' || r.status === '204');
|
|
542
|
+
const expectedStatus = okStatus ? parseInt(okStatus.status, 10) : 200;
|
|
543
|
+
lines.push(` expect(response.status).toBe(${expectedStatus});`);
|
|
544
|
+
if (expectedStatus !== 204) {
|
|
545
|
+
lines.push(` const data = await response.json();`);
|
|
546
|
+
lines.push(` expect(data).toBeDefined();`);
|
|
547
|
+
// Add property checks if schema is known
|
|
548
|
+
if (okStatus?.schema?.properties) {
|
|
549
|
+
for (const key of Object.keys(okStatus.schema.properties)) {
|
|
550
|
+
lines.push(` expect(data).toHaveProperty('${key}');`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
lines.push(` });`);
|
|
555
|
+
lines.push('');
|
|
556
|
+
// 404 test for endpoints with path params
|
|
557
|
+
if (endpoint.parameters.some((p) => p.in === 'path')) {
|
|
558
|
+
lines.push(` it('${testName} — returns 404 for invalid ID', async () => {`);
|
|
559
|
+
let badUrl = endpoint.path;
|
|
560
|
+
for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
|
|
561
|
+
badUrl = badUrl.replace(`{${param.name}}`, '99999999');
|
|
562
|
+
}
|
|
563
|
+
lines.push(` const response = await fetch(\`\${BASE_URL}${badUrl}\`, {`);
|
|
564
|
+
lines.push(` method: '${endpoint.method}',`);
|
|
565
|
+
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
566
|
+
lines.push(` });`);
|
|
567
|
+
lines.push(` expect(response.status).toBe(404);`);
|
|
568
|
+
lines.push(` });`);
|
|
569
|
+
lines.push('');
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
lines.push('});');
|
|
573
|
+
lines.push('');
|
|
574
|
+
}
|
|
575
|
+
return lines.join('\n');
|
|
576
|
+
}
|
|
577
|
+
function generatePytestTests(spec, baseUrl) {
|
|
578
|
+
const lines = [];
|
|
579
|
+
lines.push(`"""Auto-generated API tests for ${spec.info.title}"""`);
|
|
580
|
+
lines.push(`# Generated by TRUSS API MCP`);
|
|
581
|
+
lines.push('');
|
|
582
|
+
lines.push('import pytest');
|
|
583
|
+
lines.push('import requests');
|
|
584
|
+
lines.push('');
|
|
585
|
+
lines.push(`BASE_URL = "${baseUrl}"`);
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push('');
|
|
588
|
+
for (const endpoint of spec.endpoints) {
|
|
589
|
+
const funcName = `test_${endpoint.method.toLowerCase()}_${endpoint.path.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`;
|
|
590
|
+
const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
|
591
|
+
let testUrl = endpoint.path;
|
|
592
|
+
for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
|
|
593
|
+
testUrl = testUrl.replace(`{${param.name}}`, sampleValue(param.schema, param.name));
|
|
594
|
+
}
|
|
595
|
+
lines.push(`def ${funcName}():`);
|
|
596
|
+
if (endpoint.summary) {
|
|
597
|
+
lines.push(` """${endpoint.summary}"""`);
|
|
598
|
+
}
|
|
599
|
+
lines.push(` url = f"{BASE_URL}${testUrl}"`);
|
|
600
|
+
if (hasBody && endpoint.requestBody) {
|
|
601
|
+
const sampleBody = generateSampleBody(endpoint.requestBody);
|
|
602
|
+
lines.push(` payload = ${JSON.stringify(sampleBody)}`);
|
|
603
|
+
lines.push(` response = requests.${endpoint.method.toLowerCase()}(url, json=payload, headers={"Content-Type": "application/json"})`);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
lines.push(` response = requests.${endpoint.method.toLowerCase()}(url, headers={"Content-Type": "application/json"})`);
|
|
607
|
+
}
|
|
608
|
+
const okStatus = endpoint.responses.find((r) => r.status === '200' || r.status === '201' || r.status === '204');
|
|
609
|
+
const expectedStatus = okStatus ? parseInt(okStatus.status, 10) : 200;
|
|
610
|
+
lines.push(` assert response.status_code == ${expectedStatus}`);
|
|
611
|
+
if (expectedStatus !== 204) {
|
|
612
|
+
lines.push(` data = response.json()`);
|
|
613
|
+
lines.push(` assert data is not None`);
|
|
614
|
+
}
|
|
615
|
+
lines.push('');
|
|
616
|
+
lines.push('');
|
|
617
|
+
}
|
|
618
|
+
return lines.join('\n');
|
|
619
|
+
}
|
|
620
|
+
// ── Mock Server Generation ──────────────────────────────────────────
|
|
621
|
+
export function generateMockServer(spec) {
|
|
622
|
+
const routes = [];
|
|
623
|
+
const mockData = {};
|
|
624
|
+
for (const endpoint of spec.endpoints) {
|
|
625
|
+
const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
|
|
626
|
+
const status = okResp ? parseInt(okResp.status, 10) : 200;
|
|
627
|
+
let mockResponse;
|
|
628
|
+
if (okResp?.schema) {
|
|
629
|
+
mockResponse = generateMockData(okResp.schema, endpoint.path);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
mockResponse = { message: 'OK' };
|
|
633
|
+
}
|
|
634
|
+
// Convert OpenAPI path params to Express-style for json-server
|
|
635
|
+
const routePath = endpoint.path.replace(/\{(\w+)\}/g, ':$1');
|
|
636
|
+
routes.push({
|
|
637
|
+
method: endpoint.method,
|
|
638
|
+
path: routePath,
|
|
639
|
+
status,
|
|
640
|
+
response: mockResponse,
|
|
641
|
+
headers: { 'Content-Type': 'application/json' },
|
|
642
|
+
});
|
|
643
|
+
// Store in mock_data for json-server db.json format
|
|
644
|
+
const resourceName = endpoint.path
|
|
645
|
+
.split('/')[1]
|
|
646
|
+
?.replace(/[^a-zA-Z0-9]/g, '') ?? 'data';
|
|
647
|
+
if (!mockData[resourceName] && Array.isArray(mockResponse)) {
|
|
648
|
+
mockData[resourceName] = mockResponse;
|
|
649
|
+
}
|
|
650
|
+
else if (!mockData[resourceName] && typeof mockResponse === 'object') {
|
|
651
|
+
// For list endpoints, wrap in array
|
|
652
|
+
if (endpoint.method === 'GET' && !endpoint.parameters.some((p) => p.in === 'path')) {
|
|
653
|
+
mockData[resourceName] = [mockResponse];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return { routes, mock_data: mockData };
|
|
658
|
+
}
|
|
659
|
+
function generateMockData(schema, context) {
|
|
660
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
661
|
+
switch (type) {
|
|
662
|
+
case 'string': {
|
|
663
|
+
if (schema.enum)
|
|
664
|
+
return schema.enum[0];
|
|
665
|
+
if (schema.format === 'date-time')
|
|
666
|
+
return '2026-01-15T10:00:00Z';
|
|
667
|
+
if (schema.format === 'email')
|
|
668
|
+
return 'user@example.com';
|
|
669
|
+
if (schema.format === 'uri')
|
|
670
|
+
return 'https://example.com';
|
|
671
|
+
return `sample_${context.replace(/[^a-z]/gi, '_')}`;
|
|
672
|
+
}
|
|
673
|
+
case 'integer':
|
|
674
|
+
return 1;
|
|
675
|
+
case 'number':
|
|
676
|
+
return 1.0;
|
|
677
|
+
case 'boolean':
|
|
678
|
+
return true;
|
|
679
|
+
case 'array':
|
|
680
|
+
return schema.items ? [generateMockData(schema.items, context)] : [];
|
|
681
|
+
case 'object': {
|
|
682
|
+
if (!schema.properties)
|
|
683
|
+
return {};
|
|
684
|
+
const obj = {};
|
|
685
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
686
|
+
obj[key] = generateMockData(propSchema, key);
|
|
687
|
+
}
|
|
688
|
+
return obj;
|
|
689
|
+
}
|
|
690
|
+
default:
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ── Spec Comparison ─────────────────────────────────────────────────
|
|
695
|
+
export function compareSpecs(oldSpec, newSpec) {
|
|
696
|
+
const breaking = [];
|
|
697
|
+
const nonBreaking = [];
|
|
698
|
+
const newEndpoints = [];
|
|
699
|
+
const removedEndpoints = [];
|
|
700
|
+
// Build maps
|
|
701
|
+
const oldMap = new Map();
|
|
702
|
+
const newMap = new Map();
|
|
703
|
+
for (const ep of oldSpec.endpoints) {
|
|
704
|
+
oldMap.set(`${ep.method} ${ep.path}`, ep);
|
|
705
|
+
}
|
|
706
|
+
for (const ep of newSpec.endpoints) {
|
|
707
|
+
newMap.set(`${ep.method} ${ep.path}`, ep);
|
|
708
|
+
}
|
|
709
|
+
// Find removed endpoints (breaking)
|
|
710
|
+
for (const [key, ep] of oldMap) {
|
|
711
|
+
if (!newMap.has(key)) {
|
|
712
|
+
const change = {
|
|
713
|
+
path: ep.path,
|
|
714
|
+
method: ep.method,
|
|
715
|
+
description: `Endpoint removed: ${ep.method} ${ep.path}`,
|
|
716
|
+
};
|
|
717
|
+
removedEndpoints.push(change);
|
|
718
|
+
breaking.push(change);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
// Find new endpoints
|
|
722
|
+
for (const [key, ep] of newMap) {
|
|
723
|
+
if (!oldMap.has(key)) {
|
|
724
|
+
newEndpoints.push({
|
|
725
|
+
path: ep.path,
|
|
726
|
+
method: ep.method,
|
|
727
|
+
description: `New endpoint: ${ep.method} ${ep.path}`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// Compare matching endpoints
|
|
732
|
+
for (const [key, oldEp] of oldMap) {
|
|
733
|
+
const newEp = newMap.get(key);
|
|
734
|
+
if (!newEp)
|
|
735
|
+
continue;
|
|
736
|
+
// Check required parameters
|
|
737
|
+
const oldRequired = new Set(oldEp.parameters.filter((p) => p.required).map((p) => p.name));
|
|
738
|
+
const newRequired = new Set(newEp.parameters.filter((p) => p.required).map((p) => p.name));
|
|
739
|
+
// New required parameters = breaking
|
|
740
|
+
for (const name of newRequired) {
|
|
741
|
+
if (!oldRequired.has(name)) {
|
|
742
|
+
breaking.push({
|
|
743
|
+
path: oldEp.path,
|
|
744
|
+
method: oldEp.method,
|
|
745
|
+
description: `New required parameter: ${name}`,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// Removed required parameters = non-breaking (parameter relaxed)
|
|
750
|
+
for (const name of oldRequired) {
|
|
751
|
+
if (!newRequired.has(name)) {
|
|
752
|
+
nonBreaking.push({
|
|
753
|
+
path: oldEp.path,
|
|
754
|
+
method: oldEp.method,
|
|
755
|
+
description: `Required parameter removed (relaxed): ${name}`,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// New optional parameters = non-breaking
|
|
760
|
+
const oldParamNames = new Set(oldEp.parameters.map((p) => p.name));
|
|
761
|
+
for (const p of newEp.parameters) {
|
|
762
|
+
if (!oldParamNames.has(p.name) && !p.required) {
|
|
763
|
+
nonBreaking.push({
|
|
764
|
+
path: oldEp.path,
|
|
765
|
+
method: oldEp.method,
|
|
766
|
+
description: `New optional parameter: ${p.name}`,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
// Check response schema changes
|
|
771
|
+
for (const oldResp of oldEp.responses) {
|
|
772
|
+
const newResp = newEp.responses.find((r) => r.status === oldResp.status);
|
|
773
|
+
if (!newResp) {
|
|
774
|
+
breaking.push({
|
|
775
|
+
path: oldEp.path,
|
|
776
|
+
method: oldEp.method,
|
|
777
|
+
description: `Response ${oldResp.status} removed`,
|
|
778
|
+
});
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
// Check if response properties were removed (breaking)
|
|
782
|
+
if (oldResp.schema?.properties && newResp.schema?.properties) {
|
|
783
|
+
for (const key of Object.keys(oldResp.schema.properties)) {
|
|
784
|
+
if (!(key in newResp.schema.properties)) {
|
|
785
|
+
breaking.push({
|
|
786
|
+
path: oldEp.path,
|
|
787
|
+
method: oldEp.method,
|
|
788
|
+
description: `Response property '${key}' removed from ${oldResp.status}`,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// New response properties = non-breaking
|
|
793
|
+
for (const key of Object.keys(newResp.schema.properties)) {
|
|
794
|
+
if (!(key in oldResp.schema.properties)) {
|
|
795
|
+
nonBreaking.push({
|
|
796
|
+
path: oldEp.path,
|
|
797
|
+
method: oldEp.method,
|
|
798
|
+
description: `New response property '${key}' added to ${newResp.status}`,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Summary change = non-breaking
|
|
805
|
+
if (oldEp.summary !== newEp.summary) {
|
|
806
|
+
nonBreaking.push({
|
|
807
|
+
path: oldEp.path,
|
|
808
|
+
method: oldEp.method,
|
|
809
|
+
description: `Summary changed: "${oldEp.summary}" -> "${newEp.summary}"`,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
breaking_changes: breaking,
|
|
815
|
+
non_breaking: nonBreaking,
|
|
816
|
+
new_endpoints: newEndpoints,
|
|
817
|
+
removed_endpoints: removedEndpoints,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
821
|
+
function endpointToMethodName(endpoint) {
|
|
822
|
+
if (endpoint.operationId) {
|
|
823
|
+
return camelCase(endpoint.operationId);
|
|
824
|
+
}
|
|
825
|
+
const parts = endpoint.path
|
|
826
|
+
.split('/')
|
|
827
|
+
.filter((p) => p && !p.startsWith('{'))
|
|
828
|
+
.map((p) => p.replace(/[^a-zA-Z0-9]/g, ''));
|
|
829
|
+
const verb = endpoint.method.toLowerCase();
|
|
830
|
+
return camelCase(`${verb}_${parts.join('_')}`);
|
|
831
|
+
}
|
|
832
|
+
function camelCase(str) {
|
|
833
|
+
return str
|
|
834
|
+
.replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
|
|
835
|
+
.replace(/^(.)/, (_, c) => c.toLowerCase());
|
|
836
|
+
}
|
|
837
|
+
function pascalCase(str) {
|
|
838
|
+
const cc = camelCase(str);
|
|
839
|
+
return cc.charAt(0).toUpperCase() + cc.slice(1);
|
|
840
|
+
}
|
|
841
|
+
function snakeToPascal(str) {
|
|
842
|
+
return str
|
|
843
|
+
.split(/[-_]/)
|
|
844
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
845
|
+
.join('');
|
|
846
|
+
}
|
|
847
|
+
function camelToSnake(str) {
|
|
848
|
+
return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
849
|
+
}
|
|
850
|
+
function buildUrlExpr(endpoint, baseVarName) {
|
|
851
|
+
const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
|
|
852
|
+
if (pathParams.length === 0) {
|
|
853
|
+
return `\`\${${baseVarName}}${endpoint.path}\``;
|
|
854
|
+
}
|
|
855
|
+
let path = endpoint.path;
|
|
856
|
+
for (const pp of pathParams) {
|
|
857
|
+
path = path.replace(`{${pp.name}}`, `\${${pp.name}}`);
|
|
858
|
+
}
|
|
859
|
+
return `\`\${${baseVarName}}${path}\``;
|
|
860
|
+
}
|
|
861
|
+
function sampleValue(schema, name) {
|
|
862
|
+
if (!schema)
|
|
863
|
+
return '1';
|
|
864
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
865
|
+
if (type === 'integer' || type === 'number')
|
|
866
|
+
return '1';
|
|
867
|
+
if (name?.toLowerCase().includes('id'))
|
|
868
|
+
return '1';
|
|
869
|
+
return 'test';
|
|
870
|
+
}
|
|
871
|
+
function generateSampleBody(schema) {
|
|
872
|
+
if (!schema.properties)
|
|
873
|
+
return {};
|
|
874
|
+
const body = {};
|
|
875
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
876
|
+
body[key] = generateMockData(prop, key);
|
|
877
|
+
}
|
|
878
|
+
return body;
|
|
879
|
+
}
|
|
880
|
+
function groupEndpoints(endpoints) {
|
|
881
|
+
const groups = new Map();
|
|
882
|
+
for (const ep of endpoints) {
|
|
883
|
+
const group = ep.tags?.[0] ?? ep.path.split('/')[1] ?? 'API';
|
|
884
|
+
if (!groups.has(group))
|
|
885
|
+
groups.set(group, []);
|
|
886
|
+
groups.get(group).push(ep);
|
|
887
|
+
}
|
|
888
|
+
return groups;
|
|
889
|
+
}
|
|
890
|
+
//# sourceMappingURL=code-generator.js.map
|