-
Notifications
You must be signed in to change notification settings - Fork 0
input_output.md
HypGo Framework — Schema Input/Output 完整技術指南 版本:0.8.1-alpha | 2026-04
在 HypGo 中,Input 和 Output 是 Schema-first 路由的核心概念。它們不只是文檔,而是可執行的合約 — 框架用它們來驗證請求、測試回應、生成 manifest。
Input(請求合約) Output(回應合約)
│ │
├→ Contract Testing 驗證請求 body ├→ Contract Testing 驗證回應 body
├→ Manifest 記錄 input_type ├→ Manifest 記錄 output_type
├→ AI 知道要傳什麼 ├→ AI 知道會收到什麼
└→ 自動生成測試資料 └→ 自動驗證必填欄位
Input 和 Output 都是普通的 Go struct,透過 json tag 定義 JSON 欄位名:
// Input — 客戶端發送的請求 body type CreateUserReq struct { Name string `json:"name"` // 必填(非 pointer、無 omitempty) Email string `json:"email"` // 必填 Bio string `json:"bio,omitempty"` // 選填(有 omitempty) Age *int `json:"age"` // 選填(pointer) } // Output — 伺服器回傳的回應 body type UserResp struct { ID int64 `json:"id"` Name string `json:"name"` Email string `json:"email"` Bio string `json:"bio,omitempty"` CreatedAt string `json:"created_at"` }
框架透過 reflect 自動判斷每個欄位是否為必填:
| 條件 | 判斷結果 | 範例 |
|---|---|---|
非 pointer + 無 omitempty
|
必填 |
Name string \json:"name"`` |
有 omitempty
|
選填 |
Bio string \json:"bio,omitempty"`` |
| pointer 型別 | 選填 |
Age *int \json:"age"`` |
json:"-" |
忽略(不出現在 JSON 中) |
Internal string \json:"-"`` |
內部實作(pkg/schema/reflect.go):
required := !omitempty && f.Type.Kind() != reflect.Ptr
這一行決定了 Contract Testing 是否會檢查該欄位的存在。
三者有不同的職責,不應混用:
// ❌ 錯誤:用 DB model 當 Input r.Schema(schema.Route{ Input: models.User{}, // 包含 ID、CreatedAt — 客戶端不該提供 }) // ✅ 正確:分開定義 type User struct { // DB model — 對應資料庫 table ID int64 Name string CreatedAt time.Time } type CreateUserReq struct { // Input — 只有客戶端可提供的欄位 Name string `json:"name"` Email string `json:"email"` } type UserResp struct { // Output — 只有要回傳的欄位 ID int64 `json:"id"` Name string `json:"name"` Email string `json:"email"` }
| struct | 用途 | 包含 ID? | 包含密碼? | 在哪裡用 |
|---|---|---|---|---|
User |
DB model | ✅ | ✅ | Bun ORM |
CreateUserReq |
Input | ❌ | ❌ | Schema Input
|
UserResp |
Output | ✅ | ❌ | Schema Output
|
r.Schema(schema.Route{ Method: "POST", Path: "/api/users", Summary: "Create user", Input: CreateUserReq{}, // ← 傳入零值實例,框架只用型別資訊 Output: UserResp{}, // ← 傳入零值實例 }).Handle(createUserHandler)
重要:傳的是 CreateUserReq{}(零值實例),不是指標 &CreateUserReq{}。框架只讀取型別資訊(reflect.TypeOf),不使用實例的值。
當你呼叫 router.Schema(route).Handle(handler) 時,框架內部執行:
1. NewSchemaRoute(route, registrar)
│
├─ route.Input != nil ?
│ └─ route.InputName = TypeName(route.Input)
│ → reflect.TypeOf(CreateUserReq{}).Name() → "CreateUserReq"
│
├─ route.Output != nil ?
│ └─ route.OutputName = TypeName(route.Output)
│ → "UserResp"
│
└─ Responses 中的 Type != nil ?
└─ resp.TypeName = TypeName(resp.Type)
2. registrar.RegisterSchema(route, handlers...)
│
├─ router.addRoute(method, path, handlers) ← 正常路由註冊
│
└─ schema.Global().Register(route) ← 存入全域 Schema Registry
→ key = "POST /api/users"
→ value = Route{Input: CreateUserReq{}, Output: UserResp{}, ...}
// 全域 Registry 結構 type Registry struct { schemas []Route // 有序列表 byKey map[string]*Route // "METHOD /path" → Route } // 每個 Route 中與 Input/Output 相關的欄位: type Route struct { Input interface{} // Go struct 零值(CreateUserReq{}) Output interface{} // Go struct 零值(UserResp{}) InputName string // "CreateUserReq"(自動填入) OutputName string // "UserResp"(自動填入) }
Input 和 Output 欄位帶有 json:"-" tag,所以它們不會出現在 JSON/YAML 序列化中。Manifest 使用 InputName / OutputName(字串)。
hyp context 或 AutoSync 產出的 manifest 中,Input/Output 以型別名稱呈現:
routes: - method: POST path: /api/users summary: "Create user" input_type: CreateUserReq # ← InputName output_type: UserResp # ← OutputName handler_names: [controllers.CreateUser]
AI 看到 input_type: CreateUserReq 就知道:
- 這個路由接受
CreateUserReqstruct 作為 JSON body - 可以去
app/models/找到CreateUserReq的欄位定義 - 生成的 handler 要用
c.ShouldBindJSON(&req)解析
contract.TestAll(t, router) 對每個 schema 路由自動執行:
對每個路由:
1. 有 Input?
└─ generateMinimalJSON(route.Input)
→ 為每個欄位生成合理值:
string → "test"
int → 0
bool → false
slice → []
→ 結果:{"name":"test","email":"test","bio":"test"}
2. 發送 HTTP 請求
→ method = route.Method
→ path = resolvePath(route.Path) // :id → "1"
→ body = 生成的 JSON
3. 有 Output?
└─ validateResponse(responseBody, route.Output)
│
├─ json.Unmarshal(body, &OutputType{})
│ → 確認 JSON 格式正確、欄位型別匹配
│
└─ validateRequiredFields(body, OutputType)
→ 遍歷 Output struct 的所有欄位
→ 非 pointer + 非 omitempty → 必填
→ 必填欄位不在 JSON 中 → 回報錯誤
4. 驗證狀態碼
→ Responses 有宣告?用最小的 2xx 狀態碼
→ 沒有宣告?POST→201, DELETE→204, 其他→200
contract.Test(t, router, contract.TestCase{ Route: "POST /api/users", Input: `{"name":"alice","email":"alice@example.com"}`, ExpectStatus: 201, ExpectSchema: true, // ← 啟用 Output 驗證 })
ExpectSchema: true 時,框架會:
- 從 Schema Registry 查找
"POST /api/users"的 Output 型別 - 把回應 body
json.Unmarshal進UserResp{} - 檢查
UserResp中所有必填欄位都存在於回應中
hyp generate model user 自動生成配對的 struct:
// 生成的 Input struct type CreateUserReq struct { Name string `json:"name"` // 必填 Description string `json:"description,omitempty"` // 選填 } type UpdateUserReq struct { Name string `json:"name,omitempty"` // 全部選填(部分更新) Description string `json:"description,omitempty"` Active *bool `json:"active,omitempty"` // pointer = 選填 }
hyp generate controller user 生成的 handler 自動使用:
func (ctrl *UserController) Create(c *hypcontext.Context) { var req models.CreateUserReq // ← 引用 Input struct if err := c.ShouldBindJSON(&req); err != nil { errors.AbortWithAppError(c, ErrUserInvalid.With("reason", err.Error())) return } // ... 業務邏輯 ... c.JSON(201, models.UserResp{ // ← 引用 Output struct ID: user.ID, Name: req.Name, }) }
hyp ai-rules 生成的 AGENTS.md 包含路由表(如果 manifest 存在):
## Current Routes | Method | Path | Summary | |--------|------|---------| | POST | /api/users | Create user | | GET | /api/users/:id | Get user |
AI 工具看到這個表格,就知道去 manifest 查 input_type 和 output_type。
schema.TypeName(CreateUserReq{}) → "CreateUserReq" schema.TypeName(&CreateUserReq{}) → "CreateUserReq" // 自動解指標 schema.TypeName((*UserResp)(nil)) → "UserResp" // nil 指標也行 schema.TypeName(nil) → "" schema.TypeName(42) → "int"
fields := schema.FieldsOf(CreateUserReq{}) // [ // {Name: "name", Type: "string", Required: true}, // {Name: "email", Type: "string", Required: true}, // {Name: "bio", Type: "string", Required: false}, // omitempty // {Name: "age", Type: "integer", Required: false}, // pointer // ]
// 通過 err := schema.ValidateJSON( []byte(`{"name":"alice","email":"a@b.com"}`), CreateUserReq{}, ) // err == nil // 失敗:缺少必填欄位 err := schema.ValidateJSON( []byte(`{"bio":"hello"}`), CreateUserReq{}, ) // err == "missing required field: name" // 失敗:JSON 格式錯誤 err := schema.ValidateJSON( []byte(`{invalid json}`), CreateUserReq{}, ) // err == "JSON does not match schema CreateUserReq: ..."
data := schema.GenerateZeroJSON(CreateUserReq{}) // {"name":"","email":"","bio":"","age":null} data := schema.GenerateZeroJSON(nil) // {}
| Go 型別 | Schema 型別 |
|---|---|
string |
"string" |
int, int64, uint
|
"integer" |
float32, float64
|
"number" |
bool |
"boolean" |
[]T |
"array" |
map[K]V, struct
|
"object" |
一個 Input/Output struct 從定義到驗證的完整路徑:
👤 人定義 struct
│
▼
┌──────────────────────────────────────────────────────┐
│ type CreateUserReq struct { │
│ Name string `json:"name"` │
│ Email string `json:"email"` │
│ } │
└──────────────────────────────────────────────────────┘
│
▼
👤 人在 Schema 路由中引用
│
▼
┌──────────────────────────────────────────────────────┐
│ r.Schema(schema.Route{ │
│ Input: CreateUserReq{}, ← 型別資訊進入框架 │
│ Output: UserResp{}, │
│ }).Handle(handler) │
└──────────────────────────────────────────────────────┘
│
├──→ 🔧 Schema Registry 儲存型別
│ key: "POST /api/users"
│ Input: CreateUserReq{}(reflect.Type)
│ InputName: "CreateUserReq"(string)
│
├──→ 🔧 Manifest 輸出型別名稱
│ input_type: CreateUserReq
│ output_type: UserResp
│
├──→ 🤖 AI 讀 manifest → 知道 Input/Output
│ → 生成 handler 時使用正確的 struct
│
└──→ 🔧 Contract Testing 驗證
TestAll()
│
├─ generateMinimalJSON(CreateUserReq{})
│ → {"name":"test","email":"test"}
│
├─ 發送 POST /api/users,body = 上述 JSON
│
└─ validateResponse(body, UserResp{})
→ json.Unmarshal 到 UserResp
→ 檢查 id、name、email 都存在
type UserListResp struct { Data []UserResp `json:"data"` Total int `json:"total"` } r.Schema(schema.Route{ Method: "GET", Path: "/api/users", Output: UserListResp{}, // ← 列表用 ListResp }).Handle(ctrl.List)
type UpdateUserReq struct { Name string `json:"name,omitempty"` // 全部 omitempty Email string `json:"email,omitempty"` Active *bool `json:"active,omitempty"` // 用 pointer 區分「沒傳」vs「傳 false」 }
// GET — 無 Input r.Schema(schema.Route{ Method: "GET", Path: "/api/users/:id", Output: UserResp{}, // 只有 Output }).Handle(ctrl.Get) // DELETE — 無 Input、無 Output r.Schema(schema.Route{ Method: "DELETE", Path: "/api/users/:id", // Input: nil, Output: nil }).Handle(ctrl.Delete)
錯誤不用定義在 Output 中。HypGo 使用 Error Catalog 統一處理:
// 定義錯誤碼 var ErrUserNotFound = errors.Define("E_user_001", 404, "User not found", "user") // Handler 中使用 func (ctrl *UserController) Get(c *hypcontext.Context) { user := findUser(id) if user == nil { errors.AbortWithAppError(c, ErrUserNotFound.With("id", id)) return // ← 回傳 {"code":"E_user_001","message":"User not found","details":{"id":"1"}} } c.JSON(200, UserResp{...}) // ← 回傳 Output struct }
Contract Testing 只驗證成功路徑的 Output。錯誤路徑由 Error Catalog 保證格式一致。
route, ok := schema.Global().Get("POST", "/api/users") if ok { fmt.Printf("Input: %s\n", route.InputName) // "CreateUserReq" fmt.Printf("Output: %s\n", route.OutputName) // "UserResp" // 查看 Input 欄位 fields := schema.FieldsOf(route.Input) for _, f := range fields { fmt.Printf(" %s (%s) required=%v\n", f.Name, f.Type, f.Required) } }
err := schema.ValidateJSON( []byte(`{"name":"alice"}`), // 缺少 email CreateUserReq{}, ) fmt.Println(err) // "missing required field: email"
route := schema.Route{ Method: "POST", Path: "/api/users", Input: CreateUserReq{}, } // 框架內部用的是 generateMinimalJSON(unexported),但你可以用 GenerateZeroJSON data := schema.GenerateZeroJSON(CreateUserReq{}) fmt.Println(string(data)) // {"name":"","email":"","bio":"","age":null}
HypGo · Input/Output 機制詳解 · 2026-04
設計文件
套件
- config — 設定
- context — 請求上下文
- router — 路由器
- server — 伺服器
- middleware — 中介層
- websocket — WebSocket
- hidb — 資料庫 ORM
- hidb/cassandra — Cassandra
- logger — 日誌
- json — JSON 處理
- grpc — gRPC
AI 協作工具鏈
- schema — Schema-first 路由
- manifest — 專案 Manifest
- contract — Contract Testing
- errors — Typed Error Catalog
- migrate — Migration Diff
- scaffold — 智慧 Scaffold
- airules — AI Rules
CLI 命令
- hyp 總覽
- hyp new
- hyp api
- hyp run
- hyp restart
- hyp generate
- hyp migrate
- hyp context
- hyp ai-rules
- hyp chkcomment
- hyp impact
- hyp docker
- hyp health
- hyp version
- hyp difflog
Design Docs
Packages
- config — Configuration
- context — Request Context
- router — Router
- server — Server
- middleware — Middleware
- websocket — WebSocket
- hidb — Database ORM
- hidb/cassandra - Cassandra 5.0
- logger — Logger
- json — JSON
- grpc — gRPC
AI Collaboration Toolchain
- schema — Schema-first Routing
- manifest — Project Manifest
- contract — Contract Testing
- errors — Typed Error Catalog
- migrate — Migration Diff
- scaffold — Smart Scaffold
- airules — AI Rules
CLI Commands