Files
go-omni/API.md

330 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API 契约与项目生成
当前生成目录和启动扩展采用新布局,见 [项目维护指南](PROJECT.md)。`@app(defaultResponse: Name)` 在入口选择 App 默认响应。`@body(mode: raw|multipart)` 和 `@produces` 支持自动 Handler,手写 Handler 仅为显式选择的扩展。
首次接入请先看 [README](README.md):包含固定远程版本安装、可运行项目、完整 `.api` 示例和已有服务接入步骤。本页用于查阅语法与生成规则;下方带 `--framework-dir` 的命令用于框架源码开发。
本阶段目标:从 `.api` 定义生成可运行的 Go 项目;定义、绑定、校验与 OpenAPI 保持一致;重复生成不覆盖业务实现;业务代码继续独立于网络层。实现入口是 `cmd/go-omni/apigen`,运行时契约编译器是 `contract`。
## 与 go-zero 的对应关系
调研参考了 go-zero 官方的 [API DSL](https://go-zero.dev/reference/api-dsl/)、[参数规则](https://go-zero.dev/reference/api-dsl/parameter/)、[goctl API](https://go-zero.dev/reference/cli-guide/api/) 和 [项目结构](https://go-zero.dev/concepts/project-structure/)。借鉴其声明接口后生成项目的工作方式:统一的类型定义、路由声明、分组、前缀和命名中间件。实现独立编写,不引入 go-zero 或 Gin 运行时依赖。
| 使用体验 | go-omni 实现 |
| --- | --- |
| 新建接口项目 | `go-omni api new <directory>` |
| 按契约生成、更新 | `go-omni api generate --api user.api --dir .` |
| 熟悉的命令别名 | `api go -api user.api -dir .` |
| 校验契约 | `api validate --api user.api` |
| CI 检查生成产物是否过期 | `api generate --api user.api --dir . --check` |
| 业务逻辑目录 | `internal/logic/<group>`,使用标准 context.Context |
| 依赖装配 | `internal/svc` |
| 接口文档 | 自动生成 `docs/openapi.json`,同时提供 `/openapi.json` |
这是明确限定的 go-zero 风格方言,**不是完整 goctl 兼容实现**。不支持的声明和校验标签会在生成前报错。
## 立即试用
源码开发时,从 go-omni 仓库运行,显式引用本地框架:
```sh
go run ./cmd/go-omni api new \
--module example.com/omniDemo --framework-dir . ../omniDemo
cd ../omniDemo
go mod tidy
go run .
```
访问 `http://127.0.0.1:8080/api/greet/Alice` 可得到带 `data.message` 的统一响应。`api new` 的问候示例包含可运行逻辑;自定义契约新生成的 Logic 会明确返回 501,等待业务实现。
用现成的多分组定义生成项目,在 go-omni 仓库执行:
```sh
go run ./cmd/go-omni new --api examples/contracts/user.api \
--module example.com/userService --framework-dir . \
--log-output file --log-format json ../userService
```
`api generate` 在新目录中也会生成完整项目。默认只监听 localhost;`GO_OMNI_HTTP_ADDR` 修改地址,`GO_OMNI_TRANSPORT=native` 切换网络层。配置顺序为默认值 → `etc/config.yaml` → 环境变量。文件日志支持现有轮转配置。远程试用使用与 CLI 一致的固定版本,见 [发布说明](RELEASE.md)。
API 项目现在支持组合数据库、存储和日志:
```sh
go-omni new --api user.api --module example.com/userService \
--database mysql --storage local --log-output file ./userService
```
- `--database none|memory|mysql`:API 默认 `none`,`memory` 是不创建数据库连接的别名;MySQL 生成连接池、配置、事务入口和 readiness 探针。
- `--storage none|local|s3|r2|oss`:默认 none,生成统一 Files / Uploads / Downloads / UploadLinks 依赖;本地签名对象端点为 `/storage/object`。
- `--log-output stdout|stderr|file` 与 `--log-format json|text`:配置和无配置文件启动使用一致默认值。
初次选项记录在 `.goOmni/apiManifest.json`,以后直接 `api generate` 会继承。已有项目通过参数切换选项会报错,因为初始化代码属于业务方;可在新目录生成对照后手动迁移。`--force` 不允许覆盖 API 项目。new 与 api new 统一生成契约项目,不再使用旧式 CRUD 目录。
MySQL 密钥只从 `GO_OMNI_DB_DSN` 获取,非秘密池配置位于 YAML,环境变量优先。存储使用原有 `GO_OMNI_STORAGE_*`;API 项目不会额外生成公开的签名发放接口,鉴权和对象权限由业务实现。初始化失败会清理已打开资源,退出清理可重复调用。
HTTP 请求类型不能推导数据库表结构;迁移和 Repository 由业务自己维护。当前目录的完整示例见 [managedService](examples/managedService/README.md)。MySQL、事务审计和鉴权实现可参考 [旧目录用户管理示例](examples/userService/README.md),接入时按 [项目指南](PROJECT.md) 装配到 config/svc/logic。
## 接口定义
```go
syntax = "v1"
type UpdateUserReq {
UserID int64 `path:"userId,range=[1:1000000]"`
Notify bool `form:"notify,default=false"`
Name string `json:"name" binding:"required,max=50"`
Age *int `json:"age,optional,range=[0:150]"`
}
type UserResp {
UserID int64 `json:"userId"`
Name string `json:"name"`
}
@server (
group: user
prefix: /api/v1
)
service user-api {
@doc "Update a user"
@handler updateUser
put /users/:userId (UpdateUserReq) returns (UserResp)
}
```
类型和 Go 字段使用导出命名;JSON、query、path 名称使用 lowerCamelCase;Header 使用 HTTP 标准名称。响应类型定义业务返回值。未配置时使用默认公共体;当前源码支持在 `.api` 中声明公共响应模板,选择字段和结构,并按分组复用或由单接口覆盖,详见 [公共响应体](RESPONSE.md)。
支持:
- 可省略的 `syntax = "v1"`;`info` 中的 title、version 和描述性元数据。
- 单独的 `type X { ... }` 与 `type (...)`;基础标量、指针、嵌套命名类型、切片、字符串键 map。嵌套对象只使用 JSON 字段。
- 相对 `.api` 导入及 `import (...)`,重复导入去重;拒绝循环和越过入口目录的导入。单文件 1 MiB、总计 8 MiB、最多 64 个文件,限制导入/类型深度及 schema 展开规模。
- 同一 service 名称的多个块;`@server` 的 group、prefix、middleware、auth、errors、response,group 可以写 `admin/user`。
- 每个路由必须有 `@handler`;可选单行/多行 `@doc`、`@auth`、`@errors`、`@response`;GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS。
- 可省略请求或响应;有响应默认 200,无响应默认 204。Omni 扩展 `@status 201`、`@status 202`;204 不允许响应类型,HEAD 不允许响应体,GET/HEAD 不允许 JSON 请求体。
- 每个请求字段只使用一个来源:`json`、`path`、`form`、`header`。本版 `form` 明确表示 URL query,不解析 multipart 或表单 body。query 切片用重复键;header/path 使用标量。
- 来源标签中的 `optional` / `omitempty`、标量 `default`、标量 `options=a|b`、数值 `range=[1:100]`(支持开区间和单边界)。
- `binding` 的 `required`、`min`、`max`、`len`、`email`。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。
暂不支持匿名嵌入、别名、递归类型、未声明的外部 Go 类型、自定义 JSON 编解码、`[]byte`、泛型、`json:",string"`、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。
业务错误、具名鉴权、多行文档及下述开发命令从 CLI `v0.7.0-preview.1` 提供。公共响应模板从 `v0.8.0-preview` 提供,CLI 与业务项目中的框架依赖需一起升级。
## 业务错误:声明、代码和文档共用一份定义
**错误目录也可以只在 Go 中维护。** 从 `v0.11.1-preview` 起,生成路由没有声明错误列表时,继承 `omni.WithErrors(catalog)` 注册的 App 目录;`.api` 继续定义接口和返回格式,无需重复登记错误。全局中间件提前返回同样适用。显式引用 `.api` 错误时才使用路由独立目录,规则见 [目录继承](errors/README.md)。CLI 不执行运行时 Go 配置,因此静态文档不会自动枚举只在 Go 中定义的具体错误。
从 `v0.10.0-preview` 起,推荐用 `errorCatalog` 同时定义逻辑标识、对外业务码、提示和 HTTP 状态:
```text
errorCatalog CommonErrors (
defaultStatus: 200
errors: {
"UsernameInvalid": {"key": "USERNAME_INVALID", "code": 400002, "message": "用户名格式不正确"},
"PasswordInvalid": {"key": "PASSWORD_INVALID", "code": 400002, "message": "密码格式不正确"},
"Forbidden": {"key": "FORBIDDEN", "status": 403, "code": 403001, "message": "没有权限"}
}
)
```
对象键是生成函数名;`key` 是唯一逻辑标识,允许项目自己的命名;`code` 是对外业务码,可以重复。数字码需搭配 `codeType: int32` 或 `int64` 的公共响应;字符串码搭配 `string`。目录不规定返回体字段。
省略 `defaultStatus` 时为 400;设为 200 时,只作用于目录里未单独写 `status` 的条目。生成路由自动绑定它实际声明的错误目录,在 App 中间件之前生效。业务只调用 `apierrors.UsernameInvalid()`;无需再维护 `codes` / `messages`,也无需修改 main。手写路由可复用生成的 `apierrors.Catalog()`。
同一 HTTP 200 下,OpenAPI 用 `anyOf` 保留成功 DTO 和错误体,日志与指标仍识别业务失败。完整语法、路由作用域与手写项目配置见 [统一错误目录](errors/README.md),可运行契约见 [errors.api](examples/contracts/errors.api)。
从 `v0.11.0-preview` 起,目录条目支持 `params` 有序列表与 `{name}` 提示模板:
```json
"TextTooLong": {
"key": "TEXT_TOO_LONG", "code": 400002,
"message": "{label}不能超过{max}个字符",
"params": [
{"name": "label", "type": "string", "example": "企业名称"},
{"name": "max", "type": "int", "example": 100}
]
}
```
生成 `apierrors.TextTooLong(label string, max int)`,返回时 Message 已填好;固定提示保持无参数函数。声明顺序决定参数顺序。模板校验、运行时缺参/类型错误、转义及非 HTTP 使用规则见 [提示模板参数](errors/README.md)。OpenAPI 的错误目录保留参数定义,响应示例使用具体文案。
原有声明继续支持:
```go
error (
Unauthenticated 401 "UNAUTHENTICATED" "authentication required"
Forbidden 403 "FORBIDDEN" "permission denied"
UserNotFound 404 "USER_NOT_FOUND" "user not found"
EmailTaken 409 "EMAIL_TAKEN" "email already in use"
)
```
也可写单条 `error EmailTaken 409 "EMAIL_TAKEN" "email already in use"`,或把错误目录放进相对导入的 `errors.api`。
名称用 UpperCamelCase,HTTP 状态允许 200–599 中可携带响应体的最终状态(排除 204/205/304),旧声明的逻辑错误码用大写字母/数字/下划线且全局唯一,message 是可公开返回的稳定提示。`Catalog` 为生成函数保留名称。
`@server (errors: Unauthenticated)` 声明组级错误;路由 `@errors UserNotFound, EmailTaken` 追加到组级列表并去重。
`@errors none` 清空 `.api` 分组继承的错误列表,随后按未设置路由目录处理,继承 App 错误目录。未声明、重复声明或生成符号冲突会报告源码位置。
生成 `internal/apierrors/errorsGen.go`:
```go
return nil, apierrors.EmailTaken()
// 可用 apierrors.CodeEmailTaken 引用同一个错误码。
```
每次调用返回新错误,避免并发修改共享错误对象。生成器不会自动把任意数据库错误转成业务错误;由 Service/适配层调用这些函数。
内部 Cause 可能进入访问日志,敏感驱动报错应像 userService 示例一样先归类再转换。
OpenAPI 对每个状态按选定的响应模板生成结构和命名 examples,同一 409 下多个错误不会相互覆盖;平铺模式的纯业务状态还列出 code enum。
默认框架错误状态与自动添加的鉴权 401 保留开放的 code 字段,容纳校验器/中间件实际错误。
`x-omni-errors` 提供错误目录与路由关联。目录解析已定义的错误,不自动推断数据库或其他未声明错误的业务含义,也不穷举所有运行时错误。
## 鉴权:声明与路由注册一起变化
```go
auth UserAuth (
type: bearer
middleware: Auth
description: "Application access token"
)
@server (
group: user
prefix: /api/v1
auth: UserAuth
errors: Unauthenticated
)
service user-api {
@errors Forbidden, EmailTaken
@handler createUser
@status 201
post /users (CreateUserReq) returns (UserResp)
@auth none
@errors none
@handler publicStatus
get /status
}
```
`auth` 是具名契约,`middleware` 指向实际 hook;生成器把它放在普通 middleware 前面执行,并在注册任何路由前检查 hook 非空。
新 hook 未实现时返回 501,已有手写 hook 保留。需要在 hook 中验证凭证、失效时间、签发者、权限等;声明不会自动实现登录或 JWT 校验。
- `type: bearer`:对应 HTTP Bearer,可选 `bearerFormat: JWT`,这只是文档提示。
- `type: apiKey`:必须同时声明 `in: header` 或 `in: cookie` 和 `name`,例如 `X-API-Key` 或 `sessionId`。
- `auth: UserAuth` 可在组级使用;路由 `@auth OtherAuth` 替换,`@auth none` 撤销继承的声明鉴权。普通 middleware 继续保留。
- 不要再把该 auth 的 hook 重复写进普通 middleware。不支持 query token、多个方案 AND/OR、OAuth2 scopes 或隐含认证策略。
- 被保护路由自动描述 401;403 和具体错误码由 `@errors` 明确声明。未声明 auth 的旧项目仍按其原有 middleware 执行。
文档生成 `components.securitySchemes` 与逐接口 `security`;显式公共接口使用空数组。
结构遵循 [OpenAPI 3.0.3 Security Requirement](https://spec.openapis.org/oas/v3.0.3.html#security-requirement-object)。
## 文档和日常开发命令
保留单行 `@doc "摘要"`,也可以:
```go
@doc (
summary: "Create a user"
description: "Writes user data and audit in the same transaction"
tags: users, administration
deprecated: false
)
```
未设置 tags 时使用 group 分组。`info` 的 description/desc 也会进入 OpenAPI;两者不能同时声明。
在生成项目目录内,不必重复输入 API 路径:
```sh
go-omni api validate # 发现 manifest 中的入口并校验
go-omni api inspect # 方法、路径、Handler、鉴权与业务错误总览
go-omni api generate --dry-run # 预览文件变化、契约变更和 Logic 提示,完全不写文件
go-omni api generate # 实际生成,保留手写业务
go-omni api generate --check # 只读检查产物漂移及明确的 Logic 签名错误,可用于 CI
go-omni api export > openapi.json # 独立导出纯 JSON,无需启动服务
go test ./...
```
`--dry-run` 和 `--check` 不能组合。新项目预览也不创建目录;预览不是审批或锁定,实际生成会重新检查文件状态。
契约变化会定位到相关 Logic 文件,涵盖嵌套 DTO、原始 body 和流式返回。生成后签名不一致的问题仍可由 `--check` 检出;项目类型别名/build tags 等不确定情况提示 REVIEW,最终用 `go test ./...` 验证。详见 [契约变更提示](PROJECT.md#契约变更提示)。
在项目外可显式传 `--api`;生成时传 `--dir` 可定位其他项目。不会猜测多个 .api 中哪个是入口。
路由总览的 `(undeclared)` 仅表示没有声明 auth;旧式中间件仍可能做认证。
外发文档可用 export;项目内受生成器管理的 `docs/openapi.json` 请通过 generate 更新,以同步文件哈希。
## 迁移扩展
- 动态 JSON 字段 `any` / `interface{}`,包含切片和 map;外围契约仍严格。
- `scalar Name (...)` 显式引用项目类型,声明 JSON 输入/输出类型及文本绑定;支持已有 JSON/Text 转换方法。
- `@body (mode: json/raw/multipart; maxBytes: ...)`:按路由限制请求体。
- `@binding custom` 生成保留的绑定 hook;`@handlerMode manual` 生成保留的整个 Handler,仅用于自定义协议。raw/multipart 默认生成自动 Handler,将受限流交给 Logic。
- `@produces "text/plain"` 等声明无 DTO 成功输出;自动模式的 Logic 返回 `*httpx.Stream`,Handler 负责流式发送和关闭。默认状态 200。manual 模式保留项目自己的输出实现。
- 自动 multipart Handler 检查未读部分和结束边界;损坏/截断默认返回 400,超限为 413。公共状态、码和文案仍可用项目错误目录配置,见 [流式接入与错误处理](PROJECT.md#自动上传回调与下载)。
迁移扩展起始于 v0.9.0,原有手写扩展说明见 [迁移扩展指南](MIGRATION.md)。当前根目录入口和自动流式 Handler 要求 CLI 与框架依赖一起使用 v0.12.0-preview;版本说明和新目录维护规则见 [项目指南](PROJECT.md)。不兼容全部 goctl 语法。
## 参数语义
无 optional/default 的字段按“是否出现”判断必填,`false`、`0`、`""` 是已提供的值。额外声明 `binding:"required"` 才会拒绝标量零值。默认值只在字段缺失时使用,不替换显式的空值。
业务需要区分“没传”和“传了零值”时使用 `*T` 加 optional。指针允许 JSON null;普通非指针字段拒绝 null(动态 `any` 允许);`binding:"required"` 拒绝指针 null。缺失与 null 都会得到 nil,本版没有三态 Nullable 类型。响应端的 `optional` 不等于 Go 编码器的 `omitempty`;需要省略零值时显式使用 `omitempty`。
JSON 必须是一个 object,Content-Type 为 application/json。默认最多 1 MiB,包含 chunked/未知长度和尾部空白;超限返回 413。重复 JSON key、未知字段、多个 JSON 值、标量 query/header 重复值、格式和约束错误返回 400;错误 Content-Type 返回 415。失败不会部分修改目标 DTO;错误详情只保留在内部 cause,公共响应不回显原始参数。
`contract.Binder` 在服务启动时编译,随后并发复用,不会每次请求重新分析标签。CLI 用相同编译器产生文档,因此来源、必填、默认、nullable、枚举、范围和长度具有同一来源。响应类型和约束由业务实现遵守,不在每次响应上再次校验;非 nullable 集合请返回空集合而不是 nil。
## 生成目录和业务边界
```text
api/ # 开发者维护的契约及导入
main.go # 启动、日志、优雅退出
etc/config.yaml
internal/config/
internal/svc/ # 依赖初始化和释放
internal/types/typesGen.go
internal/apierrors/errorsGen.go # 声明错误后生成
internal/handler/routesGen.go
internal/handler/openapiGen.go
internal/handler/<group>/handlersGen.go
internal/logic/<group>/logic.go
internal/logic/<group>/<operation>.go
internal/repository/<group>/repository.go
internal/middleware/registryGen.go
internal/middleware/<name>.go
docs/openapi.json
.goOmni/apiManifest.json
```
Logic 方法接收标准 `context.Context` 与生成的请求 DTO;Handler 调用 Logic 并完成响应包装。网络适配器仍经 Omni/httpx 接入。Repository 是业务所有的扩展位置,不自动生成虚构 CRUD 查询。
命名中间件按声明顺序组合,第一个在最外层。新 hook 初始返回 501,必须实现后才放行;registry 自动维护,hook 文件只创建一次。缺失 hook 在注册任何路由前报错。文档通过 `x-omni-middleware` 标出命名 hook,不把名称误当成已实现的认证协议。
## 安全更新与 CI
在生成项目中安装/使用与框架版本相符的 go-omni CLI:
```sh
go-omni api validate --api api/user.api
go-omni api generate --api api/user.api --dir .
go-omni api generate --api api/user.api --dir . --check
go test ./...
```
`main.go`、`Gen.go` 和 OpenAPI 由生成器管理,manifest 保存其 SHA-256。手动修改后更新会报冲突。Logic、Repository、配置、依赖装配和中间件实现按字节保留;只补缺失文件。删除接口/分组时只移除旧的生成文件,保留业务代码;已删除类型仍被旧业务引用时,需要明确迁移该业务文件,编译器会提示。
`--check` 只读,检查产物是否过期,以及所需骨架是否缺失。契约复制到项目后,请编辑项目内的 API;外部定义与已存在副本不一致时会拒绝覆盖。一个项目由一个 API 入口管理,可以通过导入拆分文件。
生成器拒绝输出路径穿越、受管理路径上的符号链接、大小写路径冲突和无所有权的覆盖。生成前统一校验/格式化,锁文件防止两个生成器同时修改;文件以原子替换写入,普通写入失败会回滚。本版不是整个目录的崩溃原子事务:进程被强杀或机器断电后,应按 Git 恢复生成产物和 manifest,再重试。请将 manifest 与生成产物一并提交,也不要在生成过程中编辑这些文件。
生成器不自动安装依赖、不访问数据库、不推送仓库、不替业务实现权限规则。
## 验证
```sh
go test -race -p=2 ./contract ./openapi ./cmd/go-omni/...
python3 scripts/verifyManagedProject.py
```
Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/managedProjectLocal.json`;完整记录见 [第一版验收](performance/managedProjectAcceptance.md)。