Files
go-omni/API.md

22 KiB
Raw Permalink Blame History

API 契约与项目生成

当前生成目录和启动扩展采用新布局,见 项目维护指南。@app(defaultResponse: Name) 在入口选择 App 默认响应。@body(mode: raw|multipart) 和 @produces 支持自动 Handler,手写 Handler 仅为显式选择的扩展。

首次接入请先看 README:包含固定远程版本安装、可运行项目、完整 .api 示例和已有服务接入步骤。本页用于查阅语法与生成规则;下方带 --framework-dir 的命令用于框架源码开发。

本阶段目标:从 .api 定义生成可运行的 Go 项目;定义、绑定、校验与 OpenAPI 保持一致;重复生成不覆盖业务实现;业务代码继续独立于网络层。实现入口是 cmd/go-omni/apigen,运行时契约编译器是 contract。

与 go-zero 的对应关系

调研参考了 go-zero 官方的 API DSL、参数规则、goctl API 和 项目结构。借鉴其声明接口后生成项目的工作方式:统一的类型定义、路由声明、分组、前缀和命名中间件。实现独立编写,不引入 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,HTTP 端点由 server.endpoints.openapi 配置

这是明确限定的 go-zero 风格方言,不是完整 goctl 兼容实现。不支持的声明和校验标签会在生成前报错。

立即试用

源码开发时,从 go-omni 仓库运行,显式引用本地框架:

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 仓库执行:

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 一致的固定版本,见 发布说明。

API 项目现在支持组合数据库、存储和日志:

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。MySQL、事务审计和鉴权实现可参考 旧目录用户管理示例,接入时按 项目指南 装配到 config/svc/logic。

接口定义

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 中声明公共响应模板,选择字段和结构,并按分组复用或由单接口覆盖,详见 公共响应体。

支持:

  • 可省略的 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 错误时才使用路由独立目录,规则见 目录继承。CLI 不执行运行时 Go 配置,因此静态文档不会自动枚举只在 Go 中定义的具体错误。

从 v0.10.0-preview 起,推荐用 errorCatalog 同时定义逻辑标识、对外业务码、提示和 HTTP 状态:

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.api。

从 v0.11.0-preview 起,目录条目支持 params 有序列表与 {name} 提示模板:

"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 使用规则见 提示模板参数。OpenAPI 的错误目录保留参数定义,响应示例使用具体文案。

原有声明继续支持:

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:

return nil, apierrors.EmailTaken()
// 可用 apierrors.CodeEmailTaken 引用同一个错误码。

每次调用返回新错误,避免并发修改共享错误对象。生成器不会自动把任意数据库错误转成业务错误;由 Service/适配层调用这些函数。 内部 Cause 可能进入访问日志,敏感驱动报错应像 userService 示例一样先归类再转换。

OpenAPI 对每个状态按选定的响应模板生成结构和命名 examples,同一 409 下多个错误不会相互覆盖;平铺模式的纯业务状态还列出 code enum。 默认框架错误状态与自动添加的鉴权 401 保留开放的 code 字段,容纳校验器/中间件实际错误。 x-omni-errors 提供错误目录与路由关联。目录解析已定义的错误,不自动推断数据库或其他未声明错误的业务含义,也不穷举所有运行时错误。

鉴权:声明与路由注册一起变化

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。

文档和日常开发命令

保留单行 @doc "摘要",也可以:

@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 路径:

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 ./... 验证。详见 契约变更提示。 在项目外可显式传 --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。公共状态、码和文案仍可用项目错误目录配置,见 流式接入与错误处理。

迁移扩展起始于 v0.9.0,原有手写扩展说明见 迁移扩展指南。当前根目录入口和自动流式 Handler 要求 CLI 与框架依赖一起使用 v0.13.0-preview;版本说明和新目录维护规则见 项目指南。不兼容全部 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。

生成目录和业务边界

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:

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 与生成产物一并提交,也不要在生成过程中编辑这些文件。

生成器不自动安装依赖、不访问数据库、不推送仓库、不替业务实现权限规则。

验证

go test -race -p=2 ./contract ./openapi ./cmd/go-omni/...
python3 scripts/verifyManagedProject.py

Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 performance/managedProjectLocal.json;完整记录见 第一版验收。

内置健康与 OpenAPI 路由可通过配置禁用或修改路径,.api 不再固定保留 /health/live、/health/ready、/openapi.json。启用的内置路径与业务 GET 路由重叠时,生成的 NewApp 在监听前返回配置错误;禁用后可以由 .api 自己维护这些路径。见 内置接口配置。