# API 契约与项目生成 首次接入请先看 [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 ` | | 按契约生成、更新 | `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/service/`,沿用 Omni 的 Service 分层 | | 依赖装配 | `internal/servicecontext` | | 接口文档 | 自动生成 `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 ./cmd/server ``` 访问 `http://127.0.0.1:8080/api/greet/Alice` 可得到带 `data.message` 的统一响应。`api new` 的问候示例包含可运行逻辑;自定义契约新生成的 Service 会明确返回 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 项目。旧式 CRUD 脚手架仍保留。 MySQL 密钥只从 `GO_OMNI_DB_DSN` 获取,非秘密池配置位于 YAML,环境变量优先。存储使用原有 `GO_OMNI_STORAGE_*`;API 项目不会额外生成公开的签名发放接口,鉴权和对象权限由业务实现。初始化失败会清理已打开资源,退出清理可重复调用。 HTTP 请求类型不能推导数据库表结构;迁移和 Repository 由业务自己维护。完整案例见 [用户管理示例](examples/userService/README.md),覆盖真实 MySQL、事务审计、鉴权、文件导出和日志。 ## 接口定义 ```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 与业务项目中的框架依赖需一起升级。 ## 业务错误:声明、代码和文档共用一份定义 从 `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)。 原有声明继续支持: ```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` 清空继承的业务错误。未声明、重复声明或生成符号冲突会报告源码位置。 生成 `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 # 预览新增/更新/移除,完全不写文件 go-omni api generate # 实际生成,保留手写业务 go-omni api generate --check # 只读检查,可用于 CI go-omni api export > openapi.json # 独立导出纯 JSON,无需启动服务 go test ./... ``` `--dry-run` 和 `--check` 不能组合。新项目预览也不创建目录;预览不是审批或锁定,实际生成会重新检查文件状态。 在项目外可显式传 `--api`;生成时传 `--dir` 可定位其他项目。不会猜测多个 .api 中哪个是入口。 路由总览的 `(undeclared)` 仅表示没有声明 auth;旧式中间件仍可能做认证。 外发文档可用 export;项目内受生成器管理的 `docs/openapi.json` 请通过 generate 更新,以同步文件哈希。 ## 迁移扩展(v0.9.0-preview) - 动态 JSON 字段 `any` / `interface{}`,包含切片和 map;外围契约仍严格。 - `scalar Name (...)` 显式引用项目类型,声明 JSON 输入/输出类型及文本绑定;支持已有 JSON/Text 转换方法。 - `@body (mode: json/raw/multipart; maxBytes: ...)`:按路由限制请求体。 - `@binding custom` 生成保留的绑定 hook;`@handlerMode manual` 生成保留的整个 Handler,raw/multipart 使用这个入口。 - manual 可用 `@produces "text/plain"` 等声明无 DTO 成功输出。默认状态 200,初始手写 stub 返回 501。 完整语法、边界和示例见 [迁移扩展指南](MIGRATION.md)。新增生成代码要求 CLI 与框架依赖一起升级到 `v0.9.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。 ## 生成目录和业务边界 ```text api/ # 开发者维护的契约及导入 cmd/server/main.go # 启动、日志、优雅退出 etc/config.yaml internal/settings/ internal/servicecontext/ # 依赖初始化和释放 internal/types/typesGen.go internal/apierrors/errorsGen.go # 声明错误后生成 internal/handler/routesGen.go internal/handler/openapiGen.go internal/handler//handlersGen.go internal/service//service.go internal/service//.go internal/repository//repository.go internal/middleware/registryGen.go internal/middleware/.go docs/openapi.json .goOmni/apiManifest.json ``` Service 方法接收标准 `context.Context` 与生成的请求 DTO;Handler 调用 Service 并完成响应包装。网络适配器仍经 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 ./... ``` `Gen.go` 和 OpenAPI 由生成器管理,manifest 保存其 SHA-256。手动修改后更新会报冲突。Service、Repository、main、配置、依赖装配和中间件实现按字节保留;只补缺失文件。删除接口/分组时只移除旧的生成文件,保留业务代码;已删除类型仍被旧业务引用时,需要明确迁移该业务文件,编译器会提示。 `--check` 只读,检查产物是否过期,以及所需骨架是否缺失。契约复制到项目后,请编辑项目内的 API;外部定义与已存在副本不一致时会拒绝覆盖。一个项目由一个 API 入口管理,可以通过导入拆分文件。 生成器拒绝输出路径穿越、受管理路径上的符号链接、大小写路径冲突和无所有权的覆盖。生成前统一校验/格式化,锁文件防止两个生成器同时修改;文件以原子替换写入,普通写入失败会回滚。本版不是整个目录的崩溃原子事务:进程被强杀或机器断电后,应按 Git 恢复生成产物和 manifest,再重试。请将 manifest 与生成产物一并提交,也不要在生成过程中编辑这些文件。 生成器不自动安装依赖、不访问数据库、不推送仓库、不替业务实现权限规则。 ## 验证 ```sh go test -race -p=2 ./contract ./openapi ./cmd/go-omni/... python3 scripts/verifyAPI.py ``` Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/apiGenerationLocal.json`。