Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bbf4627a1 | |||
| 61c37e0dba |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.4.0
|
||||||
|
|
||||||
|
- **破坏性变更**:移除 Gin 适配器及其全部依赖,`omni.New()` 默认使用只依赖标准库的 `transport/nethttp`。
|
||||||
|
- 移除 Gin 专属的 `Config.Mode` / `WithMode` API;网络层运行模式由具体适配器或启动层自行配置。
|
||||||
|
- 公共 `httpx.Router`、`transport/transporttest.Run` 和 `omni.NewWithRouter` 保留,自研网络层无需修改业务 Handler / Service 即可接入。
|
||||||
|
- benchmark 改为标准库路由和完整 GoOmni 请求链,新增 `scripts/verifyPerformance.py` 作为 race、vet、build 和性能基线入口。
|
||||||
|
- 远程业务试用、脚手架、适配器契约和 MySQL 验收全部迁移到 `v0.4.0`。
|
||||||
|
|
||||||
|
## v0.3.1
|
||||||
|
|
||||||
|
- 新增独立业务模块验收 `scripts/verifyBusiness.py`:从远程 `v0.2.0` 升级到 `v0.3.1`,验证无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。
|
||||||
|
- 脚手架与各项远程验收脚本默认跟随 `v0.3.1`,新项目会固定到最新补丁版本。
|
||||||
|
|
||||||
## v0.3.0
|
## v0.3.0
|
||||||
|
|
||||||
- 新增公共适配器验收套件 `transport/transporttest.Run` 和远程消费验收脚本 `scripts/verifyTransports.py`。Gin、nethttp 使用同一套路由与业务处理链测试,并通过完整用户 CRUD 与 race 验证。
|
- 新增公共适配器验收套件 `transport/transporttest.Run` 和远程消费验收脚本 `scripts/verifyTransports.py`。Gin、nethttp 使用同一套路由与业务处理链测试,并通过完整用户 CRUD 与 race 验证。
|
||||||
@@ -10,6 +23,7 @@
|
|||||||
- 验证 requestId 与业务日志关联、认证授权短路、请求并发隔离、context 值与 deadline 保留,以及真实客户端取消传入 Service。
|
- 验证 requestId 与业务日志关联、认证授权短路、请求并发隔离、context 值与 deadline 保留,以及真实客户端取消传入 Service。
|
||||||
- **升级行为调整**:Gin 不再自动重定向尾部斜杠;nethttp 不再忽略尾部斜杠,通配参数包含开头的 `/`,Any 收敛为九种标准方法。HEAD 不自动回退 GET,其他方法需显式注册。404/405 为统一纯文本响应,405 附排序后的 Allow。
|
- **升级行为调整**:Gin 不再自动重定向尾部斜杠;nethttp 不再忽略尾部斜杠,通配参数包含开头的 `/`,Any 收敛为九种标准方法。HEAD 不自动回退 GET,其他方法需显式注册。404/405 为统一纯文本响应,405 附排序后的 Allow。
|
||||||
- `gin.NewWithEngine` 应用 GoOmni 路由策略;原生 Gin 扩展与 Raw 流式响应不属于可移植契约。详见 `transport/README.md`。
|
- `gin.NewWithEngine` 应用 GoOmni 路由策略;原生 Gin 扩展与 Raw 流式响应不属于可移植契约。详见 `transport/README.md`。
|
||||||
|
- 新增业务试用验收 `scripts/verifyBusiness.py`:独立目录内固定远程版本,执行 `v0.2.0` → `v0.3.0` 升级,验证生成业务服务的配置、日志、requestId、CRUD、参数校验、错误映射和优雅退出。
|
||||||
|
|
||||||
## v0.2.0
|
## v0.2.0
|
||||||
|
|
||||||
|
|||||||
20
PERFORMANCE.md
Normal file
20
PERFORMANCE.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# GoOmni 性能基线
|
||||||
|
|
||||||
|
这份基线用于比较未来自研网络层,不代表生产容量承诺。测试只测进程内请求分发和 GoOmni 请求链,不包含真实 TCP、TLS、数据库或业务存储延迟。
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/verifyPerformance.py
|
||||||
|
```
|
||||||
|
|
||||||
|
本次记录环境:Go 1.25.0、darwin/arm64、Apple M2 Max;每项连续运行 3 次:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BenchmarkTransportRouteDispatch/nethttp 568–671 ns/op 1344 B/op 13 allocs/op
|
||||||
|
BenchmarkTransportApplicationRequest/nethttp 2017–2208 ns/op 3091 B/op 34 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
`BenchmarkTransportRouteDispatch` 只覆盖标准库路由、路径参数和 `http.Handler` 调用;`BenchmarkTransportApplicationRequest` 还包含 GoOmni Context、RequestID、统一 JSON 响应和默认中间件链。
|
||||||
|
|
||||||
|
比较新网络层时,应使用相同 Go 版本、机器和 benchmark 命令,并同时运行 `-race` 全量测试。性能下降需要结合分配次数、取消传播、错误响应和业务契约结果一起判断。
|
||||||
30
README.md
30
README.md
@@ -1,17 +1,19 @@
|
|||||||
# GoOmni
|
# GoOmni
|
||||||
|
|
||||||
GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认用 Gin 作为网络适配器,业务代码只依赖 GoOmni 的 transport-neutral API,后续可以替换为自研网络层。
|
GoOmni 是一套面向业务复用的 Go 后端基础框架。当前默认使用只依赖标准库的网络适配器,业务代码只依赖 GoOmni 的 transport-neutral API,后续可以替换为自研网络层。
|
||||||
|
|
||||||
远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本:
|
远程 module 地址是 `git.yuntuo.hk/k2safe/go-omni`。业务项目可以直接引用指定版本:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go get git.yuntuo.hk/k2safe/go-omni@v0.3.0
|
go get git.yuntuo.hk/k2safe/go-omni@v0.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`v0.4.0` 移除了 Gin 和 Gin 专属的 `WithMode` 配置;`omni.New()` 直接使用标准库路由,已有业务 Handler / Service 无需修改。
|
||||||
|
|
||||||
创建新业务项目:
|
创建新业务项目:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.3.0
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.4.0
|
||||||
go-omni new --module example.com/billing ./billing
|
go-omni new --module example.com/billing ./billing
|
||||||
cd billing
|
cd billing
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -40,7 +42,7 @@ go-omni/
|
|||||||
├── validate/ # 参数校验
|
├── validate/ # 参数校验
|
||||||
├── auth/ # 认证和权限接口
|
├── auth/ # 认证和权限接口
|
||||||
├── database/ # database/sql 生命周期和事务
|
├── database/ # database/sql 生命周期和事务
|
||||||
├── transport/gin/ # Gin 网络适配器,可替换
|
├── transport/nethttp/ # 标准库网络适配器
|
||||||
├── examples/ # 示例项目
|
├── examples/ # 示例项目
|
||||||
└── cmd/ # go-omni 项目脚手架
|
└── cmd/ # go-omni 项目脚手架
|
||||||
```
|
```
|
||||||
@@ -80,7 +82,7 @@ func main() {
|
|||||||
return omni.NotFound("USER_NOT_FOUND", "user was not found")
|
return omni.NotFound("USER_NOT_FOUND", "user was not found")
|
||||||
```
|
```
|
||||||
|
|
||||||
默认 Gin 适配器位于 `transport/gin`。业务层不需要导入 Gin;如果后续实现自己的网络层,只需要实现 `httpx.Router` / `httpx.RouterGroup`,然后通过 `omni.NewWithRouter` 启动应用:
|
默认网络适配器位于 `transport/nethttp`。业务层不需要导入具体网络库;如果后续实现自己的网络层,只需要实现 `httpx.Router` / `httpx.RouterGroup`,然后通过 `omni.NewWithRouter` 启动应用:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
application := omni.NewWithRouter(myRouter)
|
application := omni.NewWithRouter(myRouter)
|
||||||
@@ -88,11 +90,11 @@ application := omni.NewWithRouter(myRouter)
|
|||||||
|
|
||||||
仓库还提供了使用标准库路由的 `transport/nethttp` 参考实现。两种适配器已通过同一套路由、业务处理链和取消传播测试,完整用户 CRUD 复用同一份 Handler / Service。
|
仓库还提供了使用标准库路由的 `transport/nethttp` 参考实现。两种适配器已通过同一套路由、业务处理链和取消传播测试,完整用户 CRUD 复用同一份 Handler / Service。
|
||||||
|
|
||||||
自研适配器可以直接运行 `transport/transporttest.Run` 验收。具体规则、v0.3.0 的斜杠 / Any / 405 行为调整,以及自研网络内核与当前 HTTP 接口的边界,见 [适配层契约](transport/README.md)。
|
自研适配器可以直接运行 `transport/transporttest.Run` 验收。具体规则、v0.4.0 的斜杠 / Any / 405 行为调整,以及自研网络内核与当前 HTTP 接口的边界,见 [适配层契约](transport/README.md)。
|
||||||
|
|
||||||
`app.Engine()` 现在返回标准 `http.Handler`,`ctx.Raw()` 返回适配器提供的 `http.ResponseWriter`,避免业务 API 锁定 Gin 类型。
|
`app.Engine()` 返回标准 `http.Handler`,`ctx.Raw()` 返回适配器提供的 `http.ResponseWriter`,业务 API 不锁定具体网络实现。
|
||||||
|
|
||||||
业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖 Gin:
|
业务代码可以直接使用 `git.yuntuo.hk/k2safe/go-omni/logx` 打结构化日志,不需要依赖具体网络库:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (s *UserService) Create(ctx *httpx.Context) error {
|
func (s *UserService) Create(ctx *httpx.Context) error {
|
||||||
@@ -166,6 +168,16 @@ userService := service.NewUsers(services)
|
|||||||
|
|
||||||
[`examples/reference`](examples/reference/README.md) 提供按 Handler / Service / Repository / Model 分层的用户 CRUD 示例。默认使用并发安全的内存存储,支持分页、邮箱唯一校验、结构化请求日志和统一错误,无需配置数据库。
|
[`examples/reference`](examples/reference/README.md) 提供按 Handler / Service / Repository / Model 分层的用户 CRUD 示例。默认使用并发安全的内存存储,支持分页、邮箱唯一校验、结构化请求日志和统一错误,无需配置数据库。
|
||||||
|
|
||||||
|
从远程版本生成并升级一个独立业务模块,可运行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 scripts/verifyBusiness.py --version v0.4.0
|
||||||
|
```
|
||||||
|
|
||||||
|
该验收会在临时目录中先固定 `v0.2.0`,再升级到 `v0.4.0`,检查无本地 replace、配置加载、结构化日志、requestId、CRUD、校验错误、业务错误和优雅退出。
|
||||||
|
|
||||||
|
标准库路由和完整 GoOmni 请求链的可重复性能基线见 [PERFORMANCE.md](PERFORMANCE.md),运行 `python3 scripts/verifyPerformance.py` 可重新采样。
|
||||||
|
|
||||||
在框架根目录运行:
|
在框架根目录运行:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -174,7 +186,7 @@ go run ./examples/reference
|
|||||||
|
|
||||||
默认地址为 `http://127.0.0.1:8080`。示例文档提供完整的 curl 操作流程。数据只保存在内存中,重启后清空。
|
默认地址为 `http://127.0.0.1:8080`。示例文档提供完整的 curl 操作流程。数据只保存在内存中,重启后清空。
|
||||||
|
|
||||||
框架的 `App` 也实现了标准 `http.Handler`,可以直接用于 `http.Server` 和 `httptest.NewServer`;业务路由可通过 `*omni.RouterGroup` 注册,无需导入 Gin。
|
框架的 `App` 也实现了标准 `http.Handler`,可以直接用于 `http.Server` 和 `httptest.NewServer`;业务路由可通过 `*omni.RouterGroup` 注册,无需导入网络库。
|
||||||
|
|
||||||
## 当前边界
|
## 当前边界
|
||||||
|
|
||||||
|
|||||||
26
ROADMAP.md
26
ROADMAP.md
@@ -18,19 +18,27 @@
|
|||||||
- 已验证初始化失败、缺少表结构、正常退出时关闭连接池,以及数据库停止后的 readiness 503、恢复后的 200。
|
- 已验证初始化失败、缺少表结构、正常退出时关闭连接池,以及数据库停止后的 readiness 503、恢复后的 200。
|
||||||
- 自动验收入口:`python3 scripts/verifyDatabase.py --version v0.2.0`;结束后清理本次测试容器、匿名卷和临时项目。
|
- 自动验收入口:`python3 scripts/verifyDatabase.py --version v0.2.0`;结束后清理本次测试容器、匿名卷和临时项目。
|
||||||
|
|
||||||
## 3. 网络适配一致性(已完成,v0.3.0)
|
## 3. 网络适配一致性(已完成,v0.4.0)
|
||||||
|
|
||||||
- Gin 与标准库适配器运行同一套契约测试。
|
- 标准库适配器运行公共契约测试,并为自研网络层保留相同的接入边界。
|
||||||
- 覆盖路由参数、分组、中间件、错误响应和取消传播。
|
- 覆盖路由参数、分组、中间件、错误响应和取消传播。
|
||||||
- 验收:切换适配器后,业务 Handler 和 Service 无需修改,约定的 HTTP 行为一致。
|
- 验收:切换适配器后,业务 Handler 和 Service 无需修改,约定的 HTTP 行为一致。
|
||||||
- 新增公共 `transport/transporttest.Run`,自研适配器可直接复用;覆盖静态 / 参数 / 通配路由、分组、405 Allow、鉴权短路、错误与 panic、响应提交、requestId、并发隔离、截止时间及真实客户端取消。
|
- 新增公共 `transport/transporttest.Run`,自研适配器可直接复用;覆盖静态 / 参数 / 通配路由、分组、405 Allow、鉴权短路、错误与 panic、响应提交、requestId、并发隔离、截止时间及真实客户端取消。
|
||||||
- 两种适配器共享明确的路径与方法约定,修复尾部斜杠、通配参数、Any 和 Gin 405 Allow 差异。
|
- 标准库路由实现明确的路径、方法、尾部斜杠、通配参数、Any 和 404/405 约定;不再引入 Gin 依赖。
|
||||||
- 完整用户 CRUD 已用相同业务注册代码在两种适配器上验证;全量 race 测试通过。
|
- 完整用户 CRUD 已用标准库适配器验证;自研适配器可直接复用同一公共契约测试包。
|
||||||
- 自动验收入口:`python3 scripts/verifyTransports.py --version v0.3.0`,从源码目录外消费远程版本与公共契约测试包。
|
- 自动验收入口:`python3 scripts/verifyTransports.py --version v0.4.0`,从源码目录外消费远程版本与公共契约测试包。
|
||||||
|
|
||||||
## 4. 实际业务试用(待完成)
|
## 4. 实际业务试用(已完成,v0.4.0)
|
||||||
|
|
||||||
- 在一个实际业务模块中使用远程固定版本,验证配置、日志、错误处理和升级流程。
|
- 在独立业务目录生成 billing service,先固定远程 `v0.2.0`,再升级到 `v0.4.0`,全程无本地 replace。
|
||||||
- 根据真实接入问题收敛公共 API,再评估稳定版范围。
|
- 验证环境配置、结构化访问日志、requestId 关联、参数校验、业务错误映射、CRUD、优雅退出和升级后的 race 测试。
|
||||||
|
- 业务试用没有要求修改 Handler / Service;现有公共 API 足以支撑一套分层业务模块。
|
||||||
|
- 自动验收入口:`python3 scripts/verifyBusiness.py --version v0.4.0`。
|
||||||
|
|
||||||
以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL 与适配层契约验收已完成,生产压测、自研网络内核与实际业务接入尚未完成。下一阶段先在真实业务模块中使用固定远程版本,再根据接入反馈收敛 API。
|
## 5. 稳定性与性能基线(进行中)
|
||||||
|
|
||||||
|
- 已加入标准库路由和完整 GoOmni 请求链 benchmark,输出分配次数和耗时,作为未来自研网络层的对照基线。
|
||||||
|
- 已具备全量 race、vet、build 验收;生产压测、长连接和真实部署规模验证仍待完成。
|
||||||
|
- 自动验收入口:`python3 scripts/verifyPerformance.py`。
|
||||||
|
|
||||||
|
以上阶段以验收结果判断完成。当前仍是基础框架试用阶段;MySQL、适配层契约和一次业务模块试用已完成,稳定性与性能验证、自研网络内核尚未完成。
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/middleware"
|
"git.yuntuo.hk/k2safe/go-omni/middleware"
|
||||||
ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin"
|
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App is the top-level GoOmni application. Its public behavior depends on
|
// App is the top-level GoOmni application. Its public behavior depends on
|
||||||
@@ -29,10 +29,10 @@ type App struct {
|
|||||||
closeErr error
|
closeErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates an application using the default Gin adapter.
|
// New creates an application using GoOmni's standard-library router.
|
||||||
func New(options ...config.Option) *App {
|
func New(options ...config.Option) *App {
|
||||||
settings := configure(options...)
|
settings := configure(options...)
|
||||||
return newApp(ginrouter.New(settings.Mode), settings)
|
return newApp(netrouter.New(), settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithRouter creates an application with a custom network layer. A custom
|
// NewWithRouter creates an application with a custom network layer. A custom
|
||||||
@@ -68,7 +68,7 @@ func newApp(router httpx.Router, settings config.Config) *App {
|
|||||||
func (a *App) Router() httpx.Router { return a.router }
|
func (a *App) Router() httpx.Router { return a.router }
|
||||||
|
|
||||||
// Engine is kept as a compatibility name for the old API. It returns a
|
// Engine is kept as a compatibility name for the old API. It returns a
|
||||||
// standard http.Handler, so callers no longer need a Gin type.
|
// standard http.Handler, so callers do not depend on a router type.
|
||||||
func (a *App) Engine() http.Handler { return a.router }
|
func (a *App) Engine() http.Handler { return a.router }
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler for standard servers and HTTP tests.
|
// ServeHTTP implements http.Handler for standard servers and HTTP tests.
|
||||||
|
|||||||
12
app_test.go
12
app_test.go
@@ -8,12 +8,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAppReturnsStandardResponse(t *testing.T) {
|
func TestAppReturnsStandardResponse(t *testing.T) {
|
||||||
app := New(WithMode(gin.TestMode))
|
app := New()
|
||||||
app.GET("/health", func(ctx *Context) error {
|
app.GET("/health", func(ctx *Context) error {
|
||||||
return ctx.OK(map[string]string{"status": "ok"})
|
return ctx.OK(map[string]string{"status": "ok"})
|
||||||
})
|
})
|
||||||
@@ -41,7 +39,7 @@ func TestAppReturnsStandardResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppReturnsAPIError(t *testing.T) {
|
func TestAppReturnsAPIError(t *testing.T) {
|
||||||
app := New(WithMode(gin.TestMode))
|
app := New()
|
||||||
app.GET("/users/:id", func(ctx *Context) error {
|
app.GET("/users/:id", func(ctx *Context) error {
|
||||||
return NotFound("USER_NOT_FOUND", "user was not found")
|
return NotFound("USER_NOT_FOUND", "user was not found")
|
||||||
})
|
})
|
||||||
@@ -63,7 +61,7 @@ func TestAppReturnsAPIError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppRecoversFromPanic(t *testing.T) {
|
func TestAppRecoversFromPanic(t *testing.T) {
|
||||||
app := New(WithMode(gin.TestMode))
|
app := New()
|
||||||
app.GET("/panic", func(ctx *Context) error {
|
app.GET("/panic", func(ctx *Context) error {
|
||||||
panic("boom")
|
panic("boom")
|
||||||
})
|
})
|
||||||
@@ -85,7 +83,7 @@ func TestAppRecoversFromPanic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppRunContextShutsDownGracefully(t *testing.T) {
|
func TestAppRunContextShutsDownGracefully(t *testing.T) {
|
||||||
app := New(WithMode(gin.TestMode), WithShutdownTimeout(time.Second))
|
app := New(WithShutdownTimeout(time.Second))
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
done := make(chan error, 1)
|
done := make(chan error, 1)
|
||||||
go func() { done <- app.RunContext(ctx, "127.0.0.1:0") }()
|
go func() { done <- app.RunContext(ctx, "127.0.0.1:0") }()
|
||||||
@@ -102,7 +100,7 @@ func TestAppRunContextShutsDownGracefully(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAppClosesResourcesInReverseOrder(t *testing.T) {
|
func TestAppClosesResourcesInReverseOrder(t *testing.T) {
|
||||||
app := New(WithMode(gin.TestMode))
|
app := New()
|
||||||
var closed []string
|
var closed []string
|
||||||
app.OnShutdown(func() error {
|
app.OnShutdown(func() error {
|
||||||
closed = append(closed, "database")
|
closed = append(closed, "database")
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ import (
|
|||||||
|
|
||||||
"git.yuntuo.hk/k2safe/go-omni"
|
"git.yuntuo.hk/k2safe/go-omni"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/auth"
|
"git.yuntuo.hk/k2safe/go-omni/auth"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequireAuthAndRole(t *testing.T) {
|
func TestRequireAuthAndRole(t *testing.T) {
|
||||||
app := omni.New(omni.WithMode(gin.TestMode))
|
app := omni.New()
|
||||||
authenticator := auth.StaticAuthenticator{"good-token": {UserID: "user-1", Roles: []string{"user"}}}
|
authenticator := auth.StaticAuthenticator{"good-token": {UserID: "user-1", Roles: []string{"user"}}}
|
||||||
private := app.Group("/private", auth.RequireAuth(authenticator))
|
private := app.Group("/private", auth.RequireAuth(authenticator))
|
||||||
private.GET("/me", func(ctx *omni.Context) error {
|
private.GET("/me", func(ctx *omni.Context) error {
|
||||||
@@ -62,7 +61,7 @@ func TestRequireAuthAndRole(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOptionalAuth(t *testing.T) {
|
func TestOptionalAuth(t *testing.T) {
|
||||||
app := omni.New(omni.WithMode(gin.TestMode))
|
app := omni.New()
|
||||||
app.GET("/public", func(ctx *omni.Context) error {
|
app.GET("/public", func(ctx *omni.Context) error {
|
||||||
principal, authenticated := auth.FromContext(ctx)
|
principal, authenticated := auth.FromContext(ctx)
|
||||||
return ctx.OK(map[string]any{"authenticated": authenticated, "userId": principal.UserID})
|
return ctx.OK(map[string]any{"authenticated": authenticated, "userId": principal.UserID})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架:
|
从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.3.0
|
go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.4.0
|
||||||
go-omni new --module example.com/billing ./billing
|
go-omni new --module example.com/billing ./billing
|
||||||
cd billing
|
cd billing
|
||||||
go mod tidy
|
go mod tidy
|
||||||
@@ -16,7 +16,7 @@ go run ./cmd/server
|
|||||||
需要选择其他已发布版本时,使用完整版本号:
|
需要选择其他已发布版本时,使用完整版本号:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go-omni new --framework-version v0.3.0 --module example.com/billing ./billing
|
go-omni new --framework-version v0.4.0 --module example.com/billing ./billing
|
||||||
```
|
```
|
||||||
|
|
||||||
`--framework-version` 禁用自动本地替换;`main`、`latest` 等浮动引用不接受为固定版本。
|
`--framework-version` 禁用自动本地替换;`main`、`latest` 等浮动引用不接受为固定版本。
|
||||||
@@ -48,8 +48,8 @@ MySQL 项目额外包含 `schema.sql`、MySQL Repository 和可选的数据库
|
|||||||
发布验收使用 Python 3 标准库脚本,不需要额外 Python 依赖:
|
发布验收使用 Python 3 标准库脚本,不需要额外 Python 依赖:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
python3 scripts/verifyScaffold.py --version v0.3.0
|
python3 scripts/verifyScaffold.py --version v0.4.0
|
||||||
python3 scripts/verifyDatabase.py --version v0.3.0
|
python3 scripts/verifyDatabase.py --version v0.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本在源码目录外安装远程 CLI、生成项目,确认固定版本且没有 `replace`,再下载依赖、
|
脚本在源码目录外安装远程 CLI、生成项目,确认固定版本且没有 `replace`,再下载依赖、
|
||||||
|
|||||||
@@ -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.3.0"
|
const defaultFrameworkVersion = "v0.4.0"
|
||||||
|
|
||||||
var frameworkVersionPattern = regexp.MustCompile(`^v[01]\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
|
var frameworkVersionPattern = regexp.MustCompile(`^v[01]\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const defaultRequestIDHeader = "X-Request-ID"
|
|||||||
|
|
||||||
// Config controls the default runtime behavior of an App.
|
// Config controls the default runtime behavior of an App.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Mode string
|
|
||||||
Address string
|
Address string
|
||||||
RequestIDHeader string
|
RequestIDHeader string
|
||||||
EnableLogger bool
|
EnableLogger bool
|
||||||
@@ -53,11 +52,6 @@ func (e *ConfigError) Error() string { return "config " + e.Field + " " + e.Mess
|
|||||||
// Option changes an App's configuration.
|
// Option changes an App's configuration.
|
||||||
type Option func(*Config)
|
type Option func(*Config)
|
||||||
|
|
||||||
// WithMode sets the default transport's running mode.
|
|
||||||
func WithMode(mode string) Option {
|
|
||||||
return func(cfg *Config) { cfg.Mode = mode }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRequestIDHeader changes the header used to read and return request IDs.
|
// WithRequestIDHeader changes the header used to read and return request IDs.
|
||||||
func WithRequestIDHeader(header string) Option {
|
func WithRequestIDHeader(header string) Option {
|
||||||
return func(cfg *Config) {
|
return func(cfg *Config) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
func TestLoadFromEnvUsesDefaults(t *testing.T) {
|
func TestLoadFromEnvUsesDefaults(t *testing.T) {
|
||||||
for _, key := range []string{
|
for _, key := range []string{
|
||||||
"GO_OMNI_MODE", "GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER",
|
"GO_OMNI_HTTP_ADDR", "GO_OMNI_REQUEST_ID_HEADER", "GO_OMNI_ENABLE_LOGGER",
|
||||||
"GO_OMNI_LOG_LEVEL", "GO_OMNI_LOG_FORMAT", "GO_OMNI_LOG_OUTPUT",
|
"GO_OMNI_LOG_LEVEL", "GO_OMNI_LOG_FORMAT", "GO_OMNI_LOG_OUTPUT",
|
||||||
"GO_OMNI_LOG_FILE", "GO_OMNI_SERVICE", "GO_OMNI_ENVIRONMENT",
|
"GO_OMNI_LOG_FILE", "GO_OMNI_SERVICE", "GO_OMNI_ENVIRONMENT",
|
||||||
"GO_OMNI_LOG_ADD_SOURCE", "GO_OMNI_SHUTDOWN_TIMEOUT",
|
"GO_OMNI_LOG_ADD_SOURCE", "GO_OMNI_SHUTDOWN_TIMEOUT",
|
||||||
@@ -29,7 +29,6 @@ func TestLoadFromEnvUsesDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadFromEnvOverridesSettings(t *testing.T) {
|
func TestLoadFromEnvOverridesSettings(t *testing.T) {
|
||||||
t.Setenv("GO_OMNI_MODE", "release")
|
|
||||||
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:9090")
|
t.Setenv("GO_OMNI_HTTP_ADDR", "127.0.0.1:9090")
|
||||||
t.Setenv("GO_OMNI_REQUEST_ID_HEADER", "X-Correlation-ID")
|
t.Setenv("GO_OMNI_REQUEST_ID_HEADER", "X-Correlation-ID")
|
||||||
t.Setenv("GO_OMNI_ENABLE_LOGGER", "true")
|
t.Setenv("GO_OMNI_ENABLE_LOGGER", "true")
|
||||||
@@ -45,7 +44,7 @@ func TestLoadFromEnvOverridesSettings(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if cfg.Mode != "release" || cfg.Address != "127.0.0.1:9090" || cfg.RequestIDHeader != "X-Correlation-ID" || !cfg.EnableLogger {
|
if cfg.Address != "127.0.0.1:9090" || cfg.RequestIDHeader != "X-Correlation-ID" || !cfg.EnableLogger {
|
||||||
t.Fatalf("unexpected application settings: %+v", cfg)
|
t.Fatalf("unexpected application settings: %+v", cfg)
|
||||||
}
|
}
|
||||||
if cfg.ShutdownTimeout != 3*time.Second || cfg.Log.Level != "debug" || cfg.Log.Format != "text" || cfg.Log.Output != "stdout" || cfg.Log.Service != "billing" || cfg.Log.Environment != "test" || !cfg.Log.AddSource {
|
if cfg.ShutdownTimeout != 3*time.Second || cfg.Log.Level != "debug" || cfg.Log.Format != "text" || cfg.Log.Output != "stdout" || cfg.Log.Service != "billing" || cfg.Log.Environment != "test" || !cfg.Log.AddSource {
|
||||||
@@ -80,11 +79,10 @@ func TestLoadFromEnvRejectsInvalidValues(t *testing.T) {
|
|||||||
|
|
||||||
func TestWithConfigAndWithLogConfig(t *testing.T) {
|
func TestWithConfigAndWithLogConfig(t *testing.T) {
|
||||||
settings := Default()
|
settings := Default()
|
||||||
settings.Mode = "release"
|
|
||||||
settings.Log.Service = "orders"
|
settings.Log.Service = "orders"
|
||||||
cfg := Default()
|
cfg := Default()
|
||||||
WithConfig(settings)(&cfg)
|
WithConfig(settings)(&cfg)
|
||||||
if cfg.Mode != "release" || cfg.Log.Service != "orders" {
|
if cfg.Log.Service != "orders" {
|
||||||
t.Fatalf("complete config was not applied: %+v", cfg)
|
t.Fatalf("complete config was not applied: %+v", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func LoadFromEnv(prefix string) (Config, error) {
|
|||||||
prefix = defaultEnvPrefix
|
prefix = defaultEnvPrefix
|
||||||
}
|
}
|
||||||
cfg := Default()
|
cfg := Default()
|
||||||
cfg.Mode = envString(prefix+"MODE", cfg.Mode)
|
|
||||||
cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address)
|
cfg.Address = envString(prefix+"HTTP_ADDR", cfg.Address)
|
||||||
cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader)
|
cfg.RequestIDHeader = envString(prefix+"REQUEST_ID_HEADER", cfg.RequestIDHeader)
|
||||||
cfg.Log.Level = envString(prefix+"LOG_LEVEL", cfg.Log.Level)
|
cfg.Log.Level = envString(prefix+"LOG_LEVEL", cfg.Log.Level)
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ examples/reference/
|
|||||||
|
|
||||||
调用路径:HTTP → Handler → Service → Repository。
|
调用路径:HTTP → Handler → Service → Repository。
|
||||||
|
|
||||||
- Handler 依赖 GoOmni,示例代码不导入 Gin,也不调用 `Engine()` / `Raw()`。
|
- Handler 依赖 GoOmni,示例代码不导入具体网络库,也不调用 `Engine()` / `Raw()`。
|
||||||
- Service、Repository、Model 不依赖 GoOmni 或 Gin,通过标准 `context.Context` 传递请求取消信号。
|
- Service、Repository、Model 不依赖 GoOmni 或网络库,通过标准 `context.Context` 传递请求取消信号。
|
||||||
- JSON 校验标签留在 Handler 的请求结构上;业务模型没有 HTTP 绑定标签。
|
- JSON 校验标签留在 Handler 的请求结构上;业务模型没有 HTTP 绑定标签。
|
||||||
- Service 返回业务错误,由 Handler 映射为 HTTP 错误;未知错误交给框架转换成通用 500 响应。
|
- Service 返回业务错误,由 Handler 映射为 HTTP 错误;未知错误交给框架转换成通用 500 响应。
|
||||||
- 通过 `application.Group("/api/v1", handler.RequestLog(logger))` 挂载自定义中间件,再注册 `/users` 子路由。日志记录请求 ID、耗时和返回错误;panic 由框架恢复中间件处理。
|
- 通过 `application.Group("/api/v1", handler.RequestLog(logger))` 挂载自定义中间件,再注册 `/users` 子路由。日志记录请求 ID、耗时和返回错误;panic 由框架恢复中间件处理。
|
||||||
@@ -156,6 +156,6 @@ go vet ./...
|
|||||||
go build ./...
|
go build ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
`TestUserCRUD` 将同一份业务注册代码分别挂载到 Gin 与 nethttp,验证替换适配器后 Handler、Service、Repository 均无需修改。
|
`TestUserCRUD` 使用标准库适配器运行完整业务注册代码;同一套 Handler、Service、Repository 也可通过 `omni.NewWithRouter` 接入自研适配器。
|
||||||
|
|
||||||
测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。
|
测试覆盖完整 CRUD、认证和角色权限、校验失败、分页边界、邮箱冲突及失败更新、删除后重用邮箱、请求 ID、日志关联、内部错误隐藏、并发唯一性、取消请求不修改数据、数据库配置解析和 HTTP 优雅退出。MySQL 测试不自动连接外部数据库。
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"git.yuntuo.hk/k2safe/go-omni/examples/reference/repository"
|
"git.yuntuo.hk/k2safe/go-omni/examples/reference/repository"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin"
|
|
||||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ type envelope struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testApp() *omni.App {
|
func testApp() *omni.App {
|
||||||
return newApp(repository.NewMemoryUsers(), testLogger(io.Discard), omni.WithMode("test"))
|
return newApp(repository.NewMemoryUsers(), testLogger(io.Discard))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testLogger(writer io.Writer) *logx.Logger {
|
func testLogger(writer io.Writer) *logx.Logger {
|
||||||
@@ -87,7 +86,6 @@ func TestProtectedWriteRoutes(t *testing.T) {
|
|||||||
repository.NewMemoryUsers(),
|
repository.NewMemoryUsers(),
|
||||||
testLogger(io.Discard),
|
testLogger(io.Discard),
|
||||||
auth.StaticAuthenticator{"user-token": {UserID: "user-1", Roles: []string{"user"}}},
|
auth.StaticAuthenticator{"user-token": {UserID: "user-1", Roles: []string{"user"}}},
|
||||||
omni.WithMode("test"),
|
|
||||||
)
|
)
|
||||||
request(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, 401, "UNAUTHENTICATED")
|
request(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, 401, "UNAUTHENTICATED")
|
||||||
created := dataAs[model.User](t, requestWithAuthorization(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, "Bearer user-token", 201, "CREATED"))
|
created := dataAs[model.User](t, requestWithAuthorization(t, app, "POST", "/api/v1/users", `{"name":"Alice","email":"alice@example.com"}`, "Bearer user-token", 201, "CREATED"))
|
||||||
@@ -110,7 +108,6 @@ func TestUserCRUD(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
newRouter func() httpx.Router
|
newRouter func() httpx.Router
|
||||||
}{
|
}{
|
||||||
{"gin", func() httpx.Router { return ginrouter.New("test") }},
|
|
||||||
{"nethttp", func() httpx.Router { return netrouter.New() }},
|
{"nethttp", func() httpx.Router { return netrouter.New() }},
|
||||||
} {
|
} {
|
||||||
t.Run(adapter.name, func(t *testing.T) {
|
t.Run(adapter.name, func(t *testing.T) {
|
||||||
@@ -226,7 +223,7 @@ func TestErrorTranslationAndRequestLogging(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
var logs bytes.Buffer
|
var logs bytes.Buffer
|
||||||
app := newApp(failingUsers{repository.NewMemoryUsers(), test.err}, testLogger(&logs), omni.WithMode("test"))
|
app := newApp(failingUsers{repository.NewMemoryUsers(), test.err}, testLogger(&logs))
|
||||||
result := request(t, app, "GET", "/api/v1/users/1", "", test.status, test.code)
|
result := request(t, app, "GET", "/api/v1/users/1", "", test.status, test.code)
|
||||||
if strings.Contains(result.Message, test.err.Error()) || len(result.Data) != 0 {
|
if strings.Contains(result.Message, test.err.Error()) || len(result.Data) != 0 {
|
||||||
t.Fatalf("internal error details leaked: %+v", result)
|
t.Fatalf("internal error details leaked: %+v", result)
|
||||||
|
|||||||
19
go.mod
19
go.mod
@@ -3,36 +3,19 @@ module git.yuntuo.hk/k2safe/go-omni
|
|||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
|
||||||
github.com/go-playground/validator/v10 v10.20.0
|
github.com/go-playground/validator/v10 v10.20.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/stretchr/testify v1.9.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
golang.org/x/net v0.25.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
61
go.sum
61
go.sum
@@ -1,22 +1,9 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
|
||||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
|
||||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
|
||||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
@@ -27,67 +14,19 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
|
|||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ type Handler func(*Context) error
|
|||||||
type Middleware func(Handler) Handler
|
type Middleware func(Handler) Handler
|
||||||
|
|
||||||
// Router is the small contract the application needs from a network layer.
|
// Router is the small contract the application needs from a network layer.
|
||||||
// The default implementation is backed by Gin, but another router or a
|
// The default implementation is backed by the standard library, but another
|
||||||
// future GoOmni transport can implement this interface without changing
|
// router or a future GoOmni transport can implement this interface without
|
||||||
// business handlers. Register routes before serving requests. Implementations
|
// changing business handlers. Register routes before serving requests. Implementations
|
||||||
// should pass transport/transporttest.Run; see transport/README.md for the
|
// should pass transport/transporttest.Run; see transport/README.md for the
|
||||||
// portable path, method, fallback and context-propagation rules.
|
// portable path, method, fallback and context-propagation rules.
|
||||||
type Router interface {
|
type Router interface {
|
||||||
@@ -78,7 +78,8 @@ func RoutePattern(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Context provides the business-facing request and response API. It contains
|
// Context provides the business-facing request and response API. It contains
|
||||||
// only standard-library HTTP types, so it remains usable when Gin is replaced.
|
// only standard-library HTTP types, so it remains usable when the network
|
||||||
|
// implementation is replaced.
|
||||||
type Context struct {
|
type Context struct {
|
||||||
request *http.Request
|
request *http.Request
|
||||||
writer http.ResponseWriter
|
writer http.ResponseWriter
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# logx
|
# logx
|
||||||
|
|
||||||
`logx` 是 GoOmni 的业务层结构化日志入口。业务代码不需要依赖 Gin:
|
`logx` 是 GoOmni 的业务层结构化日志入口。业务代码不需要依赖具体网络库:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "git.yuntuo.hk/k2safe/go-omni/logx"
|
import "git.yuntuo.hk/k2safe/go-omni/logx"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package logx provides the application logger used by GoOmni and business
|
// Package logx provides the application logger used by GoOmni and business
|
||||||
// packages. Its public API is deliberately independent of Gin so the same
|
// packages. Its public API is deliberately independent of the network layer so the same
|
||||||
// logger can be used from HTTP handlers, jobs and command line programs.
|
// logger can be used from HTTP handlers, jobs and command line programs.
|
||||||
package logx
|
package logx
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ func RequestID(header string) httpx.Middleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recovery converts panics from handlers and middleware into the standard
|
// Recovery converts panics from handlers and middleware into the standard
|
||||||
// internal error. The network adapter does not need a Gin-specific recovery
|
// internal error. Recovery belongs to GoOmni's transport-neutral middleware
|
||||||
// hook for this behavior.
|
// rather than a router-specific hook.
|
||||||
func Recovery() httpx.Middleware {
|
func Recovery() httpx.Middleware {
|
||||||
return func(next httpx.Handler) httpx.Handler {
|
return func(next httpx.Handler) httpx.Handler {
|
||||||
return func(ctx *httpx.Context) (err error) {
|
return func(ctx *httpx.Context) (err error) {
|
||||||
|
|||||||
2
omni.go
2
omni.go
@@ -34,8 +34,6 @@ func NewWithRouter(router Router, options ...Option) *App {
|
|||||||
return app.NewWithRouter(router, options...)
|
return app.NewWithRouter(router, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithMode(mode string) Option { return config.WithMode(mode) }
|
|
||||||
|
|
||||||
func WithRequestIDHeader(header string) Option { return config.WithRequestIDHeader(header) }
|
func WithRequestIDHeader(header string) Option { return config.WithRequestIDHeader(header) }
|
||||||
|
|
||||||
func WithLogger(enabled bool) Option { return config.WithLogger(enabled) }
|
func WithLogger(enabled bool) Option { return config.WithLogger(enabled) }
|
||||||
|
|||||||
136
scripts/verifyBusiness.py
Executable file
136
scripts/verifyBusiness.py
Executable file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify a real generated business module across a released framework upgrade."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from verifyScaffold import MODULE, run
|
||||||
|
|
||||||
|
|
||||||
|
def free_address():
|
||||||
|
with socket.socket() as listener:
|
||||||
|
listener.bind(("127.0.0.1", 0))
|
||||||
|
return f"127.0.0.1:{listener.getsockname()[1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--go", default="go")
|
||||||
|
parser.add_argument("--version", default="v0.4.0", help="Target framework release")
|
||||||
|
parser.add_argument("--from-version", default="v0.2.0", help="Version used before upgrade")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
env = dict(os.environ, GOWORK="off")
|
||||||
|
env["GOPRIVATE"] = ",".join(filter(None, [env.get("GOPRIVATE", ""), "git.yuntuo.hk"]))
|
||||||
|
with tempfile.TemporaryDirectory(prefix="go omni business ") as temporary:
|
||||||
|
root = Path(temporary).resolve()
|
||||||
|
env["GOBIN"] = str(root / "bin")
|
||||||
|
print(f"Installing released CLI {args.version}", flush=True)
|
||||||
|
run([args.go, "install", f"{MODULE}/cmd/go-omni@{args.version}"], root, env)
|
||||||
|
cli = root / "bin" / "go-omni"
|
||||||
|
|
||||||
|
project = root / "billing service"
|
||||||
|
run([
|
||||||
|
str(cli), "new", "--database", "memory", "--framework-version",
|
||||||
|
args.from_version, "--module", "example.com/billing", str(project),
|
||||||
|
], root, env)
|
||||||
|
run([args.go, "mod", "tidy"], project, env)
|
||||||
|
manifest = (project / "go.mod").read_text()
|
||||||
|
if f"{MODULE} {args.from_version}" not in manifest or "replace " in manifest:
|
||||||
|
raise AssertionError("initial business project was not pinned to the expected remote version")
|
||||||
|
run([args.go, "test", "-race", "./..."], project, env)
|
||||||
|
run([args.go, "get", f"{MODULE}@{args.version}"], project, env)
|
||||||
|
run([args.go, "mod", "tidy"], project, env)
|
||||||
|
upgraded = (project / "go.mod").read_text()
|
||||||
|
if f"{MODULE} {args.version}" not in upgraded or "replace " in upgraded:
|
||||||
|
raise AssertionError("business project upgrade did not produce a clean remote pin")
|
||||||
|
run([args.go, "test", "-race", "./..."], project, env)
|
||||||
|
binary = project / "billing-server"
|
||||||
|
run([args.go, "build", "-o", str(binary), "./cmd/server"], project, env)
|
||||||
|
|
||||||
|
address = free_address()
|
||||||
|
log_file = project / "business.log"
|
||||||
|
server_env = dict(env)
|
||||||
|
server_env.update(
|
||||||
|
GO_OMNI_HTTP_ADDR=address,
|
||||||
|
GO_OMNI_ENABLE_LOGGER="true",
|
||||||
|
GO_OMNI_LOG_FILE=str(log_file),
|
||||||
|
GO_OMNI_LOG_FORMAT="json",
|
||||||
|
GO_OMNI_SHUTDOWN_TIMEOUT="2s",
|
||||||
|
)
|
||||||
|
client = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||||
|
|
||||||
|
def request(method, path, body=None, request_id="business-request"):
|
||||||
|
payload = None if body is None else json.dumps(body).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://{address}{path}", data=payload, method=method,
|
||||||
|
headers={"Content-Type": "application/json", "X-Request-ID": request_id},
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with client.open(req, timeout=3) as response:
|
||||||
|
return response.status, response.headers, json.load(response)
|
||||||
|
except urllib.error.HTTPError as error:
|
||||||
|
return error.code, error.headers, json.loads(error.read())
|
||||||
|
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[str(binary)], cwd=project, env=server_env,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
deadline = time.monotonic() + 15
|
||||||
|
while True:
|
||||||
|
if process.poll() is not None:
|
||||||
|
output = process.stdout.read() if process.stdout else ""
|
||||||
|
raise RuntimeError(f"business service exited during startup: {output}")
|
||||||
|
try:
|
||||||
|
status, _, _ = request("GET", "/health/live")
|
||||||
|
if status == 200:
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
raise RuntimeError("business service did not start")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
status, headers, body = request("GET", "/health/ready")
|
||||||
|
if status != 200 or body.get("code") != "READY":
|
||||||
|
raise AssertionError(f"readiness failed: {status} {body}")
|
||||||
|
status, headers, body = request("POST", "/users", {"name": "broken"}, "invalid-request")
|
||||||
|
if status != 400 or body.get("code") != "INVALID_BODY" or headers.get("X-Request-ID") != "invalid-request":
|
||||||
|
raise AssertionError(f"validation contract failed: {status} {body}")
|
||||||
|
status, _, body = request("POST", "/users", {"name": "Alice", "email": "alice@example.com"}, "create-request")
|
||||||
|
if status != 201 or body.get("data", {}).get("email") != "alice@example.com":
|
||||||
|
raise AssertionError(f"business create failed: {status} {body}")
|
||||||
|
status, _, body = request("POST", "/users", {"name": "Duplicate", "email": "ALICE@example.com"}, "duplicate-request")
|
||||||
|
if status != 409 or body.get("code") != "EMAIL_TAKEN":
|
||||||
|
raise AssertionError(f"business error mapping failed: {status} {body}")
|
||||||
|
process.send_signal(signal.SIGTERM)
|
||||||
|
if process.wait(timeout=8) != 0:
|
||||||
|
raise AssertionError("business service did not exit cleanly")
|
||||||
|
finally:
|
||||||
|
if process.poll() is None:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=5)
|
||||||
|
|
||||||
|
content = log_file.read_text()
|
||||||
|
if '"http.request"' not in content or '"requestId":"create-request"' not in content:
|
||||||
|
raise AssertionError(f"access log did not contain business request correlation: {content}")
|
||||||
|
if '"service":"example.com/billing"' not in content:
|
||||||
|
raise AssertionError(f"business service name was not attached to logs: {content}")
|
||||||
|
if "private" in content.lower() and "password" in content.lower():
|
||||||
|
raise AssertionError("sensitive text leaked into business logs")
|
||||||
|
|
||||||
|
print(f"PASS business module: {args.from_version} -> {args.version}, clean remote pin, CRUD, validation, errors, requestId and logs", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -145,7 +145,7 @@ class Server:
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--go", default="go")
|
parser.add_argument("--go", default="go")
|
||||||
parser.add_argument("--version", default="v0.3.0")
|
parser.add_argument("--version", default="v0.4.0")
|
||||||
parser.add_argument("--cli", help="Use a locally built CLI")
|
parser.add_argument("--cli", help="Use a locally built CLI")
|
||||||
parser.add_argument("--framework-dir", help="Local framework source for pre-release checks")
|
parser.add_argument("--framework-dir", help="Local framework source for pre-release checks")
|
||||||
parser.add_argument("--image", default="mysql:8.4")
|
parser.add_argument("--image", default="mysql:8.4")
|
||||||
@@ -186,7 +186,7 @@ def main():
|
|||||||
raise AssertionError(f"integration test did not pass: {name}\n{output}")
|
raise AssertionError(f"integration test did not pass: {name}\n{output}")
|
||||||
print("PASS real MySQL: commit, error/panic/cancel rollback, pool limits, health timeout, close", flush=True)
|
print("PASS real MySQL: commit, error/panic/cancel rollback, pool limits, health timeout, close", flush=True)
|
||||||
|
|
||||||
serverEnv = dict(env, GO_OMNI_DB_DSN=database.dsn, GO_OMNI_MODE="release",
|
serverEnv = dict(env, GO_OMNI_DB_DSN=database.dsn,
|
||||||
GO_OMNI_DB_PING_TIMEOUT="500ms", GO_OMNI_ENABLE_LOGGER="true")
|
GO_OMNI_DB_PING_TIMEOUT="500ms", GO_OMNI_ENABLE_LOGGER="true")
|
||||||
server = Server(binary, project, serverEnv)
|
server = Server(binary, project, serverEnv)
|
||||||
servers.append(server)
|
servers.append(server)
|
||||||
|
|||||||
38
scripts/verifyPerformance.py
Executable file
38
scripts/verifyPerformance.py
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run GoOmni's repeatable stability checks and transport performance baseline."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--go", default="go")
|
||||||
|
parser.add_argument("--count", type=int, default=3)
|
||||||
|
args = parser.parse_args()
|
||||||
|
root = Path(__file__).resolve().parent.parent
|
||||||
|
env = dict(os.environ, GOWORK="off")
|
||||||
|
checks = [
|
||||||
|
([args.go, "test", "-race", "./..."], "race tests"),
|
||||||
|
([args.go, "vet", "./..."], "vet"),
|
||||||
|
([args.go, "build", "./..."], "build"),
|
||||||
|
(
|
||||||
|
[args.go, "test", "-run", "^$", "-bench", "^BenchmarkTransport",
|
||||||
|
"-benchmem", "-count", str(args.count), "./transport"],
|
||||||
|
"transport benchmarks",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
for command, name in checks:
|
||||||
|
print(f"Running {name}", flush=True)
|
||||||
|
result = subprocess.run(command, cwd=root, env=env, text=True,
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
print(result.stdout, end="")
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed with exit code {result.returncode}")
|
||||||
|
print("PASS stability checks and transport performance baseline", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -29,7 +29,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None):
|
|||||||
listener.bind(("127.0.0.1", 0))
|
listener.bind(("127.0.0.1", 0))
|
||||||
address = f"127.0.0.1:{listener.getsockname()[1]}"
|
address = f"127.0.0.1:{listener.getsockname()[1]}"
|
||||||
serverEnv = {key: value for key, value in env.items() if not key.startswith("GO_OMNI_")}
|
serverEnv = {key: value for key, value in env.items() if not key.startswith("GO_OMNI_")}
|
||||||
serverEnv.update(GO_OMNI_HTTP_ADDR=address, GO_OMNI_MODE="release",
|
serverEnv.update(GO_OMNI_HTTP_ADDR=address,
|
||||||
GO_OMNI_ENABLE_LOGGER="true", GO_OMNI_SHUTDOWN_TIMEOUT="2s")
|
GO_OMNI_ENABLE_LOGGER="true", GO_OMNI_SHUTDOWN_TIMEOUT="2s")
|
||||||
if cleanupMarker:
|
if cleanupMarker:
|
||||||
serverEnv["GO_OMNI_CLEANUP_MARKER"] = str(cleanupMarker)
|
serverEnv["GO_OMNI_CLEANUP_MARKER"] = str(cleanupMarker)
|
||||||
@@ -91,7 +91,7 @@ def exerciseServer(binary, directory, env, stopSignal, cleanupMarker=None):
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--go", default="go", help="Go executable")
|
parser.add_argument("--go", default="go", help="Go executable")
|
||||||
parser.add_argument("--version", default="v0.3.0", help="Released CLI version to install")
|
parser.add_argument("--version", default="v0.4.0", help="Released CLI version to install")
|
||||||
parser.add_argument("--cli", help="Use a locally built CLI instead of installing")
|
parser.add_argument("--cli", help="Use a locally built CLI instead of installing")
|
||||||
parser.add_argument("--framework-version", help="Explicit version passed to the CLI")
|
parser.add_argument("--framework-version", help="Explicit version passed to the CLI")
|
||||||
parser.add_argument("--framework-dir", help="Local checkout for pre-release verification")
|
parser.add_argument("--framework-dir", help="Local checkout for pre-release verification")
|
||||||
@@ -142,7 +142,7 @@ import (
|
|||||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||||
)
|
)
|
||||||
func main() {
|
func main() {
|
||||||
app := omni.New(omni.WithMode("release"))
|
app := omni.New()
|
||||||
app.GET("/health/live", func(ctx *omni.Context) error { return ctx.OK(map[string]string{"status": "ok"}) })
|
app.GET("/health/live", func(ctx *omni.Context) error { return ctx.OK(map[string]string{"status": "ok"}) })
|
||||||
app.OnShutdown(func() error { return os.WriteFile(os.Getenv("GO_OMNI_CLEANUP_MARKER"), []byte("closed"), 0600) })
|
app.OnShutdown(func() error { return os.WriteFile(os.Getenv("GO_OMNI_CLEANUP_MARKER"), []byte("closed"), 0600) })
|
||||||
if err := app.Run(os.Getenv("GO_OMNI_HTTP_ADDR")); err != nil { log.Fatal(err) }
|
if err := app.Run(os.Getenv("GO_OMNI_HTTP_ADDR")); err != nil { log.Fatal(err) }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from verifyScaffold import MODULE, run
|
|||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--go", default="go")
|
parser.add_argument("--go", default="go")
|
||||||
parser.add_argument("--version", default="v0.3.0")
|
parser.add_argument("--version", default="v0.4.0")
|
||||||
parser.add_argument("--framework-dir", help="Use a local checkout before release")
|
parser.add_argument("--framework-dir", help="Use a local checkout before release")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
env = dict(os.environ, GOWORK="off")
|
env = dict(os.environ, GOWORK="off")
|
||||||
@@ -29,15 +29,11 @@ def main():
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"{MODULE}/httpx"
|
"{MODULE}/httpx"
|
||||||
ginrouter "{MODULE}/transport/gin"
|
|
||||||
netrouter "{MODULE}/transport/nethttp"
|
netrouter "{MODULE}/transport/nethttp"
|
||||||
"{MODULE}/transport/transporttest"
|
"{MODULE}/transport/transporttest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestContract(t *testing.T) {{
|
func TestContract(t *testing.T) {{
|
||||||
t.Run("gin", func(t *testing.T) {{
|
|
||||||
transporttest.Run(t, func() httpx.Router {{ return ginrouter.New("test") }})
|
|
||||||
}})
|
|
||||||
t.Run("nethttp", func(t *testing.T) {{
|
t.Run("nethttp", func(t *testing.T) {{
|
||||||
transporttest.Run(t, func() httpx.Router {{ return netrouter.New() }})
|
transporttest.Run(t, func() httpx.Router {{ return netrouter.New() }})
|
||||||
}})
|
}})
|
||||||
@@ -52,7 +48,7 @@ func TestContract(t *testing.T) {{
|
|||||||
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
events = [json.loads(line) for line in output.splitlines() if line.startswith("{")]
|
||||||
if any(event.get("Action") == "skip" for event in events):
|
if any(event.get("Action") == "skip" for event in events):
|
||||||
raise AssertionError("contract suite unexpectedly skipped tests")
|
raise AssertionError("contract suite unexpectedly skipped tests")
|
||||||
for adapter in ("gin", "nethttp"):
|
for adapter in ("nethttp",):
|
||||||
name = f"TestContract/{adapter}"
|
name = f"TestContract/{adapter}"
|
||||||
if not any(event.get("Action") == "pass" and event.get("Test") == name for event in events):
|
if not any(event.get("Action") == "pass" and event.get("Test") == name for event in events):
|
||||||
raise AssertionError(f"{adapter} contract did not pass")
|
raise AssertionError(f"{adapter} contract did not pass")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ application := omni.NewWithRouter(netrouter.New())
|
|||||||
registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码
|
registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码
|
||||||
```
|
```
|
||||||
|
|
||||||
`transport/gin` 默认使用 Gin 分发请求;`transport/nethttp` 使用独立的标准库路由实现。两者共用路由声明校验和路径匹配规则,Gin 的正常请求仍由 Gin 路由树处理;它的 404/405 回退由适配层统一处理。
|
`transport/nethttp` 是当前内置的标准库路由实现。它提供一套可移植的路径、方法和错误回退规则;自研网络层可以直接实现同一个 `httpx.Router` 契约。
|
||||||
|
|
||||||
## 可移植路由规则(v0.3.0)
|
## 可移植路由规则(v0.3.0)
|
||||||
|
|
||||||
@@ -35,8 +35,6 @@ HTTP 回退响应属于路由层,不进入应用路由中间件,不携带统
|
|||||||
|
|
||||||
业务层通过 `application.Handle("HEAD", path, handler)` 或 `group.Handle("OPTIONS", path, handler)` 显式注册其他方法,同样经过框架中间件和错误处理。
|
业务层通过 `application.Handle("HEAD", path, handler)` 或 `group.Handle("OPTIONS", path, handler)` 显式注册其他方法,同样经过框架中间件和错误处理。
|
||||||
|
|
||||||
`gin.NewWithEngine` 保留原生路由和中间件,但覆盖重定向、路径清理和 404/405 策略。可移植契约与 `Allow` 集合只覆盖经 GoOmni Router 注册的路由;直接操作 `Engine()` 的原生扩展不属于该保证。
|
|
||||||
|
|
||||||
## 业务行为
|
## 业务行为
|
||||||
|
|
||||||
- 同一套 Handler、Service、中间件用于两种适配器;应用、分组、嵌套分组、路由中间件依次进入、反序退出。
|
- 同一套 Handler、Service、中间件用于两种适配器;应用、分组、嵌套分组、路由中间件依次进入、反序退出。
|
||||||
@@ -69,9 +67,9 @@ func TestContract(t *testing.T) {
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
go test -race ./transport/... ./examples/reference
|
go test -race ./transport/... ./examples/reference
|
||||||
python3 scripts/verifyTransports.py --version v0.3.0
|
python3 scripts/verifyTransports.py --version v0.4.0
|
||||||
```
|
```
|
||||||
|
|
||||||
第一条同时验证完整用户 CRUD 在两种适配器上复用相同 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包。
|
第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。
|
||||||
|
|
||||||
当前替换边界是路由与 HTTP 适配层,`App.Run` 仍使用标准库 `net/http.Server`。未来自研监听、连接管理或协议解析时,可让自研服务调用 `App.ServeHTTP`(提供标准 HTTP 请求/响应桥接),由自研启动层管理监听与关闭,并在退出时调用 `App.Close`;本阶段没有实现新的网络内核。
|
当前替换边界是路由与 HTTP 适配层,`App.Run` 仍使用标准库 `net/http.Server`。未来自研监听、连接管理或协议解析时,可让自研服务调用 `App.ServeHTTP`(提供标准 HTTP 请求/响应桥接),由自研启动层管理监听与关闭,并在退出时调用 `App.Close`;本阶段没有实现新的网络内核。
|
||||||
|
|||||||
48
transport/benchmark_test.go
Normal file
48
transport/benchmark_test.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package transport_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||||
|
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkTransportRouteDispatch(b *testing.B) {
|
||||||
|
b.Run("nethttp", func(b *testing.B) {
|
||||||
|
router := netrouter.New()
|
||||||
|
router.Handle(http.MethodGet, "/users/:id", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}))
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/users/42", nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(writer, request)
|
||||||
|
if writer.Code != http.StatusNoContent {
|
||||||
|
b.Fatalf("unexpected status %d", writer.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkTransportApplicationRequest(b *testing.B) {
|
||||||
|
b.Run("nethttp", func(b *testing.B) {
|
||||||
|
application := omni.NewWithRouter(netrouter.New())
|
||||||
|
application.GET("/health", func(c *omni.Context) error {
|
||||||
|
return c.OK(map[string]string{"status": "ok"})
|
||||||
|
})
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
|
b.ReportAllocs()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
writer := httptest.NewRecorder()
|
||||||
|
application.ServeHTTP(writer, request)
|
||||||
|
if writer.Code != http.StatusOK {
|
||||||
|
b.Fatalf("unexpected status %d", writer.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,19 +4,12 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||||
ginrouter "git.yuntuo.hk/k2safe/go-omni/transport/gin"
|
|
||||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||||
"git.yuntuo.hk/k2safe/go-omni/transport/transporttest"
|
"git.yuntuo.hk/k2safe/go-omni/transport/transporttest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAdapterContract(t *testing.T) {
|
func TestAdapterContract(t *testing.T) {
|
||||||
for _, adapter := range []struct {
|
t.Run("nethttp", func(t *testing.T) {
|
||||||
name string
|
transporttest.Run(t, func() httpx.Router { return netrouter.New() })
|
||||||
newRouter func() httpx.Router
|
})
|
||||||
}{
|
|
||||||
{"gin", func() httpx.Router { return ginrouter.New("test") }},
|
|
||||||
{"nethttp", func() httpx.Router { return netrouter.New() }},
|
|
||||||
} {
|
|
||||||
t.Run(adapter.name, func(t *testing.T) { transporttest.Run(t, adapter.newRouter) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
// Package gin adapts Gin to GoOmni's transport-neutral router contract.
|
|
||||||
// Business packages should import httpx and app, not this package.
|
|
||||||
package gin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
|
||||||
"git.yuntuo.hk/k2safe/go-omni/transport/internal/routing"
|
|
||||||
ginlib "github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Router is the default Gin-backed GoOmni router.
|
|
||||||
type Router struct {
|
|
||||||
engine *ginlib.Engine
|
|
||||||
routes []registeredRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
type registeredRoute struct {
|
|
||||||
method string
|
|
||||||
segments []routing.Segment
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a Gin router without Gin's default logger or recovery middleware.
|
|
||||||
func New(mode string) *Router {
|
|
||||||
if mode != "" {
|
|
||||||
ginlib.SetMode(mode)
|
|
||||||
}
|
|
||||||
return NewWithEngine(ginlib.New())
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWithEngine wraps an existing engine and applies GoOmni's routing policy.
|
|
||||||
// Native middleware/routes are retained; redirect and fallback settings are
|
|
||||||
// replaced. Register portable routes through Router, not Engine().
|
|
||||||
func NewWithEngine(engine *ginlib.Engine) *Router {
|
|
||||||
if engine == nil {
|
|
||||||
panic("go-omni/gin: nil engine")
|
|
||||||
}
|
|
||||||
engine.RedirectTrailingSlash = false
|
|
||||||
engine.RedirectFixedPath = false
|
|
||||||
engine.RemoveExtraSlash = false
|
|
||||||
engine.UseRawPath = false
|
|
||||||
// Gin 1.10 can carry backtracking state between method trees during its
|
|
||||||
// 405 lookup. Derive Allow from registered patterns instead of that lookup.
|
|
||||||
engine.HandleMethodNotAllowed = false
|
|
||||||
router := &Router{engine: engine}
|
|
||||||
engine.NoRoute(router.fallback)
|
|
||||||
return router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) fallback(ctx *ginlib.Context) {
|
|
||||||
allowed := make(map[string]bool)
|
|
||||||
for _, route := range r.routes {
|
|
||||||
if _, matches := routing.Match(route.segments, ctx.Request.URL.Path); matches {
|
|
||||||
allowed[route.method] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(allowed) == 0 {
|
|
||||||
http.NotFound(ctx.Writer, ctx.Request)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
methods := make([]string, 0, len(allowed))
|
|
||||||
for method := range allowed {
|
|
||||||
methods = append(methods, method)
|
|
||||||
}
|
|
||||||
sort.Strings(methods)
|
|
||||||
ctx.Header("Allow", strings.Join(methods, ", "))
|
|
||||||
http.Error(ctx.Writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Engine exposes the underlying Gin engine to adapter-specific integrations.
|
|
||||||
func (r *Router) Engine() *ginlib.Engine { return r.engine }
|
|
||||||
|
|
||||||
func (r *Router) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
r.engine.ServeHTTP(writer, request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Handle(method, path string, handler http.Handler) {
|
|
||||||
segments := routing.Parse(method, path, handler)
|
|
||||||
r.engine.Handle(method, path, adapt(handler))
|
|
||||||
r.routes = append(r.routes, registeredRoute{method: method, segments: segments})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Any(path string, handler http.Handler) {
|
|
||||||
for _, method := range routing.Methods {
|
|
||||||
r.Handle(method, path, handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) Group(path string) httpx.RouterGroup {
|
|
||||||
return &group{router: r, prefix: routing.Join("", path)}
|
|
||||||
}
|
|
||||||
|
|
||||||
type group struct {
|
|
||||||
router *Router
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *group) Handle(method, path string, handler http.Handler) {
|
|
||||||
g.router.Handle(method, routing.Join(g.prefix, path), handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *group) Any(path string, handler http.Handler) {
|
|
||||||
g.router.Any(routing.Join(g.prefix, path), handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *group) Group(path string) httpx.RouterGroup {
|
|
||||||
return &group{router: g.router, prefix: routing.Join(g.prefix, path)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func adapt(handler http.Handler) ginlib.HandlerFunc {
|
|
||||||
return func(ctx *ginlib.Context) {
|
|
||||||
params := make(map[string]string, len(ctx.Params))
|
|
||||||
for _, param := range ctx.Params {
|
|
||||||
params[param.Key] = param.Value
|
|
||||||
}
|
|
||||||
requestContext := httpx.WithRouteParams(ctx.Request.Context(), params)
|
|
||||||
requestContext = httpx.WithRoutePattern(requestContext, ctx.FullPath())
|
|
||||||
request := ctx.Request.WithContext(requestContext)
|
|
||||||
handler.ServeHTTP(ctx.Writer, request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,7 +58,7 @@ func Parse(method, path string, handler http.Handler) []Segment {
|
|||||||
return segments
|
return segments
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckConflict enforces Gin-compatible ambiguity rules for one method.
|
// CheckConflict enforces the portable ambiguity rules for one method.
|
||||||
func CheckConflict(existing, next []Segment) {
|
func CheckConflict(existing, next []Segment) {
|
||||||
for i := 0; i < len(existing) && i < len(next); i++ {
|
for i := 0; i < len(existing) && i < len(next); i++ {
|
||||||
a, b := existing[i], next[i]
|
a, b := existing[i], next[i]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Package nethttp provides a standard-library router for GoOmni, independent
|
// Package nethttp provides GoOmni's standard-library router. Register routes
|
||||||
// of Gin. Register routes before serving requests.
|
// before serving requests.
|
||||||
package nethttp
|
package nethttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRouterWorksWithoutGin(t *testing.T) {
|
func TestRouterWorks(t *testing.T) {
|
||||||
router := netrouter.New()
|
router := netrouter.New()
|
||||||
app := omni.NewWithRouter(router)
|
app := omni.NewWithRouter(router)
|
||||||
app.GET("/users/:id", func(ctx *omni.Context) error {
|
app.GET("/users/:id", func(ctx *omni.Context) error {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func newValidator() *validator.Validate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// JSON binds and validates a JSON request body using standard-library HTTP
|
// JSON binds and validates a JSON request body using standard-library HTTP
|
||||||
// types. It intentionally does not depend on Gin.
|
// types. It intentionally does not depend on a network framework.
|
||||||
func JSON(request *http.Request, value any) error {
|
func JSON(request *http.Request, value any) error {
|
||||||
if request == nil || request.Body == nil {
|
if request == nil || request.Body == nil {
|
||||||
return apierrors.BadRequest("INVALID_BODY", "request body is invalid")
|
return apierrors.BadRequest("INVALID_BODY", "request body is invalid")
|
||||||
|
|||||||
Reference in New Issue
Block a user