swagger-blocks源码剖析:InternalHelpers如何智能合并多类节点,$ref重写背后的双版本玄机
【免费下载链接】swagger-blocksDefine and serve live-updating Swagger JSON for Ruby apps.项目地址: https://gitcode.com/gh_mirrors/sw/swagger-blocks
swagger-blocks 是一款面向 Ruby 应用的 Swagger/OpenAPI JSON DSL,帮你用纯 Ruby 代码块定义并实时生成可自动刷新的 Swagger JSON,兼容 Swagger 2.0 与 OpenAPI 3.0 双版本。它的核心魅力在于:把 API 文档定义分散在 Controller、Model 等多个类里,请求时由InternalHelpers一键智能合并成完整文档。本文带你深入源码,拆解 internal_helpers.rb 的多类节点合并算法,以及$ref引用路径重写背后的双版本玄机。
🔍 30秒看懂 swagger-blocks 的架构
整个库只有 4 个核心文件撑起骨架:
| 模块 | 文件 | 职责 |
|---|---|---|
| DSL 入口 | swagger/blocks/root.rb | 对外提供build_root_json,组装最终 JSON |
| 合并引擎 | swagger/blocks/internal_helpers.rb | 收集并合并多个类的节点数据 |
| 节点基类 | swagger/blocks/node.rb | 所有节点的基类,负责版本识别与$ref重写 |
| 类级 DSL | swagger/blocks/class_methods.rb | 注入swagger_root/swagger_path/swagger_schema等 |
使用方式极其简单——任何 Ruby 类include Swagger::Blocks后即可声明文档片段,最后在文档控制器中一行代码生成全量 JSON(见 README.md 中 Docs controller 示例):
render json: Swagger::Blocks.build_root_json(SWAGGERED_CLASSES)💡 正因为 JSON 是请求时动态构建的,你改完代码刷新页面,文档就自动更新——这就是 "live-updating" 的设计本意。
🧩 InternalHelpers:多类节点合并的三步走算法
当你把PetsController、Pet、ErrorModel等一堆类传给build_root_json时,真正干活的是 parse_swaggered_classes。它的合并过程可以拆成三步:
第一步:向每个类"盘点"节点资产
遍历所有传入的类,通过私有方法 _swagger_nodes 取回各自积累的节点:
swagger_nodes = swaggered_class.send(:_swagger_nodes)每个类在声明swagger_path、swagger_schema、swagger_component时,已经把节点存进了类级别的实例变量,这里一次性取走。
第二步:Path 与 Schema 的 Map 级合并
Swagger 2.0 的接口路径和模型定义,全部走哈希合并:
path_node_map.merge!(swagger_nodes[:path_node_map]) schema_node_map.merge!(swagger_nodes[:schema_node_map])妙处在于:/pets写在 Controller A、/orders写在 Controller B,模型Pet定义在 Model 里——分属不同类的节点在merge!后自动汇成一张完整地图,类与类之间零耦合。
第三步:v3 Components 的按项合并
OpenAPI 3.0 把所有可复用资源收拢进components节点。合并时不能粗暴整体替换,否则后一个类会"吃掉"前一个类的定义。merge_components 针对 7 个子项逐一合并:
merge_components(component_node, swagger_nodes, :examples) merge_components(component_node, swagger_nodes, :parameters) merge_components(component_node, swagger_nodes, :schemas) # ... 共 7 项逻辑是"先确保目标桶存在,再把源桶内容 merge 进来",因此多个类各自声明的 schema、参数、响应体互不覆盖。这 7 个子项的声明入口都在 component_node.rb 中。
唯一性守门员:limit_root_node
合并完还要过一道校验(limit_root_node):
- 一个
swagger_root都没有 → 抛DeclarationError: swagger_root must be declared - 出现两个及以上 → 抛
DeclarationError: Only one swagger_root declaration is allowed.
错误类型定义在 errors.rb——这是很多新手漏掉文档控制器里的self后最常遇到的报错。
⚠️ 小细节:合并是"后者覆盖前者"的语义。同名 path/schema 若想叠加声明而非覆盖,应在同一个类里重复声明同名节点——class_methods.rb 会用
instance_eval把新声明合并进已有节点,而不是新建。
🪄 $ref 重写的双版本玄机
源码里最精巧的部分,藏在 node.rb 的 as_json 方法中。
你在 DSL 里写引用时只写名字:
key :'$ref', :Pet而最终 JSON 里出现的必须是完整路径。版本不同,路径前缀完全不同:
| 版本 | 目标节点类型 | 重写结果 |
|---|---|---|
| 2.0 | 任意 schema | #/definitions/Pet |
| 3.0 | SchemaNode | #/components/schemas/Pet |
| 3.0 | LinkNode | #/components/links/Pet |
| 3.0 | ParameterNode | #/components/parameters/Pet |
| 3.0 | ResponseNode | #/components/responses/Pet |
| 3.0 | RequestBodyNode | #/components/requestBodies/Pet |
| 3.0 | ExampleNode | #/components/examples/Pet |
玄机有二:
其一,版本自动探测。每个节点无需手动指定版本,Node#version 会检查数据里是swagger: '2.0'还是openapi: '3.0.0'自动判断,再配合 is_swagger_2_0? / is_openapi_3_0? 两个判定方法,让同一套节点树在两个版本的 JSON 结构间"变形"。
其二,外部引用不动。static_ref? 用正则识别以#/或http(s)://开头的值——已经写全路径的内部引用或跨文档 URL 引用会被原样保留,绝不重复加前缀。
最后,root.rb 的 build_root_json 根据版本把节点挂到不同位置:2.0 挂paths+definitions,3.0 挂paths+components,再调用as_json(version:)完成全树递归重写。同一份 Ruby 代码,两种标准各得其所——这就是"双版本玄机"的完整闭环。
🚀 快速上手三步走
- 声明根节点:在文档控制器里
include Swagger::Blocks,用swagger_root声明key :openapi, '3.0.0'(v3)或key :swagger, '2.0'(v2)及info信息 - 分散定义:Controller 里写
swagger_path,Model 里写swagger_schema,复用资源写swagger_component - 一行出文档:
render json: Swagger::Blocks.build_root_json(SWAGGERED_CLASSES),把self也放进列表(别漏了,否则没有 root 会报错)
完整可运行的声明范例见 spec/lib/swagger_v2_blocks_spec.rb 与 spec/lib/swagger_v3_blocks_spec.rb,Gemfile 集成方式参考 Gemfile。
📌 一句话总结
swagger-blocks 的精髓就藏在两个文件里:internal_helpers.rb用 Map 合并 + 按项合并把分散在多类中的节点"缝"成一张完整文档,node.rb用版本感知的$ref重写让同一份定义同时兼容 Swagger 2.0 与 OpenAPI 3.0。理解了这两处,你就掌握了它"改代码即更新文档"的全部魔法。
【免费下载链接】swagger-blocksDefine and serve live-updating Swagger JSON for Ruby apps.项目地址: https://gitcode.com/gh_mirrors/sw/swagger-blocks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考