antst/go-apispec

GitHub: antst/go-apispec

通过零注解的 Go 源码静态分析自动生成 OpenAPI 3.1 规范的工具。

Stars: 30 | Forks: 0

# go-apispec:从 Go 代码生成 OpenAPI [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/70f22dbba3045547.svg)](https://github.com/antst/go-apispec/actions/workflows/ci.yml) [![Release](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/c1cfd8edeb045553.svg)](https://github.com/antst/go-apispec/actions/workflows/release.yml) ![Coverage](https://img.shields.io/badge/coverage-96.2%25-brightgreen.svg) [![Go Version](https://img.shields.io/badge/go-1.25+-00ADD8?style=flat&logo=go)](https://go.dev/) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/antst/go-apispec/blob/main/LICENSE) [![Go Reference](https://pkg.go.dev/badge/github.com/antst/go-apispec.svg)](https://pkg.go.dev/github.com/antst/go-apispec) **go-apispec** 会分析你的 Go 源代码并生成 OpenAPI 3.1 规范(YAML 或 JSON)。将其指向你的 module——它会自动检测框架,沿着从路由到 handler 的调用图进行追踪,并从实际代码中推断请求/响应类型。 ## 快速开始 ``` # 安装 go install github.com/antst/go-apispec/cmd/apispec@latest # Generate(自动检测 framework) apispec --dir ./your-project --output openapi.yaml ``` 就这么简单。该工具会检测你的框架,找到所有路由,解析 handler 类型,并输出规范。 ## 功能 ### 框架支持 | 框架 | 路由 | 参数 | 请求体 | 响应 | 挂载/分组 | |-----------|--------|--------|-------------|-----------|-----------------| | **Chi** | 完整 | `chi.URLParam`, `r.FormValue`, `r.FormFile`, render pkg | `json.Decode`, `render.DecodeJSON` | `json.Encode`, `render.JSON`, `w.Write` | `Mount`, `Group` | | **Gin** | 完整 | `c.Param`, `c.Query` | `ShouldBindJSON`, `BindJSON` | `c.JSON`, `c.String`, `c.Data` | `Group` | | **Echo** | 完整 | `c.Param`, `c.QueryParam`, `r.FormValue`, `r.FormFile` | `c.Bind` | `c.JSON`, `c.String`, `c.Blob` | `Group` | | **Fiber** | 完整 | `c.Params`, `c.Query`, `c.FormValue`, `c.FormFile` | `c.BodyParser` | `c.JSON`, `c.Status().JSON` | `Mount`, `Group` | | **Gorilla Mux** | 完整 | 路径模板 `{id}` | `json.Decode` | `json.Encode`, `w.Write` | `PathPrefix`, `Subrouter` | | **net/http** | 基础 | 路径模板, `r.FormValue`, `r.FormFile` | `json.Decode` | `json.Encode`, `w.Write`, `http.Error` | 嵌套 `ServeMux` | 支持 Go 1.22+ 的 `ServeMux` 方法前缀模式 —— `mux.HandleFunc("GET /health/live", h)` 会生成 `get: /health/live`,而不是字面量 `/GET /health/live` 键。 完全支持同时使用**多个框架**的项目 —— 所有检测到的框架中的所有路由都会出现在规范中。 所有框架还会将 `fmt.Fprintf`、`io.Copy` 和 `io.WriteString` 检测为响应写入。 ### 分析能力 **响应检测** - 从 `w.Header().Set("Content-Type", "image/png")` 推断 Content-Type - 动态 content-type 回退:`w.Header().Set("Content-Type", doc.MimeType)` → `application/octet-stream`(变量 MIME 类型不会将 Go 字段路径泄漏到规范中) - 将 `WriteHeader(201)` + `json.Encode(user)` 合并为一个带有 schema 的单个 201 响应 - 错误辅助函数:`writeJSONError(w, http.StatusBadRequest, "msg")` → 带有 ErrorResponse schema 的 400 响应,通过 ParamArgMap 追踪函数参数 - 多次调用同一个辅助函数:`writeJSONError(w, 400, ...)` + `writeJSONError(w, 404, ...)` → 捕获两个状态码并使用正确的 schema - 状态码变量解析:`status := http.StatusCreated; w.WriteHeader(status)` → 201 - 跨函数状态码:`w.WriteHeader(getStatus())`,其中 `getStatus()` 返回一个常量 - 同一状态码的多种响应类型 → `oneOf` schema - `[]byte` 响应 → `type: string, format: binary` - 无响应体的状态码(1xx、204、304)永远不会获得 body schema(根据 RFC 7231) - 对于在没有显式调用 `WriteHeader` 的情况下写入响应体的 handler,隐式返回 200 **类型解析** - 泛型结构体实例化:`APIResponse[User]` → 带有 `Data: $ref User` 的 schema - 接口解析:通过 interface 注册的 handler → 具体实现的 schema - `interface{}` 参数解析:`respondJSON(w, 201, user)`,其中 `data` 为 `interface{}` → 从调用者的参数解析为具体的 `User` 类型 - 通过 CFG 实现的条件 HTTP 方法:`switch r.Method { case "GET": ... case "POST": ... }` → 独立的操作 - 路由路径变量:`path := "/users"; r.GET(path, h)` → 将变量解析为字面量路径 - Decode 接收者追踪:`json.NewDecoder(file).Decode(&cfg)` 不会被错误分类为请求体 - io.Copy 源追踪:`io.Copy(w, strings.NewReader(...))` → `type: string`;`io.Copy(w, file)` → `format: binary` **文档提取** - handler 函数上的 Go 文档注释 → OpenAPI 的 `summary` 和 `description` - 第一句话 → `summary`,完整注释 → `description`(单句注释不会重复) - 无需注解 —— 现有的 Go 文档注释可直接生效 - 配置覆盖优先于文档注释 **Schema 推断** - 通过缺少 `json:",omitempty"` 和 `binding:"required"` 标签推断必填字段 - Validator `dive` 标签:在 `[]string` 上的 `validate:"dive,email"` → items 的 schema 具有 `format: email` - `Mux.Vars()` map 索引表达式 → 路径参数名称 - `time.Time`、`uuid.UUID` 和自定义类型的类型映射 - 转换器类型的参数:`idStr := c.Param("id"); strconv.Atoi(idStr)` → `integer`;`strconv.ParseBool` → `boolean`;`strconv.ParseFloat` → `number`;`uuid.Parse` → `string/uuid`。适用于内联(`strconv.Atoi(r.FormValue("x"))`)和变量绑定(`if v := r.FormValue("x"); v != "" { strconv.ParseBool(v) }`)惯用法,包括在不同 `if` 初始化作用域中被遮蔽的变量 - `r.FormFile("upload")` → 带有 `string`/`binary` schema 的表单参数 - JSON-DTO 字段流推断:当解码后的 body 字段随后被传递给转换器时(`uuid.Parse(body.SourceID)`,包括指针解引用 `uuid.Parse(*body.TagsetID)`),apispec 会将转换器的 schema(例如 `format: uuid`)反向传播到结构体字段上 - 通过 `apispec:"format=uuid,type=string"` 结构体标签进行显式的字段覆盖 —— 涵盖了流分析无法到达的字段(例如,在 handler 中被读取但从未被解析的 UUID,或者是 `format: date-time` / `format: email` 提示) - 当 handler 通过 `json.Decode`、`json.Unmarshal`、`c.Bind`、`c.BodyParser` 等读取 body 时,会自动发射 `requestBody.required: true` - 通过空白标记字段进行结构体级别的验证 —— 带有标签 `apispec:"minProperties=1,anyOf=displayName|storageBucketId|temporaryLocation"` 的 `_ struct{}` —— 发射 OpenAPI 的 `minProperties` 和每个字段的 `anyOf` 块。该标记在 JSON 中是不可见的;标签值使用 `,` 表示键值对,并使用 `|` 分隔 `anyOf` 中的字段名称。对于至少需要一个字段的 PATCH endpoint 非常有用 - 安全方案检测 —— 读取 `r.Header.Get("Authorization")` 并修剪方案前缀(`strings.TrimPrefix(_, "Bearer ")` 等)的 handler 会获得一个 OpenAPI `components.securitySchemes` 条目和一个按操作的 `security:` 引用。检测会传递性地跟随调用图,因此共享的 `ValidateBearerToken(r, ...)` 辅助函数将与内联模式一样被识别。可识别的前缀:`Bearer` → `http`/`bearer`,`Basic` → `http`/`basic`,原始 header(无前缀)→ header 中的 `apiKey`。冗余的 `Authorization` header 参数会被自动抑制。通过在 `apispec.yaml` 中声明 `securitySchemes:` 来覆盖或扩展 —— 对于相同的方案名称,配置条目优先于检测结果 **输出质量** - 确定性的 YAML/JSON —— 排序的 map 键,跨运行的输出完全相同,对于 CI diff 很安全 - 默认使用短名称 —— `DocumentHandler.GetContent` 而不是 `github.com/org/.../http.Deps.DocumentHandler.GetContent` - 配置合并 —— `--config` 扩展自动检测到的框架默认值,而不是替换它们 ### 调用图可视化 交互式 Cytoscape.js 图表具有: - 具有缩放、平移和点击高亮功能的层次树布局 - CFG 分支着色:绿色(if-then),红色虚线(if-else),紫色(switch-case) - 显示 case 值的分支标签(例如,“GET”、“POST”) - 针对大型图表的分页模式(1000+ 条边) - PNG/SVG 导出 ``` apispec --dir ./my-project --output openapi.yaml --diagram diagram.html ``` ## 使用方法 ``` # 基础生成 apispec --dir ./my-project --output openapi.yaml # 使用自定义 config apispec --dir ./my-project --config apispec.yaml --output openapi.yaml # Legacy 命名(完全限定的 operationIds 和 schema 名称) apispec --dir ./my-project --output openapi.yaml --short-names=false # 附带 call graph diagram apispec --dir ./my-project --output openapi.yaml --diagram diagram.html # 跳过 CGO packages apispec --dir ./my-project --output openapi.yaml --skip-cgo # 调整大型 codebases 的限制 apispec --dir ./my-project --output openapi.yaml --max-nodes 100000 --max-recursion-depth 15 # Performance profiling apispec --dir ./my-project --output openapi.yaml --cpu-profile --mem-profile ``` ### 主要参数 | 参数 | 缩写 | 默认值 | 描述 | |------|-------|---------|-------------| | `--output` | `-o` | `openapi.json` | 输出文件(`.yaml`/`.json`) | | `--dir` | `-d` | `.` | 项目目录 | | `--config` | `-c` | — | 自定义 YAML 配置 | | `--short-names` | — | `true` | 从名称中剥离 module 路径 | | `--diagram` | `-g` | — | 将调用图保存为 HTML | | `--title` | `-t` | `Generated API` | API 标题 | | `--api-version` | `-v` | `1.0.0` | API 版本 | | `--skip-cgo` | — | `true` | 跳过 CGO 包 | | `--max-nodes` | `-mn` | `50000` | 最大调用图节点数 | | `--max-recursion-depth` | `-mrd` | `10` | 最大递归深度 | | `--verbose` | `-vb` | `false` | 详细输出 | 完整参数列表:`apispec --help` ## 编程方式使用 ``` import ( "os" "github.com/antst/go-apispec/generator" "github.com/antst/go-apispec/spec" "gopkg.in/yaml.v3" ) func main() { cfg := spec.DefaultChiConfig() // or DefaultGinConfig, DefaultEchoConfig, etc. gen := generator.NewGenerator(cfg) openapi, err := gen.GenerateFromDirectory("./your-project") if err != nil { panic(err) } data, _ := yaml.Marshal(openapi) os.WriteFile("openapi.yaml", data, 0644) } ``` ## 配置 自动检测适用于大多数项目。对于自定义行为,请创建 `apispec.yaml`: ``` info: title: My API version: 2.0.0 shortNames: true # false for legacy fully-qualified names framework: routePatterns: - callRegex: ^(?i)(GET|POST|PUT|DELETE|PATCH)$ recvTypeRegex: ^github\.com/gin-gonic/gin\.\*(Engine|RouterGroup)$ handlerArgIndex: 1 methodFromCall: true pathFromArg: true handlerFromArg: true typeMapping: - goType: time.Time openapiType: { type: string, format: date-time } - goType: uuid.UUID openapiType: { type: string, format: uuid } externalTypes: - name: github.com/gin-gonic/gin.H openapiType: { type: object, additionalProperties: true } ``` 每个框架的完整配置示例:请参阅 `internal/spec/config.go` 中的 `Default*Config()` 函数以获取内置模式。 ## 工作原理 ``` Go source → Package loading & type-checking → Framework detection → AST traversal → Call graph + CFG construction → Pattern matching → OpenAPI mapping → YAML/JSON output ``` 1. 加载并对 module 中的所有 Go 包进行类型检查 2. 从导入中检测 Web 框架(支持同时使用多个框架) 3. 构建从路由注册到 handler 的调用图 4. 通过 `golang.org/x/tools/go/cfg` 构建控制流图(CFG)以进行分支分析 5. 将路由、请求、响应和参数模式与调用图进行匹配 6. 使用 CFG 和类型分析解析条件方法、泛型类型和接口实现 7. 将 Go 类型映射到 OpenAPI schema(结构体、枚举、别名、泛型、验证器) 8. 使用排序后的键进行序列化,以确保输出确定性 ## 已知限制 这些是静态分析的固有局限性 —— 即在不执行代码的情况下分析代码: | 限制 | 示例 | 发生的情况 | |-----------|---------|-------------| | **基于反射的路由** | 通过 `reflect.Value.Call` 注册的路由 | 在静态分析中不可见 | | **计算的路径** | `r.GET("/api/" + version, handler)` | 字符串拼接不被计算;仅解析字面量和变量赋值的路径 | | **复杂的跨函数值** | `func compute() int { return a + b }; WriteHeader(compute())` | 仅解析具有单个常量返回的函数;不追踪计算出的值 | ## 交互式图表服务器 ``` # 构建并启动 go build -o apidiag ./cmd/apidiag ./apidiag --dir ./my-project --port 8080 # 打开 http://localhost:8080 ``` 提供一个 Web UI,用于通过过滤、分页和导出来探索调用图。请参阅 [cmd/apidiagREADME.md](cmd/apidiag/README.md)。 ## 开发 ``` # 构建 make build # 测试 make test # Lint make lint # Coverage make coverage # 在故意更改输出后更新 golden files # (生成至临时目录,显示 diff,需要确认) go test ./internal/engine/ -run TestUpdateGolden -v ``` ### 项目结构 ``` go-apispec/ ├── cmd/apispec/ CLI entry point ├── cmd/apidiag/ Interactive diagram server ├── generator/ High-level generator API ├── spec/ Public types (re-exports from internal/spec) ├── internal/ │ ├── core/ Framework detection │ ├── engine/ Generation engine │ ├── metadata/ AST analysis and metadata extraction │ └── spec/ OpenAPI mapping, patterns, schemas ├── pkg/patterns/ Gitignore-style pattern matching └── testdata/ Framework fixtures + golden files ``` ### Golden 文件测试 每个 `testdata/` 目录都有 `expected_openapi.json`(短名称)和 `expected_openapi_legacy.json`(完全限定名称)。Golden 文件仅使用相对路径 —— 没有特定于机器的绝对路径。 **单一代码路径**:`generateGoldenSpec()` 是比较和生成共用的单个函数。测试永远不会覆盖 golden 文件。 ``` # Compare(在 CI 中运行 — 不匹配时失败): go test ./internal/engine/ -run TestGolden -v # 在故意更改后更新(显式,从不自动): go test ./internal/engine/ -run TestUpdateGolden -v ``` ## 许可证 Apache License 2.0 —— 请参阅 [LICENSE](LICENSE)。 最初 fork 自 Ehab Terra 的 [apispec](https://github.com/ehabterra/apispec)。
标签:EVTX分析, Go, OpenAPI, Ruby工具, SOC Prime, Syscall, Web开发, 云安全监控, 代码生成, 开发工具, 日志审计, 渗透测试工具, 自动化payload嵌入, 静态分析