// 接口文档服务 import { FireDocumentBodyInterFace, FireDocumentHeadInterFace, FireDocumentInterFace, FireDocumentStoreInterFace, InterceptorType } from "../types"; import {FireCatRouter} from "../router/router"; import {fixedEndPath} from "../utils/common"; export class FireDocument { static documents: FireDocumentStoreInterFace[] = []; static appendDocument(path: string, routes: FireDocumentStoreInterFace['routes']) { FireDocument.documents.push({ path, routes }); } static createDocumentPayload(router: FireCatRouter, config: FireDocumentHeadInterFace): FireDocumentInterFace { const body: FireDocumentBodyInterFace[] = []; router.getDocumentStore().forEach(item => { item.routes.forEach(route => { const mission: FireDocumentBodyInterFace = { path: fixedEndPath(route.path || item.path), methods: route.method, rule: [], description: route.description, }; route.middlewares.forEach(intItem => { if (intItem.type == InterceptorType.RULE) { mission.rule.push(intItem.data); } }); body.push(mission); }); }); body.sort((prev, next) => prev.path.localeCompare(next.path)); return { title: config.title, description: config.description, date: config.date, version: config.version, body, }; } static server(router: FireCatRouter, path: string, config: FireDocumentHeadInterFace) { const normalizedPath = fixedEndPath(path); const jsonPath = fixedEndPath(`${normalizedPath}/json`); router.router.get(normalizedPath, (ctx)=> { const doc = FireDocument.createDocumentPayload(router, config); if (ctx.query.format === 'json') { ctx.body = doc; return; } ctx.type = 'text/html; charset=utf-8'; ctx.body = FireDocument.renderDocumentPage(doc, jsonPath); }); router.router.get(jsonPath, (ctx)=> { ctx.body = FireDocument.createDocumentPayload(router, config); }); } static renderDocumentPage(doc: FireDocumentInterFace, jsonPath: string) { const title = escapeHtml(doc.title || 'API Document'); const description = escapeHtml(doc.description || 'FireCat document service'); const version = escapeHtml(String(doc.version || '1.0.0')); const date = escapeHtml(formatDocDate(doc.date)); const routeCount = doc.body.length; const sections = createSections(doc.body); const navHtml = sections.map(section => { const children = section.items.map(item => { return `${item.methods.toUpperCase()}${escapeHtml(item.path)}`; }).join(''); return ` `; }).join(''); const contentHtml = sections.map(section => { const itemsHtml = section.items.map(item => renderEndpointCard(item)).join(''); return `

${escapeHtml(section.title)}

${section.items.length} endpoint${section.items.length > 1 ? 's' : ''}

${itemsHtml}
`; }).join(''); return ` ${title}
Document Overview

${title}

${description}。文档结构参考 Swagger 的阅读方式,按分组浏览接口,展开后可以直接查看路径、方法和请求参数规则。

接口总数 ${routeCount}
当前版本 ${version}
最后更新 ${date}
${contentHtml}
`; } } function renderEndpointCard(item: EndpointViewModel) { const description = escapeHtml(item.description || 'No description provided.'); const rules = item.rule.length > 0 ? `
${item.rule.map(rule => renderRuleCard(rule)).join('')}
` : `
该接口当前没有配置请求验证规则。
`; return `
${item.methods.toUpperCase()} ${escapeHtml(item.path)} ${description}
${item.rule.length} params
Method ${item.methods.toUpperCase()}
Path ${escapeHtml(item.path)}
${description}

Parameters

${rules}
`; } function renderRuleCard(rule: Record) { const rows = Object.entries(rule).map(([field, config]) => { const detailConfig = config || {}; const type = formatRuleValue(detailConfig.type); const required = detailConfig.optional === true ? 'false' : 'true'; const description = formatRuleValue(detailConfig.description || '-'); const constraints = Object.entries(detailConfig) .filter(([key]) => !['type', 'optional', 'description'].includes(key)) .map(([key, value]) => `${key}: ${formatRuleValue(value)}`) .join(', ') || '-'; return ` ${escapeHtml(field)} ${escapeHtml(type)} ${escapeHtml(required)} ${escapeHtml(description)} ${escapeHtml(constraints)} `; }); return `
${rows.join('')}
name type required description constraints
`; } function createSections(items: FireDocumentBodyInterFace[]): SectionViewModel[] { const groups = new Map(); items.forEach((item, index) => { const sectionTitle = getSectionTitle(item.path); const anchor = `endpoint-${index}-${toAnchor(item.path)}`; const list = groups.get(sectionTitle) || []; list.push({ ...item, anchor, }); groups.set(sectionTitle, list); }); return Array.from(groups.entries()).map(([title, sectionItems]) => ({ title, anchor: `section-${toAnchor(title)}`, items: sectionItems, })); } function getSectionTitle(path: string) { if (path === '/') { return 'Root'; } const segments = path.split('/').filter(Boolean); if (segments.length === 0) { return 'Root'; } return `/${segments[0]}`; } function formatDocDate(value?: string | Date) { if (!value) { return 'N/A'; } if (value instanceof Date) { return value.toISOString().slice(0, 10); } return String(value); } function formatRuleValue(value: unknown) { if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return String(value); } if (Array.isArray(value)) { return value.join(', '); } if (value === null || typeof value === 'undefined') { return 'null'; } return JSON.stringify(value); } function toAnchor(value: string) { return value .toLowerCase() .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-') .replace(/^-+|-+$/g, '') || 'item'; } function escapeHtml(value: string) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } interface EndpointViewModel extends FireDocumentBodyInterFace { anchor: string; } interface SectionViewModel { title: string; anchor: string; items: EndpointViewModel[]; }