nuxt3-api-gen
Version:
This tool generates TypeScript types and API methods based on a Swagger/OpenAPI schema.
197 lines (165 loc) • 9.44 kB
JavaScript
const fs = require('fs');
const axios = require('./vendor/axios.min.js');
const { execSync } = require('child_process');
// Чтение конфигурации из config.json
const params = JSON.parse(fs.readFileSync('config.json', 'utf8'));
const { swaggerUrl, outputFile, outputPath, rmMethodPrefix } = params;
async function generateApiFromSwagger(swaggerUrl, outputFile, outputPath) {
try {
console.log('Получение схемы Swagger...');
const response = await axios.get(swaggerUrl);
const swaggerSchema = response.data;
console.log('Генерация типов TypeScript...');
// Проверка и создание выходной директории
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
const typesOutputFile = `${outputPath}${outputFile.replace('.ts', '.types.ts')}`;
try {
execSync(`npx openapi-typescript ${swaggerUrl} -o ${typesOutputFile}`);
console.log(`Типы успешно сгенерированы с помощью CLI`);
} catch (cliError) {
console.log('Переход к программной генерации...');
}
console.log('Генерация методов API...');
let apiMethods = `// Автосгенерированный клиент API\n`;
apiMethods += `// Сгенерировано из ${swaggerUrl}\n\n`;
apiMethods += `import type { paths } from './${outputFile.replace('.ts', '')}.types';\n`;
apiMethods += `export const apiMethods = (options?: any) => {\n`;
apiMethods += ` return {\n`;
const groups = {};
if (!swaggerSchema.paths) {
throw new Error('В схеме Swagger не найдены пути');
}
// Создаем группы на основе тегов
for (const [path, pathItem] of Object.entries(swaggerSchema.paths)) {
if (!pathItem) continue;
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
const operation = pathItem[method];
if (!operation) continue;
// Получаем теги операции или используем дефолтную группу
const tags = operation.tags || ['default'];
const groupName = tags[0]; // Берем первый тег как основную группу
if (!groups[groupName]) {
groups[groupName] = [];
}
// Генерация имени метода
let methodName = operation.operationId;
if (!methodName) {
// Если operationId не указан, генерируем имя на основе пути и метода
const pathParts = path.split('/').filter(p => p && !p.startsWith('{'));
methodName = method.toLowerCase() +
pathParts.map(part =>
part.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('')
).join('');
}
// Очистка имени метода
methodName = methodName
.replace(/[^a-zA-Z0-9_$]/g, '_')
.replace(/^[0-9]/, '_$&');
// Определение параметров
const pathParams = (path.match(/{([^}]+)}/g) || []).map(p => p.replace(/{|}/g, ''));
const hasPathParams = pathParams.length > 0;
const hasQueryParams = operation.parameters?.some(p => p.in === 'query');
const hasBody = ['post', 'put', 'patch'].includes(method) && operation.requestBody;
// Определение типа контента для запроса и ответа
let requestContentType = 'application/json';
let responseContentType = 'application/json';
if (hasBody) {
const contentTypes = operation.requestBody?.content ? Object.keys(operation.requestBody.content) : [];
requestContentType = contentTypes.length > 0 ? contentTypes[0] : 'application/json';
}
const excludeStatusCodeStarts = ['4', '5']; // 400, 500 exclude error status codes;
const successResponse = Object.entries(operation.responses || {})
.find(([code]) => !excludeStatusCodeStarts.includes(String(code)[0])) || [];
const responseContentTypes = successResponse[1]?.content
? Object.keys(successResponse[1].content)
: [];
responseContentType = responseContentTypes.length > 0 ? responseContentTypes[0] : 'application/json';
// Генерация сигнатуры метода
let apiLine = '';
const responseCode = successResponse[0] || '200';
// Безопасное определение типа ответа
let responseType = 'unknown';
if (successResponse[1]?.content) {
const contentTypes = Object.keys(successResponse[1].content);
if (contentTypes.length > 0) {
responseType = `paths['${path}']['${method}']['responses']['${responseCode}']['content']['${contentTypes[0]}']`;
} else {
responseType = 'void';
}
} else if (successResponse[1]) {
responseType = `paths['${path}']['${method}']['responses']['${responseCode}']`;
} else {
responseType = 'void';
}
if (!hasPathParams && !hasQueryParams && !hasBody) {
apiLine = `${methodName}: () => useApi<${responseType}>('${path}', { method: '${method.toUpperCase()}', ...options }),`;
} else if (method === 'delete' && pathParams.length === 1 && pathParams[0] === 'id') {
apiLine = `${methodName}: (id: string | number) => useApi<void>(\`${path.replace('{id}', '${id}')}\`, { method: 'DELETE', ...options }),`;
} else {
const pathParamsStr = pathParams.map(p => `${p}: string | number`).join(', ');
const pathWithParams = pathParams.reduce((acc, param) =>
acc.replace(`{${param}}`, `\${${param}}`), path);
if (hasBody) {
let bodyType = 'unknown';
if (operation.requestBody?.content) {
const contentTypes = Object.keys(operation.requestBody.content);
if (contentTypes.length > 0) {
bodyType = `paths['${path}']['${method}']['requestBody']['content']['${contentTypes[0]}']`;
}
}
const isMultipart = requestContentType === 'multipart/form-data';
const bodyParam = isMultipart ? 'formData' : 'body';
if (hasQueryParams) {
const queryParamsType = operation.parameters?.some(p => p.in === 'query')
? `paths['${path}']['${method}']['parameters']['query']`
: 'Record<string, unknown>';
apiLine = `${methodName}: (${pathParamsStr ? pathParamsStr + ', ' : ''}${bodyParam}: ${bodyType}, query?: ${queryParamsType}) => ` +
`useApi<${responseType}>(\`${pathWithParams}\`, { ` +
`method: '${method.toUpperCase()}', ` +
`${isMultipart ? 'formData' : 'body'}: ${bodyParam}, ` +
`params: query, ...options }),`;
} else {
apiLine = `${methodName}: (${pathParamsStr ? pathParamsStr + ', ' : ''}${bodyParam}: ${bodyType}) => ` +
`useApi<${responseType}>(\`${pathWithParams}\`, { ` +
`method: '${method.toUpperCase()}', ` +
`${isMultipart ? 'formData' : 'body'}: ${bodyParam}, ...options }),`;
}
} else if (hasQueryParams) {
const queryParamsType = operation.parameters?.some(p => p.in === 'query')
? `paths['${path}']['${method}']['parameters']['query']`
: 'Record<string, unknown>';
apiLine = `${methodName}: (${pathParamsStr ? pathParamsStr + ', ' : ''}query?: ${queryParamsType}) => ` +
`useApi<${responseType}>(\`${pathWithParams}\`, { method: '${method.toUpperCase()}', params: query, ...options }),`;
} else {
apiLine = `${methodName}: (${pathParamsStr}) => useApi<${responseType}>(\`${pathWithParams}\`, { method: '${method.toUpperCase()}', ...options }),`;
}
}
const apiLineFormat = rmMethodPrefix ? apiLine.replace(`${groupName}_`, '') : apiLine;
groups[groupName].push(` ${apiLineFormat}`);
}
}
// Формирование окончательного вывода с группировкой по тегам
for (const [groupName, methods] of Object.entries(groups)) {
const groupNameFormat = groupName
.replace(/[^a-zA-Z0-9_$]/g, '_')
.replace(/^[0-9]/, '_$&');
apiMethods += ` ${groupNameFormat}: {\n${methods.join('\n')}\n },\n`;
}
apiMethods += ' };\n';
apiMethods += '};\n';
fs.writeFileSync(`${outputPath}${outputFile}`, apiMethods);
console.log(`Успешно сгенерировано:
- Типы: ${typesOutputFile}
- Методы API: ${outputPath}${outputFile}`
);
} catch (error) {
console.error('Ошибка при генерации API:', error);
process.exit(1);
}
}
// Выполнение
generateApiFromSwagger(swaggerUrl, outputFile, outputPath).catch(console.error);