@hestjs/scalar
Version:
HestJS Scalar API Reference Integration - Beautiful API documentation for HestJS applications
215 lines • 7.43 kB
JavaScript
import 'reflect-metadata';
/**
* OpenAPI 文档生成器
*/
export class OpenAPIGenerator {
config;
paths = {};
components = {
schemas: {},
responses: {},
parameters: {},
examples: {},
requestBodies: {},
headers: {},
securitySchemes: {},
links: {},
callbacks: {},
};
constructor(config) {
this.config = config;
if (config.components) {
this.components = { ...this.components, ...config.components };
}
}
/**
* 从控制器类生成 OpenAPI 路径
*/
addController(controller, basePath = '') {
// console.log(`Processing controller: ${controller.name}, basePath: ${basePath}`);
const controllerTags = Reflect.getMetadata('openapi:tags', controller) || [];
// console.log(`Controller tags:`, controllerTags);
// 获取HestJS的路由元数据(存储在控制器类上)
const HEST_ROUTE_KEY = Symbol.for('hest:route');
const routes = Reflect.getMetadata(HEST_ROUTE_KEY, controller) || [];
// console.log(`Found routes:`, routes);
for (const route of routes) {
const { method, path, methodName } = route;
const fullPath = this.joinPaths(basePath, path);
// console.log(`Adding route: ${method.toUpperCase()} ${fullPath} (method: ${methodName})`);
// 确保路径存在
if (!this.paths[fullPath]) {
this.paths[fullPath] = {};
}
// 生成操作对象
const operation = this.generateOperation(controller, methodName, controllerTags);
this.paths[fullPath][method.toLowerCase()] = operation;
}
// 处理类级别的schema
this.addSchemaFromClass(controller);
}
/**
* 生成操作对象
*/
generateOperation(controller, methodName, defaultTags) {
const prototype = controller.prototype;
// 基础操作信息
const operationMetadata = Reflect.getMetadata('openapi:operation', prototype, methodName) || {};
const operation = {
tags: operationMetadata.tags || defaultTags,
summary: operationMetadata.summary,
description: operationMetadata.description,
operationId: operationMetadata.operationId || `${controller.name}_${methodName}`,
responses: {},
...operationMetadata
};
// 参数
const parameters = Reflect.getMetadata('openapi:parameters', prototype, methodName);
if (parameters && parameters.length > 0) {
operation.parameters = parameters;
}
// 请求体
const requestBody = Reflect.getMetadata('openapi:requestBody', prototype, methodName);
if (requestBody) {
operation.requestBody = requestBody;
}
// 响应
const responses = Reflect.getMetadata('openapi:responses', prototype, methodName);
if (responses && Object.keys(responses).length > 0) {
operation.responses = responses;
}
else {
// 默认响应
operation.responses = {
'200': {
description: 'Success'
}
};
}
// 安全
const security = Reflect.getMetadata('openapi:security', prototype, methodName);
if (security) {
operation.security = security;
}
return operation;
}
/**
* 从类添加 Schema
*/
addSchemaFromClass(target) {
const schema = Reflect.getMetadata('openapi:schema', target);
if (schema) {
const schemaName = target.name;
this.components.schemas[schemaName] = schema;
}
// 处理属性级别的schema
const properties = Reflect.getMetadata('openapi:properties', target);
if (properties && Object.keys(properties).length > 0) {
const schemaName = target.name;
if (!this.components.schemas[schemaName]) {
this.components.schemas[schemaName] = {
type: 'object',
properties: {},
};
}
const existingSchema = this.components.schemas[schemaName];
if (!existingSchema.properties) {
existingSchema.properties = {};
}
// 合并属性
Object.assign(existingSchema.properties, properties);
// 收集required字段
const required = [];
for (const [propName, propSchema] of Object.entries(properties)) {
if (propSchema.required === true) {
required.push(propName);
}
}
if (required.length > 0) {
existingSchema.required = [...(existingSchema.required || []), ...required];
}
}
}
/**
* 标准化路径
*/
normalizePath(path) {
// 将 :id 格式转换为 {id} 格式
let normalized = path.replace(/:([^/]+)/g, '{$1}');
// 移除末尾的斜杠(除非是根路径)
if (normalized !== '/' && normalized.endsWith('/')) {
normalized = normalized.slice(0, -1);
}
return normalized;
}
/**
* 拼接路径
*/
joinPaths(basePath, path) {
// 规范化基础路径
basePath = basePath.replace(/\/$/, ''); // 移除末尾斜杠
// 规范化路径
if (path === '/') {
// 如果路径是根路径,直接使用基础路径
return basePath || '/';
}
else if (!path.startsWith('/')) {
// 如果路径不以斜杠开头,添加斜杠
path = '/' + path;
}
const fullPath = basePath + path;
return this.normalizePath(fullPath);
}
/**
* 添加全局组件
*/
addComponent(type, name, component) {
if (!this.components[type]) {
this.components[type] = {};
}
this.components[type][name] = component;
}
/**
* 生成完整的 OpenAPI 文档
*/
generateDocument() {
// 清理空的组件
const cleanComponents = {};
for (const [key, value] of Object.entries(this.components)) {
if (value && Object.keys(value).length > 0) {
cleanComponents[key] = value;
}
}
return {
openapi: '3.0.0',
info: this.config.info,
servers: this.config.servers,
paths: this.paths,
components: Object.keys(cleanComponents).length > 0 ? cleanComponents : undefined,
security: this.config.security,
tags: this.config.tags,
externalDocs: this.config.externalDocs,
};
}
/**
* 重置生成器
*/
reset() {
this.paths = {};
this.components = {
schemas: {},
responses: {},
parameters: {},
examples: {},
requestBodies: {},
headers: {},
securitySchemes: {},
links: {},
callbacks: {},
};
if (this.config.components) {
this.components = { ...this.components, ...this.config.components };
}
}
}
//# sourceMappingURL=openapi-generator.js.map