Compare commits

...

4 Commits

37 changed files with 2459 additions and 141 deletions

4
API.md
View File

@@ -109,7 +109,7 @@ service user-api {
- 来源标签中的 `optional` / `omitempty`、标量 `default`、标量 `options=a|b`、数值 `range=[1:100]`(支持开区间和单边界)。 - 来源标签中的 `optional` / `omitempty`、标量 `default`、标量 `options=a|b`、数值 `range=[1:100]`(支持开区间和单边界)。
- `binding` 的 `required`、`min`、`max`、`len`、`email`。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。 - `binding` 的 `required`、`min`、`max`、`len`、`email`。长度按 Unicode 字符或切片元素数;范围边界必须有限且位于 ±2^53 内。
暂不支持匿名嵌入、别名、递归类型、未声明的外部 Go 类型、未通过 scalar 显式接入的自定义编解码、`[]byte`、泛型、`json:",string"`、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。 暂不支持匿名嵌入、别名、递归请求类型、未声明的外部 Go 类型、未通过 scalar 显式接入的自定义编解码、`[]byte`、泛型、`json:",string"`、任意 validator 标签、go-zero 的 JWT/签名/SSE 声明,以及客户端 SDK 生成。认证验证通过声明关联的命名中间件实现。未列出的语法会明确报错。
业务错误、具名鉴权、多行文档及下述开发命令从 CLI `v0.7.0-preview.1` 提供。公共响应模板从 `v0.8.0-preview` 提供,CLI 与业务项目中的框架依赖需一起升级。 业务错误、具名鉴权、多行文档及下述开发命令从 CLI `v0.7.0-preview.1` 提供。公共响应模板从 `v0.8.0-preview` 提供,CLI 与业务项目中的框架依赖需一起升级。
@@ -331,3 +331,5 @@ python3 scripts/verifyManagedProject.py
Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/managedProjectLocal.json`;完整记录见 [第一版验收](performance/managedProjectAcceptance.md)。 Go 测试覆盖契约拒绝规则、多源绑定、零值/缺失、默认/范围/嵌套集合、文档约束、幂等更新、只读检查、业务保留、删除路由、路径边界及生成项目编译。Python 验收会在临时目录生成项目,分别启动 nethttp/native,验证真实 HTTP 请求、中间件、日志 requestId 和优雅退出;结束后清理自己创建的进程和临时文件。结果记录在 `performance/managedProjectLocal.json`;完整记录见 [第一版验收](performance/managedProjectAcceptance.md)。
内置健康与 OpenAPI 路由可通过配置禁用或修改路径,`.api` 不再固定保留 `/health/live`、`/health/ready`、`/openapi.json`。启用的内置路径与业务 GET 路由重叠时,生成的 NewApp 在监听前返回配置错误;禁用后可以由 `.api` 自己维护这些路径。见 [内置接口配置](PROJECT.md#配置内置接口)。 内置健康与 OpenAPI 路由可通过配置禁用或修改路径,`.api` 不再固定保留 `/health/live`、`/health/ready`、`/openapi.json`。启用的内置路径与业务 GET 路由重叠时,生成的 NewApp 在监听前返回配置错误;禁用后可以由 `.api` 自己维护这些路径。见 [内置接口配置](PROJECT.md#配置内置接口)。
响应 DTO 自引用与相互引用从 v0.28.2 起支持,保留强类型并生成 OpenAPI `$ref`;见[递归响应说明](DEVELOPMENT.md#递归响应-dto)。直接值循环与字段组合循环仍然非法。

View File

@@ -1,3 +1,8 @@
# v0.28.2-preview
- 修复响应 DTO 的自引用/相互引用生成,支持 slice、pointer、map;OpenAPI 使用命名组件与循环引用,保留约束、描述、示例及 scalar 输出 schema。
- 拒绝 Go 非法值循环与递归请求;保留格式化、生成、契约变更和只读检查能力。新增通用树示例与真实生成消费者回归。
# Changelog # Changelog
## v0.27.0-preview(2026-09-29) ## v0.27.0-preview(2026-09-29)

View File

@@ -1,4 +1,4 @@
# 通用业务开发指南(v0.27.0-preview) # 通用业务开发指南(v0.28.2-preview)
从 [README](README.md) 创建服务后,业务主要维护 `.api`、配置、`svc.Init`、Logic、Repository 和项目中间件。本页说明通用接入能力;字段、错误码、权限、表结构和事务边界都由项目定义。 从 [README](README.md) 创建服务后,业务主要维护 `.api`、配置、`svc.Init`、Logic、Repository 和项目中间件。本页说明通用接入能力;字段、错误码、权限、表结构和事务边界都由项目定义。
@@ -35,6 +35,30 @@ delete /item
基础类型数组还支持 int/int8/int16/int32、uint/uint16/uint32/uint64、float32/float64。nil 根数组都输出 `[]`;`[]any` 在 OpenAPI 中是 `type: array, items: {}`,不限制元素形状,但元素必须可被标准 JSON 编码器序列化。`[]uint8`(Go 的 byte slice)会编码为 base64,故明确拒绝;数字数组可用 `[]int32`,二进制用下载接口。当前不支持根嵌套数组或根自定义 scalar 数组。 基础类型数组还支持 int/int8/int16/int32、uint/uint16/uint32/uint64、float32/float64。nil 根数组都输出 `[]`;`[]any` 在 OpenAPI 中是 `type: array, items: {}`,不限制元素形状,但元素必须可被标准 JSON 编码器序列化。`[]uint8`(Go 的 byte slice)会编码为 base64,故明确拒绝;数字数组可用 `[]int32`,二进制用下载接口。当前不支持根嵌套数组或根自定义 scalar 数组。
## 递归响应 DTO
v0.28.2 起,响应 DTO 可引用自身或相互引用;名字与字段由项目定义:
```text
type Node {
Name string `json:"name"`
Children []Node `json:"children"`
Next *Node `json:"next,omitempty"`
Lookup map[string]Node `json:"lookup,omitempty"`
}
@handler tree
get /tree returns (Node)
```
Go 类型保留 `[]Node`、`*Node`、`map[string]Node`,不降级为 `any`。生成器将递归响应图中的命名 DTO 写入 `components.schemas.DTO_<类型名>`,字段使用 `$ref`;对象、数组、可空根对象、raw 及自定义响应模板都支持。可空引用使用 `anyOf` 的引用与 null 分支,描述和示例通过 `allOf` 保留,遵循 [OpenAPI 3.0.3 引用规则](https://spec.openapis.org/oas/v3.0.3.html#reference-object)。
字段命名、标签、约束、重复字段与示例继续校验,自定义 scalar 使用声明的输出类型。有限 JSON 示例按实际内容检查,仍受 32 层/10000 节点的编译限制。类型引用不会无限展开;再次生成和契约变更检查会遍历引用图并保留手写代码。
范围是 `.api` 的响应类型。递归请求 DTO(包括间接包含递归类型的请求)仍不支持,`contract.New/Describe` 与手写 `openapi.SchemaOf` 的限制不变;它们不会自动获得递归请求绑定能力。直接值递归如 `Child Node`、仅由值字段形成的相互递归,以及字段组合循环仍会报错。业务返回值必须是可序列化的有限数据,不能构造对象指针环。
升级 CLI 后,在项目自己的 `.api` 中将临时 `[]any` 恢复成 `[]项目类型`,执行 `go-omni api generate`、`go-omni api generate --check` 和 `go test ./...`。修改 `.api` 后应先生成再检查;不要手改生成的 DTO。不改变项目的菜单、分类、权限或数据构造规则。完整可运行步骤见 [递归响应示例](examples/recursiveResponse/README.md)。
## 契约驱动的请求策略 ## 契约驱动的请求策略
入口文件可以声明默认策略,单个接口按需覆盖: 入口文件可以声明默认策略,单个接口按需覆盖:
@@ -199,6 +223,31 @@ go-omni workspace check --file omni.workspace.json
各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。 各服务保持独立 module;可显式填写各服务的 `api`(相对服务目录)和 `module`。所有服务预检通过后再写;每个服务独立提交文件事务,不承诺跨项目的崩溃原子性。check 汇总格式与生成漂移;业务编译/测试仍在各 module 执行。
### 混合 API 与通用工程
v0.28.2-preview 起,workspace 支持在同一份清单中管理 API、通用工程,也支持仅包含通用工程的工作区:
```json
{
"services": [
{ "dir": "apps/interface", "kind": "api" },
{ "dir": "apps/program", "kind": "project" }
]
}
```
`kind` 可省略:根据 `.goOmni/apiManifest.json` 或 `.goOmni/projectManifest.json` 识别。没有清单时,沿用旧 API 解释,需显式提供 api 入口才能首次生成。两种清单同时存在、显式 kind 与已有清单冲突、未知 kind 均报错。project 类型不能填写 api;可选 module 用来核对工程身份,不会重写 go.mod。
| 命令 | API 工程 | 通用工程 |
| --- | --- | --- |
| workspace generate | API 生成器 | project 生成器 |
| workspace check | 契约格式检查 + API 生成漂移检查 | project 生成漂移检查 |
| workspace format | 格式化 API 契约 | 显示 SKIP,不修改 Go 文件 |
检查会继续处理独立工程并汇总错误,任一失败返回非零。生成/格式化先预检全部目标;预检失败时输出 FAIL 与其他工程的 SKIP,不写任何生成结果。清单语法、目录越界/重叠或共享目录提供方失败属于工作区级错误,会直接中止。已有文件保护与路径规则保持,事务仍以单个工程为单位。
workspace 不创建或修改 go.work、go.mod、go.sum,不替代 Go 编译和业务测试。它只调用生成器;显式 errorCatalogs 导出仍按下节约定执行 Go 提供方,配置类型不会额外启动服务。完整 [混合工程与 go.work 示例](examples/mixedWorkspace/README.md)。
### 同一份 Go 错误目录供多个服务生成 ### 同一份 Go 错误目录供多个服务生成
在 workspace 清单显式声明提供目录的 Go 包和函数: 在 workspace 清单显式声明提供目录的 Go 包和函数:

View File

@@ -5,7 +5,7 @@
## 创建与运行 ## 创建与运行
```sh ```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
go-omni project new demo --module example.com/demo go-omni project new demo --module example.com/demo
cd demo cd demo
go mod tidy go mod tidy
@@ -78,4 +78,4 @@ go test ./...
check 检查模板漂移和必要文件,不检查业务正确性,也不能替代 Go 编译与测试。新的模板版本会重新生成受管文件;不自动合并项目所有文件或更新 go.mod。未知的新清单版本会要求使用兼容 CLI。 check 检查模板漂移和必要文件,不检查业务正确性,也不能替代 Go 编译与测试。新的模板版本会重新生成受管文件;不自动合并项目所有文件或更新 go.mod。未知的新清单版本会要求使用兼容 CLI。
原有 `new` / `api new` 及 API 工程行为保留。通用工程使用自己的清单和命令,不能交给 `api generate` 或 `add` 转换;新增业务包直接写普通 Go。现有 `workspace` 命令仍面向 API 契约工程;多个通用工程可分别执行 `project check --dir <目录>`,本版不引入混合工作区配置。 原有 `new` / `api new` 及 API 工程行为保留。通用工程使用自己的清单和命令,不能交给 `api generate` 或 `add` 转换;新增业务包直接写普通 Go。从 v0.28.2-preview 起,`workspace generate/check` 可以同时处理 API 与通用工程。清单可填写 `kind: api | project`,省略时按生成清单识别;format 对通用工程显示 SKIP。见 [完整混合示例](examples/mixedWorkspace/README.md)。各 module 仍独立,workspace 命令不维护 go.work 或模块依赖。

View File

@@ -2,13 +2,17 @@
GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。 GoOmni 是面向多个项目复用的 Go 基础框架。通用工程使用 `project new` 生成配置、日志、依赖装配、生命周期和普通 Go 运行入口;需要 HTTP 接口时可选择 `.api` 工程生成,也可以在已有项目中按需引入独立模块。运行方式、业务类型和外部依赖由项目决定。
**v0.28.2 改动**:API 响应 DTO 支持 `Children []Node`、`Next *Node`、递归 map 和相互引用,生成强类型 Go 字段及有限的 OpenAPI `$ref` 图。字段约束、示例校验、生成与漂移检查继续有效;请求递归和非法值递归仍会报错。见 [可运行示例](examples/recursiveResponse/README.md)与[接入说明](DEVELOPMENT.md#递归响应-dto)。
**v0.28.1 改动**:`workspace generate/check` 支持 API 与通用工程混合处理,可选 `kind: api | project`,省略时按生成清单识别。`format` 对通用工程显示跳过;检查只读,生成前统一预检。见 [混合工作区示例](examples/mixedWorkspace/README.md)。
**v0.28 改动**:新增独立的 `project new/generate/check`,默认无 API、监听端口、业务任务或外部服务配置;保护手写代码,并等待主运行入口和登记的后台工作结束后释放依赖。见 [通用工程指南](PROGRAM.md)和[可运行示例](examples/program/README.md)。 **v0.28 改动**:新增独立的 `project new/generate/check`,默认无 API、监听端口、业务任务或外部服务配置;保护手写代码,并等待主运行入口和登记的后台工作结束后释放依赖。见 [通用工程指南](PROGRAM.md)和[可运行示例](examples/program/README.md)。
**v0.27 改动**:统一三种内置服务实现的默认/路由/硬请求体限额,配置冲突在启动前报错;停机以处理函数实际退出为资源清理前提,超时仍有请求时保留共享依赖。见 [HTTP 策略](transport/README.md#统一请求体限额)、[停机规则](lifecycle/README.md#http-请求与依赖的关闭屏障)和[可运行示例](examples/httpSafety/README.md)。 **v0.27 改动**:统一三种内置服务实现的默认/路由/硬请求体限额,配置冲突在启动前报错;停机以处理函数实际退出为资源清理前提,超时仍有请求时保留共享依赖。见 [HTTP 策略](transport/README.md#统一请求体限额)、[停机规则](lifecycle/README.md#http-请求与依赖的关闭屏障)和[可运行示例](examples/httpSafety/README.md)。
**v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.28.0-preview。同时修复 Go 1.22 下请求体限额错误的识别。 **v0.26 改动**:通用 [i18n](i18n/README.md),提供独立词典、语言匹配、回退、命名参数/条件/复数、可选 HTTP 与错误适配、检查/导出 CLI 及前端运行时。文案由项目维护;[完整示例](examples/i18n/README.md)可直接运行。CLI 与框架使用同版 v0.28.2-preview。同时修复 Go 1.22 下请求体限额错误的识别。
**从这份 README 开始接入。** 当前版本 **`v0.28.0-preview`**,通用工程采用根目录入口和 `config / svc / run`;API 工程使用 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。 **从这份 README 开始接入。** 当前版本 **`v0.28.2-preview`**,通用工程采用根目录入口和 `config / svc / run`;API 工程使用 `config / svc / logic / handler`。可以固定版本开展业务接入;文件维护边界和已有服务迁移步骤见 [项目指南](PROJECT.md),版本变化与验收见 [发布说明](RELEASE.md)。
**v0.25 改动**:`database.InTx` 返回可识别的事务阶段与结果,保留业务/驱动及回滚错误链;提交报错保守标记为结果不确定,不等同于已回滚。[使用规则](database/README.md#事务阶段与结果)、[可运行故障示例](examples/transactions/README.md)。 **v0.25 改动**:`database.InTx` 返回可识别的事务阶段与结果,保留业务/驱动及回滚错误链;提交报错保守标记为结果不确定,不等同于已回滚。[使用规则](database/README.md#事务阶段与结果)、[可运行故障示例](examples/transactions/README.md)。
@@ -40,6 +44,7 @@ Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可
| --- | --- | --- | | --- | --- | --- |
| 项目、接口与文档生成 | `go-omni new`;改 `.api` 后 `api generate`;`--check` 检查漂移 | [下方快速开始](#新项目生成并运行)、[CLI](cmd/go-omni/README.md)、[完整生成项目](examples/managedService/README.md) | | 项目、接口与文档生成 | `go-omni new`;改 `.api` 后 `api generate`;`--check` 检查漂移 | [下方快速开始](#新项目生成并运行)、[CLI](cmd/go-omni/README.md)、[完整生成项目](examples/managedService/README.md) |
| 公共字段与跨服务共享契约 | `.api` 组合对象字段;`omni.imports.json` 声明共享根;workspace 批量生成 | [公共字段示例](GENERATION.md#公共字段组合)、[共享契约与 workspace](DEVELOPMENT.md) | | 公共字段与跨服务共享契约 | `.api` 组合对象字段;`omni.imports.json` 声明共享根;workspace 批量生成 | [公共字段示例](GENERATION.md#公共字段组合)、[共享契约与 workspace](DEVELOPMENT.md) |
| 混合工程工作区 | `workspace generate/check` 按清单分发 API/project;format 仅处理 API | [可运行示例](examples/mixedWorkspace/README.md)、[工作区说明](DEVELOPMENT.md#混合-api-与通用工程) |
| 通用 Go 工程 | `go-omni project new/generate/check`;配置、日志、依赖、运行与退出 | [通用工程指南](PROGRAM.md)、[完整可运行示例](examples/program/README.md) | | 通用 Go 工程 | `go-omni project new/generate/check`;配置、日志、依赖、运行与退出 | [通用工程指南](PROGRAM.md)、[完整可运行示例](examples/program/README.md) |
| 中间件依赖工厂、可选 Repository | `omni.gen.json` 选择 factories / none;在 `New<Name>(deps)` 写一次实现 | [配置与完整工厂示例](GENERATION.md) | | 中间件依赖工厂、可选 Repository | `omni.gen.json` 选择 factories / none;在 `New<Name>(deps)` 写一次实现 | [配置与完整工厂示例](GENERATION.md) |
| 路由、分组、请求上下文 | `omni.New()`、`app.GET` / `Group` / `Use`;业务使用 `context.Context` | [基本服务](examples/basic/main.go)、[已有项目接入](#已有项目按模块接入) | | 路由、分组、请求上下文 | `omni.New()`、`app.GET` / `Group` / `Use`;业务使用 `context.Context` | [基本服务](examples/basic/main.go)、[已有项目接入](#已有项目按模块接入) |
@@ -72,7 +77,7 @@ Go **1.22+**。框架不依赖 Gin,默认使用标准库网络层,提供可
## 通用工程快速开始 ## 通用工程快速开始
```sh ```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
go-omni project new demo --module example.com/demo go-omni project new demo --module example.com/demo
cd demo cd demo
go mod tidy go mod tidy
@@ -177,7 +182,7 @@ module 地址是 `git.yuntuo.hk/k2safe/go-omni`。先确保本机 Go 和 Git 可
```sh ```sh
export GOPRIVATE="$(go env GOPRIVATE),git.yuntuo.hk" export GOPRIVATE="$(go env GOPRIVATE),git.yuntuo.hk"
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
export PATH="$(go env GOPATH)/bin:$PATH" export PATH="$(go env GOPATH)/bin:$PATH"
go-omni --help go-omni --help
``` ```
@@ -334,7 +339,7 @@ go-omni new --api catalog.api \
在业务项目的 **go.mod 所在目录**执行: 在业务项目的 **go.mod 所在目录**执行:
```sh ```sh
go get git.yuntuo.hk/k2safe/go-omni@v0.28.0-preview go get git.yuntuo.hk/k2safe/go-omni@v0.28.2-preview
``` ```
已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。 已有项目可直接复用现有目录、Service、Repository 和数据库连接,先选择一个接口接入。也可以只用 `logx`、`storage` 等包,暂时保留原来的 HTTP 框架。希望入口和 HTTP 接入都交给生成器的项目,按 [已有服务接入步骤](PROJECT.md#已有服务接入步骤) 在新目录生成后迁入业务代码。
@@ -665,7 +670,7 @@ go test ./...
`main.go`、`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Logic、Repository、中间件实现、配置和依赖装配属于项目,重复生成保留手写内容。契约变化会提示受影响的 Logic 文件和 DTO 字段,明确的签名错误会给出原签名与期望签名;生成后 `--check` 仍能检出未修复的问题。将 `.api`、`omni.gen.json`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 同时运行 `generate --check` 与 `go test ./...`。详见 [契约变更提示与检查边界](PROJECT.md#契约变更提示)。 `main.go`、`Gen.go`、`docs/openapi.json` 和 manifest 由生成器管理,手动修改会报告冲突。Logic、Repository、中间件实现、配置和依赖装配属于项目,重复生成保留手写内容。契约变化会提示受影响的 Logic 文件和 DTO 字段,明确的签名错误会给出原签名与期望签名;生成后 `--check` 仍能检出未修复的问题。将 `.api`、`omni.gen.json`、生成文件和 `.goOmni/apiManifest.json` 一起提交,CI 同时运行 `generate --check` 与 `go test ./...`。详见 [契约变更提示与检查边界](PROJECT.md#契约变更提示)。
CLI 与业务 module 固定使用 `v0.28.0-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。 CLI 与业务 module 固定使用 `v0.28.2-preview`。v0.21 及更早项目先按 [配置迁移](CONFIG.md#v022-迁移旧模式已移除) 整理旧标签、文件与 Load,再升级依赖与 CLI 并重新生成,框架维护的 main/runtimeGen 自动更新;v0.13 及更早项目按 [升级预览](DEVELOPMENT.md#v013-项目升级) 处理,不直接覆盖手写入口或业务文件。使用过旧 mapping 的项目必须先完成 [移除迁移](COPY.md#从旧-mapping-迁移),旧配置或旧生成文件会阻止生成。升级后执行生成检查与业务回归。版本变化、迁移边界和验收记录见 [发布说明](RELEASE.md)。
## 按需深入 ## 按需深入

View File

@@ -1,3 +1,38 @@
# v0.28.2-preview
修复 `.api` 响应 DTO 自引用与相互引用的生成限制。`Children []Node`、`Next *Node`、`map[string]Node` 保留强类型,响应文档使用有限的命名组件与 `$ref`;可空引用、字段描述、示例、约束和 scalar 输出类型保持准确。框架不引入业务模型。
请求绑定继续拒绝递归类型,Go 非法直接值循环与字段组合循环继续报错。已有非递归类型保持原有生成行为;无需修改运行时配置或响应 JSON。升级 CLI,在项目 `.api` 中恢复强类型后重新生成,再执行检查和项目回归。生成模板最低运行时版本不变。
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
go-omni api generate
go-omni api generate --check
go test ./...
```
[递归响应说明](DEVELOPMENT.md#递归响应-dto)、[可运行示例](examples/recursiveResponse/README.md)。56 个测试包全量 race 与 Go 1.22 集成回归、vet/build、独立 OpenAPI 校验均通过,见[验收记录](reports/recursiveResponseAcceptance.md)。
---
# v0.28.1-preview
workspace generate/check 支持 API 与通用工程混合分发,也可处理纯通用工程工作区。services 新增可选 kind(api/project);省略时按生成清单识别,保留无清单时显式 api 入口的原有用法。类型与清单冲突、歧义和不合法选项会报错,项目身份、路径与文件归属继续由各生成器校验。
check 只读并汇总独立工程的失败;format 只处理 API 契约,对通用工程明确显示 SKIP。generate 预检所有工程后再写入,预检失败时不修改其他工程;不改变各模块依赖或 go.work,不增加业务逻辑和第三方依赖。模板与运行时接口未变,新 workspace 功能需要更新 CLI,旧生成工程可直接纳入。
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.1-preview
go-omni workspace generate
go-omni workspace check
```
[混合工作区示例](examples/mixedWorkspace/README.md)、[清单规则](DEVELOPMENT.md#混合-api-与通用工程)、[验收记录](reports/workspaceAcceptance.md)。56 个测试包全量 race、5 个 CLI 包的 Go 1.22 回归、实际生成的多模块构建、vet/build 均通过。
远程空缓存验收完成:混合工作区 16 项、通用工程 30 项、原有生成器 98 项检查全部通过,均无本地 replace;见 [混合报告](reports/workspaceRemote.json)、[通用报告](reports/workspaceProgramRemote.json)、[兼容报告](reports/workspaceManagedRemote.json)。标签固定在 `0d2c6040e2694795f203304148dbaec00318c608`。
---
# v0.28.0-preview # v0.28.0-preview
新增通用 Go 工程生成命令 `project new/generate/check`。默认只有配置、日志、svc 依赖装配、生命周期与项目维护的 Run 入口,不生成 API、HTTP Handler、Swagger、业务任务或网络监听,也不自动连接数据库/队列或重试任务。常驻和一次性程序由普通 Go 代码决定。 新增通用 Go 工程生成命令 `project new/generate/check`。默认只有配置、日志、svc 依赖装配、生命周期与项目维护的 Run 入口,不生成 API、HTTP Handler、Swagger、业务任务或网络监听,也不自动连接数据库/队列或重试任务。常驻和一次性程序由普通 Go 代码决定。
@@ -18,6 +53,8 @@ go-omni project check
完整 [通用工程指南](PROGRAM.md)、[可运行示例](examples/program/README.md)、[回归记录](reports/programAcceptance.md)。56 个测试包全量 race 与 Go 1.22 回归、生成消费者、vet/build 和真实进程退出验证通过。 完整 [通用工程指南](PROGRAM.md)、[可运行示例](examples/program/README.md)、[回归记录](reports/programAcceptance.md)。56 个测试包全量 race 与 Go 1.22 回归、生成消费者、vet/build 和真实进程退出验证通过。
空缓存远程安装已完成:通用工程 30 项检查通过(26.45 秒),原有生成项目 98 项检查、10 组消费者通过(111.07 秒),均无本地 replace。见 [通用远程报告](reports/programRemote.json)、[原有能力远程报告](reports/programManagedRemote.json)。标签固定在 `0b6c0043b6c3b58648645aeee03bf80801e6aa3f`。
--- ---
# v0.27.0-preview # v0.27.0-preview

View File

@@ -1,5 +1,7 @@
# go-omni CLI # go-omni CLI
v0.28.1 的 `workspace generate/check` 支持 API 与通用工程混合处理。每个 services 项可指定 `kind: api | project`,省略时按工程清单识别,旧 API 配置继续有效。format 只处理 API 契约,通用工程显示 SKIP;check 只读并汇总各工程失败。见 [可运行示例](../../examples/mixedWorkspace/README.md)。
v0.28 新增 `project new/generate/check`:生成无 API、无监听端口和无外部服务依赖的通用 Go 程序,配置/日志/生命周期自动装配,项目维护 svc 与 Run。`project generate --dry-run` 预览,`project check` 只读检查;详见 [通用工程指南](../../PROGRAM.md)。 v0.28 新增 `project new/generate/check`:生成无 API、无监听端口和无外部服务依赖的通用 Go 程序,配置/日志/生命周期自动装配,项目维护 svc 与 Run。`project generate --dry-run` 预览,`project check` 只读检查;详见 [通用工程指南](../../PROGRAM.md)。
v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用当前同版 CLI 与框架。 v0.26 新增 `i18n check|export|types|extract`:检查独立语言资源、提取 Go 静态消息引用或已导出的错误目录,导出指定命名空间的前端包和 TypeScript 类型。输入只读,结果写 stdout;动态引用明确报告。完整命令见 [国际化工具链](../../i18n/README.md#cli-提取检查与导出)。`.api errorCatalog` 可声明 translation 引用,旧 runtime 在生成前拒绝,接入时使用当前同版 CLI 与框架。
@@ -18,7 +20,7 @@ API 新项目采用根目录 `main.go` 与 `internal/config、svc、logic、hand
```sh ```sh
# 仓库读取权限与 PATH 配置见根目录 README # 仓库读取权限与 PATH 配置见根目录 README
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.0-preview go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
go-omni new --module example.com/hello ./hello go-omni new --module example.com/hello ./hello
# 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在 # 从自己的契约生成;service.api 及其相对 import 文件需要在本地存在
@@ -31,6 +33,8 @@ go-omni new --api service.api --module example.com/example ./example
存储配置和客户端位于 config/svc,业务使用统一接口。local 存储签名端点由生成器接入;上传、下载等业务路由由 `.api` 定义。认证与存储路径由项目实现。 存储配置和客户端位于 config/svc,业务使用统一接口。local 存储签名端点由生成器接入;上传、下载等业务路由由 `.api` 定义。认证与存储路径由项目实现。
响应 DTO 支持自引用及相互引用,生成强类型 Go 字段与 OpenAPI `$ref`;见[递归响应示例](../../examples/recursiveResponse/README.md)。请求递归仍会报错。
## 日常生成 ## 日常生成
```sh ```sh
@@ -53,14 +57,14 @@ main、Gen.go、OpenAPI 和 manifest 由生成器维护;业务文件按字节
## 版本与迁移 ## 版本与迁移
CLI 与框架依赖固定使用 v0.28.0-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换: CLI 与框架依赖固定使用 v0.28.2-preview。远程安装的 CLI 默认把自身版本写入 go.mod,不添加本地 replace;`--framework-version` 可显式指定已发布的兼容版本。仅在开发框架源码时使用本地替换:
```sh ```sh
# 在 go-omni 源码仓库中执行 # 在 go-omni 源码仓库中执行
go run ./cmd/go-omni new --module example.com/hello --framework-dir . ../hello go run ./cmd/go-omni new --module example.com/hello --framework-dir . ../hello
``` ```
当前模板与依赖以 v0.28.0-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。 当前模板与依赖以 v0.28.2-preview 为基线,继续使用 manifest v2;GORM 可选适配器从 v0.14.1-preview 提供。已有项目使用 [定点升级与预览](../../DEVELOPMENT.md#v013-项目升级):原始模板可自动替换,含业务修改的基础文件按预览合并;通用清理由 Runtime 负责。独立工具命令不会被删除。受管理项目通过 `.api` 模块导入增加业务,不使用旧 `add` 命令的手动路由装配。
## 数据库迁移命令 ## 数据库迁移命令

View File

@@ -5,8 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"go/ast"
goparser "go/parser"
gotoken "go/token" gotoken "go/token"
"reflect" "reflect"
"regexp" "regexp"
@@ -69,56 +67,17 @@ func Compile(spec *Spec) (*Compiled, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
resolved := map[string]reflect.Type{} graph, err := newResponseGraph(spec, definitions, scalarTypes, scalarDefs)
resolving := map[string]bool{} if err != nil {
var resolve func(string, int) (reflect.Type, error) return nil, err
var expression func(ast.Expr, int) (reflect.Type, error) }
for _, r := range spec.Routes {
expression = func(e ast.Expr, depth int) (reflect.Type, error) { if graph.recursive[r.Request] {
if depth > 32 { return nil, fmt.Errorf("%s: recursive request DTO %s is unsupported; recursive types are supported for responses only", r.Position, r.Request)
return nil, errors.New("type nesting exceeds 32")
}
switch e := e.(type) {
case *ast.Ident:
if t, ok := scalarTypes[e.Name]; ok {
return t, nil
}
if t, ok := primitiveTypes[e.Name]; ok {
return t, nil
}
return resolve(e.Name, depth+1)
case *ast.StarExpr:
if _, double := e.X.(*ast.StarExpr); double {
return nil, errors.New("multiple pointer levels unsupported")
}
t, err := expression(e.X, depth+1)
if err != nil {
return nil, err
}
return reflect.PointerTo(t), nil
case *ast.ArrayType:
if e.Len != nil {
return nil, errors.New("use slices, not fixed arrays")
}
t, err := expression(e.Elt, depth+1)
if err != nil {
return nil, err
}
return reflect.SliceOf(t), nil
case *ast.MapType:
key, ok := e.Key.(*ast.Ident)
if !ok || key.Name != "string" {
return nil, errors.New("map keys must be string")
}
t, err := expression(e.Value, depth+1)
if err != nil {
return nil, err
}
return reflect.MapOf(reflect.TypeOf(""), t), nil
default:
return nil, errors.New("unsupported type expression")
} }
} }
resolved := map[string]reflect.Type{}
var resolve func(string, int) (reflect.Type, error)
resolve = func(name string, depth int) (reflect.Type, error) { resolve = func(name string, depth int) (reflect.Type, error) {
if depth > 32 { if depth > 32 {
return nil, errors.New("type nesting exceeds 32") return nil, errors.New("type nesting exceeds 32")
@@ -126,27 +85,13 @@ func Compile(spec *Spec) (*Compiled, error) {
if t, ok := resolved[name]; ok { if t, ok := resolved[name]; ok {
return t, nil return t, nil
} }
if resolving[name] {
return nil, fmt.Errorf("recursive type %s is unsupported", name)
}
typ, ok := definitions[name] typ, ok := definitions[name]
if !ok { if !ok {
return nil, fmt.Errorf("unknown type %s", name) return nil, fmt.Errorf("unknown type %s", name)
} }
resolving[name] = true
defer delete(resolving, name)
var fields []reflect.StructField var fields []reflect.StructField
seen := map[string]bool{} for i, f := range typ.Fields {
for _, f := range typ.Fields { t, err := graph.expression(graph.exprs[name][i], depth+1, resolve)
if !exportedName.MatchString(f.Name) || seen[f.Name] {
return nil, fmt.Errorf("%s: invalid or duplicate field %s", name, f.Name)
}
seen[f.Name] = true
expr, err := goparser.ParseExpr(f.Type)
if err != nil {
return nil, err
}
t, err := expression(expr, depth+1)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err) return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
} }
@@ -158,6 +103,12 @@ func Compile(spec *Spec) (*Compiled, error) {
} }
c := &Compiled{Spec: spec, Bindings: map[string]*contract.Binder{}} c := &Compiled{Spec: spec, Bindings: map[string]*contract.Binder{}}
for _, t := range spec.Types { for _, t := range spec.Types {
if graph.recursive[t.Name] {
if _, err := graph.component(t.Name); err != nil {
return nil, err
}
continue
}
rt, err := resolve(t.Name, 0) rt, err := resolve(t.Name, 0)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -274,7 +225,7 @@ func Compile(spec *Spec) (*Compiled, error) {
} }
var response *openapi.Schema var response *openapi.Schema
if r.Response != "" { if r.Response != "" {
s, err := responseDataSchema(r.Response, c.Bindings) s, err := responseDataSchema(r.Response, c.Bindings, graph)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -316,6 +267,13 @@ func Compile(spec *Spec) (*Compiled, error) {
return nil, err return nil, err
} }
decorateDocument(doc, spec, c.Bindings) decorateDocument(doc, spec, c.Bindings)
if len(graph.components) > 0 {
components := map[string]any{}
for name, schema := range graph.components {
components[name] = schema
}
doc["components"].(map[string]any)["schemas"] = components
}
compactDocument(doc) compactDocument(doc)
c.Document, err = json.MarshalIndent(doc, "", " ") c.Document, err = json.MarshalIndent(doc, "", " ")
return c, err return c, err

View File

@@ -23,7 +23,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
// Runtime-only releases need not raise the managed template compatibility floor. // Runtime-only releases need not raise the managed template compatibility floor.
const minimumVersion = "v0.22.0-preview" const minimumVersion = "v0.22.0-preview"
const currentVersion = "v0.28.0-preview" const currentVersion = "v0.28.2-preview"
const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n" const generatedHeader = "// Code generated by go-omni api. DO NOT EDIT.\n"
type Options struct { type Options struct {

View File

@@ -0,0 +1,299 @@
package apigen
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func compileRecursive(t *testing.T, source string) *Compiled {
t.Helper()
spec, err := parseText(t, source)
if err != nil {
t.Fatal(err)
}
c, err := Compile(spec)
if err != nil {
t.Fatal(err)
}
again, err := Compile(spec)
if err != nil || !bytes.Equal(c.Document, again.Document) {
t.Fatal("non-deterministic compilation", err)
}
return c
}
func recursiveDocument(t *testing.T, c *Compiled) map[string]any {
t.Helper()
var doc map[string]any
if err := json.Unmarshal(c.Document, &doc); err != nil {
t.Fatal(err)
}
// Inspect reference targets without recursively inlining the graph.
var visit func(any)
visit = func(value any) {
switch v := value.(type) {
case map[string]any:
if ref, ok := v["$ref"].(string); ok {
if len(v) != 1 {
t.Fatalf("OpenAPI 3.0 ref has ignored siblings: %v", v)
}
var target any = doc
for _, part := range strings.Split(strings.TrimPrefix(ref, "#/"), "/") {
m, ok := target.(map[string]any)
if !ok {
t.Fatal("bad reference", ref)
}
target = m[part]
}
if target == nil {
t.Fatal("dangling reference", ref)
}
return
}
for key, child := range v {
if key != "example" && key != "examples" {
visit(child)
}
}
case []any:
for _, child := range v {
visit(child)
}
}
}
visit(doc)
return doc
}
func TestRecursiveResponseGraph(t *testing.T) {
source := readFile(t, filepath.Join(frameworkRoot(t), "examples/recursiveResponse/service.api"))
c := compileRecursive(t, source)
doc := recursiveDocument(t, c)
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
node := schemas["DTO_Node"].(map[string]any)
props := node["properties"].(map[string]any)
children := props["children"].(map[string]any)
if children["type"] != "array" || children["maxItems"] != float64(100) || children["items"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
t.Fatal(children)
}
if props["lookup"].(map[string]any)["additionalProperties"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
t.Fatal(props)
}
next := props["next"].(map[string]any)["anyOf"].([]any)
if len(next) != 2 || next[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" || next[1].(map[string]any)["nullable"] != true {
t.Fatal(next)
}
for _, pair := range [][2]string{{"Branch", "Leaf"}, {"Leaf", "Branch"}} {
p := schemas["DTO_"+pair[0]].(map[string]any)["properties"].(map[string]any)[strings.ToLower(pair[1])].(map[string]any)
if p["anyOf"].([]any)[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_"+pair[1] {
t.Fatal(p)
}
}
if len(c.Document) > 18000 {
t.Fatal("recursive graph expanded", len(c.Document))
}
first, _ := json.Marshal(doc)
compactDocument(doc)
second, _ := json.Marshal(doc)
if !bytes.Equal(first, second) {
t.Fatal("document compaction changed references")
}
// Change tracking traverses every named dependency once, including cycles.
snapshot := snapshotContract(c.Spec)
if len(snapshot.Routes) == 0 {
t.Fatal("empty contract snapshot")
}
}
func TestRecursiveResponseRejections(t *testing.T) {
tests := []struct{ name, types, route, want string }{
{"valueSelf", "type Node { Child Node `json:\"child\"` }", "get /tree returns (Node)", "recursive value"},
{"valueMutual", "type Node { Child Other `json:\"child\"` }\ntype Other { Child Node `json:\"child\"` }", "get /tree returns (Node)", "recursive value"},
{"request", "type Node { Children []Node `json:\"children\"` }", "post /tree (Node) returns (Node)", "recursive request"},
{"wrappedRequest", "type Node { Children []Node `json:\"children\"` }\ntype Req { Value Node `json:\"value\"` }", "post /tree (Req)", "recursive request"},
{"unknown", "type Node { Children []Node `json:\"children\"`\n Bad Missing `json:\"bad\"` }", "get /tree returns (Node)", "unknown type"},
{"duplicate", "type Node { Children []Node `json:\"children\"`\n Again string `json:\"children\"` }", "get /tree returns (Node)", "conflicting composed field"},
{"nonJSON", "type Node { Children []Node `json:\"children\"`\n Name string `form:\"name\"` }", "get /tree returns (Node)", "response fields must use json"},
{"nestedNonJSON", "type Node { Children []Node `json:\"children\"`\n Detail Detail `json:\"detail\"` }\ntype Detail { Name string `form:\"name\"` }", "get /tree returns (Node)", "response fields must use json"},
{"doublePointer", "type Node { Next **Node `json:\"next\"` }", "get /tree returns (Node)", "multiple pointer"},
{"fixedArray", "type Node { Children [1]Node `json:\"children\"` }", "get /tree returns (Node)", "fixed arrays"},
{"mapKey", "type Node { Children map[int]Node `json:\"children\"` }", "get /tree returns (Node)", "map keys"},
{"badRule", "type Node { Next *Node `json:\"next\" binding:\"min=1\"` }", "get /tree returns (Node)", "requires a number"},
{"badDefault", "type Node { Next *Node `json:\"next,default=x\"` }", "get /tree returns (Node)", "default requires"},
{"upload", "type Node { Children []Node `json:\"children\"`\n Upload File `file:\"upload\"` }", "get /tree returns (Node)", "response fields must use json"},
{"composition", "type Node { Node }", "get /tree returns (Node)", "cycle"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
spec, err := parseText(t, tc.types+"\nservice demo {\n@handler tree\n"+tc.route+"\n}")
if err == nil {
_, err = Compile(spec)
}
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("want %q, got %v", tc.want, err)
}
})
}
}
func TestRecursiveResponseExamples(t *testing.T) {
for _, tc := range []struct {
value string
valid bool
}{
{`[]`, true}, {`[{"name":"first","children":[{"name":"second","children":[]}]}]`, true},
{`[{"name":"","children":[]}]`, false}, {`[{"name":"first"}]`, false},
{`[{"name":"first","children":[],"unknown":true}]`, false},
{`[{"name":"first","children":[]},{"name":123,"children":[]}]`, false},
{`[{"name":"first","children":[{"name":"second","children":[{"name":"","children":[]}]}]}]`, false},
{`null`, false}, {`{} {}`, false},
} {
t.Run(tc.value, func(t *testing.T) {
source := "type Node {\n Name string `json:\"name\" binding:\"min=1\"`\n Children []Node `json:\"children\" description:\"Nested nodes\" example:" + fmt.Sprintf("%q", tc.value) + "`\n}\nservice demo {\n @handler tree\nget /tree returns (Node)\n}"
spec, err := parseText(t, source)
if err == nil {
_, err = Compile(spec)
}
if (err == nil) != tc.valid {
t.Fatal(err)
}
})
}
for _, typ := range []string{"*Node", "map[string]*Node", "[]*Node", "*[]Node"} {
source := "type Node {\n Name string `json:\"name\"`\n Next " + typ + " `json:\"next,omitempty\" description:\"Next node\"`\n}\nservice demo {\n@handler tree\nget /tree returns (*Node)\n}"
recursiveDocument(t, compileRecursive(t, source))
}
}
func TestRecursiveResponseImportsAndMetadata(t *testing.T) {
root := t.TempDir()
writeFile(t, filepath.Join(root, "service.api"), "import \"node.api\"\nimport \"detail.api\"\nservice demo {\n@handler tree\nget /tree returns (Node)\n}")
writeFile(t, filepath.Join(root, "node.api"), "type Node {\n Children []Node `json:\"children\"`\n Detail Detail `json:\"detail\" description:\"Node detail\" example:\"{\\\"count\\\":9007199254740993,\\\"parent\\\":null}\"`\n}")
writeFile(t, filepath.Join(root, "detail.api"), "type Detail {\n Count int64 `json:\"count\"`\n Parent *Node `json:\"parent\"`\n}")
spec, err := Parse(filepath.Join(root, "service.api"))
if err != nil {
t.Fatal(err)
}
c, err := Compile(spec)
if err != nil {
t.Fatal(err)
}
doc := recursiveDocument(t, c)
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
detail := schemas["DTO_Node"].(map[string]any)["properties"].(map[string]any)["detail"].(map[string]any)
if detail["description"] != "Node detail" || detail["allOf"].([]any)[0].(map[string]any)["$ref"] != "#/components/schemas/DTO_Detail" {
t.Fatal(detail)
}
if !bytes.Contains(c.Document, []byte("9007199254740993")) {
t.Fatal("example integer lost precision")
}
// Definition order cannot change cycle classification or component schemas.
spec.Types[0], spec.Types[1] = spec.Types[1], spec.Types[0]
other, err := Compile(spec)
if err != nil || !bytes.Equal(c.Document, other.Document) {
t.Fatal("definition order changed schemas", err)
}
scalar := `scalar ID (
goImport: "example.com/scalars"
goType: ID
jsonInput: string | int64
jsonOutput: string
)
type Node {
Value ID ` + "`json:\"value\" example:\"123\"`" + `
Children []Node ` + "`json:\"children\"`" + `
}
service demo {
@handler tree
get /tree returns (Node)
}`
doc = recursiveDocument(t, compileRecursive(t, scalar))
value := doc["components"].(map[string]any)["schemas"].(map[string]any)["DTO_Node"].(map[string]any)["properties"].(map[string]any)["value"].(map[string]any)
if value["type"] != "string" || value["example"] != nil {
t.Fatal("scalar input leaked into response", value)
}
}
func TestRecursiveResponseConsumer(t *testing.T) {
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") != "1" {
t.Skip("set GO_OMNI_SCAFFOLD_INTEGRATION=1")
}
root := frameworkRoot(t)
dir := filepath.Join(t.TempDir(), "recursive")
o := Options{Dir: dir, API: filepath.Join(root, "examples/recursiveResponse/service.api"), Module: "example.com/recursive", FrameworkDir: root}
if err := generateManagedTestProject(t, o); err != nil {
t.Fatal(err)
}
doc := recursiveDocument(t, &Compiled{Document: []byte(readFile(t, filepath.Join(dir, "docs/openapi.json")))})
schemas := doc["components"].(map[string]any)["schemas"].(map[string]any)
children := schemas["DTO_Node"].(map[string]any)["properties"].(map[string]any)["children"].(map[string]any)
if children["items"].(map[string]any)["$ref"] != "#/components/schemas/DTO_Node" {
t.Fatal("generated response lost its recursive schema", children)
}
for _, name := range []string{"tree", "forest", "optional", "raw", "pair"} {
writeFile(t, filepath.Join(dir, "internal/logic/trees/"+name+".go"), "package trees\n")
}
writeFile(t, filepath.Join(dir, "internal/logic/trees/operations.go"), readFile(t, filepath.Join(root, "examples/recursiveResponse/operations.go.txt")))
writeFile(t, filepath.Join(dir, "consumer_test.go"), readFile(t, "testdata/recursive/consumer_test.go"))
o.API = filepath.Join(dir, "api/service.api")
before := snapshot(t, dir)
if err := generateManagedTestProject(t, o); err != nil {
t.Fatal(err)
}
o.Check = true
if err := generateManagedTestProject(t, o); err != nil {
t.Fatal(err)
}
if !sameFiles(before, snapshot(t, dir)) {
t.Fatal("regeneration/check modified business code")
}
runConsumerGo(t, dir, "test", "-mod=mod", "-race", "-count=1", "./...")
// A recursive descendant change must still be visible to read-only checking.
writeFile(t, o.API, strings.Replace(readFile(t, o.API), "max=100", "max=90", 1))
before = snapshot(t, dir)
if err := generateManagedTestProject(t, o); err == nil {
t.Fatal("check missed recursive DTO drift")
}
if !sameFiles(before, snapshot(t, dir)) {
t.Fatal("drift check wrote files")
}
o.Check = false
if err := generateManagedTestProject(t, o); err != nil {
t.Fatal(err)
}
o.Check = true
if err := generateManagedTestProject(t, o); err != nil {
t.Fatal(err)
}
if os.Getenv("GO_OMNI_RELEASE_CLI") != "" {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "go", "list", "-m", "-json", frameworkModule)
cmd.Dir = dir
cmd.Env = append(os.Environ(), "GOWORK=off")
data, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err, string(data))
}
var dep struct {
Version, Sum string
Replace any
}
if err := json.Unmarshal(data, &dep); err != nil {
t.Fatal(err)
}
if dep.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dep.Sum == "" || dep.Replace != nil {
t.Fatal("not a fixed remote consumer")
}
t.Logf("remote framework %s checksum %s; no local replace", dep.Version, dep.Sum)
}
}

View File

@@ -0,0 +1,370 @@
package apigen
import (
"encoding/json"
"fmt"
"go/ast"
goparser "go/parser"
"reflect"
"sort"
"strconv"
"strings"
"git.yuntuo.hk/k2safe/go-omni/contract"
"git.yuntuo.hk/k2safe/go-omni/openapi"
)
// Named response types form a graph, not an infinitely expanded reflect.StructOf
// tree. Compile each object's local fields with the existing contract compiler,
// then link named fields through OpenAPI references. Request binders still use
// the complete, nonrecursive reflection tree.
type responseGraph struct {
types map[string]Type
exprs map[string][]ast.Expr
recursive map[string]bool // includes types that can reach a cycle
scalars map[string]reflect.Type
scalarDefs []contract.Scalar
components map[string]*openapi.Schema
}
func newResponseGraph(spec *Spec, types map[string]Type, scalars map[string]reflect.Type, defs []contract.Scalar) (*responseGraph, error) {
g := &responseGraph{types: types, exprs: map[string][]ast.Expr{}, recursive: map[string]bool{}, scalars: scalars, scalarDefs: defs, components: map[string]*openapi.Schema{}}
dependencies, values := map[string][]string{}, map[string][]string{}
for _, typ := range spec.Types {
seen := map[string]bool{}
for _, f := range typ.Fields {
if !exportedName.MatchString(f.Name) || seen[f.Name] {
return nil, fmt.Errorf("%s: invalid or duplicate field %s", typ.Name, f.Name)
}
seen[f.Name] = true
e, err := goparser.ParseExpr(f.Type)
if err != nil {
return nil, err
}
// Validate every expression, including otherwise unused declarations.
if _, err = g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
dependencies[typ.Name] = append(dependencies[typ.Name], name)
return reflect.TypeOf(struct{}{}), nil
}); err != nil {
return nil, fmt.Errorf("%s.%s: %w", typ.Name, f.Name, err)
}
g.exprs[typ.Name] = append(g.exprs[typ.Name], e)
if id, ok := e.(*ast.Ident); ok && types[id.Name].Name != "" {
values[typ.Name] = append(values[typ.Name], id.Name)
}
}
}
// Only pointer/slice/map indirection can break a Go struct layout cycle.
state := map[string]int{}
var valueCycle func(string) error
valueCycle = func(name string) error {
if state[name] == 1 {
return fmt.Errorf("%s: invalid recursive value type; use a pointer, slice or map", name)
}
if state[name] == 2 {
return nil
}
state[name] = 1
for _, next := range values[name] {
if err := valueCycle(next); err != nil {
return err
}
}
state[name] = 2
return nil
}
for _, typ := range spec.Types {
if err := valueCycle(typ.Name); err != nil {
return nil, err
}
}
state = map[string]int{}
var cyclic func(string) bool
cyclic = func(name string) bool {
if state[name] == 1 {
return true
}
if state[name] == 2 {
return g.recursive[name]
}
state[name] = 1
for _, next := range dependencies[name] {
if cyclic(next) {
g.recursive[name] = true
}
}
state[name] = 2
return g.recursive[name]
}
for _, typ := range spec.Types {
cyclic(typ.Name)
}
return g, nil
}
func (g *responseGraph) expression(e ast.Expr, depth int, named func(string, int) (reflect.Type, error)) (reflect.Type, error) {
if depth > 32 {
return nil, fmt.Errorf("type nesting exceeds 32")
}
switch e := e.(type) {
case *ast.Ident:
if t := g.scalars[e.Name]; t != nil {
return t, nil
}
if t := primitiveTypes[e.Name]; t != nil {
return t, nil
}
if g.types[e.Name].Name == "" {
return nil, fmt.Errorf("unknown type %s", e.Name)
}
return named(e.Name, depth+1)
case *ast.StarExpr:
if _, double := e.X.(*ast.StarExpr); double {
return nil, fmt.Errorf("multiple pointer levels unsupported")
}
t, err := g.expression(e.X, depth+1, named)
if err != nil {
return nil, err
}
return reflect.PointerTo(t), nil
case *ast.ArrayType:
if e.Len != nil {
return nil, fmt.Errorf("use slices, not fixed arrays")
}
t, err := g.expression(e.Elt, depth+1, named)
if err != nil {
return nil, err
}
return reflect.SliceOf(t), nil
case *ast.MapType:
key, ok := e.Key.(*ast.Ident)
if !ok || key.Name != "string" {
return nil, fmt.Errorf("map keys must be string")
}
t, err := g.expression(e.Value, depth+1, named)
if err != nil {
return nil, err
}
return reflect.MapOf(reflect.TypeOf(""), t), nil
default:
return nil, fmt.Errorf("unsupported type expression")
}
}
func (g *responseGraph) component(name string) (*openapi.Schema, error) {
key := "DTO_" + name
ref := &openapi.Schema{Ref: "#/components/schemas/" + key}
if _, ok := g.components[key]; ok {
return ref, nil
}
// Register before following edges so both self and mutual references terminate.
g.components[key] = nil
typ := g.types[name]
var fields []reflect.StructField
for i, f := range typ.Fields {
t, err := g.expression(g.exprs[name][i], 0, func(string, int) (reflect.Type, error) { return reflect.TypeOf(struct{}{}), nil })
if err != nil {
return nil, err
}
tag, err := withoutExample(f)
if err != nil {
return nil, err
}
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
}
b, err := contract.Describe(reflect.StructOf(fields), g.scalarDefs...)
if err != nil {
return nil, fmt.Errorf("%s: %w", typ.Position, err)
}
s, err := b.ResponseSchema()
if err != nil {
return nil, fmt.Errorf("%s: %w", typ.Position, err)
}
for i, f := range typ.Fields {
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
property := s.Properties[wire]
// Examples are checked against a finite projection of their actual values,
// with the same contract rules and without running application decoders.
if _, ok := reflect.StructTag(fieldTag(f)).Lookup("example"); ok {
example, err := g.example(f, g.exprs[name][i])
if err != nil {
return nil, fmt.Errorf("%s.%s: %w", name, f.Name, err)
}
property.Example = example
}
property, err = g.link(g.exprs[name][i], property)
if err != nil {
return nil, err
}
s.Properties[wire] = property
}
g.components[key] = s
return ref, nil
}
func (g *responseGraph) link(e ast.Expr, s *openapi.Schema) (*openapi.Schema, error) {
switch e := e.(type) {
case *ast.Ident:
if g.types[e.Name].Name == "" {
return s, nil
}
ref, err := g.component(e.Name)
if err != nil {
return nil, err
}
if s.Nullable {
ref = nullableReference(ref)
}
if s.Description != "" || s.Example != nil {
// OpenAPI 3.0 ignores siblings on Reference Objects.
ref = &openapi.Schema{AllOf: []*openapi.Schema{ref}, Description: s.Description, Example: s.Example}
}
return ref, nil
case *ast.StarExpr:
return g.link(e.X, s)
case *ast.ArrayType:
item, err := g.link(e.Elt, s.Items)
if err != nil {
return nil, err
}
s.Items = item
case *ast.MapType:
item, err := g.link(e.Value, s.AdditionalProperties)
if err != nil {
return nil, err
}
s.AdditionalProperties = item
}
return s, nil
}
func nullableReference(s *openapi.Schema) *openapi.Schema {
return &openapi.Schema{AnyOf: []*openapi.Schema{s, {Type: "object", Nullable: true, Enum: []any{nil}}}}
}
func withoutExample(f Field) (reflect.StructTag, error) {
tags, err := contract.ParseTags(fieldTag(f))
if err != nil {
return "", err
}
delete(tags, "example")
keys := make([]string, 0, len(tags))
for key := range tags {
keys = append(keys, key)
}
sort.Strings(keys)
var parts []string
for _, key := range keys {
parts = append(parts, key+":"+strconv.Quote(tags[key]))
}
return reflect.StructTag(strings.Join(parts, " ")), nil
}
func (g *responseGraph) example(f Field, e ast.Expr) (any, error) {
raw := reflect.StructTag(fieldTag(f)).Get("example")
var value any
// Plain string examples intentionally are not JSON literals.
if f.Type == "string" || f.Type == "*string" {
value = raw
} else {
decoder := json.NewDecoder(strings.NewReader(raw))
decoder.UseNumber()
if err := decoder.Decode(&value); err != nil {
return nil, fmt.Errorf("invalid example JSON: %w", err)
}
}
budget := 10000
t, err := g.projectExample(e, []any{value}, 0, &budget)
if err != nil {
return nil, err
}
b, err := contract.Describe(reflect.StructOf([]reflect.StructField{{Name: f.Name, Type: t, Tag: reflect.StructTag(fieldTag(f))}}), g.scalarDefs...)
if err != nil {
return nil, err
}
s, err := b.ResponseSchema()
if err != nil {
return nil, err
}
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
return s.Properties[wire].Example, nil
}
// Only materialize named objects where the finite example has a non-null value.
// Empty slices/maps and null pointers stop expansion. Unioning collection values
// checks every item using one reflection type, including missing/unknown fields.
func (g *responseGraph) projectExample(e ast.Expr, values []any, depth int, budget *int) (reflect.Type, error) {
*budget--
if depth > 32 || *budget < 0 {
return nil, fmt.Errorf("example type expansion exceeds 32 levels or 10000 nodes")
}
var present []any
for _, v := range values {
if v != nil {
present = append(present, v)
}
}
switch e := e.(type) {
case *ast.Ident:
return g.expression(e, 0, func(name string, _ int) (reflect.Type, error) {
if len(present) == 0 {
return reflect.TypeOf(struct{}{}), nil
}
var fields []reflect.StructField
for i, f := range g.types[name].Fields {
wire := strings.Split(reflect.StructTag(fieldTag(f)).Get("json"), ",")[0]
var nested []any
for _, v := range present {
if obj, ok := v.(map[string]any); ok {
if child, found := obj[wire]; found {
nested = append(nested, child)
}
}
}
t, err := g.projectExample(g.exprs[name][i], nested, depth+1, budget)
if err != nil {
return nil, err
}
tag, err := withoutExample(f)
if err != nil {
return nil, err
}
fields = append(fields, reflect.StructField{Name: f.Name, Type: t, Tag: tag})
}
return reflect.StructOf(fields), nil
})
case *ast.StarExpr:
t, err := g.projectExample(e.X, present, depth+1, budget)
if err != nil {
return nil, err
}
return reflect.PointerTo(t), nil
case *ast.ArrayType:
var nested []any
for _, v := range present {
if list, ok := v.([]any); ok {
nested = append(nested, list...)
}
}
t, err := g.projectExample(e.Elt, nested, depth+1, budget)
if err != nil {
return nil, err
}
return reflect.SliceOf(t), nil
case *ast.MapType:
var nested []any
for _, v := range present {
if obj, ok := v.(map[string]any); ok {
for _, v := range obj {
nested = append(nested, v)
}
}
}
t, err := g.projectExample(e.Value, nested, depth+1, budget)
if err != nil {
return nil, err
}
return reflect.MapOf(reflect.TypeOf(""), t), nil
}
return nil, fmt.Errorf("unsupported example type")
}

View File

@@ -35,7 +35,7 @@ func responseGoType(value string) string {
} }
return "*types." + name return "*types." + name
} }
func responseDataSchema(value string, bindings map[string]*contract.Binder) (*openapi.Schema, error) { func responseDataSchema(value string, bindings map[string]*contract.Binder, graph *responseGraph) (*openapi.Schema, error) {
kind, name := responseShape(value) kind, name := responseShape(value)
if kind == "null" { if kind == "null" {
return &openapi.Schema{Type: "object", Nullable: true, Enum: []any{nil}}, nil return &openapi.Schema{Type: "object", Nullable: true, Enum: []any{nil}}, nil
@@ -56,11 +56,15 @@ func responseDataSchema(value string, bindings map[string]*contract.Binder) (*op
} }
return s.Properties["value"], nil return s.Properties["value"], nil
} }
b, ok := bindings[name] var s *openapi.Schema
if !ok { var err error
if graph.recursive[name] {
s, err = graph.component(name)
} else if b := bindings[name]; b != nil {
s, err = b.ResponseSchema()
} else {
return nil, fmt.Errorf("unknown response DTO %q; use Name, []Name, []string/[]int64/[]bool/[]any, *Name or null", name) return nil, fmt.Errorf("unknown response DTO %q; use Name, []Name, []string/[]int64/[]bool/[]any, *Name or null", name)
} }
s, err := b.ResponseSchema()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -68,6 +72,9 @@ func responseDataSchema(value string, bindings map[string]*contract.Binder) (*op
case "array": case "array":
return &openapi.Schema{Type: "array", Items: s}, nil return &openapi.Schema{Type: "array", Items: s}, nil
case "nullable": case "nullable":
if s.Ref != "" {
return nullableReference(s), nil
}
s.Nullable = true s.Nullable = true
} }
return s, nil return s, nil

View File

@@ -0,0 +1,77 @@
package main_test
import (
"context"
"encoding/json"
"io"
"net/http/httptest"
"testing"
"example.com/recursive/internal/handler"
"example.com/recursive/internal/svc"
"example.com/recursive/internal/types"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/logx"
)
func TestRecursiveHTTP(t *testing.T) {
app := omni.New()
defer app.Close()
cfg := logx.Default()
cfg.Writer = io.Discard
logger, err := logx.New(cfg)
if err != nil {
t.Fatal(err)
}
defer logger.Close()
deps, err := svc.New(context.Background(), logger)
if err != nil {
t.Fatal(err)
}
defer deps.Close()
if err := handler.RegisterRoutes(app, deps); err != nil {
t.Fatal(err)
}
for _, path := range []string{"/tree", "/forest", "/optional", "/raw", "/pair"} {
w := httptest.NewRecorder()
app.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
if w.Code != 200 {
t.Fatal(path, w.Code, w.Body.String())
}
data := w.Body.Bytes()
if path != "/raw" {
var envelope struct {
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(data, &envelope); err != nil {
t.Fatal(err)
}
data = envelope.Data
}
switch path {
case "/optional":
if string(data) != "null" {
t.Fatal(string(data))
}
case "/pair":
var pair types.Branch
if err := json.Unmarshal(data, &pair); err != nil || pair.Leaf == nil || pair.Leaf.Branch != nil {
t.Fatal(err, string(data))
}
default:
var node types.Node
if path == "/forest" {
var forest []types.Node
if err := json.Unmarshal(data, &forest); err != nil || len(forest) != 1 {
t.Fatal(err, string(data))
}
node = forest[0]
} else if err := json.Unmarshal(data, &node); err != nil {
t.Fatal(err)
}
if len(node.Children) != 1 || len(node.Children[0].Children) != 1 || node.Children[0].Children[0].Name != "leaf" || node.Lookup["entry"].Name != "lookup" {
t.Fatal(node)
}
}
}
}

View File

@@ -3,15 +3,18 @@ package apigen
import ( import (
"errors" "errors"
"fmt" "fmt"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
) )
type WorkspaceService struct { type WorkspaceService struct {
Dir string `json:"dir"` Dir string `json:"dir"`
Kind string `json:"kind,omitempty"`
API string `json:"api,omitempty"` API string `json:"api,omitempty"`
Module string `json:"module,omitempty"` Module string `json:"module,omitempty"`
} }
@@ -19,10 +22,14 @@ type WorkspaceConfig struct {
Services []WorkspaceService `json:"services"` Services []WorkspaceService `json:"services"`
ErrorCatalogs []WorkspaceCatalog `json:"errorCatalogs,omitempty"` ErrorCatalogs []WorkspaceCatalog `json:"errorCatalogs,omitempty"`
} }
type workspaceTarget struct {
dir, kind, module, entry string
err error
}
// Workspace operates independent Go modules listed in an explicit local file. // Workspace dispatches to each independent project's generator. Directory/schema
// All projects are preflighted before generate/format writes. Each generator // validation precedes any providers; all targets are preflighted before writes.
// transaction is atomic within its project; no cross-module transaction is promised. // Transactions remain per-project, not crash-atomic across the workspace.
func Workspace(path, command string, w io.Writer) error { func Workspace(path, command string, w io.Writer) error {
if command != "generate" && command != "check" && command != "format" { if command != "generate" && command != "check" && command != "format" {
return errors.New("workspace command must be generate, check or format") return errors.New("workspace command must be generate, check or format")
@@ -53,8 +60,8 @@ func Workspace(path, command string, w io.Writer) error {
return errors.New("workspace must list 1..64 services") return errors.New("workspace must list 1..64 services")
} }
root := canonical(filepath.Dir(real)) root := canonical(filepath.Dir(real))
var options []Options
seen := map[string]bool{} seen := map[string]bool{}
// Validate the entire workspace boundary before resolving or running any target.
for _, service := range cfg.Services { for _, service := range cfg.Services {
if service.Dir == "" || service.Dir == "." { if service.Dir == "" || service.Dir == "." {
return errors.New("workspace requires distinct relative service directories") return errors.New("workspace requires distinct relative service directories")
@@ -62,27 +69,26 @@ func Workspace(path, command string, w io.Writer) error {
if err = safePath(root, service.Dir+"/.goOmniWorkspaceCheck"); err != nil { if err = safePath(root, service.Dir+"/.goOmniWorkspaceCheck"); err != nil {
return err return err
} }
dir := filepath.Join(root, filepath.FromSlash(service.Dir)) key := strings.ToLower(filepath.Join(root, filepath.FromSlash(service.Dir)))
key := strings.ToLower(dir)
for prior := range seen { for prior := range seen {
if prior == key || strings.HasPrefix(key, prior+string(filepath.Separator)) || strings.HasPrefix(prior, key+string(filepath.Separator)) { if prior == key || strings.HasPrefix(key, prior+string(filepath.Separator)) || strings.HasPrefix(prior, key+string(filepath.Separator)) {
return errors.New("workspace service directories overlap") return errors.New("workspace service directories overlap")
} }
} }
seen[key] = true seen[key] = true
entry := service.API }
if entry == "" { targets := make([]workspaceTarget, 0, len(cfg.Services))
entry, err = ProjectEntry(dir) for _, service := range cfg.Services {
} else { t := workspaceTarget{dir: filepath.Join(root, filepath.FromSlash(service.Dir)), module: service.Module}
if err = safePath(dir, entry); err != nil { t.kind, t.err = workspaceKind(t.dir, service.Kind, service.API)
return err if t.err == nil && t.kind == "api" {
if service.API == "" {
t.entry, t.err = ProjectEntry(t.dir)
} else if t.err = safePath(t.dir, service.API); t.err == nil {
t.entry = filepath.Join(t.dir, filepath.FromSlash(service.API))
} }
entry = filepath.Join(dir, entry)
} }
if err != nil { targets = append(targets, t)
return err
}
options = append(options, Options{API: entry, Dir: dir, Module: service.Module, DryRun: true})
} }
var catalogs map[string][]byte var catalogs map[string][]byte
if command != "format" { if command != "format" {
@@ -90,26 +96,41 @@ func Workspace(path, command string, w io.Writer) error {
if err != nil { if err != nil {
return err return err
} }
for i := range options {
options[i].documentCatalogs = catalogs
}
} }
var failures error var failures error
for _, o := range options { report := func(t workspaceTarget, e error) {
if command == "format" { if e != nil {
spec, e := Parse(o.API) failures = errors.Join(failures, fmt.Errorf("%s: %w", t.dir, e))
if e == nil { fmt.Fprintf(w, "FAIL %s: %v\n", t.dir, e)
_, e = Compile(spec) } else if command == "format" && t.kind == "project" {
} fmt.Fprintf(w, "SKIP %s (project has no API contract to format)\n", t.dir)
err = e
} else { } else {
_, err = Generate(o) fmt.Fprintf(w, "OK %s\n", t.dir)
} }
if err != nil { }
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err)) if command == "check" {
// Each generator checks its own manifest and managed outputs. An invalid
// project must not suppress diagnostics for another independent project.
for _, t := range targets {
report(t, runWorkspaceTarget(t, command, false, catalogs))
}
return failures
}
for i := range targets {
t := &targets[i]
t.err = runWorkspaceTarget(*t, command, true, catalogs)
}
for _, t := range targets {
if t.err != nil {
report(t, t.err)
} }
} }
if failures != nil { if failures != nil {
for _, t := range targets {
if t.err == nil {
fmt.Fprintf(w, "SKIP %s (workspace preflight failed; no changes applied)\n", t.dir)
}
}
return failures return failures
} }
if command == "generate" { if command == "generate" {
@@ -119,26 +140,79 @@ func Workspace(path, command string, w io.Writer) error {
} }
} }
} }
for _, o := range options { for _, t := range targets {
switch command { report(t, runWorkspaceTarget(t, command, false, catalogs))
case "format":
_, err = FormatProject(o.API, false)
case "generate":
o.DryRun = false
_, err = Generate(o)
case "check":
_, fmtErr := FormatProject(o.API, true)
o.DryRun = false
o.Check = true
_, genErr := Generate(o)
err = errors.Join(fmtErr, genErr)
}
if err != nil {
failures = errors.Join(failures, fmt.Errorf("%s: %w", o.Dir, err))
fmt.Fprintf(w, "FAIL %s\n", o.Dir)
} else {
fmt.Fprintf(w, "OK %s\n", o.Dir)
}
} }
return failures return failures
} }
// An explicit API entry remains supported before the first API generation.
// Without manifests, omitted kind preserves the previous API interpretation.
// Manifests of both kinds or a conflicting declaration never silently choose one.
func workspaceKind(dir, kind, entry string) (string, error) {
if kind != "" && kind != "api" && kind != "project" {
return "", fmt.Errorf("unknown workspace kind %q; expected api or project", kind)
}
detected := ""
for _, candidate := range []struct{ kind, path string }{{"api", manifestPath}, {"project", ".goOmni/projectManifest.json"}} {
if err := safePath(dir, candidate.path); err != nil {
return "", err
}
if _, err := os.Lstat(filepath.Join(dir, candidate.path)); err == nil {
if detected != "" {
return "", errors.New("ambiguous project: both API and project manifests exist")
}
detected = candidate.kind
} else if !errors.Is(err, os.ErrNotExist) {
return "", err
}
}
if kind != "" && detected != "" && kind != detected {
return "", fmt.Errorf("workspace kind %q conflicts with %s manifest", kind, detected)
}
if kind == "" {
kind = detected
}
if kind == "" {
kind = "api"
}
if kind == "project" && entry != "" {
return "", errors.New("workspace project kind does not accept an api entry")
}
return kind, nil
}
func runWorkspaceTarget(t workspaceTarget, command string, preview bool, catalogs map[string][]byte) error {
if t.err != nil {
return t.err
}
if t.kind == "project" {
if command == "format" {
return nil
}
_, err := projectgen.Generate(projectgen.Options{Dir: t.dir, Module: t.module, DryRun: preview, Check: command == "check"})
return err
}
o := Options{API: t.entry, Dir: t.dir, Module: t.module, DryRun: preview, documentCatalogs: catalogs}
switch command {
case "format":
if preview {
spec, err := Parse(t.entry)
if err == nil {
_, err = Compile(spec)
}
return err
}
_, err := FormatProject(t.entry, false)
return err
case "generate":
_, err := Generate(o)
return err
case "check":
_, formatErr := FormatProject(t.entry, true)
o.Check = true
_, generateErr := Generate(o)
return errors.Join(formatErr, generateErr)
}
return errors.New("unsupported workspace command")
}

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -30,8 +31,11 @@ func Catalog() errors.Catalog {return errors.Catalog{DefaultStatus:400,Definitio
} }
writeFile(t, filepath.Join(dir, "api/omni.imports.json"), `{"roots":{"common":{"path":"../../../contracts"}},"errors":{"source":"@common/errors.json","include":["INVALID"],"routes":{"GET /api/greet/:name":["MISSING"]}}}`) writeFile(t, filepath.Join(dir, "api/omni.imports.json"), `{"roots":{"common":{"path":"../../../contracts"}},"errors":{"source":"@common/errors.json","include":["INVALID"],"routes":{"GET /api/greet/:name":["MISSING"]}}}`)
} }
if _, err := projectgen.New(projectgen.Options{Dir: filepath.Join(root, "services/program"), Module: "example.com/program", FrameworkDir: frameworkRoot(t)}); err != nil {
t.Fatal(err)
}
workspace := filepath.Join(root, "omni.workspace.json") workspace := filepath.Join(root, "omni.workspace.json")
writeFile(t, workspace, `{"errorCatalogs":[{"dir":".","package":"example.com/catalogs/internal/apierrors","function":"Catalog","output":"contracts/errors.json"}],"services":[{"dir":"services/api"},{"dir":"services/id"}]}`) writeFile(t, workspace, `{"errorCatalogs":[{"dir":".","package":"example.com/catalogs/internal/apierrors","function":"Catalog","output":"contracts/errors.json"}],"services":[{"dir":"services/api"},{"dir":"services/id"},{"dir":"services/program"}]}`)
if err := Workspace(workspace, "generate", io.Discard); err != nil { if err := Workspace(workspace, "generate", io.Discard); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -0,0 +1,226 @@
package apigen
import (
"bytes"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"testing"
"git.yuntuo.hk/k2safe/go-omni/cmd/go-omni/projectgen"
)
func mixedWorkspace(t *testing.T) (string, string) {
t.Helper()
root := t.TempDir()
if _, err := New(Options{Dir: filepath.Join(root, "apps/interface"), Module: "example.com/interface", FrameworkDir: frameworkRoot(t)}); err != nil {
t.Fatal(err)
}
if _, err := projectgen.New(projectgen.Options{Dir: filepath.Join(root, "apps/program"), Module: "example.com/program", FrameworkDir: frameworkRoot(t)}); err != nil {
t.Fatal(err)
}
config := filepath.Join(root, "omni.workspace.json")
writeFile(t, config, `{"services":[{"dir":"apps/interface"},{"dir":"apps/program"}]}`)
if err := Workspace(config, "format", io.Discard); err != nil {
t.Fatal(err)
}
if err := Workspace(config, "generate", io.Discard); err != nil {
t.Fatal(err)
}
return root, config
}
func TestWorkspaceMixedKindsAndReadOnlyCheck(t *testing.T) {
for _, explicit := range []bool{false, true} {
t.Run(map[bool]string{false: "inferred", true: "explicit"}[explicit], func(t *testing.T) {
root, config := mixedWorkspace(t)
if explicit {
writeFile(t, config, `{"services":[{"dir":"apps/interface","kind":"api","module":"example.com/interface"},{"dir":"apps/program","kind":"project","module":"example.com/program"}]}`)
}
// Workspaces do not manage the Go toolchain's workspace or dependency files.
writeFile(t, filepath.Join(root, "go.work"), "go 1.22.0\nuse (\n ./apps/interface\n ./apps/program\n)\n")
writeFile(t, filepath.Join(root, "go.work.sum"), "workspace sums marker\n")
for _, dir := range []string{"apps/interface", "apps/program"} {
writeFile(t, filepath.Join(root, dir, "go.sum"), "module sums marker\n")
}
runFile := filepath.Join(root, "apps/program/internal/run/run.go")
writeFile(t, runFile, readFile(t, runFile)+"\n// project-owned work\n")
before := snapshot(t, root)
for _, op := range []string{"check", "generate", "check"} {
var output bytes.Buffer
if err := Workspace(config, op, &output); err != nil {
t.Fatal(op, err)
}
if strings.Count(output.String(), "OK ") != 2 {
t.Fatal(output.String())
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal(op, "changed clean workspace or project-owned files")
}
}
// Formatting only touches API contracts, even when project Go has drift.
main := filepath.Join(root, "apps/program/main.go")
writeFile(t, main, readFile(t, main)+"\n// drift intentionally not formatted\n")
before = snapshot(t, root)
var output bytes.Buffer
if err := Workspace(config, "format", &output); err != nil {
t.Fatal(err)
}
if !strings.Contains(output.String(), "SKIP ") || !strings.Contains(output.String(), "apps/program") {
t.Fatal(output.String())
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("format changed a plain Go project")
}
})
}
}
func TestWorkspaceMixedFailureAggregation(t *testing.T) {
root, config := mixedWorkspace(t)
for _, dir := range []string{"apps/interface", "apps/program"} {
path := filepath.Join(root, dir, "main.go")
writeFile(t, path, readFile(t, path)+"\n// manual drift\n")
}
before := snapshot(t, root)
var output bytes.Buffer
err := Workspace(config, "check", &output)
if err == nil || !strings.Contains(err.Error(), "apps/interface") || !strings.Contains(err.Error(), "apps/program") || strings.Count(output.String(), "FAIL ") != 2 {
t.Fatal(err, output.String())
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("failed check wrote files")
}
// Identity failures also keep unrelated project diagnostics.
writeFile(t, config, `{"services":[{"dir":"apps/interface","kind":"project"},{"dir":"apps/program"}]}`)
output.Reset()
before = snapshot(t, root)
err = Workspace(config, "check", &output)
if err == nil || !strings.Contains(err.Error(), "conflicts") || strings.Count(output.String(), "FAIL ") != 2 {
t.Fatal(err, output.String())
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("identity failure changed workspace")
}
}
func TestWorkspaceMixedPreflightBeforeAnyWrites(t *testing.T) {
for _, broken := range []string{"apps/interface", "apps/program"} {
t.Run(broken, func(t *testing.T) {
root, config := mixedWorkspace(t)
stale := "apps/interface"
if stale == broken {
stale = "apps/program"
}
// The first valid generator could repair its missing output, but must not
// write if another project's ownership check will fail.
if err := os.Remove(filepath.Join(root, stale, "main.go")); err != nil {
t.Fatal(err)
}
path := filepath.Join(root, broken, "main.go")
writeFile(t, path, readFile(t, path)+"\n// manual change\n")
before := snapshot(t, root)
var output bytes.Buffer
if err := Workspace(config, "generate", &output); err == nil {
t.Fatal("bad preflight accepted")
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("partial regeneration before failed preflight")
}
if !strings.Contains(output.String(), "SKIP ") || !strings.Contains(output.String(), "FAIL ") {
t.Fatal(output.String())
}
})
}
root, config := mixedWorkspace(t)
for _, dir := range []string{"apps/interface", "apps/program"} {
if err := os.Remove(filepath.Join(root, dir, "main.go")); err != nil {
t.Fatal(err)
}
}
if err := Workspace(config, "generate", io.Discard); err != nil {
t.Fatal(err)
}
if err := Workspace(config, "check", io.Discard); err != nil {
t.Fatal(err)
}
}
func TestWorkspaceKindGuardsAndLegacyEntry(t *testing.T) {
root, config := mixedWorkspace(t)
for _, tc := range []struct{ services, needle string }{
{`[{"dir":"apps/program","kind":"worker"}]`, "unknown workspace kind"},
{`[{"dir":"apps/program","kind":"api"}]`, "conflicts"},
{`[{"dir":"apps/interface","kind":"project"}]`, "conflicts"},
{`[{"dir":"apps/program","api":"api/service.api"}]`, "does not accept"},
{`[{"dir":"apps/program","kind":"project","api":"api/service.api"}]`, "does not accept"},
{`[{"dir":"apps/program","module":"example.com/other"}]`, "module differs"},
{`[{"dir":"../outside","kind":"project"}]`, "unsafe"},
{`[{"dir":"apps/program"},{"dir":"apps/program/nested","kind":"project"}]`, "overlap"},
{`[{"dir":"apps/program","kind":"project","kind":"api"}]`, "duplicate"},
} {
writeFile(t, config, `{"services":`+tc.services+`}`)
before := snapshot(t, root)
err := Workspace(config, "check", io.Discard)
if err == nil || !strings.Contains(err.Error(), tc.needle) {
t.Fatal(tc.services, err)
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("invalid kind wrote files")
}
}
writeFile(t, config, `{"services":[{"dir":"apps/program"}]}`)
other := filepath.Join(root, "apps/program/.goOmni/apiManifest.json")
writeFile(t, other, "{}")
if err := Workspace(config, "check", io.Discard); err == nil || !strings.Contains(err.Error(), "both API and project") {
t.Fatal(err)
}
if err := os.Remove(other); err != nil {
t.Fatal(err)
}
// A malformed manifest must be routed to its owner, never fall back to API.
writeFile(t, filepath.Join(root, "apps/program/.goOmni/projectManifest.json"), "invalid")
if err := Workspace(config, "check", io.Discard); err == nil || !strings.Contains(err.Error(), "invalid project manifest") {
t.Fatal(err)
}
// Explicit API entry outside a managed project preserves the original path.
legacy := filepath.Join(root, "legacy")
writeFile(t, filepath.Join(legacy, "service.api"), "service legacy {\n@handler ping\nget /ping\n}\n")
writeFile(t, config, `{"services":[{"dir":"legacy","api":"service.api","module":"example.com/legacy"}]}`)
if err := Workspace(config, "generate", io.Discard); err != nil {
t.Fatal("legacy API entry", err)
}
}
func TestWorkspaceManifestSymlinkRejected(t *testing.T) {
root, config := mixedWorkspace(t)
path := filepath.Join(root, "apps/program/.goOmni/projectManifest.json")
target := filepath.Join(t.TempDir(), "manifest.json")
writeFile(t, target, readFile(t, path))
if err := os.Remove(path); err != nil {
t.Fatal(err)
}
if err := os.Symlink(target, path); err != nil {
t.Fatal(err)
}
before := snapshot(t, root)
for _, op := range []string{"check", "format", "generate"} {
if err := Workspace(config, op, io.Discard); err == nil || !strings.Contains(err.Error(), "symlink") {
t.Fatal(op, err)
}
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("followed symlink")
}
}
func TestWorkspaceProjectOnly(t *testing.T) {
root, config := mixedWorkspace(t)
raw, _ := json.Marshal(WorkspaceConfig{Services: []WorkspaceService{{Dir: "apps/program"}}})
writeFile(t, config, string(raw))
before := snapshot(t, root)
for _, op := range []string{"format", "generate", "check"} {
if err := Workspace(config, op, io.Discard); err != nil {
t.Fatal(op, err)
}
}
if !sameFiles(before, snapshot(t, root)) {
t.Fatal("plain workspace changed")
}
}

View File

@@ -25,7 +25,7 @@ import (
) )
const frameworkModule = "git.yuntuo.hk/k2safe/go-omni" const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
const currentVersion = "v0.28.0-preview" const currentVersion = "v0.28.2-preview"
const minimumVersion = "v0.28.0-preview" const minimumVersion = "v0.28.0-preview"
const manifestPath = ".goOmni/projectManifest.json" const manifestPath = ".goOmni/projectManifest.json"
const lockPath = ".goOmni/project.lock" const lockPath = ".goOmni/project.lock"

View File

@@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
// Source builds can carry an unpublished VCS version (including +dirty). // Source builds can carry an unpublished VCS version (including +dirty).
// Keep this fallback in sync with the next release; installed CLIs use their // Keep this fallback in sync with the next release; installed CLIs use their
// downloaded module version, which also has a module checksum. // downloaded module version, which also has a module checksum.
const defaultFrameworkVersion = "v0.28.0-preview" const defaultFrameworkVersion = "v0.28.2-preview"
// This target is not a claim that the release already exists. Source users // This target is not a claim that the release already exists. Source users
// select a local framework checkout until the module templates are published. // select a local framework checkout until the module templates are published.

View File

@@ -0,0 +1,162 @@
package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
// The same consumer is exercised with a source-built CLI before release and a
// downloaded CLI after release. Project modules are always independently built.
func TestMixedWorkspaceConsumer(t *testing.T) {
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") != "1" {
t.Skip("set GO_OMNI_SCAFFOLD_INTEGRATION=1")
}
root := t.TempDir()
cli := os.Getenv("GO_OMNI_RELEASE_CLI")
local := cli == ""
framework, err := filepath.Abs("../..")
if err != nil {
t.Fatal(err)
}
if local {
cli = filepath.Join(t.TempDir(), "go-omni")
workspaceCommand(t, "", nil, false, "go", "build", "-o", cli, ".")
}
modulePath := "git.yuntuo.hk/k2safe/go-omni"
for _, kind := range []string{"api", "project"} {
dir := filepath.Join(root, kind)
args := []string{kind, "new", dir, "--module", "example.com/" + kind}
if local {
args = append(args, "--framework-dir", framework)
}
workspaceCommand(t, root, nil, false, cli, args...)
workspaceCommand(t, dir, nil, false, "go", "mod", "tidy")
data := workspaceCommand(t, dir, nil, false, "go", "list", "-m", "-json", modulePath)
if !local {
var dep struct {
Version, Sum string
Replace any
}
if err := json.Unmarshal([]byte(data), &dep); err != nil {
t.Fatal(err)
}
if dep.Version != os.Getenv("GO_OMNI_RELEASE_VERSION") || dep.Replace != nil || dep.Sum == "" {
t.Fatal("consumer is not a fixed remote dependency", dep)
}
t.Logf("%s remote framework %s checksum %s; no local replace", kind, dep.Version, dep.Sum)
}
}
write := func(name, data string) {
t.Helper()
if err := os.WriteFile(filepath.Join(root, name), []byte(data), 0644); err != nil {
t.Fatal(err)
}
}
write("omni.workspace.json", `{"services":[{"dir":"api","kind":"api"},{"dir":"project","kind":"project"}]}`)
for _, op := range []string{"format", "generate", "check"} {
output := workspaceCommand(t, root, nil, false, cli, "workspace", op)
if op == "format" && !strings.Contains(output, "SKIP ") {
t.Fatal("missing project format skip", output)
}
}
// Run the actual Go multi-module toolchain independently of generation.
workspaceCommand(t, root, nil, false, "go", "work", "init", "./api", "./project")
work, err := filepath.EvalSymlinks(filepath.Join(root, "go.work"))
if err != nil {
t.Fatal(err)
}
workspaceCommand(t, root, []string{"GOWORK=" + work}, false, "go", "test", "-race", "./api/...", "./project/...")
workspaceCommand(t, root, []string{"GOWORK=" + work}, false, "go", "build", "./api/...", "./project/...")
before := workspaceSnapshot(t, root)
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
t.Fatal("check modified Go workspace/module files")
}
// Omitted kinds discover both released manifests as well.
write("omni.workspace.json", `{"services":[{"dir":"api"},{"dir":"project"}]}`)
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
originals := map[string]string{}
for _, kind := range []string{"api", "project"} {
name := kind + "/main.go"
data, err := os.ReadFile(filepath.Join(root, name))
if err != nil {
t.Fatal(err)
}
originals[name] = string(data)
write(name, string(data)+"\n// intentional drift\n")
}
before = workspaceSnapshot(t, root)
output := workspaceCommand(t, root, nil, true, cli, "workspace", "check")
if strings.Count(output, "FAIL ") != 2 {
t.Fatal("did not aggregate both projects", output)
}
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
t.Fatal("failed check wrote files")
}
// The healthy project's missing output must stay missing when another fails.
write("api/main.go", originals["api/main.go"])
if err := os.Remove(filepath.Join(root, "api/main.go")); err != nil {
t.Fatal(err)
}
before = workspaceSnapshot(t, root)
workspaceCommand(t, root, nil, true, cli, "workspace", "generate")
if !reflect.DeepEqual(before, workspaceSnapshot(t, root)) {
t.Fatal("partial workspace generation")
}
write("project/main.go", originals["project/main.go"])
workspaceCommand(t, root, nil, false, cli, "workspace", "generate")
workspaceCommand(t, root, nil, false, cli, "workspace", "check")
}
func workspaceCommand(t *testing.T, dir string, extra []string, wantFailure bool, name string, args ...string) string {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, name, args...)
if dir != "" {
cmd.Dir = dir
}
cmd.Env = append(append(os.Environ(), "GOWORK=off"), extra...)
data, err := cmd.CombinedOutput()
if wantFailure {
exit, ok := err.(*exec.ExitError)
if !ok || exit.ExitCode() != 1 {
t.Fatalf("expected exit 1 from %s %v: %v\n%s", name, args, err, data)
}
} else if err != nil {
t.Fatalf("%s %v: %v\n%s", name, args, err, data)
}
t.Logf("%s %v\n%s", name, args, data)
return string(data)
}
func workspaceSnapshot(t *testing.T, root string) map[string]string {
t.Helper()
files := map[string]string{}
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
data, e := os.ReadFile(path)
if e != nil {
return e
}
rel, e := filepath.Rel(root, path)
if e != nil {
return e
}
files[rel] = string(data)
}
return nil
})
if err != nil {
t.Fatal(err)
}
return files
}

View File

@@ -28,3 +28,5 @@ overrides it. Application transport read timeouts still control slow clients.
See [API.md](../API.md) for source tags, validation rules, null/presence behavior, See [API.md](../API.md) for source tags, validation rules, null/presence behavior,
strict decoding and supported type limits. This API leaves the existing strict decoding and supported type limits. This API leaves the existing
`Context.BindJSON/BindQuery/BindURI` behavior unchanged. `Context.BindJSON/BindQuery/BindURI` behavior unchanged.
Recursive response DTOs in `.api` are compiled by the CLI as named OpenAPI references; see [the response guide](../DEVELOPMENT.md#递归响应-dto). This does not change the nonrecursive request type requirement of `New`, `ForType`, or `Describe`.

View File

@@ -0,0 +1,39 @@
# API 与通用工程混合工作区
本例只定义生成工程的类型与目录,不包含业务模型、外部服务或特定调度方式。使用 v0.28.2-preview 或更新版本 CLI。
下面从一个空目录创建两个独立 Go module;API 工程来自 API 模板,通用工程默认不监听端口。
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
mkdir mixed-demo
cd mixed-demo
go-omni api new apps/interface --module example.com/interface
go-omni project new apps/program --module example.com/program
```
将本目录的 `omni.workspace.json` 复制到 `mixed-demo` 根目录,然后执行:
```sh
go-omni workspace format # 格式化 API 契约;通用工程显示 SKIP
go-omni workspace generate
go-omni workspace check
```
也可以省略两个 `kind`:CLI 根据各工程的生成清单识别。显式声明与清单冲突会报错;不会根据目录名称猜测类型。
Go 多模块编译由 Go 工具链独立管理:
```sh
(cd apps/interface && go mod tidy)
(cd apps/program && go mod tidy)
go work init ./apps/interface ./apps/program
go test ./apps/interface/... ./apps/program/...
go build ./apps/interface/... ./apps/program/...
```
workspace check 不修改 `go.work`、`go.mod`、`go.sum` 或项目源文件;不启动应用。若显式配置了共享错误目录提供方,仍会按既有约定构建并调用该 Go 导出函数,见 [目录导出规则](../../DEVELOPMENT.md#同一份-go-错误目录供多个服务生成)。
手工修改任一生成的 `main.go` 后,workspace check 会报告对应工程失败并返回非零退出码;两个工程都存在问题时,会汇总报告。workspace generate 先预检所有工程,再进行各自的文件事务;一个预检失败时,其他工程也不写入。不承诺多个模块之间的崩溃事务性。
本例命令与 `go.work` 编译流程由 `cmd/go-omni/TestMixedWorkspaceConsumer` 在本地和远程发布验收中执行。

View File

@@ -0,0 +1,6 @@
{
"services": [
{ "dir": "apps/interface", "kind": "api" },
{ "dir": "apps/program", "kind": "project" }
]
}

View File

@@ -0,0 +1,32 @@
# 递归响应类型
通用节点、树集合和相互引用对象的响应示例。没有数据库、权限或业务服务依赖。
在 go-omni 仓库根目录执行(新建独立临时目录):
```sh
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.28.2-preview
example_dir="$(mktemp -d)/recursive"
go-omni api generate --api examples/recursiveResponse/service.api --dir "$example_dir" --module example.com/recursive
# 以下文件仅是本次新生成示例的占位实现,替换为示例自身维护的逻辑。
for name in tree forest optional raw pair; do
printf 'package trees\n' > "$example_dir/internal/logic/trees/$name.go"
done
cp examples/recursiveResponse/operations.go.txt "$example_dir/internal/logic/trees/operations.go"
cd "$example_dir"
go mod tidy
go-omni api generate --check
go test ./...
go run .
```
按启动日志显示的监听地址访问 `/tree`、`/forest`、`/optional`、`/raw`、`/pair`。前三层节点通过 `[]types.Node` 编译和返回;`/optional` 返回 null,`/raw` 直接返回节点,`/pair` 返回相互引用类型的有限实例。`docs/openapi.json` 中的 `DTO_Node` 通过 `$ref` 引用自身,`DTO_Branch`、`DTO_Leaf` 互相引用。
嵌套集合遵循普通 Go JSON 规则,示例将叶节点的 Children 初始化为空切片,使其输出 `[]`;框架不会递归改写项目返回数据。业务数据不能包含实际指针环。
框架开发时可在首次生成命令后加 `--framework-dir .` 使用本地代码。回归同时覆盖本地与已发布远程 CLI、真实 HTTP 响应、生成幂等与只读漂移检查:
```sh
# 回到 go-omni 仓库根目录执行
GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./cmd/go-omni/apigen -run Recursive
```

View File

@@ -0,0 +1,22 @@
package trees
import (
"context"
"example.com/recursive/internal/types"
)
func sample() types.Node {
return types.Node{Name: "root", Children: []types.Node{
{Name: "child", Children: []types.Node{
{Name: "leaf", Children: []types.Node{}},
}},
}, Lookup: map[string]types.Node{"entry": {Name: "lookup", Children: []types.Node{}}}}
}
func (s *Logic) Tree(context.Context) (*types.Node, error) { node := sample(); return &node, nil }
func (s *Logic) Forest(context.Context) ([]types.Node, error) { return []types.Node{sample()}, nil }
func (s *Logic) Optional(context.Context) (*types.Node, error) { return nil, nil }
func (s *Logic) Raw(context.Context) (*types.Node, error) { node := sample(); return &node, nil }
func (s *Logic) Pair(context.Context) (*types.Branch, error) {
return &types.Branch{Leaf: &types.Leaf{}}, nil
}

View File

@@ -0,0 +1,33 @@
type Node {
Name string `json:"name" binding:"min=1"`
Children []Node `json:"children" binding:"max=100"`
Next *Node `json:"next,omitempty"`
Lookup map[string]Node `json:"lookup,omitempty"`
}
type Branch {
Leaf *Leaf `json:"leaf"`
}
type Leaf {
Branch *Branch `json:"branch"`
}
@server (group: trees)
service recursive-example {
@handler tree
get /tree returns (Node)
@handler forest
get /forest returns ([]Node)
@handler optional
get /optional returns (*Node)
@handler raw
@response raw
get /raw returns (Node)
@handler pair
get /pair returns (Branch)
}

View File

@@ -17,6 +17,8 @@ import (
) )
type Schema struct { type Schema struct {
Ref string `json:"$ref,omitempty"`
AllOf []*Schema `json:"allOf,omitempty"`
MaxBytes *int64 `json:"x-omni-max-bytes,omitempty"` MaxBytes *int64 `json:"x-omni-max-bytes,omitempty"`
Example any `json:"example,omitempty"` Example any `json:"example,omitempty"`
EnumDescriptions []string `json:"x-enum-descriptions,omitempty"` EnumDescriptions []string `json:"x-enum-descriptions,omitempty"`

View File

@@ -2,6 +2,7 @@ package openapi
import ( import (
"encoding/json" "encoding/json"
"reflect"
"strings" "strings"
"testing" "testing"
@@ -14,6 +15,8 @@ func TestSchemaAdditionalPropertiesAndExactNumbers(t *testing.T) {
`{"type":"object","additionalProperties":true}`, `{"type":"object","additionalProperties":true}`,
`{"type":"object","additionalProperties":{"type":"string"}}`, `{"type":"object","additionalProperties":{"type":"string"}}`,
`{"type":"integer","default":18446744073709551615,"enum":[18446744073709551615]}`, `{"type":"integer","default":18446744073709551615,"enum":[18446744073709551615]}`,
`{"$ref":"#/components/schemas/Node"}`,
`{"description":"Node","allOf":[{"anyOf":[{"$ref":"#/components/schemas/Node"},{"type":"object","nullable":true,"enum":[null]}]}]}`,
} { } {
var s Schema var s Schema
if err := json.Unmarshal([]byte(text), &s); err != nil { if err := json.Unmarshal([]byte(text), &s); err != nil {
@@ -30,6 +33,9 @@ func TestSchemaAdditionalPropertiesAndExactNumbers(t *testing.T) {
if err = json.Unmarshal(b, &roundTrip); err != nil { if err = json.Unmarshal(b, &roundTrip); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !reflect.DeepEqual(s, roundTrip) {
t.Fatalf("schema round trip changed: %s", b)
}
} }
api, _ := New("Exact numbers", "1") api, _ := New("Exact numbers", "1")
s := &Schema{Type: "integer", Enum: []any{json.Number("18446744073709551615")}} s := &Schema{Type: "integer", Enum: []any{json.Number("18446744073709551615")}}

View File

@@ -32,4 +32,9 @@
## 远程发布验收 ## 远程发布验收
发布后运行 `scripts/verifyProgramRelease.py` 与原有 `scripts/verifyManagedRelease.py`,使用空模块缓存安装固定远程 CLI,禁止本地 replace。结果在完成后补入本文件及 JSON 报告。 2026-09-30,两个独立空模块缓存安装远程 v0.28.0-preview,均未使用本地 replace:
- [通用工程远程报告](programRemote.json):30 项检查通过,耗时 26.45 秒。测试直接从下载的框架模块运行,并通过远程 CLI 生成实际消费者;包含示例的一次性运行。
- [既有生成项目远程报告](programManagedRemote.json):98 项检查、10 组消费者通过,耗时 111.07 秒。覆盖原有配置、HTTP 关闭、上传策略、国际化及 TypeScript 消费者;TypeScript 5.6.3 通过 npm exec 提供,未跳过该检查。
- 两组报告的远程模块 checksum 一致:`h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=`。
- 发布标签固定在代码提交 `0b6c0043b6c3b58648645aeee03bf80801e6aa3f`;标签对象 `50caea1fc81a52080c0f0832d8c4a63cac786859`。代码与标签已推送,验收文档随后独立提交,不移动发布标签。

View File

@@ -0,0 +1,292 @@
{
"passed": true,
"frameworkVersion": "v0.28.0-preview",
"frameworkSum": "h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=",
"freshModuleCache": true,
"noLocalReplace": true,
"durationSeconds": 111.07,
"checks": [
"remoteCLIInstall",
"freshModuleCache",
"fixedVersionNoReplace",
"contractChangePreview",
"persistentLogicSignatureFailure",
"businessSourcePreserved",
"fixedConsumerBuild",
"rootGoRun",
"nethttpAndNativeSignals",
"listenerReleased",
"default404And405",
"goOwnedErrorCatalog",
"unwrappedDownloads",
"rawCallback",
"multipart400And413",
"nethttpAndNativeTransfers",
"sharedGoCatalogExport",
"documentOnlyErrorSelection",
"workspaceCheckNoWrites",
"staleCatalogRejected",
"twoIndependentServiceBuilds",
"initializationAndListenFailureCleanup",
"drainBeforeDependencyClose",
"generatedConsumerRaceTests",
"errorParamsAndInheritance",
"manualHooksAndScalars",
"dependencyFatalShutdown",
"fatalProcessExitCode",
"initializationShutdownNotification",
"configuredBuiltinEndpoints",
"projectHealthEnvelope",
"endpointConflictCleanup",
"responseArrayNullableNull",
"primitiveAndDynamicArrays",
"strictRequestPolicies",
"declarativeMultipartBinding",
"backgroundStopDrainResourceOrder",
"backgroundInitAndListenFailureCleanup",
"backgroundTimeoutRetainsResourcesAndExits",
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
"remoteHTTPClientRaceTests",
"remoteHTTPProxyAndTimeouts",
"remoteHTTPHeaderIsolation",
"remoteHTTPPoolAndDrainBudgets",
"remoteHTTPExample",
"remoteExplicitConfigurationRaceTests",
"literalAndRequiredDefaultReferences",
"remoteCheckConfigBeforeDependencies",
"removedImplicitEnvironmentRules",
"remotePreciseModuleDiagnostics",
"remoteJSONSuccessStatuses",
"remoteRejectedStatusBodyCleanup",
"remoteCopyTypePreflight",
"remoteEmptyCollectionTypeRejection",
"remoteLoggerTimeZoneIsolation",
"remoteCORSPolicyRace",
"remoteCORSErrorResponses",
"remoteCORSTransports",
"generatedCORSConfigOnly",
"remoteUnifiedBodyPolicyTransports",
"remoteBodyLimitsBeforeMiddleware",
"remoteHardLimitConflictBeforeServing",
"remoteCanceledHandlerCleanupBarrier",
"remoteExternalServingResourceRetention",
"remoteAdmissionCloseRace",
"remoteRuntimeFinishRetainsHTTPDependencies",
"remoteHTTP2DisconnectedHandlerDrain",
"remoteNativeEarly413AndBusinessHTTP200",
"remoteHTTPSafetyExampleBuild",
"remoteTransactionStages",
"remoteTransactionCauseChains",
"remoteCommitOutcomeUnknown",
"remoteCancellationOutcomeUnknown",
"remoteTransactionPanicAndConcurrency",
"remoteTransactionPublicErrors",
"remoteTransactionExample",
"remoteI18nRuntimeRace",
"remoteI18nGeneratedConsumer",
"remoteI18nHTTPBusinessInvariants",
"remoteI18nSourceOnlyFallback",
"remoteCLDRSamplesAndBrowserConformance",
"remoteTypeScriptConsumer",
"remoteI18nCLIExtractionCheckExportTypes",
"remoteI18nNamespaceIsolation",
"remoteBrowserRaceAndFailureRetention",
"remoteJSONReaderCausePreservation",
"flatFieldComposition",
"copyxThreeEndpointConsumer",
"explicitNullAndMoneyPolicies",
"fieldAdditionWithoutCopyRuleChanges",
"middlewareFactoryInjectionAndCleanup",
"optionalRepositoryPreservesBusiness",
"generatedUnifiedConfig",
"jsonYamlXmlAndCustomDecoder",
"configValidationBeforeDependencies",
"configEnvProvenance",
"configExtensionPreserved",
"configLoaderDriftCheck"
],
"sourceHashes": {
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
"cmd/go-omni/apigen/generate.go": "e89e8b9cabc18903d231ef22396ff94b9f2f6d233df889669ed3644cf283317f",
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
"scripts/verifyManagedRelease.py": "6860e3f440c4d4f51a55085e50066b1a43cd31d5068c2f7a01a708c16823aef7",
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
"cmd/go-omni/apigen/responseShape.go": "5b705130b66a3d4aaed842025b75abe54e6427acac8366366ca88ae9862429e1",
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
}
}

View File

@@ -0,0 +1,74 @@
{
"passed": true,
"frameworkVersion": "v0.28.0-preview",
"frameworkSum": "h1:nD9Xtn7RhHy81n+qp96rlcJlxpDc276Per+dt1low1g=",
"freshModuleCache": true,
"noLocalReplace": true,
"durationSeconds": 26.45,
"tests": [
"TestConflictRollsBackEarlierWrites",
"TestUnsafePaths",
"TestGeneratedProgramConsumer",
"TestOwnershipAndDrift",
"TestRecordedTemplateUpgrade",
"TestProjectGuards/module",
"TestProjectGuards/oldFramework",
"TestProjectGuards/manifest",
"TestProjectGuards/lock",
"TestProjectGuards/otherGenerator",
"TestProjectGuards/symlink",
"TestProjectGuards"
],
"checks": [
"remoteCLIInstall",
"installedVersionDefault",
"generatedConsumerBuildRaceVet",
"defaultNoAPIFiles",
"defaultNoNetworkListener",
"finiteProgram",
"SIGINT",
"SIGTERM",
"initializationFailureAndPanic",
"runErrorPanicGoexit",
"fatalComponentFailure",
"nonzeroProcessFailureAndTimeoutExits",
"partialStartupDraining",
"acknowledgementBeforeResourceClose",
"workContextSurvivesGracefulStop",
"cancellationGrace",
"unresponsiveRunAndBackgroundWork",
"retainedResourcesAndLogger",
"explicitConfigurationOnly",
"checkConfigWithoutDependencies",
"handwrittenFilesPreserved",
"generatedFileConflict",
"missingGeneratedFileRestored",
"missingHandwrittenFileNotRecreated",
"readOnlyCheckAndDryRun",
"templateDriftUpgrade",
"manifestIdentityAndVersionGuards",
"symlinkAndLockProtection",
"fileTransactionRollback",
"publishedExampleFiniteRun"
],
"sourceHashes": {
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
"cmd/go-omni/projectgen/generate.go": "c4b1ba2d1f09eca02a031330ed9a1e486bab711b37c77b72f0f9dd0906e1f8e4",
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
}
}

View File

@@ -0,0 +1,26 @@
# v0.28.2 递归响应验收
范围:`.api` 响应 DTO 的自引用与相互引用,使用通用 Node/Branch/Leaf;不加入接入项目的业务字段或业务行为。非递归响应保持原生成路径,递归请求与非法值循环仍拒绝。
## 本地回归
2026-09-30,macOS arm64:
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:56 个测试包通过,apigen 集成包 112.675 秒。
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./...`:56 个测试包通过,apigen 集成包 91.536 秒。
- 新增生成文档断言后,重新运行专项 `go test -race -count=1 ./cmd/go-omni/apigen -run Recursive`(开启集成):通过,4.318 秒。
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
- 示例通过实际 CLI 导出后,`@apidevtools/swagger-cli@4.0.4 validate` 确认完整 OpenAPI 3.0.3 文档有效。
## 覆盖内容
- 强类型三层树、递归 slice/map/pointer、可空对象、raw、数组根响应以及相互引用的有限值,生成消费者真实 HTTP 返回和 Go JSON 解码。
- `$ref` 目标完整、没有被忽略的引用同级属性、可空引用的 null 分支、循环引用不展开、文档压缩幂等。
- 跨源文件引用、声明顺序无关、直接值与引用混合的合法类型、scalar 输出 schema、字段约束、描述、精确 int64 示例。
- 有限递归示例、空集合、缺失/未知字段、深层非法值、异构数组错误、非法 null 与额外 JSON 值。
- 非法直接值循环、相互值循环、字段组合循环、递归请求及间接递归请求、未知类型、非法标签、重复字段、上传字段、双指针、固定数组和错误 map 键。
- 反复生成、手写代码保留、只读检查、递归字段变更诊断;请求绑定与原有生成消费者继续全量回归。
## 发布验收
远程验收使用 `scripts/verifyManagedRelease.py`;新增递归消费者与其他消费者一起用空模块缓存安装固定版本 CLI,并核对 module checksum 和无本地 replace。通用工程与混合工作区分别运行其原有发布验收脚本。结果在发布后记录。

View File

@@ -0,0 +1,34 @@
# v0.28.1 混合工作区验收
2026-09-30,macOS arm64。范围为 CLI 工程识别与命令分发,不改变运行时接口、生成模板归属或业务规则。
## 发布前回归
- `GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race -count=1 ./...`:56 个测试包通过,包含实际生成消费者。
- `GOTOOLCHAIN=go1.22.12 GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -count=1 ./cmd/go-omni/...`:5 个 CLI 包通过,含 API 与通用工程消费者。
- `go vet ./...`、`go build ./...`、`git diff --check`:通过。
## 行为覆盖
- 显式 kind 与按清单自动识别,两类工程混合及纯通用工程工作区。
- 原有无 kind 的 API 清单、首次生成时显式 api 入口保持兼容。
- 未知类型、类型与已有清单冲突、两种清单并存、project 携带 api、错误 module、损坏清单、重复 JSON 键、越界/重叠路径与符号链接均拒绝。
- check 汇总两个工程的生成漂移,也在一个工程身份错误时继续检查其他工程;失败与成功均不写文件。
- format 对通用工程显示 SKIP,即使它的 Go 文件存在漂移也不修改;API 格式化规则保持。
- 任意一类工程预检失败时,另一类工程的待补生成文件不被提前写入;所有预检通过后可补回缺失产物。
- 手写文件、go.work、go.work.sum、各 module 的 go.mod/go.sum 保持。
- 原有显式共享 Go 错误目录提供方在加入通用工程后仍可生成、检查,失败不改项目文件。
- 真实 CLI 创建两个独立 module,执行混合 format/generate/check,然后用 Go 原生 go.work 完成 race 测试与多模块构建。
- 真实 CLI 遇到两个工程漂移时汇总输出,退出码 1;失败生成保持原工作区内容,修复后可恢复生成与检查。
检查只涉及生成产物,不替代业务测试。显式 errorCatalogs 仍执行受信任的 Go 导出函数;生成事务按单个工程提交,不保证跨模块的崩溃原子性。
## 远程发布验收
三个独立空模块缓存安装固定远程 v0.28.1-preview,均未使用本地 replace:
- [混合工作区报告](workspaceRemote.json):16 项检查通过,耗时 21.74 秒。远程 CLI 创建两类 module,完成显式/隐式类型识别、混合生成检查、真实 go.work race/构建、失败汇总与写入前预检。
- [通用工程报告](workspaceProgramRemote.json):30 项检查通过,耗时 25.69 秒,含真实进程退出、资源保留与手写代码保护。
- [既有生成器报告](workspaceManagedRemote.json):98 项检查、10 组消费者通过,耗时 108.92 秒。包含配置、HTTP 安全、国际化与 TypeScript;TypeScript 5.6.3 通过 npm exec 提供,未跳过。
- 三组远程模块 checksum 一致:`h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=`。
- 发布标签固定在代码提交 `0d2c6040e2694795f203304148dbaec00318c608`,标签对象 `e0e483b2741f92c8e44a0e45e56d78a920cb3ebb`。验收文档随后独立提交,发布标签不移动。

View File

@@ -0,0 +1,292 @@
{
"passed": true,
"frameworkVersion": "v0.28.1-preview",
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
"freshModuleCache": true,
"noLocalReplace": true,
"durationSeconds": 108.92,
"checks": [
"remoteCLIInstall",
"freshModuleCache",
"fixedVersionNoReplace",
"contractChangePreview",
"persistentLogicSignatureFailure",
"businessSourcePreserved",
"fixedConsumerBuild",
"rootGoRun",
"nethttpAndNativeSignals",
"listenerReleased",
"default404And405",
"goOwnedErrorCatalog",
"unwrappedDownloads",
"rawCallback",
"multipart400And413",
"nethttpAndNativeTransfers",
"sharedGoCatalogExport",
"documentOnlyErrorSelection",
"workspaceCheckNoWrites",
"staleCatalogRejected",
"twoIndependentServiceBuilds",
"initializationAndListenFailureCleanup",
"drainBeforeDependencyClose",
"generatedConsumerRaceTests",
"errorParamsAndInheritance",
"manualHooksAndScalars",
"dependencyFatalShutdown",
"fatalProcessExitCode",
"initializationShutdownNotification",
"configuredBuiltinEndpoints",
"projectHealthEnvelope",
"endpointConflictCleanup",
"responseArrayNullableNull",
"primitiveAndDynamicArrays",
"strictRequestPolicies",
"declarativeMultipartBinding",
"backgroundStopDrainResourceOrder",
"backgroundInitAndListenFailureCleanup",
"backgroundTimeoutRetainsResourcesAndExits",
"generatedIncompleteHTTPDrainRetainsDependenciesAndLogger",
"remoteHTTPClientRaceTests",
"remoteHTTPProxyAndTimeouts",
"remoteHTTPHeaderIsolation",
"remoteHTTPPoolAndDrainBudgets",
"remoteHTTPExample",
"remoteExplicitConfigurationRaceTests",
"literalAndRequiredDefaultReferences",
"remoteCheckConfigBeforeDependencies",
"removedImplicitEnvironmentRules",
"remotePreciseModuleDiagnostics",
"remoteJSONSuccessStatuses",
"remoteRejectedStatusBodyCleanup",
"remoteCopyTypePreflight",
"remoteEmptyCollectionTypeRejection",
"remoteLoggerTimeZoneIsolation",
"remoteCORSPolicyRace",
"remoteCORSErrorResponses",
"remoteCORSTransports",
"generatedCORSConfigOnly",
"remoteUnifiedBodyPolicyTransports",
"remoteBodyLimitsBeforeMiddleware",
"remoteHardLimitConflictBeforeServing",
"remoteCanceledHandlerCleanupBarrier",
"remoteExternalServingResourceRetention",
"remoteAdmissionCloseRace",
"remoteRuntimeFinishRetainsHTTPDependencies",
"remoteHTTP2DisconnectedHandlerDrain",
"remoteNativeEarly413AndBusinessHTTP200",
"remoteHTTPSafetyExampleBuild",
"remoteTransactionStages",
"remoteTransactionCauseChains",
"remoteCommitOutcomeUnknown",
"remoteCancellationOutcomeUnknown",
"remoteTransactionPanicAndConcurrency",
"remoteTransactionPublicErrors",
"remoteTransactionExample",
"remoteI18nRuntimeRace",
"remoteI18nGeneratedConsumer",
"remoteI18nHTTPBusinessInvariants",
"remoteI18nSourceOnlyFallback",
"remoteCLDRSamplesAndBrowserConformance",
"remoteTypeScriptConsumer",
"remoteI18nCLIExtractionCheckExportTypes",
"remoteI18nNamespaceIsolation",
"remoteBrowserRaceAndFailureRetention",
"remoteJSONReaderCausePreservation",
"flatFieldComposition",
"copyxThreeEndpointConsumer",
"explicitNullAndMoneyPolicies",
"fieldAdditionWithoutCopyRuleChanges",
"middlewareFactoryInjectionAndCleanup",
"optionalRepositoryPreservesBusiness",
"generatedUnifiedConfig",
"jsonYamlXmlAndCustomDecoder",
"configValidationBeforeDependencies",
"configEnvProvenance",
"configExtensionPreserved",
"configLoaderDriftCheck"
],
"sourceHashes": {
"lifecycle/runtime.go": "d34fb26879932153671b9cbd164daae484b23dca08b8a3be5b1b89f8f6168e47",
"lifecycle/phases.go": "6e72499d2c5fcecb57cf712e76e3a03fed61a4caf528f873f2058291a8ab0b4d",
"config/lifecycle_test.go": "fe31c371acc3b2df2f58ecfb7b6a0f29d444410f43c0d49bad6cbf72a1a87248",
"task/task.go": "acb1a2cc0e26029331065545582c30e0c547e3b6869f7d8b16c7d25ab0077545",
"cmd/go-omni/scaffold/templates/api-runtime.go.tmpl": "e8575c04b0923d924d96e4334731313076284fd068c4990715b7a07a2724f969",
"cmd/go-omni/apigen/templates.go": "866a7ab9cc28f987645785203213e10722490970d5a1fd3c060d88db8b1278dd",
"config/project.go": "169e13921afa6a9336fcde3310675580799335ce55f076cbe60cdf3cfbd8cac2",
"contract/bind.go": "25ef07654abe42fe5f7e9cf5e5aa3bf102b311502bdbf16c0346970f87ee3053",
"contract/policy.go": "2c8e7efb07d59fea2c00ab39694d4df52a5bea6495ce23cac1c470584af74da1",
"contract/multipart.go": "db0377599b1b8b39246d41129513e8187109e44c1892971244d581c22ccc8ecd",
"contract/file.go": "20dd2de17e4854d452afc3c65f0293d504fb8fc25e91583ef45207fdab30948d",
"cmd/go-omni/apigen/business_test.go": "3b484352282d945f5a58047682079e3d64d4b0bf6ec6bafc23151b1197bb3942",
"cmd/go-omni/apigen/generate.go": "92abd77a4f4172d5388a6b4efc43910eea281fa30a7ceab7aee75cab6fbb03c1",
"lifecycle/controller.go": "1fb9393df1908afed93d87a1bf3ecea8d79d78e437a656b56bb7f1e5f6e51d31",
"config/endpoints.go": "9f5123f962d52b71e5b73d5942316170875e895129a94e8b669ce782969ef27d",
"health/health.go": "41f07f27a7fdcfa7973c2c554a22b73129ebc5d44e945e70e054e1cff24ab866",
"httpx/response.go": "99e29724e4fa51d2c7ec1a52892c24ac5daf210a2ee547c5e69a175015298efd",
"app/app.go": "2e672b3f6376049dd9695f799698439909f1d649d4338d13478e0dbf1de68800",
"app/transport.go": "8c665f47447d9839bba7ba3efa290316be9635179d2276b0da0d0f8eda3842ec",
"scripts/verifyManagedRelease.py": "6860e3f440c4d4f51a55085e50066b1a43cd31d5068c2f7a01a708c16823aef7",
"scripts/verifyManagedProject.py": "b0e4b1aea4f44d61470c35c41f971e0882ae36b0eb5df2be190400d62fc60a18",
"cmd/go-omni/apigen/managed_test.go": "e0b2b5aa6c4d6383590b467d57d7b4e00505ca35d6b6cde2475e4ad574041e94",
"cmd/go-omni/apigen/testdata/managed/options.go": "30d48e4b5aab476c887ac53cc89b48b2b7e250c8cd25e55fecb49bb2a3589247",
"cmd/go-omni/apigen/testdata/managed/background.go": "fb357b5deefd5187f2df406be5189fc4d57da857bd55b474f4726bf065c77167",
"cmd/go-omni/apigen/testdata/managed/client.go": "cb4a07539adbd163c13fc6cbb914518506b0c60a88473589f212f3406e763e4b",
"cmd/go-omni/apigen/testdata/managed/phases_test.go": "94a5787218c0d92159b473d390197858241ede2cab591df0976e611ed279dcfe",
"cmd/go-omni/apigen/testdata/managed/shutdown_test.go": "b078bd9052e513ab6173a1e65cf54b28d3cfad445cb50518d9e2e76736071047",
"cmd/go-omni/apigen/testdata/managed/consumer_test.go": "589e2c54d9ad7f64e88ac556329a52062a27b4e898deb9a97feff200787c32c1",
"cmd/go-omni/apigen/testdata/managed/operations.go": "a1d91744abe020cbfb476512cffb7dad422a95da1c3be6957fdcfe27c361030b",
"cmd/go-omni/apigen/testdata/business/accessFactory.go": "2f982cf39b5e1a4be98596ce5d0a296af0fbf8c94616543a268e67ec376f31b4",
"cmd/go-omni/apigen/testdata/business/client.go": "d4cbaeec18dfb3a0e2821217e8eb0515000e351a745a93ec811dabca44e58c8a",
"cmd/go-omni/apigen/testdata/business/consumer_test.go": "e4159862a729ca2c5c9699840cc16a56350fc2914c43841f08946e9a6e9c8091",
"cmd/go-omni/apigen/testdata/business/operations.go": "11ded7921a0753f22975c525e334e3d45366143625e41d24fe500504e785c64c",
"cmd/go-omni/apigen/responseShape.go": "5b705130b66a3d4aaed842025b75abe54e6427acac8366366ca88ae9862429e1",
"cmd/go-omni/apigen/documentRefs.go": "7b841a668154d872493104eda0f9049340c67d9323eb99f1621a1ef6ba343891",
"cmd/go-omni/apigen/catalogDocument.go": "94df60328b2f105c35b73d9c6362ea14abadce6cb18e4d9921193d2d46854252",
"cmd/go-omni/apigen/workspaceCatalog.go": "c00784220b379ed80deb07c060532b756cbc47434ecb8b1ca10bc12b12159328",
"config/references.go": "ea709ddb32a9c5601662a875765d376b8490c4c766ecdb4365f462b5a2b10e16",
"cmd/go-omni/apigen/arrayResponse_test.go": "9e3e4475f76683d3346d349cdfa94e9e75cf913ac6979bd7a4ed86628466d879",
"cmd/go-omni/apigen/testdata/arrays/values.go": "d5e7f3adf12aabb301f46c296e8c894a7b2f6f70a6b6e7e02c74da7f1fd17bc5",
"cmd/go-omni/apigen/testdata/arrays/consumer_test.go": "435932f2952ad897943e2cda83b04eccb2950579d8d88772661541e1e2a2333d",
"cmd/go-omni/apigen/composition.go": "7ed3761b27d9c932d8ddc43e63c2aca798bdbe38f2b40571af8916c8401ea650",
"cmd/go-omni/apigen/generatorConfig.go": "78db0c7808cf6ec997c5b64a4a5fbea11b74049709bc5900ccfbc5d8dabebbc8",
"cmd/go-omni/apigen/copyConsumer_test.go": "e08ae4daed5dd25ab89d8872f653caf19499984dcaddc9835c13884c410fc0c3",
"cmd/go-omni/apigen/generatorConfig_test.go": "a6d8c8ebea5f1accf0e37dd044e57b48e422821fe75086d8d2eb3abe3acb147c",
"cmd/go-omni/apigen/middlewareFactory.go": "cc0220edb3eff25d7f66b2c37e206455f8730a2b909613713322bee7caf72303",
"cmd/go-omni/apigen/testdata/composition/accessFactory.go": "e1e5057ff79f806b3218e93249e8191d097b5c4844977204a8c32848443cba33",
"cmd/go-omni/apigen/testdata/composition/domain.go": "ab27ebd7ad5e3ed5505a2aa268b9b5ceedbb01477b59011bec63bb270114078b",
"cmd/go-omni/apigen/testdata/composition/amount.go": "f2d1834502d71d9ecf72ebe3b1077e5ef41f44fd816abd4617ac19f9bb962fdd",
"cmd/go-omni/apigen/testdata/composition/copy.go": "32d37d6f1f0e9aa2409aeb90b052801d1cdca70e504c2dd294cdae01422c9cb7",
"cmd/go-omni/apigen/testdata/composition/client.go": "19102e191c0471cae9e7405623906d0d804865c40b80ff1d5b240a41ceab6a4e",
"cmd/go-omni/apigen/testdata/composition/service.api": "01d230d5409756e2f3eb602a7fe065efa7d9599f7a4089da4bf8f07a2474444e",
"cmd/go-omni/apigen/testdata/composition/consumer_test.go": "74aaf60318cd0de67495fc9142b3f67c3b6e232bb48b46b3e134de5aadd225ad",
"cmd/go-omni/apigen/testdata/composition/dependencies.go": "edc153204b06da00c0e45c52db43a4a22f8ffe22c4a6d3f3ebec0137b861f84d",
"cmd/go-omni/apigen/testdata/composition/converters.go": "ecdcfe31c75ca815dab8d1a82533b12cbbd40e12b3a5c444e815097f6c4df08e",
"copyx/check_test.go": "4095de3e3ba946573edb0b2baad5b492eb559565745693e144ca008eb740e1a2",
"copyx/check.go": "aa53985a2d8b73a18c51d112552bfecd06b60079cd119ebeed5ef4a5d3b0db22",
"copyx/options.go": "edea2f3d2043bf813e56dedf0aea319aaa52301d3b589f41fef986e33a77dc32",
"copyx/copy.go": "179a5a1be871813765074d945aa341945d2a4a9a42928e2b30e9839f9795aa9a",
"copyx/graph.go": "ae6a2a11a8175033d327b580cd570c97fe9d2ddb814bb50884cc19a90766badd",
"copyx/benchmark_test.go": "b1bf408957cadea47b47215ffb099d140d5d1a2eb061024a6d0c1d769f469a79",
"copyx/copy_test.go": "89b65a6281591d7cd5823cc1c7d2c384c6bd446ef06aa1763ddc3acaa84dfb4a",
"config/decoders.go": "6d31f67c82563c2d63e37ad1ce9b9fbad6232b2b785b9b58dd025be857f0fc28",
"config/config.go": "cbf09c158a9add8289befd9960f911a8cf42de0b840a57278bc71bf800121ec8",
"config/cors_test.go": "b40e76dd943f28c80a06eeb2dc74a536b217c4918c4a21d7de870404483a377a",
"config/errorCatalog_test.go": "a9cca5233da1ebf9280a022e10ff07e50f99cf01eac8ae70b905fe12665e3d9d",
"config/suggestions.go": "5220f78bc8e4fda44f4cedc6cc6227895417d83d49eaef3f54711f3a0f9e1245",
"config/http_test.go": "975dc288486ba537cf7fd7ee87df792ba7eceed6ab0a58e6a5c0660ce724ab9d",
"config/response_test.go": "b4ccb111f813f80dc180a7ddb11034a409b9409cbb1b57e654c04d678465d201",
"config/projectAdvanced_test.go": "48b5493fe364e1022d63f1f2b556e2e1554b14af8f994074ec9b048fc6108fa4",
"config/file_test.go": "019ad4356838a592826daaf3bdcc7706b74a0d7782abcbe9bd6aba161aaa6576",
"config/moduleDiagnostics_test.go": "68b9bba485617e3cde57773b8c24cd9554636f6364151bbfcad48548408c83b4",
"config/references_test.go": "e19f0a42bbe08d1a723aff63e2d77d22feba08da98ecf5d47aa675e7105da850",
"config/errorParams_test.go": "fe61921a2e5fbdf32a623299fec42799287b0af1ab19b2f8fdd4bc97583e582d",
"config/http.go": "d02858d522ad63e86185f5d87d3fec9718141c75b7199fe75d0894dfa016d24d",
"config/loader.go": "50a2347960cc98f337b7a179e3054af0bfd22db879b58bee4ffd09e3f693d051",
"config/file.go": "2fbf4cd55d13944d7a7a208696f4a845a2089058d5f6e463c1e769e56bf77404",
"config/diagnostic.go": "cf818cd90941a5c2d9a6279dc4c3b90d71495db22846625fdf769dadefcf077a",
"config/bind.go": "0eabe1534eb671f13c938021183a1f3df829283d6fb587b849d22253dda278b2",
"config/clone.go": "6146bf8a7d7549d975142a843db5c6f95c67e98b62cbd98e20c77842abd38040",
"config/endpoints_test.go": "3368cd6af3e5902a1a4f7ea9f67e2da9e3d525f5c63f7e78aaeaae8fdfbf8394",
"config/config_test.go": "112202bdd153e9e87a43629b842e02caaad54e9516e1f7cd18c301f74cb69cc7",
"config/loader_test.go": "854de1586e64ac00acd7970be929af8a1b17d9a027bf92544f6221ccef38ceb9",
"cmd/go-omni/apigen/configLoader_test.go": "75c47861be382558434883d25c8deb1a567bd305290db624e572d2dcf302afbf",
"cmd/go-omni/apigen/testdata/configuration/consumer_test.go": "0cf2d8b6fabf8dc4652d33cadb5a462e6f3a527463d8965fcb54123ae6cb6cc4",
"cmd/go-omni/scaffold/templates/api-load.go.tmpl": "c8089dfa50ea09de60a18f06463e83e8f0fa9fbb6795fd615e37822d7b2ab708",
"cmd/go-omni/scaffold/templates/api-loader.go.tmpl": "847ac407827808022dcf493823b58671e1ae50349b173a5e4500580e07c64150",
"httpc/config.go": "08241015b758648b0eb793e1856b88898ec57d8021c8e542db7d1dd6a2dcf2e6",
"httpc/request_test.go": "ae410eaa2cac54bbc15515a524387dde7bd7aa61a8fcc8e820108cbcc85c7b98",
"httpc/request.go": "43c1a4611331c8ac81eab98d5782fe8d4d271c14daa219e8a2134e029b65064d",
"httpc/status_test.go": "4f71d3f4aede466c02f978c5b4a4d3ffc83edee5916ce8d0505d01878ddef2a5",
"httpc/client.go": "a5a99b70bd1b1828a260bb41a6d9dc42d85f18a6b33ec822315374871d6af959",
"httpc/benchmark_test.go": "3ac0ccfb227b00fcbca42f89686fb67e3e00f4ed23ac5dfa686663ae18843a07",
"httpc/config_test.go": "84bc068fdc7964ae16c133374ecd6e07278253e33cea132ebcafc681378d7559",
"httpc/client_test.go": "972ebd2efab618629e394484c288afe4e675fcfe0c50121d9fa9071680d6e763",
"httpc/json.go": "e4a3caf19f24c3f58494311cd887dbb00acbdb4d9efc1f36988e19cb0f9abe7c",
"examples/httpClient/main.go": "8572f68d1bdbe9e3ce36b6f53d6ac7ef98966e0ac596ba57bb0811314a832eb5",
"examples/httpClient/main_test.go": "32f5513b2a2702214882c0163e0686548d8eb527977cde23829b54175d95010e",
"logx/logx_test.go": "0f99eff445decdd81a55b3d5374c22d2e038d2a4748adb2c9367d6d03e481671",
"logx/redaction.go": "e288c01cb0e30a79855b9e1b886a802c5614298aba5f39b2619eafb1f187f9ae",
"logx/adapters_test.go": "f0c57ca90075c82f9552ec43e58ab0613b43dea89411d9db50ba5fcb5be3dddf",
"logx/production_test.go": "5a6f957d409d657a1e712e72aab3e3275b2ba21dc81dd3be69523c90d47cc458",
"logx/timeZone_test.go": "523a864a1d55e32a770b9816634d6445ad468cab2e7d8d86588b1c428ec04824",
"logx/adapters.go": "5b558685e88cb37889238f73e9670d4db2b509fd39d512ddfca25de1b5f4aea8",
"logx/logx.go": "18dd28f8378e403773e238d1c8ce4acd05838d2863475ab7a5e278f5ba7cb6fb",
"logx/timeZone.go": "1b82737362f880f4a8461fd20637ef64eef5a5db475a49e0c9daa27a6ca7b636",
"logx/rotation.go": "e278c5ffbdba36c39b15c8b31299abe402695edaf525eb5b03d4328cf35fce61",
"internal/configdiag/error.go": "a25f9f398cd0e5ea1a0d049c8a7c264c85be67c469619b41220cf56afffdbda6",
"database/mysql/mysql.go": "e5f95b97dd79eb64d493255d2fd2f5895538131021eb8a6068de8ff6f484702c",
"database/connector.go": "5a973ea4bc53fee2b72f1925f5cf4e3d83a3c935a1455406f4a7abd46c47c007",
"database/database.go": "5cf05972b56870d89da5290e3b0db777b0253c3b4214cee3d47ab7fa6d76a263",
"examples/developerTools/config.yaml": "1e672ba1b0b16a417840a09468f785776c51408ee973b55b1910cb736a8e29d9",
"examples/developerTools/README.md": "3ba0d700dc3e3788a2a57367273b8d6b0f2acbf2f6cf38149edeef84ce898908",
"examples/developerTools/main.go": "a799d3106b4a64258b7c1cbad9c776cab840526795416b07b114628e3bd4c440",
"cors/cors.go": "a0feb191b5ecc43b0a0c889af5c1ec960fdc55bfa813aff49413008077e64f70",
"cors/config.go": "973300087dee87fd35bb56a2c40b6ace75f85b08e6749cc509421aab9135e2e4",
"cors/cors_test.go": "49b20eb72ba4c40b949a711fba0bdcd342d793a8a010771204e696c563b3e08c",
"app/cors_test.go": "7dcfcbb9c605a16bbd5455177a1f527dbb18f673e6ebb2b93d7b874c64c5d56a",
"cmd/go-omni/apigen/corsConsumer_test.go": "8b5ce07474f50d89ba1d7d155b433e7103785b350db13df8cacd527b7070e240",
"cmd/go-omni/apigen/testdata/cors/consumer_test.go": "677c559b8c6fc77aa6d8a69c0c1296f1fbefaa7f2f64cf80609dbe4027e254ab",
"examples/cors/main.go": "39d76c4e2a4929f18f2acbfe03ab10c50b7276e56345de4d12eb962b5f6cc5b7",
"examples/cors/config.yaml": "a78f83433810cb385ac9a8debbeb6932ebef71d85df073afbfc1d928fb7bf89f",
"scripts/corsBrowser/main.go": "2654778e9c32829f6dadefe572551f9feb891da110741a08bf9243c7284c3ba2",
"database/transaction.go": "037f14ed495e6d024e13e2033c2e469b138dc5c1e1d6f6a9a73e723e094e71e1",
"database/transaction_test.go": "b8fb122c145836c07ca062dc8c89c07ae7482885d33c3f2c239cca21e97b72d4",
"database/transactionResponse_test.go": "bc59b17605c5b5e051fc987caf6dafbbb5d71f4b1380a5ceec8f2dafaafb6af7",
"database/mysqlConnector_test.go": "db5a9f5a99fd46e8e84c4452663704172ddc26a57ad6af84d3165041ff5357e9",
"examples/transactions/main.go": "ac2a17556b63c0bbb8a5c726381b6929df235da089549937a70d3696e2cba3f2",
"examples/transactions/demoDriver.go": "950b5f9bf0d20b8b2bc7590d502e110f2815fc394df789cac39e324149870ab6",
"i18n/bundle.go": "cd0b54e779fa9f8e3a1b5d4c7456ae06f0f39618365362de329d4b61104abb93",
"i18n/types.go": "cc912f7c7dc598de758cec79568fd0bdfac6cab8a8333a6ce0570680236d415a",
"i18n/plural_test.go": "ad3a430f370bc51503aa5bce335dcb5962f6e8226f2925956ddd419fd9c230d8",
"i18n/export.go": "99608af1d487fced1bba49b77157e7a1f51afb4341fbb607f09920dfca94155a",
"i18n/pattern.go": "1c7a91e48f449124d2de90abebd698401aef93896fcdfcf24a83e12a048c5192",
"i18n/README.md": "11a933abe712c961ca1c3b138d4440bc440a7b4c6e30654849e604efe4be056c",
"i18n/plural.go": "9b69e23568a4ea240d5d4bd2c62237932245a8a91aa451386f4a7d9d76d774f9",
"i18n/load.go": "6e43d781ba90db6de78d2138e496161c391797a735211cae7342abebac2c5618",
"i18n/bundle_test.go": "f9e30d36955722f51e7d9dfe685515441451459547a3235b97064de7dda4d8f9",
"i18n/i18ntool/extract_test.go": "dd62077d190e4264348f18e8f86d25e324eebf2d471918f27da8f0c552bf46c6",
"i18n/i18ntool/extract.go": "f0ab49435f61750b06ee8105ff016be5fe06c8404f90163708d9f7e423ae89c0",
"i18n/errori18n/errors_test.go": "2c2b74b1752d4367035c9f6b9f0f79bd05dac583f37f4e4de97d2163e667be4d",
"i18n/errori18n/errors.go": "539a9eac6b841a566bc808363e9505226c7510d5dc277da621dbefb16cb06ecd",
"i18n/browser/runtime.d.mts": "fd2f18dbb6d1b1dfe2392a5e0831354f0fb2ac8815328d8b422ab79503889ad7",
"i18n/browser/runtime.test.mjs": "83b219f3b72cdeb417f6223f9f0942ef8d3b43d5051b1c66dc475c7634d65acf",
"i18n/browser/README.md": "8a3de821c6cdf57c99ee2b56fa0deeb7557b9d11a9a77bf05b00c1ae22bf34ab",
"i18n/browser/conformance.mjs": "95b7325ef24513b53eb5b8d3475fe3c5c7c56bc2da2cb1aa74e097b2ee410623",
"i18n/browser/assets.go": "a20c679902c5d96dba13b0df31ef98e3516f0ebdd1c403740a6a720a412b0b84",
"i18n/browser/runtime.mjs": "a38f6378a835661ee9198ff910eafd04f4e56051c113d0637703e59e4ae3cd3c",
"i18n/browser/testdata/consumer.mts": "84e3e26d2d86a03b30e3fcd27c3bb51c268ac89ef115f8def274321250b46e08",
"i18n/cldr/LICENSE": "220ba0e1c43b99530d2d5bdb892a99dca0989414f51ab695ecd90163eaa1ec3b",
"i18n/cldr/README.md": "cd5aef06e415cf21913a5b8736c29abbc02df449868c16cd08be8aa7e0c8fea0",
"i18n/cldr/plurals.json": "ce19668400e9f7904e8b3939d1f0a2872fe8da7bdea9f45f5d50b3a2bac3b382",
"i18n/httpi18n/http_test.go": "1f0266d2d2a54bfe767efe76bfedce6130f6c988dd04be801416336a95a5d606",
"i18n/httpi18n/http.go": "ac22eeb795b62b74d0b9a233b6fb79a337f35d430e6a136fb31f643ee4512f0d",
"examples/i18n/index.html": "7d0c9e1fdaf96c883cbe8e671ae094d83c2742a9f76b820649d58d62f4944c12",
"examples/i18n/config.json": "c3e12efcedbfc6a8ca73cae9cd4cc3617ee363af8747aec630808e6f5e1eb682",
"examples/i18n/README.md": "bf5c3ae49b921b1c25d06e19d5ecb743f349ea9d085b9d9da79f1391a29fa9e1",
"examples/i18n/main.go": "3f8599781e423a9e3a968f5310fcc80d2daf5f09b733050c1e13c4fea589f29e",
"examples/i18n/main_test.go": "c99f736841fe579d2134762d4962da73d6ab88090e5921ce8c26edf5134bee9b",
"examples/i18n/locales/ui.en.json": "fa8b210ad1a78baae8f444420c983c5498025a1a3ea0bd15d64d8125629bee42",
"examples/i18n/locales/ui.zh-Hans.json": "04761003bea2a02b1e746a1f458f50fa2e0fdb3706d02bc5cb5a4f16378c5021",
"examples/i18n/locales/errors.zh-Hans.json": "d9113b720f4c284b90c41a6db4a867d272db4e951cd284eeb00ca69e3fc21385",
"app/i18n_test.go": "4d6125b78c33c7df759584b84f791d9a041673d6536503c6671a71e20b588d39",
"errors/localization.go": "a047c33e9c4ad9ca10aacfa0800dec51a5db788c1299fba0fb9b361abef41c85",
"cmd/go-omni/i18n.go": "be95a89de3fcc34baaea76669f30616902b0f5d7d6ef018e9c29a8e38106fb6a",
"cmd/go-omni/apigen/i18nVersion.go": "c4187e5d77164b8e906294b63e8d351db49a6d587b4f77a32529f6a4b75cb1c9",
"cmd/go-omni/apigen/testdata/i18n/consumer_test.go": "b62ad30f5912c50e1f6bb1737fd75430ead113989990acb6dcdc93f5d88de0af",
"contract/policy_test.go": "d1ea81c923dda58ae75323cc1eb9773517e63180153dbdf19eb35a634c9890db",
"app/body.go": "4064b96fc38c5b75ec17f0d82e0dec79242640c4d0903bf544e4fda057e5397a",
"app/router.go": "b0418e18fd546b855a4cce9ff19efc25b274d9b9c52144e12338bb5d97a5be9f",
"app/requests.go": "c4cb453e8215ed594e6755b256b4b1bbcd3b870584f4be19480d4dd1b9e0aa3d",
"app/bodyPolicy_test.go": "505afa7bcc3f70009ef2af2c455e377277db01c2b883c8e5204de0cbebf25cbb",
"app/requests_test.go": "b008b5599e1f5eea01a08f15220588bd581a2d5e98658d53f0acb273dd946137",
"app/transport_test.go": "10f086eeb77e3b11e83768bbbb0372aa68056a8c8839a18edfaf597b5acff689",
"httpx/context.go": "e3a7d3cd31bc6bddaf21c2be35887cf5c85e69ec5e6ff82178884cb0e4696ffb",
"internal/httplimits/limits.go": "98ba40ceda78fcf4b08d51b0851de851329f568fd35b0a4aedefd4b48a43e780",
"lifecycle/runtime_test.go": "ccd302f78535215bea69d7c8ad8c812c79c52cca087353358206cb6b5926d34b",
"transport/http2/server.go": "e0b7fdb3069583d042df5117b36a3fffc423ee452b0a9da162e3604fcce80b49",
"transport/http2/lifecycle_test.go": "2a298b8e466ff4145105338904ec5d9e19087c91d829d6ae5cad8c8b1ee05b15",
"transport/native/server.go": "8c2fc503bc157265838b453bfe289ec68eeb2bdaf63f6dd1e585536a8278ba0c",
"transport/native/earlyReject_test.go": "0fd598e24199beb55bead10d741d739f7361345584c4430b4a97c35d59d62774",
"examples/httpSafety/main.go": "4651b3806e7a8030101ba6a70794fcc18a50d2939fe0a59d7f80bb4a7de30f15"
}
}

View File

@@ -0,0 +1,74 @@
{
"passed": true,
"frameworkVersion": "v0.28.1-preview",
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
"freshModuleCache": true,
"noLocalReplace": true,
"durationSeconds": 25.69,
"tests": [
"TestConflictRollsBackEarlierWrites",
"TestUnsafePaths",
"TestGeneratedProgramConsumer",
"TestOwnershipAndDrift",
"TestRecordedTemplateUpgrade",
"TestProjectGuards/module",
"TestProjectGuards/oldFramework",
"TestProjectGuards/manifest",
"TestProjectGuards/lock",
"TestProjectGuards/otherGenerator",
"TestProjectGuards/symlink",
"TestProjectGuards"
],
"checks": [
"remoteCLIInstall",
"installedVersionDefault",
"generatedConsumerBuildRaceVet",
"defaultNoAPIFiles",
"defaultNoNetworkListener",
"finiteProgram",
"SIGINT",
"SIGTERM",
"initializationFailureAndPanic",
"runErrorPanicGoexit",
"fatalComponentFailure",
"nonzeroProcessFailureAndTimeoutExits",
"partialStartupDraining",
"acknowledgementBeforeResourceClose",
"workContextSurvivesGracefulStop",
"cancellationGrace",
"unresponsiveRunAndBackgroundWork",
"retainedResourcesAndLogger",
"explicitConfigurationOnly",
"checkConfigWithoutDependencies",
"handwrittenFilesPreserved",
"generatedFileConflict",
"missingGeneratedFileRestored",
"missingHandwrittenFileNotRecreated",
"readOnlyCheckAndDryRun",
"templateDriftUpgrade",
"manifestIdentityAndVersionGuards",
"symlinkAndLockProtection",
"fileTransactionRollback",
"publishedExampleFiniteRun"
],
"sourceHashes": {
"cmd/go-omni/internal/genfs/files.go": "97e338c74ddb3145eb0ec942b69d5c21c83dfb2f9f2165376fc83366ae1e93af",
"cmd/go-omni/internal/genfs/files_test.go": "a659b31ddf195c80d48cfc00302a3f4c6d5a1ed68b58d241da03a44ad54fe34a",
"cmd/go-omni/project.go": "07e362664053e59dbb8f8f46d4c7be9d66afb93a7142a810109b66cd296ae8a6",
"cmd/go-omni/projectgen/consumer_test.go": "312442d56599c8264f14aef03722a221a72e70c038875f11c3dbfc19e9872883",
"cmd/go-omni/projectgen/generate.go": "c200bb490623622aef4586baef2d3becf158ca6a6833abb9abe8d1cb9b138d6a",
"cmd/go-omni/projectgen/generate_test.go": "f113258f0150a9f569dbe422b9f40e8ba0db3d115caf09a2c81432a8ae126e03",
"cmd/go-omni/projectgen/templates/.gitignore.tmpl": "82abf6aa84ec1c3ec218b09b5ce465af616c4fe8ff6de6db09abef962a137401",
"cmd/go-omni/projectgen/templates/README.md.tmpl": "3d01821fb4af9e97ca92f47e80a520d6f64ef136e49a8bcd4fed53dec32bde23",
"cmd/go-omni/projectgen/templates/etc_config.yaml.tmpl": "c224c68f50ea00a10e9dfd317347ac843c9107738861dab7824f7a7b1ae3a1ad",
"cmd/go-omni/projectgen/templates/internal_config_config.go.tmpl": "55111dbe02f0b91ed58a36449dcee8c9d7338251730764cda0de3e77e4a82f28",
"cmd/go-omni/projectgen/templates/internal_config_loadGen.go.tmpl": "95fb848065e3e516a8026f4439ede6d8be100e1726a56a124a6e49cb1d9d817d",
"cmd/go-omni/projectgen/templates/internal_config_loader.go.tmpl": "c9ad9f26a7d84622d7f2114a020b57ba6871a86ccf6cbc015c63674026ce81c3",
"cmd/go-omni/projectgen/templates/internal_run_run.go.tmpl": "51c2b4b407aa8b350e666be49b9d888ee42070627e28303ae2e1b0c9b4d73c95",
"cmd/go-omni/projectgen/templates/internal_svc_runtimeGen.go.tmpl": "8e259634ac7fbac0d2a13629864b2a5c89e43b6e9c477db9272b00cf49dc47d1",
"cmd/go-omni/projectgen/templates/internal_svc_serviceContext.go.tmpl": "1ed62fd86fd050468cac1c158a438deed01d4c25b4b634eec4ccd84b24df476f",
"cmd/go-omni/projectgen/templates/main.go.tmpl": "c59a7773f00c31c1a67e6a0093de26bc6f2dc8dbe34c61c1fa7e1b341e4afcc2",
"cmd/go-omni/projectgen/testdata/consumer_test.go": "38df45a69e843dfe6897d0296337e10b507076ea3c7a32901c1cad340915b0ee",
"scripts/verifyProgramRelease.py": "9127743ee929cb32701e4a063372a817ead3fd2795c2722b75f2a0c9123fa85a"
}
}

View File

@@ -0,0 +1,34 @@
{
"passed": true,
"frameworkVersion": "v0.28.1-preview",
"frameworkSum": "h1:tah8kFIFZMuPK/veNBu/AjB6bdmlp5DgN+fTuNNkQmA=",
"freshModuleCache": true,
"noLocalReplace": true,
"durationSeconds": 21.74,
"checks": [
"remoteCLIInstall",
"remoteAPIModule",
"remoteGenericModule",
"explicitKinds",
"manifestInferredKinds",
"mixedFormatWithProjectSkip",
"mixedGeneration",
"mixedCheck",
"nativeGoWorkBuild",
"nativeGoWorkRaceTests",
"moduleAndWorkspaceMetadataPreserved",
"bothProjectFailuresAggregated",
"nonzeroFailureExit",
"failedCheckIsReadOnly",
"crossProjectPreflight",
"missingGeneratedOutputRestored"
],
"sourceHashes": {
"cmd/go-omni/apigen/workspace.go": "cc39732ed9a41bb2dfa14933a830a7f10e8e3b2edc2ada07c376a738c0ec77e0",
"cmd/go-omni/apigen/workspaceMixed_test.go": "119f2a2aadc2b3ee2471bed1309c6ad8e36488b258f2ab1ba5ba5bab4219643f",
"cmd/go-omni/apigen/workspaceCatalog_test.go": "372874322aece46714351f088b5741642fe4e63d95062743a575958fa58b8b39",
"cmd/go-omni/workspace.go": "b7bbd3d9d58b3987090edf7e3714d7d8396e7c332174022d918a52e16396397d",
"cmd/go-omni/workspace_test.go": "0170d18158325262765d8b3067f55ca3ce9fe994f2d030cafee3e82b13bfcd4b",
"scripts/verifyWorkspaceRelease.py": "0495dc97c68ac17a7969088cf94b1cbbfa6e53f18fb36147c64bc34edb260bca"
}
}

View File

@@ -124,7 +124,7 @@ def main():
checks += ["sharedGoCatalogExport", "documentOnlyErrorSelection", "workspaceCheckNoWrites", "staleCatalogRejected", "twoIndependentServiceBuilds"] checks += ["sharedGoCatalogExport", "documentOnlyErrorSelection", "workspaceCheckNoWrites", "staleCatalogRejected", "twoIndependentServiceBuilds"]
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version) env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(cli), GO_OMNI_RELEASE_VERSION=args.version)
tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject", "TestPrimitiveArrayConsumer", "TestCopyxThreeEndpointConsumer", "TestGeneratedConfigurationConsumer", "TestGeneratedCORSConsumer", "TestI18nGeneratedConsumer"] tests = ["TestManagedGeneratedProject", "TestErrorInheritanceConsumer", "TestErrorParamsConsumer", "TestCompatibilityConsumer", "TestBusinessGeneratedProject", "TestPrimitiveArrayConsumer", "TestCopyxThreeEndpointConsumer", "TestGeneratedConfigurationConsumer", "TestGeneratedCORSConsumer", "TestI18nGeneratedConsumer", "TestRecursiveResponseConsumer"]
print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True) print("Running generated consumers against the remote framework, including lifecycle failure cleanup", flush=True)
output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env) output = run(["go", "test", "-race", "-count=1", "-json", "-timeout=240s", "./cmd/go-omni/apigen", "-run", "^(" + "|".join(tests) + ")$"], ROOT, env)
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")] events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
@@ -135,6 +135,7 @@ def main():
assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch" assert len(sums) == len(tests) and all(s == dependency["Sum"] for s in sums), "remote consumer checksum mismatch"
checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "primitiveAndDynamicArrays", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"] checks += ["initializationAndListenFailureCleanup", "drainBeforeDependencyClose", "generatedConsumerRaceTests", "errorParamsAndInheritance", "manualHooksAndScalars", "dependencyFatalShutdown", "fatalProcessExitCode", "initializationShutdownNotification", "configuredBuiltinEndpoints", "projectHealthEnvelope", "endpointConflictCleanup", "responseArrayNullableNull", "primitiveAndDynamicArrays", "strictRequestPolicies", "declarativeMultipartBinding", "backgroundStopDrainResourceOrder", "backgroundInitAndListenFailureCleanup", "backgroundTimeoutRetainsResourcesAndExits"]
checks += ["generatedIncompleteHTTPDrainRetainsDependenciesAndLogger"] checks += ["generatedIncompleteHTTPDrainRetainsDependenciesAndLogger"]
checks += ["recursiveResponseTypedHTTP", "recursiveResponseReferenceGraph", "recursiveResponseRegenerationAndDrift"]
# Run tests from the downloaded module, not the framework checkout. # Run tests from the downloaded module, not the framework checkout.
http_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/httpc"], starter, env) http_output = run(["go", "test", "-race", "-count=1", "-json", MODULE + "/httpc"], starter, env)

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Exercise mixed workspace generation/check and native Go multi-module builds remotely."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import tempfile
import time
from verifyErrorInheritanceRelease import run
ROOT = Path(__file__).resolve().parents[1]
MODULE = "git.yuntuo.hk/k2safe/go-omni"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True)
parser.add_argument("--report", type=Path, default=ROOT / "reports/workspaceRemote.json")
args = parser.parse_args()
if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?", args.version):
parser.error("a fixed release version is required")
env = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")}
env.update(GOWORK="off", GOFLAGS="", GOPRIVATE="git.yuntuo.hk", GONOSUMDB="git.yuntuo.hk")
started = time.monotonic()
with tempfile.TemporaryDirectory(prefix="goOmniWorkspaceRelease-") as directory:
temp = Path(directory)
(temp / "bin").mkdir()
env.update(GOBIN=str(temp / "bin"), GOMODCACHE=str(temp / "modcache"))
print(f"Installing remote CLI {args.version} in an empty cache", flush=True)
run(["go", "install", MODULE + "/cmd/go-omni@" + args.version], temp, env)
dependency = json.loads(run(["go", "mod", "download", "-json", MODULE + "@" + args.version], temp, env))
assert dependency["Version"] == args.version and dependency.get("Sum")
downloaded = Path(dependency["Dir"])
env.update(GO_OMNI_SCAFFOLD_INTEGRATION="1", GO_OMNI_RELEASE_CLI=str(temp / "bin/go-omni"), GO_OMNI_RELEASE_VERSION=args.version)
print("Testing mixed API/project consumers and go.work builds from the downloaded release", flush=True)
output = run(["go", "test", "-race", "-json", "-count=1", "-timeout=240s", "./cmd/go-omni",
"-run", "^TestMixedWorkspaceConsumer$"], downloaded, env)
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
passed = [e["Test"] for e in events if e.get("Action") == "pass" and "Test" in e]
assert passed == ["TestMixedWorkspaceConsumer"], output
evidence = "".join(e.get("Output", "") for e in events)
assert evidence.count("; no local replace") == 2, evidence
assert dependency["Sum"] in evidence, evidence
sources = ["cmd/go-omni/apigen/workspace.go", "cmd/go-omni/apigen/workspaceMixed_test.go",
"cmd/go-omni/apigen/workspaceCatalog_test.go", "cmd/go-omni/workspace.go",
"cmd/go-omni/workspace_test.go", "scripts/verifyWorkspaceRelease.py"]
report = {
"passed": True, "frameworkVersion": args.version, "frameworkSum": dependency["Sum"],
"freshModuleCache": True, "noLocalReplace": True,
"durationSeconds": round(time.monotonic() - started, 2),
"checks": ["remoteCLIInstall", "remoteAPIModule", "remoteGenericModule", "explicitKinds",
"manifestInferredKinds", "mixedFormatWithProjectSkip", "mixedGeneration", "mixedCheck",
"nativeGoWorkBuild", "nativeGoWorkRaceTests", "moduleAndWorkspaceMetadataPreserved",
"bothProjectFailuresAggregated", "nonzeroFailureExit", "failedCheckIsReadOnly",
"crossProjectPreflight", "missingGeneratedOutputRestored"],
"sourceHashes": {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest() for p in sources},
}
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
print(f"PASS remote mixed workspace {args.version}: {len(report['checks'])} checks", flush=True)
if __name__ == "__main__":
main()