fix: inherit App error catalogs on generated routes without local errors
This commit is contained in:
@@ -25,6 +25,8 @@ go run ./cmd/go-omni api validate --api examples/contracts/user.api
|
||||
|
||||
`v0.11.0-preview` 增加有序模板参数,生成带类型的错误函数和具体文档示例;示例见 [errorParams.api](../../examples/contracts/errorParams.api)。新生成代码需要同版或更新框架。
|
||||
|
||||
`v0.11.1-preview` 修复未声明错误的生成路由遮蔽 App 错误目录:错误可只在 Go 中注册一次,`.api` 不必重复维护;返回格式和路由参数继续按契约生成。
|
||||
|
||||
支持范围、参数语义、目录及安全更新策略见 [API 开发指南](../../API.md)。本版借鉴 go-zero 的工作流,不宣称兼容全部 goctl 语法;仍通过 Omni/httpx 保留网络层替换能力。
|
||||
|
||||
## 日志与对象存储按需装配
|
||||
@@ -79,8 +81,8 @@ Handler / Service / Repository / Model 与模块注册入口。模块名为 lowe
|
||||
从远程安装,生成一个带 Handler / Service / Repository / Model 分层的服务骨架:
|
||||
|
||||
```sh
|
||||
GOPRIVATE=git.yuntuo.hk go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.11.0-preview
|
||||
go-omni new --framework-version v0.11.0-preview --module example.com/billing ./billing
|
||||
GOPRIVATE=git.yuntuo.hk go install git.yuntuo.hk/k2safe/go-omni/cmd/go-omni@v0.11.1-preview
|
||||
go-omni new --framework-version v0.11.1-preview --module example.com/billing ./billing
|
||||
cd billing
|
||||
go mod tidy
|
||||
go run ./cmd/server
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestErrorCatalogConsumer(t *testing.T) {
|
||||
testCatalogConsumer(t, "errors.api", "errorCatalog")
|
||||
}
|
||||
|
||||
func testCatalogConsumer(t *testing.T, apiFile, fixtureName string) {
|
||||
func testCatalogConsumer(t *testing.T, apiFile, fixtureName string, extraFiles ...map[string]string) {
|
||||
t.Helper()
|
||||
if os.Getenv("GO_OMNI_SCAFFOLD_INTEGRATION") == "" {
|
||||
t.Skip("consumer integration")
|
||||
@@ -118,12 +118,24 @@ func testCatalogConsumer(t *testing.T, apiFile, fixtureName string) {
|
||||
}
|
||||
generate(false)
|
||||
|
||||
fixture := readFile(t, filepath.Join("testdata", fixtureName, "consumer_test.go"))
|
||||
writeFile(t, filepath.Join(dir, "consumer_test.go"), fixture)
|
||||
files := map[string]string{"consumer_test.go": "consumer_test.go"}
|
||||
for _, extra := range extraFiles {
|
||||
for target, source := range extra {
|
||||
files[target] = source
|
||||
}
|
||||
}
|
||||
before := map[string]string{}
|
||||
for target, source := range files {
|
||||
fixture := readFile(t, filepath.Join("testdata", fixtureName, source))
|
||||
writeFile(t, filepath.Join(dir, target), fixture)
|
||||
before[target] = fixture
|
||||
}
|
||||
generate(false)
|
||||
generate(true)
|
||||
if readFile(t, filepath.Join(dir, "consumer_test.go")) != fixture {
|
||||
t.Fatal("regeneration overwrote manual code")
|
||||
for target, fixture := range before {
|
||||
if readFile(t, filepath.Join(dir, target)) != fixture {
|
||||
t.Fatalf("regeneration overwrote %s", target)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||||
defer cancel()
|
||||
|
||||
12
cmd/go-omni/apigen/errorInheritance_test.go
Normal file
12
cmd/go-omni/apigen/errorInheritance_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package apigen
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestErrorInheritanceConsumer(t *testing.T) {
|
||||
testCatalogConsumer(t, "errorInheritance.api", "errorInheritance", map[string]string{
|
||||
"internal/service/account/plain.go": "service.go",
|
||||
"internal/service/account/raw.go": "emptyService.go",
|
||||
"internal/service/account/cleared.go": "emptyService.go",
|
||||
"internal/handler/account/boundedManual.go": "bounded.go",
|
||||
})
|
||||
}
|
||||
@@ -368,7 +368,7 @@ func renderProject(o Options, c *Compiled, starter bool) (map[string][]byte, map
|
||||
policySource += fmt.Sprintf("%q: newResponseTemplate(%q),\n", definition.Name, string(data))
|
||||
}
|
||||
policySource += "}\nfunc newResponseTemplate(raw string)response.Encoder{var cfg response.Config;decoder:=json.NewDecoder(strings.NewReader(raw));decoder.UseNumber();if err:=decoder.Decode(&cfg);err!=nil{panic(err)};return response.MustNew(cfg)}\n"
|
||||
policySource += "func responseEncoder(name string,raw bool,catalogJSON string)response.Encoder{encoder:=responseTemplates[name];if encoder==nil{panic(\"unknown response template\")};var catalog omnierrors.Catalog;if err:=json.Unmarshal([]byte(catalogJSON),&catalog);err!=nil{panic(err)};encoder=response.MustWithErrors(catalog,encoder);if raw{encoder=response.RawJSON(encoder)};return encoder}\n"
|
||||
policySource += "func responseEncoder(name string,raw bool,catalogJSON string)response.Encoder{encoder:=responseTemplates[name];if encoder==nil{panic(\"unknown response template\")};var catalog omnierrors.Catalog;if err:=json.Unmarshal([]byte(catalogJSON),&catalog);err!=nil{panic(err)};if len(catalog.Definitions)>0{encoder=response.MustWithErrors(catalog,encoder)};if raw{encoder=response.RawJSON(encoder)};return encoder}\n"
|
||||
put(generated, "internal/handler/responseGen.go", generatedHeader+policySource)
|
||||
imports := fmt.Sprintf("import (\"fmt\"; %q; %q; %q;", frameworkModule+"/httpx", frameworkModule+"/openapi", o.Module+"/internal/servicecontext")
|
||||
routes := "func RegisterRoutes(router openapi.Registrar, deps *servicecontext.Context) error {\n if router==nil || deps==nil {return fmt.Errorf(\"router and dependencies required\")}\n"
|
||||
|
||||
13
cmd/go-omni/apigen/testdata/errorInheritance/bounded.go
vendored
Normal file
13
cmd/go-omni/apigen/testdata/errorInheritance/bounded.go
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"io"
|
||||
)
|
||||
|
||||
func (h *Handler) Bounded(c *httpx.Context) error {
|
||||
if _, err := io.Copy(io.Discard, c.Request().Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Text(200, "ACK")
|
||||
}
|
||||
129
cmd/go-omni/apigen/testdata/errorInheritance/consumer_test.go
vendored
Normal file
129
cmd/go-omni/apigen/testdata/errorInheritance/consumer_test.go
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
package consumer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"example.com/errorCatalog/internal/handler"
|
||||
"example.com/errorCatalog/internal/servicecontext"
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/logx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/response"
|
||||
)
|
||||
|
||||
func TestGoOwnedCatalogOnGeneratedRoutes(t *testing.T) {
|
||||
catalog := apierrors.Catalog{DefaultStatus: 400, Definitions: map[string]apierrors.Definition{
|
||||
"USERNAME_INVALID": {Code: 400002, Message: "用户名格式不正确"},
|
||||
"TEXT_TOO_LONG": {Status: 200, Code: 400003, Message: "{label}不能超过{max}个字符", Params: []apierrors.Parameter{{Name: "label", Type: "string"}, {Name: "max", Type: "int"}}},
|
||||
"INTERNAL_ERROR": {Status: 500, Code: 9999, Message: "系统错误"},
|
||||
"BODY_TOO_LARGE": {Status: 413, Code: 413001, Message: "请求体超限"},
|
||||
}}
|
||||
lc := logx.Default()
|
||||
lc.Writer = io.Discard
|
||||
logger, err := logx.New(lc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer logger.Close()
|
||||
deps, err := servicecontext.New(context.Background(), logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer deps.Close()
|
||||
format := response.Config{CodeType: "int32", SuccessCode: 0, ErrorCode: 9999, MessageField: "msg", DataMode: "always", RequestIDField: "-"}
|
||||
app := omni.New(omni.WithErrors(catalog), omni.WithResponse(format))
|
||||
defer app.Close()
|
||||
resolve := func(c *httpx.Context) error {
|
||||
switch c.Request().URL.Query().Get("kind") {
|
||||
case "long":
|
||||
return apierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": "企业名称", "max": 100})
|
||||
case "unknown":
|
||||
return apierrors.ByCode("NOT_REGISTERED")
|
||||
default:
|
||||
return apierrors.ByCode("USERNAME_INVALID")
|
||||
}
|
||||
}
|
||||
app.Use(func(next httpx.Handler) httpx.Handler {
|
||||
return func(c *httpx.Context) error {
|
||||
if c.Header("X-Early") == "true" {
|
||||
return resolve(c)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
})
|
||||
app.GET("/ordinary", resolve)
|
||||
if err := handler.RegisterRoutes(app, deps); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Explicit route policy stays independent of App errors and response fields.
|
||||
local := apierrors.Catalog{Definitions: map[string]apierrors.Definition{"USERNAME_INVALID": {Status: 409, Code: 409001, Message: "路由提示"}}}
|
||||
localFormat := format
|
||||
localFormat.MessageField = "detail"
|
||||
app.HandleWithResponse("GET", "/independent", response.MustWithErrors(local, response.MustNew(localFormat)), resolve)
|
||||
for _, early := range []string{"false", "true"} {
|
||||
for _, test := range []struct {
|
||||
kind string
|
||||
status, code int
|
||||
message string
|
||||
}{
|
||||
{"invalid", 400, 400002, "用户名格式不正确"}, {"long", 200, 400003, "企业名称不能超过100个字符"}, {"unknown", 500, 9999, "系统错误"},
|
||||
} {
|
||||
var ordinary string
|
||||
for _, path := range []string{"/ordinary", "/plain", "/raw", "/cleared"} {
|
||||
r := httptest.NewRequest("GET", path+"?kind="+test.kind, nil)
|
||||
r.Header.Set("X-Early", early)
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, r)
|
||||
var body map[string]any
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err, w.Body.String())
|
||||
}
|
||||
if w.Code != test.status || body["code"] != float64(test.code) || body["msg"] != test.message {
|
||||
t.Fatal(path, early, w.Code, body)
|
||||
}
|
||||
if value, ok := body["data"]; !ok || value != nil {
|
||||
t.Fatal("data:null missing", body)
|
||||
}
|
||||
if path == "/ordinary" {
|
||||
ordinary = w.Body.String()
|
||||
} else if w.Body.String() != ordinary {
|
||||
t.Fatal("generated response differs", ordinary, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
r := httptest.NewRequest("GET", "/independent", nil)
|
||||
r.Header.Set("X-Early", early)
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, r)
|
||||
if w.Code != 409 || !strings.Contains(w.Body.String(), `"detail":"路由提示"`) || !strings.Contains(w.Body.String(), `"code":409001`) {
|
||||
t.Fatal(w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"/plain", "/raw"} {
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, httptest.NewRequest("GET", path+"?kind=success", nil))
|
||||
if w.Code != 200 || !strings.Contains(w.Body.String(), `"ok":true`) {
|
||||
t.Fatal(w.Code, w.Body.String())
|
||||
}
|
||||
if path == "/raw" && strings.Contains(w.Body.String(), "code") {
|
||||
t.Fatal("raw success wrapped")
|
||||
}
|
||||
}
|
||||
for _, size := range []int{8, 65} {
|
||||
w := httptest.NewRecorder()
|
||||
app.ServeHTTP(w, httptest.NewRequest("POST", "/bounded", strings.NewReader(strings.Repeat("x", size))))
|
||||
if size == 8 {
|
||||
if w.Code != 200 || w.Body.String() != "ACK" {
|
||||
t.Fatal(w.Code, w.Body.String())
|
||||
}
|
||||
} else if w.Code != 413 || !strings.Contains(w.Body.String(), "请求体超限") {
|
||||
t.Fatal(w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
1
cmd/go-omni/apigen/testdata/errorInheritance/emptyService.go
vendored
Normal file
1
cmd/go-omni/apigen/testdata/errorInheritance/emptyService.go
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package account
|
||||
29
cmd/go-omni/apigen/testdata/errorInheritance/service.go
vendored
Normal file
29
cmd/go-omni/apigen/testdata/errorInheritance/service.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/errorCatalog/internal/types"
|
||||
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
||||
)
|
||||
|
||||
func check(kind string) (*types.CheckResponse, error) {
|
||||
switch kind {
|
||||
case "success":
|
||||
return &types.CheckResponse{OK: true}, nil
|
||||
case "long":
|
||||
return nil, apierrors.ByCode("TEXT_TOO_LONG").WithParams(map[string]any{"label": "企业名称", "max": 100})
|
||||
case "unknown":
|
||||
return nil, apierrors.ByCode("NOT_REGISTERED")
|
||||
default:
|
||||
return nil, apierrors.ByCode("USERNAME_INVALID")
|
||||
}
|
||||
}
|
||||
func (s *Service) Plain(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
func (s *Service) Raw(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
func (s *Service) Cleared(ctx context.Context, req *types.CheckRequest) (*types.CheckResponse, error) {
|
||||
return check(req.Kind)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const frameworkModule = "git.yuntuo.hk/k2safe/go-omni"
|
||||
// Source builds can carry an unpublished VCS version (including +dirty).
|
||||
// Keep this fallback in sync with the next release; installed CLIs use their
|
||||
// downloaded module version, which also has a module checksum.
|
||||
const defaultFrameworkVersion = "v0.11.0-preview"
|
||||
const defaultFrameworkVersion = "v0.11.1-preview"
|
||||
|
||||
// This target is not a claim that the release already exists. Source users
|
||||
// select a local framework checkout until the module templates are published.
|
||||
|
||||
Reference in New Issue
Block a user