-
Notifications
You must be signed in to change notification settings - Fork 0
contract_en.md
Automatically validates handler behavior based on schema-first route metadata, ensuring AI-generated code conforms to declared contracts.
When AI generates a handler, how do you know it's correct? Contract Testing makes "schema as contract" -- both request and response body structures must conform to the Input/Output types declared in Schema():
Schema declares Input: CreateUserRequest{Name, Email}
Schema declares Output: UserResponse{ID, Name, Email}
|
Request {"name":"alice"} <- missing email -> Input validation fails
Response {"id":1,"name":"alice"} <- missing email -> Output validation fails
Contract Testing validates both Input (request body) and Output (response body):
-
Input validation: When
ExpectSchema: trueand the Schema defines anInputtype, it automatically validates whethertc.InputJSON conforms to the Input struct - Output validation: Validates whether the handler response JSON conforms to the Output struct (including required field checks)
import ( "testing" "github.com/maoxiaoyue/hypgo/pkg/contract" ) func TestCreateUser(t *testing.T) { r := setupRouter() // Router with Schema routes registered contract.Test(t, r, contract.TestCase{ Route: "POST /api/users", Input: `{"name":"alice","email":"alice@test.com"}`, ExpectStatus: 201, ExpectSchema: true, // Validate response conforms to Output schema }) }
Test all schema-registered routes with a single line of code:
func TestAllRoutes(t *testing.T) { r := setupRouter() contract.TestAll(t, r) // Automatically generates tests for each schema route }
TestAll will:
- Get all registered schema routes from
schema.Global() - Auto-generate a minimal valid request body for each route
- Auto-resolve path parameters (
:id->1) - Validate status codes and response schema
No schema needed -- just verify the route exists and returns the correct status code:
func TestHealthEndpoint(t *testing.T) { r := setupRouter() contract.TestRoute(t, r, "GET", "/health", 200) }
type TestCase struct { Route string // "METHOD /path", e.g. "POST /api/users" Input string // JSON request body Headers map[string]string // Custom request headers Query map[string]string // URL query parameters ExpectStatus int // Expected HTTP status code ExpectSchema bool // Whether to validate response against Output schema ExpectBody string // Exact match response body (optional) }
When ExpectSchema is true, contract will:
- Look up the route's schema from
schema.Global() - Get the
Outputtype (Go struct) - Attempt to deserialize the response body JSON into that struct
- Check that all required fields are present
// Schema declaration r.Schema(schema.Route{ Method: "GET", Path: "/api/users/:id", Output: UserResponse{}, // Has 3 required fields: ID, Name, Email }).Handle(getUserHandler) // If handler returns {"id":1} -> fails (missing name, email) // If handler returns {"id":1,"name":"a","email":"b"} -> passes
| struct tag | Required? |
|---|---|
json:"name" |
Yes |
json:"bio,omitempty" |
No |
Bio *string \json:"bio"`` |
No (pointer) |
If ExpectStatus > 0, validates that the response status code matches.
If ExpectBody is not empty, trims whitespace and performs an exact match against the response body.
TestAll automatically distinguishes protocols:
| Protocol | Behavior |
|---|---|
REST (Protocol empty or "rest") |
Sends HTTP request, validates Input/Output and status code |
| gRPC / Bot / MCP / WebSocket / CLI | Validates schema definition completeness (Command non-empty, Summary non-empty, type names populated) |
// TestAll with mixed protocols schema.RegisterGRPC("UserService/CreateUser", "Create user", req, resp) schema.RegisterBot("/start", "Start the bot", nil, WelcomeMsg{}) contract.TestAll(t, router) // → REST routes: full HTTP testing // → gRPC routes: schema completeness validation // → Bot routes: schema completeness validation
For REST routes, TestAll uses the following strategies to auto-generate test cases:
| Path | Resolved Result |
|---|---|
/api/users/:id |
/api/users/1 |
/api/users/:userId/posts/:postId |
/api/users/1/posts/1 |
/api/:slug |
/api/test-slug |
/api/:name |
/api/test |
/files/*filepath |
/files/test.txt |
| Condition | Inferred Status Code |
|---|---|
| Responses has explicit 2xx declaration | Uses the smallest 2xx |
| POST (no declaration) | 201 |
| DELETE (no declaration) | 204 |
| Others (no declaration) | 200 |
Body is auto-generated only for POST, PUT, PATCH. Fills reasonable default values based on the Input struct fields:
| Go Type | Generated Value |
|---|---|
string |
"test" |
int, int64
|
0 |
float64 |
0.0 |
bool |
false |
[]T |
[] |
map[K]V |
{} |
package api_test import ( "testing" "github.com/maoxiaoyue/hypgo/pkg/contract" "github.com/maoxiaoyue/hypgo/pkg/router" "github.com/maoxiaoyue/hypgo/pkg/schema" ) type CreateUserReq struct { Name string `json:"name"` Email string `json:"email"` } type UserResp struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` } func setupRouter() *router.Router { r := router.New() r.Schema(schema.Route{ Method: "POST", Path: "/api/users", Summary: "Create user", Input: CreateUserReq{}, Output: UserResp{}, Responses: map[int]schema.ResponseSchema{ 201: {Description: "Created"}, }, }).Handle(createUserHandler) r.Schema(schema.Route{ Method: "GET", Path: "/api/users/:id", Output: UserResp{}, }).Handle(getUserHandler) r.GET("/health", healthHandler) return r } // Manual test for a specific route func TestCreateUser(t *testing.T) { r := setupRouter() contract.Test(t, r, contract.TestCase{ Route: "POST /api/users", Input: `{"name":"alice","email":"alice@test.com"}`, ExpectStatus: 201, ExpectSchema: true, }) } // Test with query parameters func TestSearchUsers(t *testing.T) { r := setupRouter() contract.Test(t, r, contract.TestCase{ Route: "GET /api/users", Query: map[string]string{"page": "1", "limit": "10"}, ExpectStatus: 200, }) } // Test with custom headers func TestAuthRequired(t *testing.T) { r := setupRouter() contract.Test(t, r, contract.TestCase{ Route: "GET /api/admin", Headers: map[string]string{"Authorization": "Bearer test-token"}, ExpectStatus: 200, }) } // One-click test for all schema routes func TestAllContracts(t *testing.T) { r := setupRouter() contract.TestAll(t, r) } // Simple route check func TestHealth(t *testing.T) { r := setupRouter() contract.TestRoute(t, r, "GET", "/health", 200) }
pkg/contract/
├── contract.go Test(), TestAll(), TestRoute() core test functions
├── validate.go validateResponse(), validateRequest(), validateRequiredFields()
├── generate.go generateTestCase(), generateMinimalJSON(), resolvePath()
└── contract_test.go 24 unit tests
pkg/contract -> pkg/router (Router.ServeHTTP to execute requests)
-> pkg/schema (Global() to look up schema metadata)
-> net/http/httptest (simulate HTTP requests)
Schema-first Routes Contract Testing
+------------------+ +------------------+
| Route{ | | TestCase{ |
| Input: Req{} |----->| ExpectSchema: |
| Output: Resp{} | | true |
| Responses: ... | | } |
| } | | |
+------------------+ +------------------+
| |
+--- schema.Global() ----+
(shared Registry)
Routes not registered with Schema() cannot undergo schema validation, but can still use TestRoute() to check status codes.
設計文件
套件
- 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