genapi v2 设计基线
状态:已批准并进入实现。本文描述 v2 的当前合同;不再保留早期兼容方案。
1. 目标
genapi v2 面向真实多微服务 OpenAPI 项目,解决四个问题:
- service ID 与 Swagger 文件名、URL 解耦;
- 同名 schema 以 service 隔离,绝不错误合并;
- 公开类型路径在无关后端变化下保持稳定;
- 多 service 全部准备成功后再事务替换产物。
v2 是破坏性版本。当前没有需要保护的既有用户,因此不保留旧字段、旧导出或兼容开关。
2. 非目标
- 不在内核内修复特定后端框架的非标准 Swagger;业务通过 hooks 规范化。
- 不根据 schema 结构猜测两个不同 identity 是否“其实相同”。
- 不为碰撞自动加 service 前缀、数字后缀或按顺序跳过。
- 不把 Swagger 下载 URL 当成应用运行时
baseURL。 - 不承诺目标 schema 或其依赖发生真实语义变化时类型仍不变。
3. 权威身份
3.1 project 与 service
project identity = project.name
service identity = project.name + service.namename 必须是稳定 kebab-case 配置 ID。它参与:
- 生成目录;
- service 类型文件名;
- 客户端对象 key;
- service namespace;
- manifest service key。
displayName 只用于人类可读输出,不参与身份。
3.2 schema
schema identity = project.name + service.name + full schema name禁止只用短名或只用 full schema name 跨 service 合并。同名 schema 在不同 service 中可以有完全不同结构。
4. Swagger source
每个 service 必须声明:
source: { file: string } | { url: string }选择对象而不是裸字符串,原因是:
- 不根据字符串形态猜测协议;
- TypeScript 可静态保证 file/url 互斥;
- 运行时可以拒绝未知字段和不合法 URL;
- 以后新增其他来源时仍保持可判别 union。
4.1 file
- 绝对路径直接使用;
- 相对路径以配置项目的 cwd 为基准;
- 生成器直接读取,不复制、不重命名;
- 文件位置不进入生成注释、manifest identity 或类型名称。
4.2 url
- 必须是完整 HTTP(S) URL;
- 禁止 URL 内嵌用户名或密码;
- online 下载到
outputDir/<project>/<service>.json; - offline 从同一路径读取缓存;
- 下载使用同目录临时文件加 rename,避免半写缓存。
URL 只定位 OpenAPI 文档。生成客户端的运行时 baseURL 由应用初始化 create<Project>Client 时提供。
5. 配置合同
interface ServiceConfig {
name: string;
displayName: string;
source: { file: string } | { url: string };
hooks?: GenapiHooks;
}
interface ProjectConfig {
name: string;
displayName: string;
services: ServiceConfig[];
hooks?: GenapiHooks;
typeExports?: ProjectTypeExportConfig;
}
type TypeExportStrategy = "service-namespace" | "flat-strict" | "canonical-only";
interface NamespaceRule {
service?: string;
match: string;
namespace: string;
}
interface SchemaAliasRule {
service: string;
schema: string;
exportAs: string;
namespace?: string;
}
interface ProjectTypeExportConfig {
strategy?: TypeExportStrategy;
namespaces?: NamespaceRule[];
aliases?: SchemaAliasRule[];
}
interface XApiForgeConfig {
timeoutMs?: number; // 默认 15_000
maxSwaggerBytes?: number; // 默认 64 MiB
outputDir: string;
genOutputDir?: string;
mode?: "online" | "offline";
projects: ProjectConfig[];
hooks?: GenapiHooks;
}明确移除:
- root
defaultPath; - project/service
baseHost; - service
port、path; - root
namespacePrefixAliases; AllSchemas;- 所有 compatibility 配置。
即使 JavaScript 绕过 TypeScript,运行时校验也会拒绝这些已移除字段。
6. 单次规范化流水线
每个 service 只形成一份 normalized document:
read source
→ JSON parse
→ raw OpenAPI 3.x validation
→ freeze originalDocument
→ structuredClone
→ transformSwagger
→ normalized OpenAPI 3.x validation
→ validate JSON values + canonicalize object keys
→ deep-freeze authoritative normalized document
→ internal $ref validation + PathItem reference index
├─ openapi-typescript isolated safe view
├─ endpoint metadata
├─ schema export inventory
└─ canonical manifest hash所有下游视图必须复用同一份深度冻结的 document,不能分别重新读取或分别跑 hook。openapi-typescript 的 Redocly bundler 会改写传入对象,还会把 example、default、vendor extension 等任意 JSON 数据里的 $ref 误当成语义引用。因此只给该第三方一个从 normalized document 派生的一次性安全视图:仅剔除 literal JSON 内的 $ref、$dynamicRef、$recursiveRef、$anchor key;schema、response、PathItem 等语义位置不变。这不是第二次规范化,endpoint、alias、manifest 和 hook context 仍读取权威冻结对象。
6.1 canonical 化
- 递归排序 JSON object key;
- 保留所有 array 顺序;
- 拒绝
undefined、NaN、BigInt、class instance 等非 JSON 值; - 把
__proto__等特殊名称保留为普通 JSON key; - 拒绝循环引用;
- hash 基于 canonical JSON。
对象 key 顺序不属于 JSON/OpenAPI 语义。数组可能承载 enum、oneOf、parameters 等语义,因此不能排序。
7. 类型导出
7.1 canonical API
始终生成:
export interface SchemasByService {
identity: IdentityComponents["schemas"];
}
export type ServiceName = keyof SchemasByService;
export type SchemaOf<Service extends ServiceName, Name extends keyof SchemasByService[Service]> = SchemasByService[Service][Name];7.2 service-namespace
默认策略。外层 namespace 仅由稳定 service ID 推导:
fill-center → FillCenterSchemas每个 schema 的自动短名只在所属 service namespace 内参与碰撞检查。跨 service 同名无冲突。
7.3 flat-strict
显式选择的 project 顶层便利导出策略:
Catalog.ProductDto
InvoiceDto所有 service 在 project 作用域共同参与碰撞检查。任何同层短名、alias/namespace 同名,或与 SchemaOf、SchemasByService、ServiceName、内部 components import 名称冲突,均在写盘前失败。flat-strict 不按 service 自动加前缀,也不属于兼容模式。
7.4 prefix namespace
projects[].typeExports.namespaces 的层级由策略决定:
service-namespace → FillCenterSchemas.Infrastructure.OrganizationUnitDto
flat-strict → Infrastructure.OrganizationUnitDto匹配顺序固定:
- service 专属规则优先;
- 最长前缀优先;
- 稳定字典序兜底。
配置数组顺序不参与结果。
7.5 explicit alias
只用于确定的 service + full schema name:
{
service: "identity",
schema: "First.UserDto",
exportAs: "FirstUserDto",
namespace: "OptionalInnerNamespace"
}namespace 在 service-namespace 下是 service 内层,在 flat-strict 下是 project 顶层。规则指向不存在的 service/schema、目标重复、alias 与 namespace 同名,均在写盘前失败。
7.6 canonical-only
只生成 canonical API,不生成便利 namespace。此模式不能配置 prefix namespace 或 alias。
8. 稳定性合同
8.1 保证稳定
以下变化不改变未受影响 identity 的公开访问路径:
- service 配置重排;
- schema JSON object key 重排;
- source 文件改名、移动或 URL 迁移;
- file 与等价 URL cache 切换;
- 其他 service 的 schema 增加、删除或类型变化;
- 同一 service 中、不属于目标 schema 依赖闭包的 schema 变化。
实现手段:
- source 不参与身份;
- service/schema 统一稳定排序;
- canonical document;
- service namespace 隔离;
- 不自动重命名;
- manifest 只记录 service key 与 normalized hash;
- 生成注释不包含 source 地址或时间戳。
8.2 必然变化
以下是显式破坏或真实语义变化:
- project/service ID 改名;
- full schema name 改名;
- schema 自身改变;
- schema 的
$ref依赖改变; - 主动修改 alias/namespace 配置;
- service 集合变化导致 project 集合类型增减成员。
稳定的含义是“未受影响的 identity 不被重命名、移动或错误重绑定”,不是把真实后端变化隐藏起来。
9. 客户端生成
每个 service 单独生成 paths 并单独调用 createNet<Paths>。不交叉合并 paths,避免多个 Swagger 复制公共 endpoint 时产生错误类型交集。
所有 service 共用同一个运行时地址时,可直接传 Core NetConfig。这是 v2 的简洁初始化形式,不是旧配置兼容开关。
运行时配置支持:
createDemoClient({
defaults: {
baseURL: "https://gateway.example.test",
authService
},
services: {
infrastructure: {
baseURL: "https://infrastructure.example.test"
}
}
});优先级固定为 service override > project defaults。这里的 baseURL 是业务请求地址,与 Swagger source 无关。
10. hooks
优先级是完整对象覆盖:
service > project > rootservice 写 {} 表示显式禁用继承。hook 后的 document 必须仍是 OpenAPI 3.x,且语义位置只允许可解析的内部 $ref。业务 example/default/value/vendor extension 是任意 JSON,其中同名 key 不参与引用校验。当前依赖会把语义位置的 $dynamicRef/$recursiveRef 静默降级为 unknown,因此内核明确拒绝这两种引用,而不是生成虚假的弱类型。业务特殊格式由业务 hook 处理,内核不包含 ABP、SpringBoot 或其他框架特判。
通用 runtime enum helper 在 transformSwagger 中把 override/append 合并进标准 enum 一次,并把 service 级运行时导出清单写入该 service 的 hook context。transformTypes 只读取冻结的 normalizedDocument 和创建 helper 时的 metadata 快照;api-types.ts 再按现有 service/prefix/alias 规则重导出 const、Options、Labels 和 extensions。禁止重新读取 swaggerPath 或使用跨 service 全局缓存。
11. 原子生成
prepare every service in memory
→ render every project into staging
→ compare/check
→ acquire root lock
→ persist transaction state
→ rename old output to backup
→ rename staging to output
→ remove backup and transaction state任何准备失败都不创建正式输出。提交失败会自动恢复 backup;若进程中断,下次生成先恢复 transaction state。
--check 只比较,不写正式输出。
12. manifest
{
"formatVersion": 2,
"generator": "@xapiforge/genapi",
"generatorVersion": "...",
"openapiTypescriptVersion": "...",
"typescriptVersion": "...",
"esbuildVersion": "...",
"project": "demo",
"services": {
"base": {
"normalizedDocumentHash": "sha256:..."
}
}
}不写生成时间、绝对路径或 source URL,保证相同规范化输入得到相同产物。
13. 验证矩阵
逐项实现与证据见 genapi v2 严格 review 矩阵。
自动回归必须覆盖:
- file 名与 service ID 不同;
- file 移动、file/url cache 切换不漂移;
- service 配置重排不漂移;
- JSON object key 重排不漂移;
- 跨 service 同名 schema 保持异构;
- 同 service 同 namespace 短名碰撞失败;
flat-strict跨 service 碰撞失败且显式 alias 可解除;- alias/namespace 与 canonical 顶层名称碰撞失败;
- prefix 规则重排不漂移;
- 三种导出策略均可编译;
- URL online、offline cache、dry-run 与 file 直读分支;
- 无关 schema 增删改不破坏稳定 import;
- external
$ref在写盘前失败; --check只读;- 多 project 全准备后才写;
- stale file 清理;
- transaction 中断恢复;
- 独立 consumer CLI;
- 真实
tmp/dcenter多微服务生成与 typecheck。
真实业务 Swagger、生成验证项目和临时包放在根目录 tmp/。该目录必须保持 Git ignored,不得提交。
14. 已知边界
- OpenAPI 数组顺序变化可能改变输出;数组可能有语义,生成器不会擅自排序。
- external
$ref暂不支持。 $dynamicRef/$recursiveRef暂不支持;必须先由业务 hook 规范化为普通内部$ref。- 单个 Swagger 默认最多 64 MiB;确有更大规范时必须显式提高
maxSwaggerBytes,避免远程或本地异常输入无上限占用内存。 - hook 代码本身必须确定;hook 若注入时间、随机数或环境差异,生成器无法替它保证稳定。
- openapi-typescript 版本变化可能改变格式或类型;manifest 固定记录版本,升级必须作为显式变更评审。